Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,50 @@ const op = await composite.getOp(toolId) // CompositeOp.ALL or CompositeOp.A
const terms = await composite.getTerms(toolId) // [{ predicate, negate }]
```

### WalletStateAttestationPredicate

Gates access based on offchain-signed wallet-state attestations. Designed for cross-chain wallet state that cannot be evaluated natively in EVM (e.g., Solana, XRPL, Bitcoin holdings). An offchain issuer evaluates conditions against the relevant chain, signs a verdict, and the onchain predicate verifies the signature via the RIP-7212 P-256 precompile.

| Field | Value |
|-------|-------|
| Requirement `kind` | `0x7a111640` (`IWalletStateAttestation` interface ID) |
| Requirement `data` (getRequirements) | `abi.encode(string issuerJWKSURI, bytes32 conditionHash)` |
| Proof `data` (hasAccess) | `abi.encode(bool pass, address wallet, bytes32 conditionHash, uint256 blockNumber, bytes32 r, bytes32 s, bytes32 messageHash)` |
| Signature verification | ECDSA P-256 via RIP-7212 precompile (~3,450 gas) |

This is a third-party predicate type. No canonical deployment exists; each issuer deploys their own instance. The `IWalletStateAttestation` marker interface is not pinned in `IRequirementTypes.sol` but is a valid extension per the spec's open `kind` namespace.

**Decode requirements via SDK:**
```typescript
import { decodeRequirement, WALLET_STATE_ATTESTATION_KIND } from "@opensea/tool-sdk"

