diff --git a/SKILLS.md b/SKILLS.md index ff71348..fd32e9a 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -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": "", + "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: @@ -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 diff --git a/src/__tests__/decode-requirement.test.ts b/src/__tests__/decode-requirement.test.ts new file mode 100644 index 0000000..e33f7fd --- /dev/null +++ b/src/__tests__/decode-requirement.test.ts @@ -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", + }) + }) +}) diff --git a/src/cli/commands/inspect.ts b/src/cli/commands/inspect.ts index 24e9c2e..eed3cf6 100644 --- a/src/cli/commands/inspect.ts +++ b/src/cli/commands/inspect.ts @@ -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" @@ -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 || ""}`) + 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([ diff --git a/src/index.ts b/src/index.ts index 53d9460..808ce2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,7 @@ export type { DecodedRequirement, DecodedSubscriptionRequirement, DecodedUnknownRequirement, + DecodedWalletStateAttestationRequirement, DescribeToolAccessOptions, ToolAccessDescription, } from "./lib/onchain/access.js" @@ -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 { diff --git a/src/lib/onchain/access.ts b/src/lib/onchain/access.ts index 4df3437..51fdb6a 100644 --- a/src/lib/onchain/access.ts +++ b/src/lib/onchain/access.ts @@ -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 = { @@ -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}` @@ -183,6 +191,7 @@ export type DecodedRequirement = | DecodedERC721Requirement | DecodedERC1155Requirement | DecodedSubscriptionRequirement + | DecodedWalletStateAttestationRequirement | DecodedUnknownRequirement /** @@ -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 } }