const decoded = decodeRequirement(req)
if (decoded.type === "walletStateAttestation") {
// decoded.issuerJwksUri — URL to fetch the issuer's JWKS public key set
// decoded.conditionHash — identifies which condition set the predicate enforces
console.log(`Issuer JWKS: ${decoded.issuerJwksUri}`)
console.log(`Condition: ${decoded.conditionHash}`)
}
```

**Manifest access declaration (manual):**
```json
{
"access": {
"logic": "AND",
"requirements": [
{
"kind": "0x7a111640",
"data": "<abi.encode(string issuerJWKSURI, bytes32 conditionHash)>",
"label": "Cross-chain wallet attestation required"
}
]
}
}
```

**Reference implementation:** [douglasborthwick-crypto/insumer-examples](https://github.com/douglasborthwick-crypto/insumer-examples)

### SDK Helpers for Reading Predicate Requirements

Use `describeToolAccess` to read a tool's predicate name, requirements, and logic from the registry, and `decodeRequirement` to decode the raw `kind`/`data` into typed objects:
Expand All @@ -780,6 +824,9 @@ for (const req of description.requirements) {
case "subscription":
console.log(`Requires subscription (min tier ${decoded.minTier}) from ${decoded.collection}`)
break
case "walletStateAttestation":
console.log(`Requires attestation from ${decoded.issuerJwksUri} (condition: ${decoded.conditionHash})`)
break
case "unknown":
console.log(`Unknown requirement kind ${decoded.kind}`)
break
Expand Down
94 changes: 94 additions & 0 deletions src/__tests__/decode-requirement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { encodeAbiParameters, getAddress } from "viem"
import { describe, expect, it } from "vitest"
import {
type AccessRequirementInfo,
decodeRequirement,
ERC721_KIND,
ERC1155_KIND,
SUBSCRIPTION_KIND,
WALLET_STATE_ATTESTATION_KIND,
} from "../lib/onchain/access.js"

const COLLECTION = getAddress("0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa")

describe("decodeRequirement", () => {
it("decodes an ERC-721 holding requirement", () => {
const data = encodeAbiParameters([{ type: "address" }], [COLLECTION])
const req: AccessRequirementInfo = {
kind: ERC721_KIND,
data,
label: "Hold an NFT",
}
expect(decodeRequirement(req)).toEqual({
type: "erc721",
collection: COLLECTION,
})
})

it("decodes an ERC-1155 holding requirement", () => {
const data = encodeAbiParameters(
[{ type: "address" }, { type: "uint256" }],
[COLLECTION, 42n],
)
const req: AccessRequirementInfo = {
kind: ERC1155_KIND,
data,
label: "Hold token #42",
}
expect(decodeRequirement(req)).toEqual({
type: "erc1155",
collection: COLLECTION,
tokenId: 42n,
})
})

it("decodes a subscription requirement", () => {
const data = encodeAbiParameters(
[{ type: "address" }, { type: "uint8" }],
[COLLECTION, 2],
)
const req: AccessRequirementInfo = {
kind: SUBSCRIPTION_KIND,
data,
label: "Pro tier",
}
expect(decodeRequirement(req)).toEqual({
type: "subscription",
collection: COLLECTION,
minTier: 2,
})
})

it("decodes a wallet-state-attestation requirement", () => {
const issuerJwksUri = "https://issuer.example.com/.well-known/jwks.json"
const conditionHash =
"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" as const
const data = encodeAbiParameters(
[{ type: "string" }, { type: "bytes32" }],
[issuerJwksUri, conditionHash],
)
const req: AccessRequirementInfo = {
kind: WALLET_STATE_ATTESTATION_KIND,
data,
label: "Cross-chain wallet attestation",
}
expect(decodeRequirement(req)).toEqual({
type: "walletStateAttestation",
issuerJwksUri,
conditionHash,
})
})

it("returns unknown for an unrecognized kind", () => {
const req: AccessRequirementInfo = {
kind: "0xdeadbeef",
data: "0xc0ffee",
label: "Mystery",
}
expect(decodeRequirement(req)).toEqual({
type: "unknown",
kind: "0xdeadbeef",
data: "0xc0ffee",
})
})
})
31 changes: 31 additions & 0 deletions src/cli/commands/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
IAccessPredicateABI,
SubscriptionPredicateABI,
} from "../../lib/onchain/abis.js"
import { decodeRequirement } from "../../lib/onchain/access.js"
import { computeManifestHash } from "../../lib/onchain/hash.js"
import { ToolRegistryClient } from "../../lib/onchain/registry.js"
import { getChain } from "./get-chain.js"
Expand Down Expand Up @@ -149,6 +150,36 @@ export const inspectCommand = new Command("inspect")
),
)
}
} else if (predicateName === "WalletStateAttestationPredicate") {
try {
const [requirements] = await publicClient.readContract({
address: config.accessPredicate,
abi: IAccessPredicateABI,
functionName: "getRequirements",
args: [toolId],
})
for (let i = 0; i < requirements.length; i++) {
const r = requirements[i]
const decoded = decodeRequirement(r)
console.log(` Attestation requirement [${i}]:`)
console.log(` Label: ${r.label || "<no label>"}`)
if (decoded.type === "walletStateAttestation") {
console.log(` Issuer JWKS: ${decoded.issuerJwksUri}`)
console.log(` Condition hash: ${decoded.conditionHash}`)
} else {
console.log(` Kind: ${r.kind}`)
if (r.data !== "0x") {
console.log(` Data: ${r.data}`)
}
}
}
} catch (err) {
console.error(
pc.yellow(
` Warning: Failed to read attestation config: ${err instanceof Error ? err.message : String(err)}`,
),
)
}
} else if (predicateName === "CompositePredicate") {
try {
const [op, terms] = await Promise.all([
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export type {
DecodedRequirement,
DecodedSubscriptionRequirement,
DecodedUnknownRequirement,
DecodedWalletStateAttestationRequirement,
DescribeToolAccessOptions,
ToolAccessDescription,
} from "./lib/onchain/access.js"
Expand All @@ -109,6 +110,7 @@ export {
ERC721_KIND,
ERC1155_KIND,
SUBSCRIPTION_KIND,
WALLET_STATE_ATTESTATION_KIND,
} from "./lib/onchain/access.js"
export type { Deployment, PredicateKind } from "./lib/onchain/chains.js"
export {
Expand Down
17 changes: 17 additions & 0 deletions src/lib/onchain/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,13 @@ export async function describeToolAccess(
export const ERC721_KIND = "0xbdf8c428" as const
export const ERC1155_KIND = "0xcb429230" as const
export const SUBSCRIPTION_KIND = "0x44387cc2" as const
export const WALLET_STATE_ATTESTATION_KIND = "0x7a111640" as const

const KNOWN_KINDS = {
[ERC721_KIND]: "erc721",
[ERC1155_KIND]: "erc1155",
[SUBSCRIPTION_KIND]: "subscription",
[WALLET_STATE_ATTESTATION_KIND]: "walletStateAttestation",
} as const

export type DecodedERC721Requirement = {
Expand All @@ -173,6 +175,12 @@ export type DecodedSubscriptionRequirement = {
minTier: number
}

export type DecodedWalletStateAttestationRequirement = {
type: "walletStateAttestation"
issuerJwksUri: string
conditionHash: `0x${string}`
}

export type DecodedUnknownRequirement = {
type: "unknown"
kind: `0x${string}`
Expand All @@ -183,6 +191,7 @@ export type DecodedRequirement =
| DecodedERC721Requirement
| DecodedERC1155Requirement
| DecodedSubscriptionRequirement
| DecodedWalletStateAttestationRequirement
| DecodedUnknownRequirement

/**
Expand Down Expand Up @@ -218,5 +227,13 @@ export function decodeRequirement(req: AccessRequirementInfo): DecodedRequiremen
return { type: "subscription", collection, minTier }
}

if (knownType === "walletStateAttestation") {
const [issuerJwksUri, conditionHash] = decodeAbiParameters(
[{ type: "string" }, { type: "bytes32" }],
req.data,
)
return { type: "walletStateAttestation", issuerJwksUri, conditionHash }
}

return { type: "unknown", kind: req.kind, data: req.data }
}