diff --git a/EIP-7730-COMPARISON.md b/EIP-7730-COMPARISON.md new file mode 100644 index 0000000..4e938aa --- /dev/null +++ b/EIP-7730-COMPARISON.md @@ -0,0 +1,190 @@ +# Contract Metadata vs EIP-7730: Comparison Analysis + +## Overview + +EIP-7730 ("Structured Data Clear Signing") and Contract Metadata solve different problems: + +- **EIP-7730**: "What am I about to sign?" — Display formatting for transaction signing prompts in wallets +- **Contract Metadata**: "What is this contract and what do its functions do?" — Human-readable context for contract exploration, understanding, and interaction + +There is surprisingly little functional overlap despite both being "metadata for smart contracts." + +## Where We Align + +- Both use `chainId + address` for contract identity +- Both have parameter formatting (their `format` = our `displayHint`) +- Both have enum/constant mapping +- Both have timestamp, duration, token amount, address, and NFT formatting + +## Where We're Stronger (Unique Value) + +### Contract-Level Context +EIP-7730 has almost no contract-level documentation. Their `metadata` object only holds `owner`, `contractName`, `info: {url, deploymentDate}`, and `token: {name, ticker, decimals}`. + +We provide: +- `contract.description` / `shortDescription` — prose explanation +- `contract.origin` — provenance and history +- `contract.about[]` — rich content sections with headings and bodies +- `contract.risks[]` — known risks and caveats +- `contract.audits[]` — security audit references (auditor, URL, date, scope) +- `contract.links[]` — labeled external links +- `contract.tags[]` — searchable categorization +- `contract.category` — primary category (nft, token, defi, identity, etc.) +- `contract.presentation` — visual hints (primaryColor, icon) + +### Function Documentation +EIP-7730 has `intent` (a one-line summary) and `interpolatedIntent` (a template string). No prose descriptions, no warnings, no examples, no cross-references. + +We provide: +- `function.title` — human-readable name +- `function.description` — what the function does, explained for end users +- `function.warning` — risk warnings displayed before interaction +- `function.examples[]` — preset parameter examples for quick interaction +- `function.related[]` — cross-references between functions for navigation +- `function.deprecated` — deprecation notices +- `function.returns` — return value metadata with labels and display hints + +### Input Guidance +EIP-7730 only covers **output display** (formatting data that's already been constructed for signing). It has zero concept of **input collection**. + +We provide: +- `inputHint` — how to collect parameter input (`ens-resolve`, `address-book`, `slider`, `dropdown`, `hidden`, `connected-address`, `token-amount`) +- `validation` — min/max/pattern/enum validation rules + +### Events and Errors +EIP-7730 does not cover events or custom errors at all. + +We provide full metadata for both, with the same parameter enrichment (labels, descriptions, display hints). + +## Where EIP-7730 Is Stronger + +### EIP-712 Typed Data Support +Many high-risk user interactions happen via off-chain typed message signing (Permit, Permit2, Uniswap orders, OpenSea listings, etc.). EIP-7730 has an entire parallel `context.eip712` binding mechanism for these. Contract Metadata now supports EIP-712 via the `messages` top-level object, but EIP-7730's approach is more mature with domain binding and type hash matching. + +### Dynamic Cross-Field References +EIP-7730 can express relationships between parameters within a single transaction: +```json +{ + "format": "tokenAmount", + "params": { + "tokenPath": "asset" + } +} +``` +This says "format this uint256 as a token amount, looking up the token address from the `asset` parameter in the same call." Our `displayHint.tokenAddress` only supports static addresses. + +### Composability / Includes +EIP-7730 supports file inheritance via `includes`: +```json +{ + "includes": "https://registry.example/erc20.json", + "context": { "contract": { "deployments": [...] } } +} +``` +A generic ERC-20 description can be written once and included by hundreds of token files. Contract Metadata now supports this via the `includes` field and interface files (`interfaces/erc20.json`, `interfaces/erc721.json`), though our approach uses named identifiers rather than URLs. + +### Reusable Definitions +```json +{ + "display": { + "definitions": { + "tokenAmount": { "label": "Amount", "format": "tokenAmount", "params": { ... } } + }, + "formats": { + "transfer(address,uint256)": { + "fields": [{ "$ref": "$.display.definitions.tokenAmount", "path": "value" }] + } + } + } +} +``` +Shared field format specs referenced by `$ref`. We have no equivalent. + +### Multi-Chain Deployments +One EIP-7730 file can cover the same contract across multiple chains: +```json +{ + "context": { + "contract": { + "deployments": [ + { "chainId": 1, "address": "0x..." }, + { "chainId": 137, "address": "0x..." }, + { "chainId": 42161, "address": "0x..." } + ] + } + } +} +``` +We use one file per chainId + address. + +### Factory Contract Support +Both standards support factory-deployed contracts. EIP-7730 binds via deploy event + factory deployments array. Contract Metadata uses a similar `factory` field with `address` and `deployEvent`. EIP-7730's approach is slightly more flexible with its `deployments` array supporting multiple chains. + +### Conditional Field Visibility +```json +{ + "visible": { "ifNotIn": ["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"] } +} +``` +Fields can be shown/hidden based on runtime values. We have no equivalent. + +### Interpolated Intent +Template strings with embedded formatted values: +``` +"Swap {path.amountIn} for at least {path.amountOutMinimum}" +→ "Swap 1,000 USDC for at least 0.25 WETH" +``` + +## Why Both Standards Should Exist + +| Concern | EIP-7730 | Contract Metadata | +|---------|----------|-------------------| +| When is it used? | At signing time | During exploration and before interaction | +| What does it format? | Transaction calldata + EIP-712 messages | The entire contract interface | +| Who consumes it? | Wallets (Ledger, MetaMask) | Explorers, dApps, documentation tools | +| What does it explain? | "You are about to send 100 USDC to vitalik.eth" | "This function transfers tokens. Warning: check the recipient. Related: approve(), balanceOf()" | +| Input or output? | Output formatting only | Both input guidance and output formatting | +| Contract context? | Minimal (name, owner, url) | Rich (description, origin, about, risks, audits, links) | +| Events/errors? | No | Yes | +| Return values? | No | Yes | + +The two standards are genuinely complementary. A complete contract metadata ecosystem would use Contract Metadata for understanding and exploration, and EIP-7730 for the signing moment. + +## EIP-7730 Schema Details + +### Full Format Types + +**Integer formats:** `raw`, `amount` (native currency), `tokenAmount` (ERC-20 with dynamic lookup), `nftName` (NFT name + ID), `date` (unix timestamp or blockheight), `duration` (seconds to human), `unit` (SI units), `enum` (value-to-label), `chainId` (chain ID to name) + +**String formats:** `raw` + +**Bytes formats:** `raw`, `calldata` (recursive embedded call resolution) + +**Address formats:** `raw`, `addressName` (ENS/trusted name resolution), `tokenTicker` (show ERC-20 symbol), `interoperableAddressName` (ERC-7930) + +### Their Parameter Path System +EIP-7730 uses JSON-path-like expressions to reference fields: +- `path` — location in calldata/message (e.g., `"to"`, `"details.token"`) +- `value` — literal constant (e.g., `"Approve"`) +- `tokenPath` — dynamic reference to another field holding a token address +- `calldataPath` — reference to embedded calldata for recursive resolution + +### Their Metadata Object +```json +{ + "metadata": { + "owner": "Uniswap Labs", + "contractName": "Universal Router", + "info": { "url": "https://uniswap.org", "deploymentDate": "2023-01-15" }, + "token": { "name": "USD Coin", "ticker": "USDC", "decimals": 6 }, + "constants": { "MAX_UINT": "0xffffffff..." }, + "enums": { "orderType": { "0": "LIMIT", "1": "MARKET" } }, + "maps": { + "bridgeDestination": { + "keyPath": "chainId", + "values": { "1": "Ethereum", "137": "Polygon" } + } + } + } +} +``` diff --git a/README.md b/README.md index bef5768..6bc2a70 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Contract Metadata -Human-readable context for smart contracts. +Human-readable context and clear-signing metadata for smart contracts. -Contract Metadata is a JSON standard that layers human-readable context on top of onchain data. It enriches smart contracts at every level -- contract descriptions, function titles and warnings, semantic type annotations, input guidance, and event/error enrichment -- giving wallets, explorers, and dApps the information they need to present contract interactions in terms users understand. +Contract Metadata is a JSON standard that layers human-readable context on top of onchain data. It enriches smart contracts at every level -- contract descriptions, clear-signing intents, ordered display fields, input guidance, and event/error enrichment -- giving wallets, explorers, and dApps the information they need to present contract interactions in terms users understand. + +The draft now treats ERC-7730-style clear-signing primitives as a native subset: canonical named ABI fragments, `intent`, `interpolatedIntent`, ordered `fields`, path roots (`#`, `$`, `@`), display `format`s, reusable `metadata`/`display` definitions, and EIP-712 binding context. **[Read the full specification](./eip-draft.md)** @@ -20,16 +22,24 @@ offerPunkForSaleToAddress(uint256, uint256, address) "chainId": 1, "address": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", "functions": { - "offerPunkForSaleToAddress": { + "offerPunkForSaleToAddress(uint256 punkIndex,uint256 minSalePriceInWei,address toAddress)": { "title": "List Punk for Sale (Private)", "description": "List a punk for sale to a specific address only.", "warning": "This creates a binding offer. The buyer can purchase at any time.", - "intent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} to {toAddress}", - "params": { - "punkIndex": { "label": "Punk", "type": "token-id" }, - "minSalePriceInWei": { "label": "Price", "type": "eth" }, - "toAddress": { "label": "Buyer", "type": "address" } - } + "intent": "List Punk for Sale", + "interpolatedIntent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} to {toAddress}", + "fields": [ + { + "path": "punkIndex", + "label": "Punk", + "format": "nftName", + "params": { + "collection": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" + } + }, + { "path": "minSalePriceInWei", "label": "Price", "format": "amount" }, + { "path": "toAddress", "label": "Buyer", "format": "addressName" } + ] } } } @@ -45,6 +55,21 @@ validate.ts Schema + semantic validation script eip-draft.md Full EIP specification ``` +## Standard Includes + +Reusable interface metadata lives under `schema/interfaces/` and can be pulled into a contract with `includes`. + +```json +{ + "includes": [ + "interface:erc20", + "interface:erc20-permit" + ] +} +``` + +Use `interface:erc20` for standard ERC-20 function and event metadata. Add `interface:erc20-permit` only for tokens that implement EIP-2612 Permit; Permit is an optional EIP-712 signing flow, not part of base ERC-20. + ## Validation ```bash diff --git a/contracts/0x036721e5a769cc48b3189efbb9cce4471e8a48b1.json b/contracts/0x036721e5a769cc48b3189efbb9cce4471e8a48b1.json index d1308d2..e34ce48 100644 --- a/contracts/0x036721e5a769cc48b3189efbb9cce4471e8a48b1.json +++ b/contracts/0x036721e5a769cc48b3189efbb9cce4471e8a48b1.json @@ -2,7 +2,9 @@ "$schema": "https://1001-digital.github.io/contract-metadata/v1/schema.json", "chainId": 1, "address": "0x036721e5a769cc48b3189efbb9cce4471e8a48b1", - "includes": ["interface:erc721"], + "includes": [ + "interface:erc721" + ], "meta": { "version": 1, "lastUpdated": "2026-04-05T00:00:00Z", @@ -17,23 +19,47 @@ "Onchain SVG rendering depends on contract storage — future hard forks could theoretically affect rendering" ], "category": "nft", - "tags": ["nft", "generative-art", "onchain", "burn-mechanic", "svg"], + "tags": [ + "nft", + "generative-art", + "onchain", + "burn-mechanic", + "svg" + ], "links": [ { "label": "Etherscan", "url": "https://etherscan.io/address/0x036721e5A769Cc48B3189EFbb9ccE4471E8A48B1" }, - { "label": "Website", "url": "https://checks.art" }, - { "label": "Jack Butcher", "url": "https://www.jack.art/checks-originals" } + { + "label": "Website", + "url": "https://checks.art" + }, + { + "label": "Jack Butcher", + "url": "https://www.jack.art/checks-originals" + } ], "theme": { "accent": "#E84AA9" }, "groups": { - "minting": { "label": "Minting", "order": 1 }, - "art": { "label": "Artwork", "order": 2 }, - "compositing": { "label": "Compositing", "order": 3 }, - "info": { "label": "Token Info", "order": 4 } + "minting": { + "label": "Minting", + "order": 1 + }, + "art": { + "label": "Artwork", + "order": 2 + }, + "compositing": { + "label": "Compositing", + "order": 3 + }, + "info": { + "label": "Token Info", + "order": 4 + } }, "functions": { "mint": { @@ -50,7 +76,21 @@ "description": "Address to receive the Originals", "type": "address" } - } + }, + "fields": [ + { + "path": "tokenIds", + "label": "edition token IDs", + "description": "The Edition token IDs to mint", + "format": "raw" + }, + { + "path": "recipient", + "label": "recipient", + "description": "Address to receive the Originals", + "format": "addressName" + } + ] }, "getCheck": { "title": "Get Check", @@ -65,8 +105,26 @@ } }, "examples": [ - { "label": "Look up Check #1", "params": { "tokenId": "1" } }, - { "label": "Look up Check #8000", "params": { "tokenId": "8000" } } + { + "label": "Look up Check #1", + "params": { + "tokenId": "1" + } + }, + { + "label": "Look up Check #8000", + "params": { + "tokenId": "8000" + } + } + ], + "fields": [ + { + "path": "tokenId", + "label": "token ID", + "description": "The Check token ID (matches the original edition number)", + "format": "tokenId" + } ] }, "svg": { @@ -75,9 +133,22 @@ "stateMutability": "view", "group": "art", "params": { - "tokenId": { "label": "token ID", "type": "token-id" } + "tokenId": { + "label": "token ID", + "type": "token-id" + } }, - "related": ["colors", "tokenURI"] + "related": [ + "colors", + "tokenURI" + ], + "fields": [ + { + "path": "tokenId", + "label": "token ID", + "format": "tokenId" + } + ] }, "colors": { "title": "Colors", @@ -85,24 +156,49 @@ "stateMutability": "view", "group": "art", "params": { - "tokenId": { "label": "token ID", "type": "token-id" } + "tokenId": { + "label": "token ID", + "type": "token-id" + } }, - "related": ["svg", "getCheck"] + "related": [ + "svg", + "getCheck" + ], + "fields": [ + { + "path": "tokenId", + "label": "token ID", + "format": "tokenId" + } + ] }, - "tokenURI": { + "tokenURI(uint256 tokenId)": { "title": "Token URI", "description": "Returns the full onchain metadata and base64-encoded SVG for a Check. No external hosting or IPFS dependency.", "stateMutability": "view", "group": "art", "params": { - "tokenId": { "label": "token ID", "type": "token-id" } + "tokenId": { + "label": "token ID", + "type": "token-id" + } }, - "related": ["svg"] + "related": [ + "svg" + ], + "fields": [ + { + "path": "tokenId", + "label": "token ID", + "format": "tokenId" + } + ] }, "composite": { "title": "Composite", "description": "Burn two Checks of the same tier to create one Check of the next tier (e.g. two 80-checks become one 40-check). The kept token ID carries forward.", - "intent": "Composite Check #{tokenId} with #{burnId}", + "intent": "Composite", "group": "compositing", "warning": "Permanently burns one token. The burn cannot be reversed.", "_component": "checks-composite-preview", @@ -122,7 +218,32 @@ "description": "By default, the kept token's colors carry into the composite. Enable swap to use the burned token's colors instead — the kept token ID survives, but it looks like the burned one" } }, - "related": ["simulateComposite", "compositeMany", "getCheck"] + "related": [ + "simulateComposite", + "compositeMany", + "getCheck" + ], + "interpolatedIntent": "Composite Check #{tokenId} with #{burnId}", + "fields": [ + { + "path": "tokenId", + "label": "keep token ID", + "description": "The token ID to keep (carries forward)", + "format": "tokenId" + }, + { + "path": "burnId", + "label": "burn token ID", + "description": "The token ID to burn", + "format": "tokenId" + }, + { + "path": "swap", + "label": "swap visual", + "description": "By default, the kept token's colors carry into the composite. Enable swap to use the burned token's colors instead — the kept token ID survives, but it looks like the burned one", + "format": "raw" + } + ] }, "compositeMany": { "title": "Composite Many", @@ -139,19 +260,57 @@ "description": "The token IDs to burn (matched by index with keep IDs)" } }, - "related": ["composite"] + "related": [ + "composite" + ], + "fields": [ + { + "path": "tokenIds", + "label": "keep token IDs", + "description": "The token IDs to keep", + "format": "raw" + }, + { + "path": "burnIds", + "label": "burn token IDs", + "description": "The token IDs to burn (matched by index with keep IDs)", + "format": "raw" + } + ] }, "simulateComposite": { "title": "Simulate Composite", "description": "Preview what a composite would produce without executing it. Returns the resulting Check data.", - "intent": "Preview composite of #{tokenId} with #{burnId}", + "intent": "Simulate Composite", "stateMutability": "view", "group": "compositing", "params": { - "tokenId": { "label": "keep token ID", "type": "token-id" }, - "burnId": { "label": "burn token ID", "type": "token-id" } + "tokenId": { + "label": "keep token ID", + "type": "token-id" + }, + "burnId": { + "label": "burn token ID", + "type": "token-id" + } }, - "related": ["simulateCompositeSVG", "composite"] + "related": [ + "simulateCompositeSVG", + "composite" + ], + "interpolatedIntent": "Preview composite of #{tokenId} with #{burnId}", + "fields": [ + { + "path": "tokenId", + "label": "keep token ID", + "format": "tokenId" + }, + { + "path": "burnId", + "label": "burn token ID", + "format": "tokenId" + } + ] }, "simulateCompositeSVG": { "title": "Simulate Composite SVG", @@ -159,10 +318,31 @@ "stateMutability": "view", "group": "compositing", "params": { - "tokenId": { "label": "keep token ID", "type": "token-id" }, - "burnId": { "label": "burn token ID", "type": "token-id" } + "tokenId": { + "label": "keep token ID", + "type": "token-id" + }, + "burnId": { + "label": "burn token ID", + "type": "token-id" + } }, - "related": ["simulateComposite", "composite"] + "related": [ + "simulateComposite", + "composite" + ], + "fields": [ + { + "path": "tokenId", + "label": "keep token ID", + "format": "tokenId" + }, + { + "path": "burnId", + "label": "burn token ID", + "format": "tokenId" + } + ] }, "infinity": { "title": "Infinity (Black Check)", @@ -176,12 +356,23 @@ "description": "64 single-check token IDs to burn" } }, - "related": ["composite", "getCheck"] + "related": [ + "composite", + "getCheck" + ], + "fields": [ + { + "path": "tokenIds", + "label": "token IDs", + "description": "64 single-check token IDs to burn", + "format": "raw" + } + ] }, "inItForTheArt": { "title": "Sacrifice", "description": "Sacrifice one Check to transfer its visual identity to another. Burns the sacrificed token and overwrites the receiving token's visual genome.", - "intent": "Sacrifice Check #{burnId} to transfer its visual to #{tokenId}", + "intent": "Sacrifice", "group": "compositing", "warning": "Permanently burns the sacrificed token.", "params": { @@ -196,7 +387,24 @@ "type": "token-id" } }, - "related": ["inItForTheArts"] + "related": [ + "inItForTheArts" + ], + "interpolatedIntent": "Sacrifice Check #{burnId} to transfer its visual to #{tokenId}", + "fields": [ + { + "path": "tokenId", + "label": "receiving token ID", + "description": "The token that receives the visual", + "format": "tokenId" + }, + { + "path": "burnId", + "label": "sacrificed token ID", + "description": "The token to burn (its visual transfers to the receiver)", + "format": "tokenId" + } + ] }, "inItForTheArts": { "title": "Sacrifice Many", @@ -204,20 +412,49 @@ "group": "compositing", "warning": "Permanently burns the sacrificed tokens.", "params": { - "tokenIds": { "label": "receiving token IDs" }, - "burnIds": { "label": "sacrificed token IDs" } + "tokenIds": { + "label": "receiving token IDs" + }, + "burnIds": { + "label": "sacrificed token IDs" + } }, - "related": ["inItForTheArt"] + "related": [ + "inItForTheArt" + ], + "fields": [ + { + "path": "tokenIds", + "label": "receiving token IDs", + "format": "raw" + }, + { + "path": "burnIds", + "label": "sacrificed token IDs", + "format": "raw" + } + ] }, "burn": { "title": "Burn", "description": "Permanently burn a Check. The token is destroyed and removed from supply.", - "intent": "Burn Check #{tokenId}", + "intent": "Burn", "group": "compositing", "warning": "Irreversible — the token is permanently destroyed.", "params": { - "tokenId": { "label": "token ID", "type": "token-id" } - } + "tokenId": { + "label": "token ID", + "type": "token-id" + } + }, + "interpolatedIntent": "Burn Check #{tokenId}", + "fields": [ + { + "path": "tokenId", + "label": "token ID", + "format": "tokenId" + } + ] }, "resolveEpochIfNecessary": { "title": "Resolve Epoch", @@ -229,7 +466,9 @@ "description": "Returns the current epoch index.", "stateMutability": "view", "group": "info", - "related": ["getEpochData"] + "related": [ + "getEpochData" + ] }, "getEpochData": { "title": "Epoch Data", @@ -237,21 +476,32 @@ "stateMutability": "view", "group": "info", "params": { - "index": { "label": "epoch index" } + "index": { + "label": "epoch index" + } }, - "related": ["getEpoch"] + "related": [ + "getEpoch" + ], + "fields": [ + { + "path": "index", + "label": "epoch index", + "format": "raw" + } + ] }, - "totalSupply": { + "totalSupply()": { "title": "Total Supply", "description": "Current number of Checks Originals in existence (minted minus burned).", "stateMutability": "view", "group": "info" }, - "name": { + "name()": { "description": "The name of the contract (Checks).", "group": "info" }, - "symbol": { + "symbol()": { "description": "The token symbol (CHECKS).", "group": "info" } @@ -260,36 +510,62 @@ "Composite": { "description": "Emitted when two Checks are composited, burning one and producing a piece with fewer checks (e.g. 80 → 40).", "params": { - "tokenId": { "label": "kept token ID", "type": "token-id" }, - "burnId": { "label": "burned token ID", "type": "token-id" }, - "divisor": { "label": "new divisor" } + "tokenId": { + "label": "kept token ID", + "type": "token-id" + }, + "burnId": { + "label": "burned token ID", + "type": "token-id" + }, + "divisor": { + "label": "new divisor" + } } }, "Sacrifice": { "description": "Emitted when a Check is sacrificed to transfer its visual identity to another.", "params": { - "burnId": { "label": "sacrificed token ID", "type": "token-id" }, - "tokenId": { "label": "receiving token ID", "type": "token-id" } + "burnId": { + "label": "sacrificed token ID", + "type": "token-id" + }, + "tokenId": { + "label": "receiving token ID", + "type": "token-id" + } } }, "Infinity": { "description": "Emitted when 64 single-check tokens are burned to create a Black Check.", "params": { - "blackCheckId": { "label": "Black Check token ID", "type": "token-id" }, - "tokenIds": { "label": "burned token IDs" } + "blackCheckId": { + "label": "Black Check token ID", + "type": "token-id" + }, + "tokenIds": { + "label": "burned token IDs" + } } }, "NewEpoch": { "description": "Emitted when a new epoch is created for the commit-reveal randomness scheme.", "params": { - "epoch": { "label": "epoch index" }, - "revealBlock": { "label": "reveal block" } + "epoch": { + "label": "epoch index" + }, + "revealBlock": { + "label": "reveal block" + } } }, "MetadataUpdate": { "description": "Emitted when a token's metadata changes (e.g. after compositing).", "params": { - "_tokenId": { "label": "token ID", "type": "token-id" } + "_tokenId": { + "label": "token ID", + "type": "token-id" + } } } } diff --git a/contracts/0x59e16fccd424cc24e280be16e11bcd56fb0ce547.json b/contracts/0x59e16fccd424cc24e280be16e11bcd56fb0ce547.json index 6707134..ef069cc 100644 --- a/contracts/0x59e16fccd424cc24e280be16e11bcd56fb0ce547.json +++ b/contracts/0x59e16fccd424cc24e280be16e11bcd56fb0ce547.json @@ -17,21 +17,51 @@ "Expired name premiums — recently expired names carry a decaying premium that can be very high initially" ], "category": "identity", - "tags": ["ens", "identity", "naming", "dns", "infrastructure"], + "tags": [ + "ens", + "identity", + "naming", + "dns", + "infrastructure" + ], "links": [ - { "label": "Etherscan", "url": "https://etherscan.io/address/0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547" }, - { "label": "ENS", "url": "https://ens.domains" }, - { "label": "Documentation", "url": "https://docs.ens.domains" }, - { "label": "ENS App", "url": "https://app.ens.domains" } + { + "label": "Etherscan", + "url": "https://etherscan.io/address/0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547" + }, + { + "label": "ENS", + "url": "https://ens.domains" + }, + { + "label": "Documentation", + "url": "https://docs.ens.domains" + }, + { + "label": "ENS App", + "url": "https://app.ens.domains" + } ], "theme": { "accent": "#5284FF" }, "groups": { - "registration": { "label": "Registration", "order": 1 }, - "renewal": { "label": "Renewal", "order": 2 }, - "info": { "label": "Info", "order": 3 }, - "admin": { "label": "Admin", "order": 4 } + "registration": { + "label": "Registration", + "order": 1 + }, + "renewal": { + "label": "Renewal", + "order": 2 + }, + "info": { + "label": "Info", + "order": 3 + }, + "admin": { + "label": "Admin", + "order": 4 + } }, "functions": { "available": { @@ -46,8 +76,26 @@ } }, "examples": [ - { "label": "Check \"vitalik\"", "params": { "label": "vitalik" } }, - { "label": "Check \"ethereum\"", "params": { "label": "ethereum" } } + { + "label": "Check \"vitalik\"", + "params": { + "label": "vitalik" + } + }, + { + "label": "Check \"ethereum\"", + "params": { + "label": "ethereum" + } + } + ], + "fields": [ + { + "path": "label", + "label": "name", + "description": "The name to check (without .eth suffix, e.g. \"vitalik\")", + "format": "raw" + } ] }, "rentPrice": { @@ -66,9 +114,32 @@ "type": "duration" } }, - "related": ["available", "register"], + "related": [ + "available", + "register" + ], "examples": [ - { "label": "Price \"alice\" for 1 year", "params": { "label": "alice", "duration": "31536000" } } + { + "label": "Price \"alice\" for 1 year", + "params": { + "label": "alice", + "duration": "31536000" + } + } + ], + "fields": [ + { + "path": "label", + "label": "name", + "description": "The name to price (without .eth suffix)", + "format": "raw" + }, + { + "path": "duration", + "label": "duration", + "description": "Registration duration in seconds (minimum 28 days / 2419200 seconds)", + "format": "duration" + } ] }, "valid": { @@ -81,7 +152,15 @@ "label": "name", "description": "The name to validate (without .eth suffix)" } - } + }, + "fields": [ + { + "path": "label", + "label": "name", + "description": "The name to validate (without .eth suffix)", + "format": "raw" + } + ] }, "makeCommitment": { "title": "Make Commitment", @@ -94,7 +173,18 @@ "description": "Registration details: label, owner, duration, secret, resolver, records data, reverse record flags, and referrer" } }, - "related": ["commit", "register"] + "related": [ + "commit", + "register" + ], + "fields": [ + { + "path": "registration", + "label": "registration", + "description": "Registration details: label, owner, duration, secret, resolver, records data, reverse record flags, and referrer", + "format": "raw" + } + ] }, "commit": { "title": "Commit", @@ -106,7 +196,18 @@ "description": "The commitment hash from makeCommitment()" } }, - "related": ["makeCommitment", "register"] + "related": [ + "makeCommitment", + "register" + ], + "fields": [ + { + "path": "commitment", + "label": "commitment", + "description": "The commitment hash from makeCommitment()", + "format": "raw" + } + ] }, "register": { "title": "Register", @@ -120,12 +221,25 @@ "description": "The same Registration struct used in makeCommitment(): label, owner, duration, secret, resolver, data, reverseRecord, referrer" } }, - "related": ["commit", "makeCommitment", "rentPrice", "available"] + "related": [ + "commit", + "makeCommitment", + "rentPrice", + "available" + ], + "fields": [ + { + "path": "registration", + "label": "registration", + "description": "The same Registration struct used in makeCommitment(): label, owner, duration, secret, resolver, data, reverseRecord, referrer", + "format": "raw" + } + ] }, "renew": { "title": "Renew", "description": "Extend the registration of a .eth name. Anyone can renew any name — you don't have to be the owner. Only the base price is charged (no premium). Excess ETH is refunded.", - "intent": "Renew {label}.eth for {duration}", + "intent": "Renew", "group": "renewal", "warning": "Sends ETH to cover the renewal cost.", "params": { @@ -143,7 +257,30 @@ "description": "Optional referrer identifier" } }, - "related": ["rentPrice"] + "related": [ + "rentPrice" + ], + "interpolatedIntent": "Renew {label}.eth for {duration}", + "fields": [ + { + "path": "label", + "label": "name", + "description": "The name to renew (without .eth suffix)", + "format": "raw" + }, + { + "path": "duration", + "label": "duration", + "description": "Additional time to add in seconds", + "format": "duration" + }, + { + "path": "referrer", + "label": "referrer", + "description": "Optional referrer identifier", + "format": "raw" + } + ] }, "commitments": { "title": "Commitment Timestamp", @@ -156,22 +293,39 @@ } }, "returns": { - "_0": { "type": "timestamp" } - } + "_0": { + "type": "timestamp" + } + }, + "fields": [ + { + "path": "_0", + "label": "commitment hash", + "format": "raw" + } + ] }, "minCommitmentAge": { "title": "Min Commitment Age", "description": "Minimum time (in seconds) that must pass between commit() and register(). Typically 60 seconds.", "stateMutability": "view", "group": "info", - "returns": { "_0": { "type": "duration" } } + "returns": { + "_0": { + "type": "duration" + } + } }, "maxCommitmentAge": { "title": "Max Commitment Age", "description": "Maximum time (in seconds) a commitment remains valid. After this, the commitment expires and a new one must be made. Typically 24 hours.", "stateMutability": "view", "group": "info", - "returns": { "_0": { "type": "duration" } } + "returns": { + "_0": { + "type": "duration" + } + } }, "withdraw": { "title": "Withdraw", @@ -183,17 +337,46 @@ "description": "Recover ERC-20 tokens accidentally sent to this contract. Only callable by the contract owner.", "group": "admin", "params": { - "_token": { "label": "token address", "type": "address" }, - "_to": { "label": "recipient", "type": "address" }, - "_amount": { "label": "amount" } - } + "_token": { + "label": "token address", + "type": "address" + }, + "_to": { + "label": "recipient", + "type": "address" + }, + "_amount": { + "label": "amount" + } + }, + "fields": [ + { + "path": "_token", + "label": "token address", + "format": "addressName" + }, + { + "path": "_to", + "label": "recipient", + "format": "addressName" + }, + { + "path": "_amount", + "label": "amount", + "format": "raw" + } + ] }, "owner": { "title": "Owner", "description": "The address that controls admin functions (withdraw, recover funds).", "stateMutability": "view", "group": "admin", - "returns": { "_0": { "type": "address" } } + "returns": { + "_0": { + "type": "address" + } + } }, "supportsInterface": { "title": "Supports Interface", @@ -201,31 +384,70 @@ "stateMutability": "view", "group": "info", "params": { - "interfaceID": { "label": "interface ID" } - } + "interfaceID": { + "label": "interface ID" + } + }, + "fields": [ + { + "path": "interfaceID", + "label": "interface ID", + "format": "raw" + } + ] } }, "events": { "NameRegistered": { "description": "Emitted when a .eth name is successfully registered.", "params": { - "label": { "label": "name" }, - "labelhash": { "label": "labelhash" }, - "owner": { "label": "owner", "type": "address" }, - "baseCost": { "label": "base cost", "type": "eth" }, - "premium": { "label": "premium", "type": "eth" }, - "expires": { "label": "expires", "type": "timestamp" }, - "referrer": { "label": "referrer" } + "label": { + "label": "name" + }, + "labelhash": { + "label": "labelhash" + }, + "owner": { + "label": "owner", + "type": "address" + }, + "baseCost": { + "label": "base cost", + "type": "eth" + }, + "premium": { + "label": "premium", + "type": "eth" + }, + "expires": { + "label": "expires", + "type": "timestamp" + }, + "referrer": { + "label": "referrer" + } } }, "NameRenewed": { "description": "Emitted when a .eth name is renewed.", "params": { - "label": { "label": "name" }, - "labelhash": { "label": "labelhash" }, - "cost": { "label": "cost", "type": "eth" }, - "expires": { "label": "new expiry", "type": "timestamp" }, - "referrer": { "label": "referrer" } + "label": { + "label": "name" + }, + "labelhash": { + "label": "labelhash" + }, + "cost": { + "label": "cost", + "type": "eth" + }, + "expires": { + "label": "new expiry", + "type": "timestamp" + }, + "referrer": { + "label": "referrer" + } } } } diff --git a/contracts/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb.json b/contracts/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb.json index 1109f87..48fc05c 100644 --- a/contracts/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb.json +++ b/contracts/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb.json @@ -17,23 +17,47 @@ "ETH withdrawal pattern — sale proceeds and outbid returns must be manually withdrawn via pendingWithdrawals" ], "category": "nft", - "tags": ["nft", "collectible", "pfp", "pixel-art", "og"], + "tags": [ + "nft", + "collectible", + "pfp", + "pixel-art", + "og" + ], "links": [ { "label": "Etherscan", "url": "https://etherscan.io/address/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB" }, - { "label": "Larva Labs", "url": "https://larvalabs.com/cryptopunks" }, - { "label": "Website", "url": "https://www.cryptopunks.app" } + { + "label": "Larva Labs", + "url": "https://larvalabs.com/cryptopunks" + }, + { + "label": "Website", + "url": "https://www.cryptopunks.app" + } ], "theme": { "accent": "#ff04b4" }, "groups": { - "info": { "label": "Contract Info", "order": 1 }, - "ownership": { "label": "Ownership", "order": 2 }, - "marketplace": { "label": "Marketplace", "order": 3 }, - "genesis": { "label": "Original Assignment", "order": 4 } + "info": { + "label": "Contract Info", + "order": 1 + }, + "ownership": { + "label": "Ownership", + "order": 2 + }, + "marketplace": { + "label": "Marketplace", + "order": 3 + }, + "genesis": { + "label": "Original Assignment", + "order": 4 + } }, "functions": { "name": { @@ -96,13 +120,45 @@ "label": "punk index", "description": "The punk ID (0–9999)", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } + } + }, + "returns": { + "_0": { + "label": "owner", + "type": "address" } }, - "returns": { "_0": { "label": "owner", "type": "address" } }, "examples": [ - { "label": "Check owner of Punk #0", "params": { "_0": "0" } }, - { "label": "Check owner of Punk #7804", "params": { "_0": "7804" } } + { + "label": "Check owner of Punk #0", + "params": { + "_0": "0" + } + }, + { + "label": "Check owner of Punk #7804", + "params": { + "_0": "7804" + } + } + ], + "fields": [ + { + "path": "_0", + "label": "punk index", + "description": "The punk ID (0–9999)", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } ] }, "balanceOf": { @@ -111,25 +167,65 @@ "description": "Check how many punks an address owns.", "stateMutability": "view", "group": "ownership", - "params": { "_0": { "label": "holder", "type": "address" } } + "params": { + "_0": { + "label": "holder", + "type": "address" + } + }, + "fields": [ + { + "path": "_0", + "label": "holder", + "format": "addressName" + } + ] }, "transferPunk": { "order": 2, "title": "Transfer Punk", "description": "Transfer a punk you own to another address. Does not go through the marketplace.", - "intent": "Transfer Punk #{punkIndex} to {to}", + "intent": "Transfer Punk", "group": "ownership", "warning": "This is a direct transfer with no payment involved. Make sure you intend to send the punk for free.", "params": { - "to": { "label": "recipient", "type": "address" }, + "to": { + "label": "recipient", + "type": "address" + }, "punkIndex": { "label": "punk index", "description": "The punk ID to transfer (0–9999)", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, - "related": ["punkIndexToAddress"] + "related": [ + "punkIndexToAddress" + ], + "interpolatedIntent": "Transfer Punk #{punkIndex} to {to}", + "fields": [ + { + "path": "to", + "label": "recipient", + "format": "addressName" + }, + { + "path": "punkIndex", + "label": "punk index", + "description": "The punk ID to transfer (0–9999)", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "punksOfferedForSale": { "order": 0, @@ -141,16 +237,47 @@ "_0": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, "returns": { - "isForSale": { "label": "for sale", "type": "boolean" }, - "punkIndex": { "label": "punk", "type": "token-id" }, - "seller": { "label": "seller", "type": "address" }, - "minValue": { "label": "min price", "type": "eth" }, - "onlySellTo": { "label": "Only sell to", "type": "address" } - } + "isForSale": { + "label": "for sale", + "type": "boolean" + }, + "punkIndex": { + "label": "punk", + "type": "token-id" + }, + "seller": { + "label": "seller", + "type": "address" + }, + "minValue": { + "label": "min price", + "type": "eth" + }, + "onlySellTo": { + "label": "Only sell to", + "type": "address" + } + }, + "fields": [ + { + "path": "_0", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "punkBids": { "order": 1, @@ -162,43 +289,94 @@ "_0": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, "returns": { - "hasBid": { "label": "has bid", "type": "boolean" }, - "punkIndex": { "label": "punk", "type": "token-id" }, - "bidder": { "label": "bidder", "type": "address" }, - "value": { "label": "bid amount", "type": "eth" } - } + "hasBid": { + "label": "has bid", + "type": "boolean" + }, + "punkIndex": { + "label": "punk", + "type": "token-id" + }, + "bidder": { + "label": "bidder", + "type": "address" + }, + "value": { + "label": "bid amount", + "type": "eth" + } + }, + "fields": [ + { + "path": "_0", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "buyPunk": { "order": 2, "title": "Buy Punk", "description": "Buy a punk that is currently listed for sale. Send ETH equal to or greater than the asking price.", - "intent": "Buy Punk #{punkIndex}", + "intent": "Buy Punk", "group": "marketplace", "warning": "Sends ETH — make sure msg.value meets the asking price.", "params": { "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, - "related": ["punksOfferedForSale", "offerPunkForSale"] + "related": [ + "punksOfferedForSale", + "offerPunkForSale" + ], + "interpolatedIntent": "Buy Punk #{punkIndex}", + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "offerPunkForSale": { "order": 3, "title": "List Punk for Sale", "description": "List a punk you own for sale on the built-in marketplace at a minimum price.", - "intent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} minimum", + "intent": "List Punk for Sale", "group": "marketplace", "params": { "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } }, "minSalePriceInWei": { "label": "minimum price", @@ -206,7 +384,30 @@ "type": "eth" } }, - "related": ["punkNoLongerForSale", "buyPunk"] + "related": [ + "punkNoLongerForSale", + "buyPunk" + ], + "interpolatedIntent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} minimum", + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + }, + { + "path": "minSalePriceInWei", + "label": "minimum price", + "description": "Minimum sale price", + "format": "amount" + } + ] }, "offerPunkForSaleToAddress": { "order": 4, @@ -217,16 +418,49 @@ "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } + }, + "minSalePriceInWei": { + "label": "minimum price", + "type": "eth" }, - "minSalePriceInWei": { "label": "minimum price", "type": "eth" }, "toAddress": { "label": "buyer", "description": "Only this address can buy the punk", "type": "address" } }, - "related": ["offerPunkForSale", "buyPunk"] + "related": [ + "offerPunkForSale", + "buyPunk" + ], + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + }, + { + "path": "minSalePriceInWei", + "label": "minimum price", + "format": "amount" + }, + { + "path": "toAddress", + "label": "buyer", + "description": "Only this address can buy the punk", + "format": "addressName" + } + ] }, "punkNoLongerForSale": { "order": 5, @@ -237,26 +471,65 @@ "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, - "related": ["offerPunkForSale"] + "related": [ + "offerPunkForSale" + ], + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "enterBidForPunk": { "order": 6, "title": "Place Bid", "description": "Place a bid on a punk by sending ETH. Your ETH is held in the contract until the bid is accepted or you withdraw it.", - "intent": "Place bid on Punk #{punkIndex}", + "intent": "Place Bid", "group": "marketplace", "warning": "Your ETH will be locked in the contract until the bid is accepted or withdrawn.", "params": { "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, - "related": ["withdrawBidForPunk", "acceptBidForPunk", "punkBids"] + "related": [ + "withdrawBidForPunk", + "acceptBidForPunk", + "punkBids" + ], + "interpolatedIntent": "Place bid on Punk #{punkIndex}", + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "acceptBidForPunk": { "order": 7, @@ -267,7 +540,10 @@ "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } }, "minPrice": { "label": "minimum price", @@ -275,7 +551,29 @@ "type": "eth" } }, - "related": ["enterBidForPunk", "punkBids"] + "related": [ + "enterBidForPunk", + "punkBids" + ], + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + }, + { + "path": "minPrice", + "label": "minimum price", + "description": "Minimum bid amount you will accept (protects against front-running)", + "format": "amount" + } + ] }, "withdrawBidForPunk": { "order": 8, @@ -286,10 +584,29 @@ "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } }, - "related": ["enterBidForPunk", "punkBids"] + "related": [ + "enterBidForPunk", + "punkBids" + ], + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "pendingWithdrawals": { "order": 9, @@ -297,16 +614,36 @@ "description": "Check the amount of ETH an address can withdraw from sales or returned bids.", "stateMutability": "view", "group": "marketplace", - "params": { "_0": { "label": "address", "type": "address" } }, - "returns": { "_0": { "type": "eth" } }, - "related": ["withdraw"] + "params": { + "_0": { + "label": "address", + "type": "address" + } + }, + "returns": { + "_0": { + "type": "eth" + } + }, + "related": [ + "withdraw" + ], + "fields": [ + { + "path": "_0", + "label": "address", + "format": "addressName" + } + ] }, "withdraw": { "order": 10, "title": "Withdraw ETH", "description": "Withdraw ETH owed to you from punk sales or outbid returns.", "group": "marketplace", - "related": ["pendingWithdrawals"] + "related": [ + "pendingWithdrawals" + ] }, "getPunk": { "order": 0, @@ -318,9 +655,25 @@ "punkIndex": { "label": "punk index", "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "validation": { + "min": "0", + "max": "9999" + } } - } + }, + "fields": [ + { + "path": "punkIndex", + "label": "punk index", + "format": "tokenId", + "input": { + "validation": { + "min": "0", + "max": "9999" + } + } + } + ] }, "allPunksAssigned": { "order": 1, @@ -348,63 +701,118 @@ "PunkTransfer": { "description": "Emitted when a punk is transferred between addresses.", "params": { - "from": { "label": "from" }, - "to": { "label": "to" }, - "punkIndex": { "label": "punk index", "type": "token-id" } + "from": { + "label": "from" + }, + "to": { + "label": "to" + }, + "punkIndex": { + "label": "punk index", + "type": "token-id" + } } }, "PunkOffered": { "description": "Emitted when a punk is listed for sale.", "params": { - "punkIndex": { "label": "punk index", "type": "token-id" }, - "minValue": { "label": "minimum price", "type": "eth" }, - "toAddress": { "label": "exclusive buyer" } + "punkIndex": { + "label": "punk index", + "type": "token-id" + }, + "minValue": { + "label": "minimum price", + "type": "eth" + }, + "toAddress": { + "label": "exclusive buyer" + } } }, "PunkBidEntered": { "description": "Emitted when a bid is placed on a punk.", "params": { - "punkIndex": { "label": "punk index", "type": "token-id" }, - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "bidder" } + "punkIndex": { + "label": "punk index", + "type": "token-id" + }, + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "bidder" + } } }, "PunkBidWithdrawn": { "description": "Emitted when a bid is withdrawn.", "params": { - "punkIndex": { "label": "punk index", "type": "token-id" }, - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "bidder" } + "punkIndex": { + "label": "punk index", + "type": "token-id" + }, + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "bidder" + } } }, "PunkBought": { "description": "Emitted when a punk is sold.", "params": { - "punkIndex": { "label": "punk index", "type": "token-id" }, - "value": { "label": "sale price", "type": "eth" }, - "fromAddress": { "label": "seller" }, - "toAddress": { "label": "buyer" } + "punkIndex": { + "label": "punk index", + "type": "token-id" + }, + "value": { + "label": "sale price", + "type": "eth" + }, + "fromAddress": { + "label": "seller" + }, + "toAddress": { + "label": "buyer" + } } }, "PunkNoLongerForSale": { "description": "Emitted when a punk is delisted from the marketplace.", "params": { - "punkIndex": { "label": "punk index", "type": "token-id" } + "punkIndex": { + "label": "punk index", + "type": "token-id" + } } }, "Assign": { "description": "Emitted during the initial claim phase when a punk was assigned to an address.", "params": { - "to": { "label": "claimer" }, - "punkIndex": { "label": "punk index", "type": "token-id" } + "to": { + "label": "claimer" + }, + "punkIndex": { + "label": "punk index", + "type": "token-id" + } } }, "Transfer": { "description": "Emitted on punk transfer (legacy event, predates ERC-721).", "params": { - "from": { "label": "from" }, - "to": { "label": "to" }, - "value": { "label": "count" } + "from": { + "label": "from" + }, + "to": { + "label": "to" + }, + "value": { + "label": "count" + } } } } diff --git a/contracts/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.json b/contracts/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.json index cdae77f..3792335 100644 --- a/contracts/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.json +++ b/contracts/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.json @@ -2,7 +2,9 @@ "$schema": "https://1001-digital.github.io/contract-metadata/v1/schema.json", "chainId": 1, "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "includes": ["interface:erc20"], + "includes": [ + "interface:erc20" + ], "meta": { "version": 1, "lastUpdated": "2026-03-28T00:00:00Z", @@ -16,28 +18,38 @@ "No Permit (EIP-2612) support — predates gasless approval standard" ], "category": "token", - "tags": ["token", "weth", "defi-primitive"], + "tags": [ + "token", + "weth", + "defi-primitive" + ], "links": [ - { "label": "Etherscan", "url": "https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" } + { + "label": "Etherscan", + "url": "https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } ], "theme": { "accent": "#c33c3c" }, "groups": { - "wrap": { "label": "Wrap / Unwrap", "order": 1 } + "wrap": { + "label": "Wrap / Unwrap", + "order": 1 + } }, "functions": { - "deposit": { + "deposit()": { "title": "Wrap ETH", "description": "Wrap ETH into WETH. Send ETH with this transaction to receive the same amount in WETH.", "intent": "Wrap ETH into WETH", "group": "wrap", "warning": "Sends ETH value — make sure msg.value is set." }, - "withdraw": { + "withdraw(uint256 wad)": { "title": "Unwrap WETH", "description": "Unwrap WETH back to native ETH. Burns WETH and sends you ETH.", - "intent": "Unwrap {wad} WETH back to ETH", + "intent": "Unwrap WETH", "group": "wrap", "params": { "wad": { @@ -46,67 +58,166 @@ "type": "eth" } }, - "related": ["deposit", "balanceOf"] + "related": [ + "deposit", + "balanceOf" + ], + "interpolatedIntent": "Unwrap {wad} WETH back to ETH", + "fields": [ + { + "path": "wad", + "label": "amount", + "description": "Amount of WETH to unwrap (in wei)", + "format": "amount" + } + ] }, - "balanceOf": { + "balanceOf(address account)": { "title": "Balance", "description": "Check the WETH balance of any address.", "params": { - "_0": { "label": "holder", "type": "address" } + "account": { + "label": "holder", + "type": "address" + } + }, + "returns": { + "_0": { + "type": "eth" + } }, - "returns": { "_0": { "type": "eth" } }, "examples": [ { "label": "Check Vitalik's balance", - "params": { "_0": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } + "params": { + "account": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + } + } + ], + "fields": [ + { + "path": "account", + "label": "holder", + "format": "addressName" } ] }, - "totalSupply": { + "totalSupply()": { "title": "Total Supply", "description": "Total amount of WETH in existence (ETH locked in the contract).", - "returns": { "_0": { "type": "eth" } } + "returns": { + "_0": { + "type": "eth" + } + } }, - "allowance": { + "allowance(address owner,address spender)": { "description": "Check how much WETH an address has approved another address to spend.", - "returns": { "_0": { "type": "eth" } } + "returns": { + "_0": { + "type": "eth" + } + } }, - "approve": { + "approve(address spender,uint256 value)": { "description": "Approve a spender to transfer up to the given amount of your WETH.", - "intent": "Approve {guy} to spend {wad} WETH", + "intent": "Approve to spend WETH", "params": { - "guy": { + "spender": { "label": "spender", "description": "Address to approve", "type": "address" }, - "wad": { "label": "amount", "type": "eth" } - } + "value": { + "label": "amount", + "type": "eth" + } + }, + "interpolatedIntent": "Approve {spender} to spend {value} WETH", + "fields": [ + { + "path": "spender", + "label": "spender", + "description": "Address to approve", + "format": "addressName" + }, + { + "path": "value", + "label": "amount", + "format": "amount" + } + ] }, - "transfer": { + "transfer(address to,uint256 value)": { "description": "Transfer WETH to another address.", - "intent": "Transfer {wad} WETH to {dst}", + "intent": "Transfer WETH to", "params": { - "dst": { "label": "recipient", "type": "address" }, - "wad": { "label": "amount", "type": "eth" } - } + "to": { + "label": "recipient", + "type": "address" + }, + "value": { + "label": "amount", + "type": "eth" + } + }, + "interpolatedIntent": "Transfer {value} WETH to {to}", + "fields": [ + { + "path": "to", + "label": "recipient", + "format": "addressName" + }, + { + "path": "value", + "label": "amount", + "format": "amount" + } + ] }, - "transferFrom": { + "transferFrom(address from,address to,uint256 value)": { "description": "Transfer WETH from one address to another, using a previously set allowance.", - "intent": "Transfer {wad} WETH from {src} to {dst}", + "intent": "Transfer WETH from allowance", "params": { - "src": { "label": "from", "type": "address" }, - "dst": { "label": "to", "type": "address" }, - "wad": { "label": "amount", "type": "eth" } - } + "from": { + "label": "from", + "type": "address" + }, + "to": { + "label": "to", + "type": "address" + }, + "value": { + "label": "amount", + "type": "eth" + } + }, + "interpolatedIntent": "Transfer {value} WETH from {from} to {to}", + "fields": [ + { + "path": "from", + "label": "from", + "format": "addressName" + }, + { + "path": "to", + "label": "to", + "format": "addressName" + }, + { + "path": "value", + "label": "amount", + "format": "amount" + } + ] }, - "name": { + "name()": { "description": "The name of the token (Wrapped Ether)." }, - "symbol": { + "symbol()": { "description": "The token symbol (WETH)." }, - "decimals": { + "decimals()": { "description": "Number of decimal places (18, same as ETH)." } }, @@ -114,31 +225,55 @@ "Deposit": { "description": "Emitted when ETH is wrapped into WETH.", "params": { - "dst": { "label": "depositor" }, - "wad": { "label": "amount", "type": "eth" } + "dst": { + "label": "depositor" + }, + "wad": { + "label": "amount", + "type": "eth" + } } }, "Withdrawal": { "description": "Emitted when WETH is unwrapped back to ETH.", "params": { - "src": { "label": "withdrawer" }, - "wad": { "label": "amount", "type": "eth" } + "src": { + "label": "withdrawer" + }, + "wad": { + "label": "amount", + "type": "eth" + } } }, "Approval": { "description": "Emitted when an approval is set.", "params": { - "src": { "label": "owner" }, - "guy": { "label": "spender" }, - "wad": { "label": "amount", "type": "eth" } + "src": { + "label": "owner" + }, + "guy": { + "label": "spender" + }, + "wad": { + "label": "amount", + "type": "eth" + } } }, "Transfer": { "description": "Emitted when WETH is transferred.", "params": { - "src": { "label": "from" }, - "dst": { "label": "to" }, - "wad": { "label": "amount", "type": "eth" } + "src": { + "label": "from" + }, + "dst": { + "label": "to" + }, + "wad": { + "label": "amount", + "type": "eth" + } } } } diff --git a/contracts/0xedcbf19024e928c9d903fcf4785e42ad7c271193.json b/contracts/0xedcbf19024e928c9d903fcf4785e42ad7c271193.json index 0a03cf5..5b7ac33 100644 --- a/contracts/0xedcbf19024e928c9d903fcf4785e42ad7c271193.json +++ b/contracts/0xedcbf19024e928c9d903fcf4785e42ad7c271193.json @@ -2,7 +2,9 @@ "$schema": "https://1001-digital.github.io/contract-metadata/v1/schema.json", "chainId": 1, "address": "0xedcbf19024e928c9d903fcf4785e42ad7c271193", - "includes": ["interface:erc721"], + "includes": [ + "interface:erc721" + ], "meta": { "version": 1, "lastUpdated": "2026-04-03T00:00:00Z", @@ -19,28 +21,49 @@ "Excess ETH is refunded to msg.sender — ensure your wallet can receive ETH refunds" ], "category": "utility", - "tags": ["subscription", "patronage", "nft", "onchain-svg", "membership"], + "tags": [ + "subscription", + "patronage", + "nft", + "onchain-svg", + "membership" + ], "links": [ { "label": "Etherscan", "url": "https://etherscan.io/address/0xeDCBf19024e928c9D903fCf4785e42AD7C271193" }, - { "label": "EVM.NOW", "url": "https://evm.now" }, - { "label": "GitHub", "url": "https://github.com/1001-digital/support" } + { + "label": "EVM.NOW", + "url": "https://evm.now" + }, + { + "label": "GitHub", + "url": "https://github.com/1001-digital/support" + } ], "theme": { "accent": "#484848" }, "groups": { - "subscription": { "label": "Subscription", "order": 1 }, - "info": { "label": "Info", "order": 2 }, - "admin": { "label": "Admin", "order": 3 } + "subscription": { + "label": "Subscription", + "order": 1 + }, + "info": { + "label": "Info", + "order": 2 + }, + "admin": { + "label": "Admin", + "order": 3 + } }, "functions": { "support": { "title": "Support", "description": "Subscribe or extend a subscription at a given tier. Pays in ETH (converted from USD via Chainlink). Third parties can gift or extend subscriptions, but only the recipient or owner can change tiers. Excess ETH is refunded.", - "intent": "Support {recipient} at tier {tier} for {duration} months", + "intent": "Support", "group": "subscription", "featured": true, "warning": "Sends ETH. The Partner tier (3) is blocked for direct purchase. Excess ETH is refunded to your address.", @@ -69,7 +92,40 @@ "type": "duration" } }, - "related": ["estimate", "isActive", "currentTier"] + "related": [ + "estimate", + "isActive", + "currentTier" + ], + "interpolatedIntent": "Support {recipient} at tier {tier} for {duration} months", + "fields": [ + { + "path": "recipient", + "label": "recipient", + "description": "Address to receive the subscription", + "format": "addressName" + }, + { + "path": "tier", + "label": "tier", + "description": "Subscription tier: 0 = Supporter ($10/mo), 1 = Gold ($69/mo), 2 = Platinum ($250/mo), 3 = Partner ($1,000/mo, grant-only)", + "format": "enum", + "params": { + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum", + "3": "Partner (grant-only)" + } + } + }, + { + "path": "duration", + "label": "months", + "description": "Number of months to subscribe (12+ months for 20% discount)", + "format": "duration" + } + ] }, "estimate": { "title": "Estimate Cost", @@ -81,7 +137,11 @@ "label": "tier", "type": { "type": "enum", - "values": { "0": "Supporter", "1": "Gold", "2": "Platinum" } + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum" + } } }, "duration": { @@ -96,7 +156,10 @@ } }, "returns": { - "ethCost": { "label": "ETH cost", "type": "eth" }, + "ethCost": { + "label": "ETH cost", + "type": "eth" + }, "adjustedDuration": { "label": "adjusted duration", "description": "Duration after any hook adjustments" @@ -120,7 +183,36 @@ } } ], - "related": ["support", "tierPrices"] + "related": [ + "support", + "tierPrices" + ], + "fields": [ + { + "path": "tier", + "label": "tier", + "format": "enum", + "params": { + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum" + } + } + }, + { + "path": "duration", + "label": "months", + "description": "Number of months", + "format": "duration" + }, + { + "path": "supporter", + "label": "supporter", + "description": "Address to check (affects cost if upgrading/downgrading)", + "format": "addressName" + } + ] }, "grant": { "title": "Grant Subscription", @@ -154,7 +246,45 @@ "type": "timestamp" } }, - "related": ["support"] + "related": [ + "support" + ], + "fields": [ + { + "path": "recipient", + "label": "recipient", + "description": "Address to receive the subscription", + "format": "addressName" + }, + { + "path": "tier", + "label": "tier", + "format": "enum", + "params": { + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum", + "3": "Partner" + } + } + }, + { + "path": "duration", + "label": "months", + "description": "Number of months to grant", + "format": "raw" + }, + { + "path": "startAt", + "label": "start time", + "description": "Custom start timestamp (0 to start immediately)", + "format": "date", + "params": { + "encoding": "timestamp" + } + } + ] }, "isActive": { "title": "Is Active", @@ -167,7 +297,17 @@ "type": "address" } }, - "related": ["subscription", "currentTier"] + "related": [ + "subscription", + "currentTier" + ], + "fields": [ + { + "path": "supporter", + "label": "supporter", + "format": "addressName" + } + ] }, "subscription": { "title": "Subscription ID", @@ -181,9 +321,23 @@ } }, "returns": { - "_0": { "label": "subscription ID", "type": "token-id" } + "_0": { + "label": "subscription ID", + "type": "token-id" + } }, - "related": ["subscriptions", "isActive", "currentTier"] + "related": [ + "subscriptions", + "isActive", + "currentTier" + ], + "fields": [ + { + "path": "_0", + "label": "supporter", + "format": "addressName" + } + ] }, "subscriptions": { "title": "Subscription Data", @@ -197,11 +351,31 @@ } }, "returns": { - "createdAt": { "label": "created", "type": "timestamp" }, - "startedAt": { "label": "current period start", "type": "timestamp" }, - "expiresAt": { "label": "expires", "type": "timestamp" } + "createdAt": { + "label": "created", + "type": "timestamp" + }, + "startedAt": { + "label": "current period start", + "type": "timestamp" + }, + "expiresAt": { + "label": "expires", + "type": "timestamp" + } }, - "related": ["subscription", "currentTier", "tierPeriods"] + "related": [ + "subscription", + "currentTier", + "tierPeriods" + ], + "fields": [ + { + "path": "_0", + "label": "subscription ID", + "format": "tokenId" + } + ] }, "currentTier": { "title": "Current Tier", @@ -227,9 +401,21 @@ } } }, - "_1": { "label": "active" } + "_1": { + "label": "active" + } }, - "related": ["subscription", "isActive"] + "related": [ + "subscription", + "isActive" + ], + "fields": [ + { + "path": "subscriptionId", + "label": "subscription ID", + "format": "tokenId" + } + ] }, "tierPeriods": { "title": "Tier History", @@ -242,7 +428,17 @@ "type": "token-id" } }, - "related": ["currentTier", "subscriptions"] + "related": [ + "currentTier", + "subscriptions" + ], + "fields": [ + { + "path": "subscriptionId", + "label": "subscription ID", + "format": "tokenId" + } + ] }, "tierPrices": { "title": "Tier Price", @@ -262,7 +458,22 @@ } } } - } + }, + "fields": [ + { + "path": "_0", + "label": "tier", + "format": "enum", + "params": { + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum", + "3": "Partner" + } + } + } + ] }, "totalTiers": { "title": "Total Tiers", @@ -270,21 +481,34 @@ "stateMutability": "view", "group": "info" }, - "totalSupply": { + "totalSupply()": { "title": "Total Subscriptions", "description": "Total number of subscriptions ever created (each represented as an NFT).", "stateMutability": "view", "group": "info" }, - "tokenURI": { + "tokenURI(uint256 tokenId)": { "title": "Token Metadata", "description": "Returns the fully onchain metadata and SVG artwork for a subscription NFT. The card shows the supporter's address, tier badge, dates, and status.", "stateMutability": "view", "group": "info", "params": { - "tokenId": { "label": "subscription ID", "type": "token-id" } + "tokenId": { + "label": "subscription ID", + "type": "token-id" + } }, - "related": ["subscriptions", "currentTier"] + "related": [ + "subscriptions", + "currentTier" + ], + "fields": [ + { + "path": "tokenId", + "label": "subscription ID", + "format": "tokenId" + } + ] }, "hook": { "title": "Hook Contract", @@ -292,7 +516,10 @@ "stateMutability": "view", "group": "info", "returns": { - "_0": { "label": "hook address", "type": "address" } + "_0": { + "label": "hook address", + "type": "address" + } } }, "renderer": { @@ -301,7 +528,10 @@ "stateMutability": "view", "group": "info", "returns": { - "_0": { "label": "renderer address", "type": "address" } + "_0": { + "label": "renderer address", + "type": "address" + } } }, "logo": { @@ -331,7 +561,28 @@ "label": "price (USD, 8 decimals)", "description": "Monthly price in USD with 8 decimal places (e.g. 1000000000 = $10)" } - } + }, + "fields": [ + { + "path": "tier", + "label": "tier", + "format": "enum", + "params": { + "values": { + "0": "Supporter", + "1": "Gold", + "2": "Platinum", + "3": "Partner" + } + } + }, + { + "path": "priceUSD", + "label": "price (USD, 8 decimals)", + "description": "Monthly price in USD with 8 decimal places (e.g. 1000000000 = $10)", + "format": "raw" + } + ] }, "addTier": { "title": "Add Tier", @@ -342,7 +593,15 @@ "label": "price (USD, 8 decimals)", "description": "Monthly price in USD with 8 decimal places" } - } + }, + "fields": [ + { + "path": "priceUSD", + "label": "price (USD, 8 decimals)", + "description": "Monthly price in USD with 8 decimal places", + "format": "raw" + } + ] }, "setHook": { "title": "Set Hook", @@ -354,7 +613,15 @@ "description": "Hook contract address (address(0) to disable)", "type": "address" } - } + }, + "fields": [ + { + "path": "_hook", + "label": "hook address", + "description": "Hook contract address (address(0) to disable)", + "format": "addressName" + } + ] }, "setLogo": { "title": "Set Logo", @@ -370,7 +637,14 @@ "label": "renderer address", "type": "address" } - } + }, + "fields": [ + { + "path": "_renderer", + "label": "renderer address", + "format": "addressName" + } + ] }, "withdraw": { "title": "Withdraw", @@ -382,22 +656,40 @@ "description": "The address that controls admin functions.", "stateMutability": "view", "group": "admin", - "returns": { "_0": { "type": "address" } } + "returns": { + "_0": { + "type": "address" + } + } }, "pendingOwner": { "title": "Pending Owner", "description": "The address nominated to become the new owner (two-step transfer). Must call acceptOwnership() to complete.", "stateMutability": "view", "group": "admin", - "returns": { "_0": { "type": "address" } } + "returns": { + "_0": { + "type": "address" + } + } }, "transferOwnership": { "title": "Transfer Ownership", "description": "Nominate a new owner. The nominee must call acceptOwnership() to complete the transfer. Only callable by the current owner.", "group": "admin", "params": { - "newOwner": { "label": "new owner", "type": "address" } - } + "newOwner": { + "label": "new owner", + "type": "address" + } + }, + "fields": [ + { + "path": "newOwner", + "label": "new owner", + "format": "addressName" + } + ] }, "acceptOwnership": { "title": "Accept Ownership", @@ -409,7 +701,11 @@ "description": "The timestamp when subscriptions become available for purchase.", "stateMutability": "view", "group": "info", - "returns": { "_0": { "type": "timestamp" } } + "returns": { + "_0": { + "type": "timestamp" + } + } }, "setSaleStart": { "title": "Set Sale Start", @@ -420,7 +716,17 @@ "label": "sale start", "type": "timestamp" } - } + }, + "fields": [ + { + "path": "_saleStart", + "label": "sale start", + "format": "date", + "params": { + "encoding": "timestamp" + } + } + ] }, "supportsInterface": { "title": "Supports Interface", @@ -435,7 +741,10 @@ "title": "Subscription Created/Extended", "description": "Emitted when a subscription is created, extended, or tier-changed.", "params": { - "supporter": { "label": "supporter", "type": "address" }, + "supporter": { + "label": "supporter", + "type": "address" + }, "tier": { "label": "tier", "type": { @@ -448,11 +757,25 @@ } } }, - "subscriptionId": { "label": "subscription ID", "type": "token-id" }, - "duration": { "label": "months" }, - "paid": { "label": "ETH paid", "type": "eth" }, - "startedAt": { "label": "started", "type": "timestamp" }, - "expiresAt": { "label": "expires", "type": "timestamp" } + "subscriptionId": { + "label": "subscription ID", + "type": "token-id" + }, + "duration": { + "label": "months" + }, + "paid": { + "label": "ETH paid", + "type": "eth" + }, + "startedAt": { + "label": "started", + "type": "timestamp" + }, + "expiresAt": { + "label": "expires", + "type": "timestamp" + } } }, "TierPriceUpdated": { @@ -470,26 +793,40 @@ } } }, - "priceUSD": { "label": "price (USD, 8 decimals)" } + "priceUSD": { + "label": "price (USD, 8 decimals)" + } } }, "HookUpdated": { "description": "Emitted when the subscription hook contract is changed.", "params": { - "hook": { "label": "hook address", "type": "address" } + "hook": { + "label": "hook address", + "type": "address" + } } }, "Withdrawal": { "description": "Emitted when collected ETH is withdrawn by the owner.", "params": { - "to": { "label": "recipient", "type": "address" }, - "amount": { "label": "amount", "type": "eth" } + "to": { + "label": "recipient", + "type": "address" + }, + "amount": { + "label": "amount", + "type": "eth" + } } }, "MetadataUpdate": { "description": "Emitted when a subscription NFT's metadata changes (ERC-4906). Signals marketplaces to refresh the token.", "params": { - "tokenId": { "label": "subscription ID", "type": "token-id" } + "tokenId": { + "label": "subscription ID", + "type": "token-id" + } } }, "LogoUpdated": { @@ -498,7 +835,10 @@ "RendererUpdated": { "description": "Emitted when the renderer contract is updated.", "params": { - "renderer": { "label": "renderer address", "type": "address" } + "renderer": { + "label": "renderer address", + "type": "address" + } } } }, diff --git a/contracts/0xf3e732194ba87d68fd6ec3d5b1bf813599c8406a.json b/contracts/0xf3e732194ba87d68fd6ec3d5b1bf813599c8406a.json index 7ab2f6a..73f9e5e 100644 --- a/contracts/0xf3e732194ba87d68fd6ec3d5b1bf813599c8406a.json +++ b/contracts/0xf3e732194ba87d68fd6ec3d5b1bf813599c8406a.json @@ -17,19 +17,41 @@ "Royalty enforcement only applies to the built-in marketplace — external transfers via transferOwnership bypass royalties" ], "category": "nft", - "tags": ["art", "generative", "onchain", "1-of-1", "webgl", "interactive"], + "tags": [ + "art", + "generative", + "onchain", + "1-of-1", + "webgl", + "interactive" + ], "links": [ { "label": "Etherscan", "url": "https://etherscan.io/address/0xf3e732194ba87d68fd6ec3d5b1bf813599c8406a" }, - { "label": "Artist", "url": "https://han.io" }, - { "label": "X (Twitter)", "url": "https://x.com/hanrgb" } + { + "label": "Artist", + "url": "https://han.io" + }, + { + "label": "X (Twitter)", + "url": "https://x.com/hanrgb" + } ], "groups": { - "artwork": { "label": "Artwork", "order": 1 }, - "marketplace": { "label": "Marketplace", "order": 2 }, - "admin": { "label": "Admin", "order": 3 } + "artwork": { + "label": "Artwork", + "order": 1 + }, + "marketplace": { + "label": "Marketplace", + "order": 2 + }, + "admin": { + "label": "Admin", + "order": 3 + } }, "functions": { "name": { @@ -74,21 +96,34 @@ "description": "The current owner of the artwork.", "stateMutability": "view", "group": "marketplace", - "returns": { "_0": { "type": "address" } } + "returns": { + "_0": { + "type": "address" + } + } }, "royaltyPercentage": { "title": "Royalty Percentage", "description": "The percentage of each sale paid as a royalty to the royalty recipient.", "stateMutability": "view", "group": "marketplace", - "returns": { "_0": { "label": "royalty %", "type": "percentage" } } + "returns": { + "_0": { + "label": "royalty %", + "type": "percentage" + } + } }, "royaltyRecipient": { "title": "Royalty Recipient", "description": "The address that receives royalty payments from marketplace sales.", "stateMutability": "view", "group": "marketplace", - "returns": { "_0": { "type": "address" } } + "returns": { + "_0": { + "type": "address" + } + } }, "currentOffer": { "title": "Current Offer", @@ -96,15 +131,24 @@ "stateMutability": "view", "group": "marketplace", "returns": { - "active": { "label": "for sale", "type": "boolean" }, - "value": { "label": "price", "type": "eth" }, + "active": { + "label": "for sale", + "type": "boolean" + }, + "value": { + "label": "price", + "type": "eth" + }, "toAddress": { "label": "reserved for", "description": "If set, only this address can buy. Zero address means open to anyone.", "type": "address" } }, - "related": ["buyNow", "listForSale"] + "related": [ + "buyNow", + "listForSale" + ] }, "currentBid": { "title": "Current Bid", @@ -112,11 +156,23 @@ "stateMutability": "view", "group": "marketplace", "returns": { - "active": { "label": "has bid", "type": "boolean" }, - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "bidder", "type": "address" } + "active": { + "label": "has bid", + "type": "boolean" + }, + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "bidder", + "type": "address" + } }, - "related": ["placeBid", "acceptBid"] + "related": [ + "placeBid", + "acceptBid" + ] }, "buyNow": { "title": "Buy Now", @@ -125,7 +181,10 @@ "group": "marketplace", "featured": true, "warning": "Sends ETH. The exact listed price must be sent — no more, no less.", - "related": ["currentOffer", "listForSale"] + "related": [ + "currentOffer", + "listForSale" + ] }, "placeBid": { "title": "Place Bid", @@ -133,56 +192,107 @@ "intent": "Place a bid on Mnemonic", "group": "marketplace", "warning": "Your ETH will be locked in the contract until the owner accepts or you withdraw.", - "related": ["currentBid", "withdrawBid", "acceptBid"] + "related": [ + "currentBid", + "withdrawBid", + "acceptBid" + ] }, "acceptBid": { "title": "Accept Bid", "description": "Accept the current highest bid. The artwork is transferred to the bidder and the bid amount (minus royalty) is credited to the owner's pending withdrawals. Only the owner can call this.", "group": "marketplace", - "related": ["currentBid", "placeBid"] + "related": [ + "currentBid", + "placeBid" + ] }, "withdrawBid": { "title": "Withdraw Bid", "description": "Withdraw your active bid, returning your locked ETH. Only the current bidder can call this.", "group": "marketplace", - "related": ["placeBid", "currentBid"] + "related": [ + "placeBid", + "currentBid" + ] }, "listForSale": { "title": "List for Sale", "description": "List the artwork for sale at a fixed price, open to anyone. Only the owner can call this.", - "intent": "List Mnemonic for sale at {salePriceInWei}", + "intent": "List for Sale", "group": "marketplace", "params": { - "salePriceInWei": { "label": "price", "type": "eth" } + "salePriceInWei": { + "label": "price", + "type": "eth" + } }, - "related": ["listForSaleToAddress", "cancelFromSale", "buyNow"] + "related": [ + "listForSaleToAddress", + "cancelFromSale", + "buyNow" + ], + "interpolatedIntent": "List Mnemonic for sale at {salePriceInWei}", + "fields": [ + { + "path": "salePriceInWei", + "label": "price", + "format": "amount" + } + ] }, "listForSaleToAddress": { "title": "List for Sale (Private)", "description": "List the artwork for sale to a specific address only. Only the owner can call this.", - "intent": "List Mnemonic for sale at {salePriceInWei} to {toAddress}", + "intent": "List for Sale (Private)", "group": "marketplace", "params": { - "salePriceInWei": { "label": "price", "type": "eth" }, + "salePriceInWei": { + "label": "price", + "type": "eth" + }, "toAddress": { "label": "buyer", "description": "Only this address can purchase the artwork", "type": "address" } }, - "related": ["listForSale", "cancelFromSale", "buyNow"] + "related": [ + "listForSale", + "cancelFromSale", + "buyNow" + ], + "interpolatedIntent": "List Mnemonic for sale at {salePriceInWei} to {toAddress}", + "fields": [ + { + "path": "salePriceInWei", + "label": "price", + "format": "amount" + }, + { + "path": "toAddress", + "label": "buyer", + "description": "Only this address can purchase the artwork", + "format": "addressName" + } + ] }, "cancelFromSale": { "title": "Cancel Sale", "description": "Remove the artwork from sale. Only the owner can call this.", "group": "marketplace", - "related": ["listForSale", "currentOffer"] + "related": [ + "listForSale", + "currentOffer" + ] }, "withdraw": { "title": "Withdraw ETH", "description": "Withdraw ETH owed to you from sales or returned bids.", "group": "marketplace", - "related": ["pendingWithdrawals"] + "related": [ + "pendingWithdrawals" + ] }, "pendingWithdrawals": { "title": "Pending Withdrawals", @@ -190,30 +300,65 @@ "stateMutability": "view", "group": "marketplace", "params": { - "_0": { "label": "address", "type": "address" } + "_0": { + "label": "address", + "type": "address" + } }, "returns": { - "_0": { "type": "eth" } + "_0": { + "type": "eth" + } }, - "related": ["withdraw"] + "related": [ + "withdraw" + ], + "fields": [ + { + "path": "_0", + "label": "address", + "format": "addressName" + } + ] }, "transferOwnership": { "title": "Transfer Ownership", "description": "Transfer the artwork to another address without going through the marketplace. Bypasses royalties. If the new owner has an active bid, it is refunded. Any active sale listing is canceled.", - "intent": "Transfer Mnemonic to {newOwner}", + "intent": "Transfer Ownership", "group": "admin", "warning": "This is a direct transfer with no payment involved. Royalties are not collected. Make sure you intend to send the artwork for free.", "params": { - "newOwner": { "label": "new owner", "type": "address" } - } + "newOwner": { + "label": "new owner", + "type": "address" + } + }, + "interpolatedIntent": "Transfer Mnemonic to {newOwner}", + "fields": [ + { + "path": "newOwner", + "label": "new owner", + "format": "addressName" + } + ] }, "setRoyaltyRecipient": { "title": "Set Royalty Recipient", "description": "Change the address that receives royalty payments. Only the current royalty recipient can call this.", "group": "admin", "params": { - "newRoyaltyRecipient": { "label": "new recipient", "type": "address" } - } + "newRoyaltyRecipient": { + "label": "new recipient", + "type": "address" + } + }, + "fields": [ + { + "path": "newRoyaltyRecipient", + "label": "new recipient", + "format": "addressName" + } + ] } }, "events": { @@ -221,61 +366,118 @@ "title": "Artpiece Created", "description": "Emitted when the contract is deployed and the artwork is created.", "params": { - "creator": { "label": "creator", "type": "address" } + "creator": { + "label": "creator", + "type": "address" + } } }, "ArtpieceTransferred": { "title": "Artpiece Transferred", "description": "Emitted when ownership of the artwork changes.", "params": { - "oldOwner": { "label": "from", "type": "address" }, - "newOwner": { "label": "to", "type": "address" } + "oldOwner": { + "label": "from", + "type": "address" + }, + "newOwner": { + "label": "to", + "type": "address" + } } }, "ListedForSale": { "description": "Emitted when the artwork is listed for sale.", "params": { - "value": { "label": "price", "type": "eth" }, - "fromAddress": { "label": "seller", "type": "address" }, - "toAddress": { "label": "reserved for", "type": "address" } + "value": { + "label": "price", + "type": "eth" + }, + "fromAddress": { + "label": "seller", + "type": "address" + }, + "toAddress": { + "label": "reserved for", + "type": "address" + } } }, "SaleCompleted": { "description": "Emitted when the artwork is sold via buyNow.", "params": { - "value": { "label": "sale price", "type": "eth" }, - "fromAddress": { "label": "seller", "type": "address" }, - "toAddress": { "label": "buyer", "type": "address" } + "value": { + "label": "sale price", + "type": "eth" + }, + "fromAddress": { + "label": "seller", + "type": "address" + }, + "toAddress": { + "label": "buyer", + "type": "address" + } } }, "SaleCanceled": { "description": "Emitted when a sale listing is canceled.", "params": { - "value": { "label": "listed price", "type": "eth" }, - "fromAddress": { "label": "seller", "type": "address" }, - "toAddress": { "label": "was reserved for", "type": "address" } + "value": { + "label": "listed price", + "type": "eth" + }, + "fromAddress": { + "label": "seller", + "type": "address" + }, + "toAddress": { + "label": "was reserved for", + "type": "address" + } } }, "BidPlaced": { "description": "Emitted when a bid is placed on the artwork.", "params": { - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "bidder", "type": "address" } + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "bidder", + "type": "address" + } } }, "BidAccepted": { "description": "Emitted when the owner accepts a bid.", "params": { - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "seller", "type": "address" }, - "toAddress": { "label": "bidder", "type": "address" } + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "seller", + "type": "address" + }, + "toAddress": { + "label": "bidder", + "type": "address" + } } }, "BidWithdrawn": { "description": "Emitted when a bidder withdraws their bid.", "params": { - "value": { "label": "bid amount", "type": "eth" }, - "fromAddress": { "label": "bidder", "type": "address" } + "value": { + "label": "bid amount", + "type": "eth" + }, + "fromAddress": { + "label": "bidder", + "type": "address" + } } } }, diff --git a/eip-draft.md b/eip-draft.md index 7ec4d86..87c755d 100644 --- a/eip-draft.md +++ b/eip-draft.md @@ -1,7 +1,7 @@ --- eip: TBD title: Contract Metadata -description: A JSON standard for layering human-readable context on top of smart contract ABIs. +description: A JSON standard for layering human-readable context and clear-signing metadata on top of smart contract ABIs. author: YGG (@yougogirldoteth), Jalil Sebastian Wahdatehagh (@jwahdatehagh) discussions-to: TBD status: Draft @@ -12,7 +12,7 @@ created: 2026-04-01 ## Abstract -This EIP defines a JSON metadata format that enriches smart contracts with human-readable context at every level: contract descriptions, function titles and warnings, semantic type annotations for parameters, input guidance, and event/error enrichment. It layers on top of the ABI and NatSpec without replacing either, giving wallets, explorers, and dApps the information they need to present contract interactions in terms users can understand. +This EIP defines a JSON metadata format that enriches smart contracts with human-readable context at every level: contract descriptions, clear-signing intents, ordered display fields, semantic value formats, input guidance, and event/error enrichment. It layers on top of the ABI and NatSpec without replacing either, giving wallets, explorers, and dApps the information they need to present contract interactions in terms users can understand while remaining forward-compatible with ERC-7730-style clear signing. ## Motivation @@ -20,7 +20,7 @@ Smart contracts expose two layers of machine-readable information: the **ABI** ( When someone encounters a contract in a wallet, explorer, or dApp, they see raw function signatures like `offerPunkForSaleToAddress(uint256, uint256, address)` with no context about what happens when they call it, what the risks are, or what the parameters actually mean in human terms. A `uint256` could represent an ETH amount, a timestamp, a token ID, or a percentage in basis points. The ABI doesn't say which. -NatSpec provides basic descriptions (including user-facing `@notice` text), but it's flat text embedded in source code. It can't express semantic types, input guidance, or contract-level context, and is unavailable for unverified contracts. +NatSpec provides basic descriptions (including user-facing `@notice` text), but it's flat text embedded in source code. It can't express semantic display fields, structured clear-signing intents, input guidance, or contract-level context, and is unavailable for unverified contracts. ## Specification @@ -56,10 +56,14 @@ A metadata file describes a single deployed contract: | `address` | `string` | REQUIRED | The contract address (lowercase, checksummed addresses MUST be accepted) | | `includes` | `array` | OPTIONAL | Interface identifiers to include (e.g. `["interface:erc721"]`) | | `meta` | `object` | OPTIONAL | Document housekeeping (version, lastUpdated, locale, signature) | +| `metadata` | `object` | OPTIONAL | Reusable constants, enums, maps, and shared metadata definitions | +| `display` | `object` | OPTIONAL | Reusable clear-signing field definitions | +| `deployments` | `array` | OPTIONAL | Additional chain/address deployment records for the same contract | +| `factory` | `object` | OPTIONAL | Factory deployment context and instance-discovery metadata | #### Contract-Level Context -The following fields provide context about the contract itself. The fields `name`, `symbol`, `description`, `image`, `banner_image`, `featured_image`, `external_link`, and `collaborators` are compatible with [ERC-7572](./eip-7572.md) -- a contract-metadata document with `name` present is a valid ERC-7572 `contractURI()` response. The `theme` color model is inspired by [ENSIP-18](https://docs.ens.domains/ensip/18). +The following fields provide context about the contract itself. The fields `name`, `symbol`, `description`, `image`, `banner_image`, `featured_image`, `external_link`, and `collaborators` are compatible with [ERC-7572](./eip-7572.md) -- a contract-metadata document with `name` present is a valid ERC-7572 `contractURI()` response. The `theme` color model is inspired by [ENSIP-18](https://docs.ens.domains/ensip/18). The same document MAY also carry reusable metadata and display definitions so that documentation, input guidance, and clear-signing context are authored once. | Field | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------------------------ | @@ -84,10 +88,19 @@ The following fields provide context about the contract itself. The fields `name | Field | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------------------ | | `groups` | `object` | OPTIONAL | Named groups for organizing functions | -| `functions` | `object` | OPTIONAL | Per-function metadata, keyed by name, signature, or 4-byte selector | -| `events` | `object` | OPTIONAL | Per-event metadata, keyed by name, signature, or 32-byte topic hash | -| `errors` | `object` | OPTIONAL | Per-error metadata, keyed by name, signature, or 4-byte selector | -| `messages` | `object` | OPTIONAL | EIP-712 typed message metadata, keyed by primary type name | +| `functions` | `object` | OPTIONAL | Per-function metadata, keyed preferably by canonical named ABI fragment | +| `events` | `object` | OPTIONAL | Per-event metadata, keyed preferably by canonical named ABI fragment | +| `errors` | `object` | OPTIONAL | Per-error metadata, keyed preferably by canonical named ABI fragment | +| `messages` | `object` | OPTIONAL | EIP-712 typed message metadata, keyed preferably by primary type name | + +#### Deployment Context + +The common `chainId` and `address` pair describe a single deployed contract. For contracts that exist across multiple networks or are discovered through a factory, authors MAY include `deployments` and `factory` context alongside the single-deployment fields. + +- `deployments` SHOULD list the relevant chain/address pairs for the same contract identity. +- `factory` SHOULD describe the factory contract, deploy event, or instance-discovery pattern used to materialize clones or child contracts. + +Consumers that understand only `chainId` and `address` MUST be able to ignore the richer deployment context without losing the basic contract identity. ### Contract-Level Example @@ -126,14 +139,14 @@ Functions, events, and errors are keyed by one of three formats: | Format | When to use | Example | | ----------------- | ---------------------------- | --------------------------------------------------- | | `name` | No overloads, verified ABI | `"transfer"` | -| `name(type,type)` | Overloaded functions | `"safeTransferFrom(address,address,uint256,bytes)"` | +| `name(type name,type name)` | Clear-signing metadata and overloads | `"safeTransferFrom(address from,address to,uint256 tokenId,bytes data)"` | | `0xabcdef12` | Unverified contract / no ABI | `"0xa9059cbb"` | -**Bare name** is the default for verified contracts without overloaded functions. When a contract has multiple functions with the same name but different parameter types (overloads), the full Solidity-style signature MUST be used to disambiguate. For unverified contracts where no ABI is available, the 4-byte function selector (the first 4 bytes of `keccak256(signature)`) SHOULD be used. +For forward compatibility with clear signing, the preferred key format is the canonical named ABI fragment, for example `transfer(address to,uint256 value)`. Bare names remain acceptable for simple verified contracts, but they do not carry enough information to express parameter paths or clear-signing fields on their own. When a contract has multiple functions with the same name but different parameter types (overloads), the full Solidity-style signature MUST be used to disambiguate. For unverified contracts where no ABI is available, the 4-byte function selector (the first 4 bytes of `keccak256(signature)`) SHOULD be used. The same formats apply to events and errors. For events, the selector is the full 32-byte topic hash (`0x` + 64 hex chars). For errors, it is the 4-byte selector like functions. -Consumers SHOULD match by name first, then fall back to signature or selector lookup. +Consumers SHOULD match by canonical named ABI fragment first when available, then fall back to bare name, signature, or selector lookup. ### Function Metadata @@ -142,30 +155,44 @@ Each function entry MAY include the following fields: ```json { "functions": { - "offerPunkForSaleToAddress": { + "offerPunkForSaleToAddress(uint256 punkIndex,uint256 minSalePriceInWei,address toAddress)": { "title": "List Punk for Sale (Private)", "description": "List a punk for sale to a specific address only, at a minimum price.", "group": "marketplace", "warning": "This creates a binding offer. The buyer can purchase at any time.", "featured": true, "hidden": false, - "intent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} to {toAddress}", - "related": ["offerPunkForSale", "buyPunk"], - "params": { - "punkIndex": { + "intent": "List Punk for Sale", + "interpolatedIntent": "List Punk #{punkIndex} for sale at {minSalePriceInWei} to {toAddress}", + "fields": [ + { + "path": "punkIndex", "label": "Punk", "description": "The punk ID to list (0-9999)", - "type": "token-id", - "validation": { "min": "0", "max": "9999" } + "format": "nftName", + "params": { + "collection": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" + }, + "input": { + "validation": { "min": "0", "max": "9999" } + } }, - "minSalePriceInWei": { + { + "path": "minSalePriceInWei", "label": "Price", - "type": "eth" + "format": "amount" }, - "toAddress": { + { + "path": "toAddress", "label": "Buyer", "description": "Only this address can buy the punk", - "type": "address" + "format": "addressName" + } + ], + "related": ["offerPunkForSale", "buyPunk"], + "input": { + "punkIndex": { + "validation": { "min": "0", "max": "9999" } } } } @@ -180,58 +207,97 @@ Each function entry MAY include the following fields: - `warning` (string): Cautionary text displayed to the user. - `featured` (boolean): If `true`, highlights this as a primary action. - `hidden` (boolean): If `true`, suppresses the function from the default UI. -- `intent` (string): Human-readable sentence template rendered with formatted parameter values. +- `intent` (string): Short human-readable summary of the action. It SHOULD be stable across renderers and may omit dynamic values. +- `interpolatedIntent` (string): Human-readable sentence template rendered with formatted field values for clear signing. +- `fields` (array): Ordered clear-signing and display metadata entries. +- `input` (object): Input, autofill, and validation guidance for write flows. - `related` (array of strings): Keys of related functions. -- `params` (object): Per-parameter metadata, keyed by ABI parameter name. +- `params` (object): Per-parameter metadata, keyed by ABI parameter name. This MAY remain as an input-oriented compatibility layer, but clear-signing consumers SHOULD rely on `fields`. + +### Clear-Signing Fields + +`fields` is the canonical ordered list of values shown to a user for review or signing. Each field entry MAY be nested and MAY include any of the following members: `path`, `label`, `description`, `format`, `params`, `visible`, `fields`, `$ref`, `value`, `input`, and `preview`. + +- `path` identifies the source value for the field. +- `label` provides the human-readable name shown alongside the value. +- `format` is the display primitive and determines how the value is rendered. +- `params` configures the chosen formatter. +- `visible` controls whether the field is shown unconditionally or only under specific conditions. +- `fields` nests child fields for composite values such as structs, arrays, or grouped display sections. +- `$ref` points to a reusable field definition in `display.definitions` or a reusable metadata definition. +- `value` provides a literal value when the field is a constant rather than a data reference. + +Fields SHOULD preserve the order declared in the document. Consumers MAY use the order to drive both display and clear-signing sentence construction. + +### Path Roots -### Semantic Types +Field paths use ERC-7730-style roots: -The `type` field on a parameter is a semantic annotation that tells consumers what a value _represents_. A `uint256` in the ABI carries no meaning beyond "256-bit unsigned integer." Semantic types bridge that gap -- consumers use them to render appropriate UI for both display (read) and input (write) contexts. +- `#` refers to the decoded structured data being described, such as function arguments or typed message fields. +- `$` refers to the metadata document itself, including reusable `metadata` constants, enums, maps, and display definitions. +- `@` refers to the surrounding execution context, such as transaction or message envelope data like `from`, `to`, `value`, `chainId`, or similar container values. -#### String Types +Consumers SHOULD treat paths as structure-aware references rather than raw string labels. Paths MAY address nested values, array elements, and reusable definitions as long as the consumer can resolve them deterministically. -| Type | Meaning | -| -------------- | ----------------------------------------------------------------- | -| `eth` | Value in wei, represents an ETH amount | -| `gwei` | Value in gwei | -| `timestamp` | Unix timestamp (display: formatted date, input: date picker) | -| `address` | Ethereum address (with ENS resolution) | -| `boolean` | Boolean value | -| `blocknumber` | Block number | -| `duration` | Duration in seconds | -| `bytes32-utf8` | bytes32 encoding a UTF-8 string | -| `token-id` | Token ID / NFT identifier | -| `percentage` | Percentage value (0-100) | -| `basis-points` | Value in basis points (1/100th of a percent) | -| `token-amount` | Token amount (display: formatted balance, input: with max button) | -| `date` | Date value | -| `datetime` | Date and time value | -| `hidden` | Not shown to the user; value is auto-populated (see `autofill`) | +### Display Formats -#### Object Types +The `format` field is the semantic display primitive. It tells consumers how to render a value for clear signing and read-only presentation. A Solidity `uint256` carries no meaning beyond "256-bit unsigned integer"; display formats bridge that gap by saying whether the value is an amount, a timestamp, an address, a token ID, or something else. -Types that need additional configuration MUST use an object form: +#### String Formats + +| Format | Meaning | +| -------------------------- | ------------------------------------------------------------------------- | +| `raw` | Render the primitive value without semantic conversion | +| `amount` | Render a native-currency amount | +| `tokenAmount` | Render an ERC-20 or native token amount using token metadata | +| `nftName` | Render an NFT collection item by collection and token ID | +| `date` | Render a timestamp, block height, or other date-like integer | +| `duration` | Render a duration in human-readable units | +| `unit` | Render a number with a configured unit | +| `enum` | Render a raw value through a label map | +| `chainId` | Render a chain ID as a network name | +| `addressName` | Render an address with trusted name resolution where available | +| `tokenTicker` | Render an address as a token ticker where available | +| `interoperableAddressName` | Render an interoperable address name | +| `calldata` | Decode and render embedded calldata using the target contract and selector | + +Legacy aliases such as `eth`, `timestamp`, `address`, `token-id`, and `token-amount` MAY be accepted by consumers for older documents, but new clear-signing metadata SHOULD use the ERC-7730-aligned format names above. + +#### Format Examples + +Formats that need additional configuration use field-level `params`: ```jsonc -// Address with options -{ "type": "address", "ens": true, "addressBook": true } +// Address with name resolution +{ "path": "toAddress", "format": "addressName" } // Token amount for a specific token -{ "type": "token-amount", "tokenAddress": "0x..." } +{ "path": "amount", "format": "tokenAmount", "params": { "token": "0x..." } } // Token ID for a specific NFT collection -{ "type": "token-id", "tokenAddress": "0x..." } +{ "path": "tokenId", "format": "nftName", "params": { "collection": "0x..." } } -// Enum -- display: show label, input: render as select dropdown -{ "type": "enum", "values": { "0": "Pending", "1": "Active" } } +// Enum -- display through a label map +{ "path": "status", "format": "enum", "params": { "values": { "0": "Pending", "1": "Active" } } } -// Slider -- input: render as range slider -{ "type": "slider", "min": "0", "max": "9999", "step": "1" } +// Date encoded as a unix timestamp +{ "path": "deadline", "format": "date", "params": { "encoding": "timestamp" } } ``` -### Autofill +Common formatter params SHOULD be understood as part of the display format contract: + +- `token` or `tokenPath` identifies the ERC-20 or native asset used for token-aware display. +- `collection` or `collectionPath` identifies the NFT collection used for token ID or NFT display. +- `chainId` or `chainIdPath` binds a formatter to a specific chain or chain-derived context. +- `threshold` and `message` allow special-case rendering such as sentinel amounts, unlimited approvals, or other notable values. +- `encoding` describes how to interpret date-like values, such as timestamp, block height, or calendar date encoding. +- `calldata` formatters MAY use structural params such as selector, target, recipient, spender, amount, or other field paths needed to present call data in a human-readable way. -The `autofill` field specifies a source to pre-populate an input with. It is separate from `type` -- one describes the value, the other controls the default. +### Input, Autofill, and Validation + +Input guidance is separate from display formatting. Consumers MAY use `input`, `autofill`, and `validation` to drive how a value is collected, while `format` controls how that value is rendered. Where older documents use `type`, they are describing the input-side hinting model, not the clear-signing display primitive. + +The `autofill` field specifies a source to pre-populate an input with. #### String Autofill Values @@ -250,13 +316,16 @@ For literal constants: { "type": "constant", "value": "86400" } ``` -A parameter MAY combine `type` and `autofill`: +A value MAY combine display format, autofill, and validation guidance: ```json -"from": { - "label": "from", - "type": "address", - "autofill": "connected-address" +"input": { + "from": { + "autofill": "connected-address", + "validation": { + "pattern": "^0x[0-9a-fA-F]{40}$" + } + } } ``` @@ -278,42 +347,53 @@ Individual functions, events, errors, and messages MAY also have an `order` fiel ### Intent Templates -Functions MAY include an `intent` template -- a human-readable sentence rendered with formatted parameter values: +Functions SHOULD expose a short `intent` plus an `interpolatedIntent` that can be rendered from the canonical `fields` list. The `intent` is the stable human summary, while `interpolatedIntent` is the value-bearing sentence used for clear signing: ```json { "functions": { - "composite": { + "composite(uint256 tokenId,uint256 burnId)": { "title": "Composite", - "intent": "Composite Check #{tokenId} with #{burnId}", - "params": { - "tokenId": { + "intent": "Composite Check", + "interpolatedIntent": "Composite Check #{tokenId} with #{burnId}", + "fields": [ + { + "path": "tokenId", "label": "Keep Token ID", + "format": "nftName", + "params": { + "collection": "0x036721e5a769cc48b3189efbb9cce4471e8a48b1" + }, "preview": { "image": "eip155:1/erc721:0x036721e5a769cc48b3189efbb9cce4471e8a48b1/{tokenId}" } }, - "burnId": { + { + "path": "burnId", "label": "Burn Token ID", + "format": "nftName", + "params": { + "collection": "0x036721e5a769cc48b3189efbb9cce4471e8a48b1" + }, "preview": { "image": "ipfs://Qme/{burnId}" } } - } + ] } } } ``` -After the user fills in parameters, the intent renders as: **"Composite Check #4200 with #8000"**. Placeholders use `{paramName}` syntax. Prefix with `#` to prepend a hash symbol (e.g. `#{tokenId}` renders as `#4200`). Values MUST be formatted using their `type` before insertion. +After the user fills in fields, the interpolated intent renders as the user-facing sentence. Placeholders SHOULD resolve against field paths, and consumers MUST format values using the declared `format` before insertion. Prefixing a placeholder with `#` MAY still be used as a display convention for hash-style identifiers, but the underlying reference model is the field path rather than a bare parameter name. ### Parameter Previews -Parameters MAY include a `preview` object to show a visual preview as the user fills in values. The `image` field specifies a URI template that resolves to an image for the current parameter value: +Fields MAY include a `preview` object to show a visual preview as the user fills in values. The `image` field specifies a URI template that resolves to an image for the current field value: ```json "preview": { "image": "eip155:1/erc721:0x036721e5a769cc48b3189efbb9cce4471e8a48b1/{tokenId}" } ``` -URI templates use `{paramName}` interpolation -- the same syntax as intent templates. Supported URI formats: +URI templates MAY interpolate field values. The same placeholder conventions used for clear-signing sentences apply here. Supported URI formats: | Format | Example | Use case | | ----------- | ------------------------------------------ | ------------------------------------------------ | @@ -324,6 +404,17 @@ URI templates use `{paramName}` interpolation -- the same syntax as intent templ Consumers SHOULD resolve CAIP-19 and CAIP-29 URIs by fetching the token's metadata (e.g. via `tokenURI` or `uri`) and extracting the image. IPFS and HTTPS URIs resolve directly to the image content. +### Reusable Metadata and Display Definitions + +To avoid repetition and support clear-signing reuse, a document MAY include reusable `metadata` and `display` namespaces. + +- `metadata.constants` stores named literal values that can be referenced from fields, messages, or other definitions. +- `metadata.enums` stores reusable label sets for repeated enum-like values. +- `metadata.maps` stores reusable lookup tables or path-based mappings. +- `display.definitions` stores named reusable field definitions that can be referenced with `$ref`. + +These namespaces are document-local unless brought in through includes. They SHOULD be shallow-mergeable in the same way as other top-level sections, so authors can override or extend shared definitions without implicit deep merging. + ### Interface Includes Common interface metadata (ERC-20, ERC-721, etc.) can be defined once and included by contract files: @@ -347,11 +438,15 @@ Includes support two formats: - **`interface:` prefix** -- references a named interface file in the `interfaces/` subdirectory relative to the `$schema` URL (e.g. `"interface:erc721"` resolves to `interfaces/erc721.json` next to the schema file). These files contain `groups`, `functions`, `events`, `errors`, and `messages`. - **URL** -- fetches the metadata file from the given URL. The resolved file can live anywhere and follows the same structure. -Multiple includes merge left-to-right. Contract-specific metadata is then applied on top. +Multiple includes merge left-to-right. Contract-specific metadata is then applied on top, including any reusable `metadata` or `display` definitions. + +Optional capabilities SHOULD be modeled as separate includes rather than added to a base interface. For example, a token that implements EIP-2612 Permit can include both `interface:erc20` and `interface:erc20-permit`, while an ERC-20 token without Permit support includes only `interface:erc20`. This keeps common ERC-20 clear-signing metadata reusable without advertising unsupported EIP-712 signing flows. #### Merge Semantics -The merge is _shallow per top-level key within each section_. When a contract defines a function that also exists in an included interface, the contract's entire function object replaces the interface's. There is no deep merge of `params`, `returns`, or other nested fields. This means if you override a function, you MUST re-declare everything you want to keep (params, returns, types, etc.). +The merge is _shallow per top-level key within each section_. When a contract defines a function that also exists in an included interface, the contract's entire function object replaces the interface's. There is no deep merge of `fields`, `params`, `returns`, or other nested fields. This means if you override a function, you MUST re-declare everything you want to keep (fields, params, returns, formats, and related metadata). + +Because the merge is key-based, overrides MUST use the same key form as the included interface entry. If an included interface uses `transfer(address to,uint256 value)`, then a contract-specific override for that function MUST use `transfer(address to,uint256 value)` rather than `transfer`; otherwise both entries remain after merging. ``` # Merge order for includes: ["interface:erc20", "interface:erc721"] @@ -363,43 +458,77 @@ The merge is _shallow per top-level key within each section_. When a contract de ### EIP-712 Message Metadata -Off-chain signing flows (Permit, Seaport orders, etc.) MAY be described with the `messages` object: +Off-chain signing flows (Permit, Seaport orders, etc.) MAY be described with the `messages` object. Message entries SHOULD be keyed by the canonical EIP-712 primary type, and the authored metadata SHOULD capture the same clear-signing primitives as functions: `title`, `description`, `warning`, `intent`, `interpolatedIntent`, and ordered `fields`. ```json { "messages": { - "Permit": { + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)": { "title": "Token Permit", "description": "Approve a spender to transfer your tokens without a separate approve transaction.", "warning": "This grants token spending permission. Verify the spender address carefully.", - "intent": "Permit {spender} to spend {value} of your tokens until {deadline}", - "fields": { - "owner": { "label": "owner", "type": "address" }, - "spender": { "label": "spender", "type": "address" }, - "value": { "label": "amount", "type": "eth" }, - "nonce": { "label": "nonce" }, - "deadline": { "label": "deadline", "type": "timestamp" } - } + "intent": "Approve token spending", + "interpolatedIntent": "Approve {spender} to spend {value} until {deadline}", + "eip712": { + "primaryType": "Permit", + "domain": { + "name": "Example Token", + "version": "1", + "chainId": 1, + "verifyingContract": "0x0000000000000000000000000000000000000000" + } + }, + "fields": [ + { "path": "owner", "label": "Owner", "format": "addressName" }, + { "path": "spender", "label": "Spender", "format": "addressName" }, + { + "path": "value", + "label": "Amount", + "format": "tokenAmount", + "params": { "tokenPath": "@.domain.verifyingContract" } + }, + { "path": "nonce", "label": "Nonce", "format": "raw" }, + { + "path": "deadline", + "label": "Deadline", + "format": "date", + "params": { "encoding": "timestamp" } + } + ] } } } ``` -Messages are keyed by EIP-712 primary type name and MUST be defined on the contract that verifies them. Each message supports the same enrichment as functions: `title`, `description`, `warning`, `intent`, and `fields` with the same parameter metadata (label, description, type). +Messages are keyed by EIP-712 primary type name or canonical type signature and MUST be defined on the contract that verifies them. Each message SHOULD also carry the domain and binding context needed for verification, including chain, verifying contract, and any message-specific constants or reusable definitions. Message `fields` SHOULD follow the same `path`, `label`, `format`, `params`, `visible`, `fields`, `$ref`, and `value` model used by functions. + +### Native ERC-7730 Forward Compatibility + +This standard is not an ERC-7730 file, but its clear-signing subset intentionally uses the same primitives: + +- canonical named ABI fragments for function display entries +- `intent` and `interpolatedIntent` +- ordered `fields` +- ERC-7730-style path roots `#`, `$`, and `@` +- ERC-7730-aligned `format` names and formatter `params` +- reusable `metadata.constants`, `metadata.enums`, `metadata.maps`, and `display.definitions` +- deployment, factory, and EIP-712 binding context + +Documents that use these primitives conform to the clear-signing profile. A consumer that understands ERC-7730-style clear signing should be able to read the clear-signing profile directly from Contract Metadata. ### Extensions -Publishers MAY use custom extension objects on the root document, functions, events, errors, messages, and parameters. Extension names MUST start with an `_` character followed by a letter. Consumers that do not understand a given extension MUST ignore it. +Publishers MAY use custom extension objects on the root document, `metadata`, `display`, functions, events, errors, messages, fields, and parameters. Extension names MUST start with an `_` character followed by a letter. Consumers that do not understand a given extension MUST ignore it. ```json { "functions": { - "colors": { + "colors(uint256 tokenId)": { "title": "Check Colors", "description": "Get the colors of a given Check.", - "params": { - "tokenId": { "label": "Check", "type": "token-id" } - }, + "fields": [ + { "path": "tokenId", "label": "Check", "format": "nftName" } + ], "_component": { "type": "color-map", "columns": "8" @@ -421,11 +550,11 @@ No standard keys will ever begin with `_`, so the namespace is reserved for exte ### Why not extend NatSpec? -NatSpec is embedded in Solidity source code and targets developers. It cannot express semantic types, input guidance, or contract-level context like categories, risks, and audits. It is also unavailable for unverified contracts. A separate JSON format allows metadata to be authored, versioned, and served independently of the contract source. +NatSpec is embedded in Solidity source code and targets developers. It cannot express semantic display formats, clear-signing fields, input guidance, or contract-level context like categories, risks, and audits. It is also unavailable for unverified contracts. A separate JSON format allows metadata to be authored, versioned, and served independently of the contract source. -### Why semantic types instead of just labels? +### Why display formats instead of just labels? -Labels help humans but not machines. A label "Price" on a `uint256` still doesn't tell a wallet whether to format the value as ETH, display a date picker, or show an NFT preview. Semantic types enable consumers to render appropriate UI automatically. +Labels help humans but not machines. A label "Price" on a `uint256` still doesn't tell a wallet whether to format the value as ETH, display a date, or show an NFT preview. Display formats enable consumers to render appropriate UI automatically, while input guidance remains separate and explicit. ### Why shallow merge for includes? @@ -433,13 +562,13 @@ Deep merging creates ambiguity about which nested fields take precedence and mak ### Why three key formats (name, signature, selector)? -Bare names are the common case and the most readable. Signatures are needed for overloaded functions. Selectors are needed for unverified contracts where no ABI is available. Supporting all three covers the full spectrum of real-world contracts. +Bare names are the common case and the most readable. Canonical named ABI fragments are preferred when clear-signing fields are present because they carry parameter names. Signatures are needed for overloaded functions. Selectors are needed for unverified contracts where no ABI is available. Supporting all three covers the full spectrum of real-world contracts. ## Backwards Compatibility -This EIP introduces a new metadata format and does not modify any existing standards. It is fully complementary to ABIs, NatSpec, ERC-7572, and EIP-7730. +This EIP introduces a new metadata format and does not modify any existing standards. It is fully complementary to ABIs, NatSpec, ERC-7572, and ERC-7730. -Contract-level fields (`name`, `symbol`, `description`, `image`, `banner_image`, `featured_image`, `external_link`, `collaborators`) are placed at the top level to maintain backwards compatibility with [ERC-7572](./eip-7572.md). A contract-metadata document with `name` present is a valid ERC-7572 `contractURI()` response -- existing consumers that understand only ERC-7572 will read the fields they recognize and ignore the rest. +Contract-level fields (`name`, `symbol`, `description`, `image`, `banner_image`, `featured_image`, `external_link`, `collaborators`) are placed at the top level to maintain backwards compatibility with [ERC-7572](./eip-7572.md). A contract-metadata document with `name` present is a valid ERC-7572 `contractURI()` response -- existing consumers that understand only ERC-7572 will read the fields they recognize and ignore the rest. Additional context such as `metadata`, `display`, `deployments`, and `factory` should be additive and MUST NOT interfere with consumers that only understand the older ERC-7572 surface. ## Reference Implementation @@ -465,7 +594,7 @@ A malicious metadata author could assign misleading labels or descriptions to fu ### Intent Template Injection -Intent templates use `{paramName}` interpolation. Consumers MUST sanitize rendered intent strings to prevent injection attacks (e.g. XSS in web-based wallets). Parameter values MUST be treated as untrusted input during rendering. +Intent templates use field-based interpolation. Consumers MUST sanitize rendered intent strings to prevent injection attacks (e.g. XSS in web-based wallets). Field values MUST be treated as untrusted input during rendering. ### Extension Safety diff --git a/schema/contract-metadata.schema.json b/schema/contract-metadata.schema.json index 1e67e51..ee84aa2 100644 --- a/schema/contract-metadata.schema.json +++ b/schema/contract-metadata.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/1001-digital/contract-metadata/schema/contract-metadata.schema.json", "title": "Contract Metadata", - "description": "Human-readable context for smart contracts. Enriches ABI data with titles, descriptions, semantic type annotations, and presentation hints.", + "description": "Human-readable context for smart contracts. Enriches ABI data with titles, descriptions, semantic type annotations, and ERC-7730-compatible clear-signing metadata.", "type": "object", "required": ["$schema", "chainId", "address"], "additionalProperties": false, @@ -27,6 +27,17 @@ "items": { "type": "string" }, "description": "Interface identifiers to include (e.g. \"interface:erc20\", \"interface:erc721\"). Included metadata is merged with contract-specific metadata taking priority." }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/$defs/Deployment" + }, + "description": "Known deployments for this contract. Use when the same metadata applies across multiple chain/address pairs." + }, + "factory": { + "$ref": "#/$defs/Factory", + "description": "Factory deployment context for clone or child contracts." + }, "meta": { "$ref": "#/$defs/DocumentMeta", "description": "Document-level housekeeping: revision tracking, locale, and authenticity." @@ -103,6 +114,18 @@ }, "description": "Security audit references." }, + "metadata": { + "$ref": "#/$defs/Metadata", + "description": "Grouped descriptive metadata and ERC-7730-style contract metadata." + }, + "display": { + "$ref": "#/$defs/Display", + "description": "Reusable clear-signing display metadata, including format definitions and named field templates." + }, + "eip712": { + "$ref": "#/$defs/EIP712Context", + "description": "Default EIP-712 typed-data context for signing flows." + }, "theme": { "$ref": "#/$defs/Theme", "description": "Visual theme for UI rendering. Color model inspired by ENSIP-18." @@ -116,7 +139,7 @@ }, "functions": { "type": "object", - "description": "Per-function metadata. Keys can be a bare ABI name (e.g. \"transfer\"), a full Solidity signature for overloaded functions (e.g. \"safeTransferFrom(address,address,uint256,bytes)\"), or a 4-byte selector for unverified contracts (e.g. \"0xa9059cbb\").", + "description": "Per-function metadata. Keys SHOULD be canonical named ABI fragments (e.g. \"transfer(address to,uint256 value)\") for ERC-7730 compatibility. Bare ABI names and 4-byte selectors remain valid for backwards compatibility.", "additionalProperties": { "$ref": "#/$defs/FunctionMeta" } @@ -137,7 +160,7 @@ }, "messages": { "type": "object", - "description": "EIP-712 typed message metadata, keyed by primary type name (e.g. \"Permit\", \"Order\"). Describes off-chain signing flows verified by this contract.", + "description": "EIP-712 typed message metadata, keyed by primary type name, canonical type signature, or another stable message identifier. Supports ERC-7730-style signing display metadata.", "additionalProperties": { "$ref": "#/$defs/MessageMeta" } @@ -174,6 +197,337 @@ } } }, + "Deployment": { + "type": "object", + "description": "A deployed contract instance identified by chain ID and address.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "required": ["chainId", "address"], + "properties": { + "chainId": { + "type": "integer", + "minimum": 1, + "description": "Chain ID of the deployment." + }, + "address": { + "type": "string", + "pattern": "^0x[0-9a-f]{40}$", + "description": "Lowercase contract address." + }, + "label": { + "type": "string", + "description": "Optional human-readable label for the deployment." + } + } + }, + "Factory": { + "type": "object", + "description": "Factory deployment context used to discover contract instances.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "address": { + "type": "string", + "pattern": "^0x[0-9a-f]{40}$", + "description": "Factory contract address on the top-level chain." + }, + "deployEvent": { + "type": "string", + "description": "Canonical event fragment emitted when a new instance is deployed." + }, + "instancePath": { + "type": "string", + "description": "Path within the deploy event that contains the deployed instance address." + }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/$defs/Deployment" + }, + "description": "Factory deployments across chains." + } + } + }, + "Metadata": { + "type": "object", + "description": "Grouped descriptive metadata and ERC-7730-style metadata containers.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "name": { + "type": "string", + "description": "Human-readable contract name." + }, + "contractName": { + "type": "string", + "description": "ERC-7730-style contract name." + }, + "contractVersion": { + "type": "string", + "description": "Version string for the contract or signing context." + }, + "symbol": { + "type": "string", + "description": "Contract or token symbol." + }, + "description": { + "type": "string", + "description": "Short human-readable description." + }, + "image": { + "type": "string", + "format": "uri", + "description": "Contract image or logo URI." + }, + "banner_image": { + "type": "string", + "format": "uri", + "description": "Banner image URI." + }, + "featured_image": { + "type": "string", + "format": "uri", + "description": "Featured image URI." + }, + "external_link": { + "type": "string", + "format": "uri", + "description": "Primary external URL for the project." + }, + "collaborators": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{40}$" + }, + "description": "Ethereum addresses of authorized metadata editors." + }, + "about": { + "type": "string", + "description": "Long-form context and explanations in Markdown format." + }, + "category": { + "type": "string", + "description": "Primary category." + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Free-form tags for search and categorization." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "External links." + }, + "risks": { + "type": "array", + "items": { "type": "string" }, + "description": "Known risks or caveats users should be aware of." + }, + "audits": { + "type": "array", + "items": { + "$ref": "#/$defs/AuditReference" + }, + "description": "Security audit references." + }, + "theme": { + "$ref": "#/$defs/Theme", + "description": "Visual theme for UI rendering." + }, + "info": { + "type": "object", + "description": "Free-form ERC-7730-style contract information container.", + "additionalProperties": true + }, + "token": { + "type": "object", + "description": "Free-form token metadata container for ERC-7730-style signing context.", + "additionalProperties": true + }, + "constants": { + "type": "object", + "description": "Reusable named constants.", + "additionalProperties": true + }, + "enums": { + "type": "object", + "description": "Reusable named enum definitions.", + "additionalProperties": true + }, + "maps": { + "type": "object", + "description": "Reusable named map definitions.", + "additionalProperties": true + } + } + }, + "Display": { + "type": "object", + "description": "Reusable ERC-7730-style display metadata for signing and review.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "definitions": { + "type": "object", + "description": "Reusable named field definitions.", + "additionalProperties": { + "$ref": "#/$defs/FieldMeta" + } + }, + "formats": { + "type": "object", + "description": "Named display formats keyed by ABI fragment, message signature, or other stable identifier.", + "additionalProperties": { + "$ref": "#/$defs/DisplayFormatMeta" + } + }, + "constants": { + "type": "object", + "description": "Reusable named constants.", + "additionalProperties": true + }, + "enums": { + "type": "object", + "description": "Reusable named enum definitions.", + "additionalProperties": true + }, + "maps": { + "type": "object", + "description": "Reusable named map definitions.", + "additionalProperties": true + } + } + }, + "EIP712Context": { + "type": "object", + "description": "EIP-712 typed-data context for a contract or message flow.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "name": { + "type": "string", + "description": "EIP-712 domain name." + }, + "version": { + "type": "string", + "description": "EIP-712 domain version." + }, + "chainId": { + "type": "integer", + "minimum": 1, + "description": "Chain ID for the typed-data context." + }, + "verifyingContract": { + "type": "string", + "pattern": "^0x[0-9a-f]{40}$", + "description": "Verifying contract address." + }, + "salt": { + "type": "string", + "description": "Optional EIP-712 domain salt." + }, + "primaryType": { + "type": "string", + "description": "Primary EIP-712 type name." + }, + "signature": { + "type": "string", + "description": "Canonical human-readable signature for the typed data." + }, + "typeEncoding": { + "type": "string", + "description": "Canonical EIP-712 type encoding." + }, + "domain": { + "$ref": "#/$defs/EIP712Domain", + "description": "Structured EIP-712 domain object." + }, + "types": { + "type": "object", + "description": "EIP-712 type definitions keyed by type name.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/EIP712TypeField" + } + } + }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/$defs/Deployment" + }, + "description": "Deployments for the verifying contract or typed-data context." + } + } + }, + "EIP712Domain": { + "type": "object", + "description": "Structured EIP-712 domain values.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "chainId": { + "type": "integer", + "minimum": 1 + }, + "verifyingContract": { + "type": "string", + "pattern": "^0x[0-9a-f]{40}$" + }, + "salt": { + "type": "string" + } + } + }, + "EIP712TypeField": { + "type": "object", + "description": "Single EIP-712 typed-data field definition.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": ["name", "type"] + }, "Theme": { "type": "object", "description": "Visual theme colors for UI rendering. Inspired by ENSIP-18. Accent and text colors should maintain a 4.5:1 contrast ratio against the background.", @@ -267,6 +621,100 @@ } } }, + "DisplayFormat": { + "type": "string", + "enum": [ + "raw", + "amount", + "tokenAmount", + "nftName", + "date", + "datetime", + "duration", + "unit", + "enum", + "chainId", + "chain-id", + "addressName", + "tokenTicker", + "interoperableAddressName", + "calldata", + "eth", + "gwei", + "boolean", + "blockNumber", + "blocknumber", + "tokenId", + "token-id", + "percentage", + "basisPoints", + "basis-points", + "bytes32Utf8", + "bytes32-utf8", + "hidden" + ], + "description": "ERC-7730-aligned display format for a field or value." + }, + "DisplayFormatMeta": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "signature": { + "type": "string", + "description": "Canonical ABI fragment or message signature this display format describes." + }, + "title": { + "type": "string", + "description": "Human-readable short label." + }, + "description": { + "type": "string", + "description": "Human-readable explanation of the signing display." + }, + "warning": { + "type": "string", + "description": "Risk warning displayed to users." + }, + "intent": { + "type": "string", + "description": "Short intent string for the display format." + }, + "interpolatedIntent": { + "type": "string", + "description": "Template string rendered with formatted field values." + }, + "fields": { + "type": "array", + "description": "Ordered list of display fields.", + "items": { + "$ref": "#/$defs/FieldMeta" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths that must be present." + }, + "excluded": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths that must be omitted." + }, + "eip712": { + "$ref": "#/$defs/EIP712Context", + "description": "EIP-712 context for the display format." + } + } + }, "FunctionMeta": { "type": "object", "additionalProperties": false, @@ -289,9 +737,17 @@ "type": "string", "description": "What the function does, explained for end users." }, + "signature": { + "type": "string", + "description": "Canonical named ABI fragment for the function, if the object key is not already that signature." + }, "intent": { "type": "string", - "description": "Template string rendered with formatted parameter values. Use {paramName} placeholders (e.g. \"Transfer {wad} to {dst}\"). Prefix with # to prepend a hash symbol (e.g. \"Buy Punk #{punkIndex}\" renders as \"Buy Punk #7804\"). Values are formatted using their type before insertion." + "description": "Short clear-signing summary of the action. Dynamic values SHOULD be placed in interpolatedIntent instead." + }, + "interpolatedIntent": { + "type": "string", + "description": "ERC-7730-style template string rendered with formatted field values." }, "group": { "type": "string", @@ -321,6 +777,34 @@ "$ref": "#/$defs/ParamMeta" } }, + "input": { + "type": "object", + "description": "Input metadata keyed by parameter name. This mirrors params and is retained for forward-compatible authoring.", + "additionalProperties": { + "$ref": "#/$defs/InputMeta" + } + }, + "fields": { + "type": "array", + "description": "Ordered clear-signing display fields. This is the preferred forward-compatible surface.", + "items": { + "$ref": "#/$defs/FieldMeta" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths or parameter names that must be present." + }, + "excluded": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths or parameter names that must be omitted." + }, "returns": { "type": "object", "description": "Return value metadata, keyed by ABI return name or _0, _1, etc.", @@ -369,20 +853,60 @@ "type": "string", "description": "What signing this message does, explained for end users." }, + "signature": { + "type": "string", + "description": "Canonical typed-message signature or primary type signature." + }, + "typeEncoding": { + "type": "string", + "description": "Canonical EIP-712 type encoding for the message, if known." + }, "intent": { "type": "string", "description": "Template string rendered with formatted field values. Use {fieldName} placeholders (e.g. \"Permit {spender} to spend {value}\")." }, + "interpolatedIntent": { + "type": "string", + "description": "ERC-7730-style template string rendered with formatted field values." + }, "warning": { "type": "string", "description": "Risk warning displayed to users before signing." }, "fields": { - "type": "object", - "description": "Field metadata, keyed by EIP-712 field name.", - "additionalProperties": { - "$ref": "#/$defs/ParamMeta" - } + "description": "Field metadata. Array form is preferred for ERC-7730-compatible ordered display; object form is retained for backwards compatibility.", + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/FieldMeta" + } + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ParamMeta" + } + } + ] + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths or names that must be present." + }, + "excluded": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field paths or names that must be omitted." + }, + "eip712": { + "$ref": "#/$defs/EIP712Context", + "description": "EIP-712 context for the message." } } }, @@ -481,11 +1005,333 @@ } } }, + "InputMeta": { + "$ref": "#/$defs/ParamMeta", + "description": "Alias for ParamMeta used where the schema wants to name input metadata explicitly." + }, + "FieldParams": { + "type": "object", + "description": "Formatter-specific parameters for a field.", + "additionalProperties": true, + "properties": { + "token": { + "type": "string", + "description": "Literal token contract address or metadata reference." + }, + "tokenAddress": { + "type": "string", + "pattern": "^0x[0-9a-f]{40}$" + }, + "tokenPath": { + "type": "string", + "description": "Path to the token context or token metadata source." + }, + "tokenTicker": { + "type": "string", + "description": "Ticker to use when rendering the field." + }, + "nativeCurrencyAddress": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Address or addresses that should be treated as the native currency sentinel." + }, + "collection": { + "type": "string", + "description": "Literal NFT collection address or metadata reference." + }, + "collectionPath": { + "type": "string", + "description": "Path to the NFT collection address." + }, + "chainId": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string" + } + ], + "description": "Chain ID used for resolving token or collection metadata." + }, + "chainIdPath": { + "type": "string", + "description": "Path to the chain ID used for resolving token or collection metadata." + }, + "decimals": { + "type": "integer", + "minimum": 0 + }, + "decimalsPath": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "symbolPath": { + "type": "string" + }, + "namePath": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "threshold": { + "type": "string" + }, + "message": { + "type": "string" + }, + "encoding": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true, + "description": "Inline enum or lookup values." + }, + "calleePath": { + "type": "string", + "description": "Path to the contract address used to decode embedded calldata." + }, + "selector": { + "type": "string", + "description": "Literal function selector for embedded calldata." + }, + "selectorPath": { + "type": "string", + "description": "Path to the function selector for embedded calldata." + }, + "calldataPath": { + "type": "string", + "description": "Path to embedded calldata." + }, + "amountPath": { + "type": "string", + "description": "Path to transaction value or amount context for embedded calldata." + }, + "spenderPath": { + "type": "string", + "description": "Path to spender context for embedded calldata." + }, + "recipientPath": { + "type": "string", + "description": "Path to recipient context for embedded calldata." + } + } + }, + "VisibleRule": { + "oneOf": [ + { + "type": "string", + "enum": ["always", "never", "auto", "optional"] + }, + { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "path": { + "type": "string", + "description": "Path to inspect when deciding whether the field is visible." + }, + "exists": { + "type": "boolean" + }, + "equals": true, + "notEquals": true, + "in": { + "type": "array", + "items": true + }, + "notIn": { + "type": "array", + "items": true + }, + "ifIn": { + "type": "array", + "items": true + }, + "ifNotIn": { + "type": "array", + "items": true + }, + "ifEquals": true, + "ifNotEquals": true, + "mustMatch": true, + "ifNotMatch": true + } + } + ], + "description": "Visibility rule for a field." + }, + "FieldPreview": { + "type": "object", + "description": "Visual preview shown as the user fills or reviews a field value.", + "additionalProperties": false, + "properties": { + "image": { + "type": "string", + "description": "URI template that resolves to an image for the current field value." + } + } + }, + "Encryption": { + "type": "object", + "description": "Optional encryption metadata for a field value.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "scheme": { + "type": "string", + "description": "Encryption scheme identifier." + }, + "plaintextType": { + "type": "string", + "description": "Expected plaintext type after decryption." + }, + "fallbackLabel": { + "type": "string", + "description": "Label to display when the value cannot be decrypted." + }, + "keyId": { + "type": "string", + "description": "Identifier of the key used for encryption." + }, + "recipient": { + "type": "string", + "description": "Recipient or public key reference." + }, + "params": { + "type": "object", + "description": "Scheme-specific encryption parameters.", + "additionalProperties": true + } + } + }, + "FieldMeta": { + "type": "object", + "description": "A display field or reusable field reference. Supports ERC-7730-style paths, literal values, and reusable references.", + "additionalProperties": false, + "patternProperties": { + "^_[a-zA-Z]": { + "description": "Extension object. Names must start with _ followed by a letter. Consumers that do not understand a given extension must ignore it." + } + }, + "properties": { + "path": { + "type": "string", + "description": "Path to the value within the current context." + }, + "$ref": { + "type": "string", + "description": "Reference to a reusable field definition." + }, + "value": true, + "label": { + "type": "string", + "description": "Human-readable label for the field." + }, + "description": { + "type": "string", + "description": "Optional human-readable explanation of the field." + }, + "format": { + "$ref": "#/$defs/DisplayFormat", + "description": "Display format used to render the field." + }, + "params": { + "$ref": "#/$defs/FieldParams", + "description": "Formatter-specific parameters." + }, + "visible": { + "$ref": "#/$defs/VisibleRule", + "description": "Visibility rule for the field." + }, + "fields": { + "type": "array", + "description": "Nested child fields for grouped or structured values.", + "items": { + "$ref": "#/$defs/FieldMeta" + } + }, + "input": { + "$ref": "#/$defs/InputMeta", + "description": "Input metadata for the same value." + }, + "preview": { + "$ref": "#/$defs/FieldPreview", + "description": "Visual preview metadata for the field." + }, + "encryption": { + "$ref": "#/$defs/Encryption", + "description": "Optional encryption metadata for the field value." + } + } + }, "Type": { "oneOf": [ { "type": "string", - "enum": ["eth", "gwei", "timestamp", "address", "boolean", "blocknumber", "duration", "bytes32-utf8", "token-id", "percentage", "basis-points", "token-amount", "date", "datetime", "hidden"], + "enum": [ + "eth", + "gwei", + "timestamp", + "address", + "boolean", + "blocknumber", + "blockNumber", + "duration", + "bytes32-utf8", + "bytes32Utf8", + "token-id", + "tokenId", + "percentage", + "basis-points", + "basisPoints", + "token-amount", + "tokenAmount", + "date", + "datetime", + "raw", + "amount", + "unit", + "enum", + "chain-id", + "chainId", + "addressName", + "tokenTicker", + "interoperableAddressName", + "calldata", + "nftName", + "hidden" + ], "description": "Semantic type. Tells consumers what a value represents and how to render it in both read and write contexts." }, { @@ -509,7 +1355,7 @@ }, { "type": "object", - "required": ["type", "tokenAddress"], + "required": ["type"], "additionalProperties": false, "properties": { "type": { "const": "token-amount" }, @@ -517,13 +1363,25 @@ "type": "string", "pattern": "^0x[0-9a-f]{40}$", "description": "The token contract address (lowercase)." + }, + "tokenPath": { + "type": "string", + "description": "Path to the token context or token metadata source." } }, + "oneOf": [ + { + "required": ["tokenAddress"] + }, + { + "required": ["tokenPath"] + } + ], "description": "Token amount for a specific ERC-20, ERC-721, or ERC-1155. Display: format with decimals and symbol. Input: show balance and max button." }, { "type": "object", - "required": ["type", "tokenAddress"], + "required": ["type"], "additionalProperties": false, "properties": { "type": { "const": "token-id" }, @@ -531,8 +1389,20 @@ "type": "string", "pattern": "^0x[0-9a-f]{40}$", "description": "The token contract address (lowercase)." + }, + "tokenPath": { + "type": "string", + "description": "Path to the token context or token metadata source." } }, + "oneOf": [ + { + "required": ["tokenAddress"] + }, + { + "required": ["tokenPath"] + } + ], "description": "Token ID for a specific ERC-721 or ERC-1155." }, { diff --git a/schema/interface.schema.json b/schema/interface.schema.json index f577080..1ff46ca 100644 --- a/schema/interface.schema.json +++ b/schema/interface.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/1001-digital/contract-metadata/schema/interface.schema.json", "title": "Contract Metadata Interface", - "description": "Reusable metadata for standard interfaces (ERC-20, ERC-721, etc.). Provides groups, functions, events, and messages that can be included by contract metadata files via the includes field.", + "description": "Reusable metadata for standard interfaces (ERC-20, ERC-721, etc.). Provides groups, functions, events, messages, and forward-compatible ERC-7730-style metadata that can be included by contract metadata files via the includes field.", "type": "object", "additionalProperties": false, "properties": { @@ -15,7 +15,7 @@ }, "functions": { "type": "object", - "description": "Per-function metadata. Keys can be a bare ABI name, a full Solidity signature for overloads, or a 4-byte selector for unverified contracts.", + "description": "Per-function metadata. Keys SHOULD be canonical named ABI fragments for ERC-7730 compatibility. Contract-specific overrides of included interface entries must use the same key form to replace them under shallow merge semantics.", "additionalProperties": { "$ref": "contract-metadata.schema.json#/$defs/FunctionMeta" } @@ -36,10 +36,29 @@ }, "messages": { "type": "object", - "description": "EIP-712 typed message metadata, keyed by primary type name.", + "description": "EIP-712 typed message metadata, keyed by primary type name, canonical type signature, or another stable message identifier.", "additionalProperties": { "$ref": "contract-metadata.schema.json#/$defs/MessageMeta" } + }, + "deployments": { + "type": "array", + "description": "Known deployments that the interface metadata applies to.", + "items": { + "$ref": "contract-metadata.schema.json#/$defs/Deployment" + } + }, + "metadata": { + "$ref": "contract-metadata.schema.json#/$defs/Metadata", + "description": "Grouped descriptive metadata for the interface." + }, + "display": { + "$ref": "contract-metadata.schema.json#/$defs/Display", + "description": "Reusable clear-signing display metadata for the interface." + }, + "eip712": { + "$ref": "contract-metadata.schema.json#/$defs/EIP712Context", + "description": "Default EIP-712 typed-data context for the interface." } } } diff --git a/schema/interfaces/erc20-permit.json b/schema/interfaces/erc20-permit.json new file mode 100644 index 0000000..1da7788 --- /dev/null +++ b/schema/interfaces/erc20-permit.json @@ -0,0 +1,54 @@ +{ + "messages": { + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)": { + "title": "Token Permit (EIP-2612)", + "description": "Approve a spender to transfer your tokens via an off-chain signature, avoiding a separate approve transaction.", + "warning": "This grants token spending permission. Verify the spender address and amount carefully. Phishing sites commonly request Permit signatures to drain wallets.", + "intent": "Approve token spending", + "interpolatedIntent": "Approve {spender} to spend {value} until {deadline}", + "eip712": { + "primaryType": "Permit" + }, + "fields": [ + { + "path": "owner", + "label": "Owner", + "description": "The token holder granting the approval", + "format": "addressName" + }, + { + "path": "spender", + "label": "Spender", + "description": "The address being approved to spend tokens", + "format": "addressName" + }, + { + "path": "value", + "label": "Amount", + "description": "Maximum amount the spender can transfer", + "format": "tokenAmount", + "params": { + "tokenPath": "@.domain.verifyingContract", + "threshold": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "message": "Unlimited" + } + }, + { + "path": "nonce", + "label": "Nonce", + "description": "Sequential nonce preventing signature replay", + "format": "raw" + }, + { + "path": "deadline", + "label": "Deadline", + "description": "Unix timestamp after which the permit expires", + "format": "date", + "params": { + "encoding": "timestamp" + } + } + ] + } + } +} diff --git a/schema/interfaces/erc20.json b/schema/interfaces/erc20.json index 5925ca9..d75b6e0 100644 --- a/schema/interfaces/erc20.json +++ b/schema/interfaces/erc20.json @@ -1,116 +1,204 @@ { + "metadata": { + "constants": { + "maxUint256": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + }, + "display": { + "definitions": { + "erc20Amount": { + "label": "Amount", + "format": "tokenAmount", + "params": { + "tokenPath": "@.to" + } + } + } + }, "groups": { - "erc20": { "label": "ERC-20", "order": 99 } + "erc20": { + "label": "ERC-20", + "order": 99 + } }, "functions": { - "name": { + "name()": { "title": "Token Name", + "signature": "name()", "description": "The name of the token.", "stateMutability": "view", "group": "erc20" }, - "symbol": { + "symbol()": { "title": "Token Symbol", + "signature": "symbol()", "description": "The token symbol.", "stateMutability": "view", "group": "erc20" }, - "decimals": { + "decimals()": { "title": "Decimals", + "signature": "decimals()", "description": "Number of decimal places used by the token.", "stateMutability": "view", "group": "erc20" }, - "totalSupply": { + "totalSupply()": { "title": "Total Supply", + "signature": "totalSupply()", "description": "Total amount of tokens in existence.", "stateMutability": "view", "group": "erc20" }, - "balanceOf": { + "balanceOf(address account)": { "title": "Balance", + "signature": "balanceOf(address account)", "description": "Check the token balance of an address.", "stateMutability": "view", "group": "erc20", - "params": { - "_0": { "label": "holder", "type": "address" } - } + "fields": [ + { + "path": "account", + "label": "Holder", + "format": "addressName" + } + ] }, - "allowance": { + "allowance(address owner,address spender)": { "title": "Allowance", + "signature": "allowance(address owner,address spender)", "description": "Check how many tokens an address has approved another address to spend.", "stateMutability": "view", "group": "erc20", - "params": { - "_0": { "label": "owner", "type": "address" }, - "_1": { "label": "spender", "type": "address" } - } + "fields": [ + { + "path": "owner", + "label": "Owner", + "format": "addressName" + }, + { + "path": "spender", + "label": "Spender", + "format": "addressName" + } + ] }, - "approve": { + "approve(address spender,uint256 value)": { "title": "Approve", + "signature": "approve(address spender,uint256 value)", "description": "Approve a spender to transfer up to the given amount of your tokens.", "group": "erc20", - "intent": "Approve {_0} to spend {_1} tokens", + "intent": "Approve", + "interpolatedIntent": "Approve {spender} to spend {value}", "warning": "Approving unlimited amounts is common but carries risk if the spender contract is compromised.", - "params": { - "_0": { "label": "spender", "type": "address" }, - "_1": { "label": "amount" } - }, - "related": ["allowance", "transferFrom"] + "fields": [ + { + "path": "spender", + "label": "Spender", + "format": "addressName" + }, + { + "path": "value", + "label": "Amount", + "format": "tokenAmount", + "params": { + "tokenPath": "@.to", + "threshold": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "message": "Unlimited" + } + } + ], + "related": [ + "allowance", + "transferFrom" + ] }, - "transfer": { + "transfer(address to,uint256 value)": { "title": "Transfer", + "signature": "transfer(address to,uint256 value)", "description": "Transfer tokens to another address.", "group": "erc20", - "intent": "Transfer {_1} tokens to {_0}", - "params": { - "_0": { "label": "recipient", "type": "address" }, - "_1": { "label": "amount" } - }, - "related": ["balanceOf"] + "intent": "Send", + "interpolatedIntent": "Send {value} to {to}", + "fields": [ + { + "path": "value", + "label": "Amount", + "format": "tokenAmount", + "params": { + "tokenPath": "@.to" + } + }, + { + "path": "to", + "label": "To", + "format": "addressName" + } + ], + "related": [ + "balanceOf" + ] }, - "transferFrom": { + "transferFrom(address from,address to,uint256 value)": { "title": "Transfer From", + "signature": "transferFrom(address from,address to,uint256 value)", "description": "Transfer tokens from one address to another, using a previously set allowance.", "group": "erc20", - "params": { - "_0": { "label": "from", "type": "address" }, - "_1": { "label": "to", "type": "address" }, - "_2": { "label": "amount" } - }, - "related": ["approve", "allowance"] - } - }, - "messages": { - "Permit": { - "title": "Token Permit (EIP-2612)", - "description": "Approve a spender to transfer your tokens via an off-chain signature, avoiding a separate approve transaction.", - "warning": "This grants token spending permission. Verify the spender address and amount carefully. Phishing sites commonly request Permit signatures to drain wallets.", - "intent": "Permit {spender} to spend {value} of your tokens until {deadline}", - "fields": { - "owner": { "label": "owner", "description": "The token holder granting the approval", "type": "address" }, - "spender": { "label": "spender", "description": "The address being approved to spend tokens", "type": "address" }, - "value": { "label": "amount", "description": "Maximum amount the spender can transfer" }, - "nonce": { "label": "nonce", "description": "Sequential nonce preventing signature replay" }, - "deadline": { "label": "deadline", "description": "Unix timestamp after which the permit expires", "type": "timestamp" } - } + "intent": "Transfer from allowance", + "interpolatedIntent": "Transfer {value} from {from} to {to}", + "fields": [ + { + "path": "from", + "label": "From", + "format": "addressName" + }, + { + "path": "to", + "label": "To", + "format": "addressName" + }, + { + "path": "value", + "label": "Amount", + "format": "tokenAmount", + "params": { + "tokenPath": "@.to" + } + } + ], + "related": [ + "approve", + "allowance" + ] } }, "events": { "Transfer": { "description": "Emitted when tokens are transferred.", "params": { - "from": { "label": "from" }, - "to": { "label": "to" }, - "value": { "label": "amount" } + "from": { + "label": "from" + }, + "to": { + "label": "to" + }, + "value": { + "label": "amount" + } } }, "Approval": { "description": "Emitted when an approval is set.", "params": { - "owner": { "label": "owner" }, - "spender": { "label": "spender" }, - "value": { "label": "amount" } + "owner": { + "label": "owner" + }, + "spender": { + "label": "spender" + }, + "value": { + "label": "amount" + } } } } diff --git a/schema/interfaces/erc721.json b/schema/interfaces/erc721.json index 034541e..125a65e 100644 --- a/schema/interfaces/erc721.json +++ b/schema/interfaces/erc721.json @@ -1,144 +1,315 @@ { + "display": { + "definitions": { + "erc721Token": { + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + } + }, "groups": { - "erc721": { "label": "ERC-721", "order": 99 } + "erc721": { + "label": "ERC-721", + "order": 99 + } }, "functions": { - "name": { + "name()": { "title": "Name", + "signature": "name()", "description": "The name of the token collection.", "stateMutability": "view", "group": "erc721" }, - "symbol": { + "symbol()": { "title": "Symbol", + "signature": "symbol()", "description": "The token symbol.", "stateMutability": "view", "group": "erc721" }, - "totalSupply": { + "totalSupply()": { "title": "Total Supply", + "signature": "totalSupply()", "description": "Total number of tokens in existence.", "stateMutability": "view", "group": "erc721" }, - "balanceOf": { + "balanceOf(address owner)": { "title": "Balance", + "signature": "balanceOf(address owner)", "description": "Check how many tokens an address owns.", "stateMutability": "view", "group": "erc721", - "params": { - "owner": { "label": "holder", "type": "address" } - } + "fields": [ + { + "path": "owner", + "label": "Holder", + "format": "addressName" + } + ] }, - "ownerOf": { + "ownerOf(uint256 tokenId)": { "title": "Owner", + "signature": "ownerOf(uint256 tokenId)", "description": "Look up the current owner of a specific token.", "stateMutability": "view", "group": "erc721", - "params": { - "tokenId": { "label": "token ID", "type": "token-id" } - }, + "fields": [ + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ], "returns": { - "_0": { "label": "owner", "type": "address" } + "_0": { + "label": "owner", + "type": "address" + } } }, - "tokenURI": { + "tokenURI(uint256 tokenId)": { "title": "Token URI", + "signature": "tokenURI(uint256 tokenId)", "description": "Returns the metadata URI for a specific token.", "stateMutability": "view", "group": "erc721", - "params": { - "tokenId": { "label": "token ID", "type": "token-id" } - } + "fields": [ + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ] }, - "approve": { + "approve(address to,uint256 tokenId)": { "title": "Approve", + "signature": "approve(address to,uint256 tokenId)", "description": "Approve an address to transfer a specific token on your behalf.", "group": "erc721", - "intent": "Approve {to} to transfer token #{tokenId}", - "params": { - "to": { "label": "operator", "type": "address" }, - "tokenId": { "label": "token ID", "type": "token-id" } - } + "intent": "Approve NFT transfer", + "interpolatedIntent": "Approve {to} to transfer {tokenId}", + "fields": [ + { + "path": "to", + "label": "Operator", + "format": "addressName" + }, + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ] }, - "getApproved": { + "getApproved(uint256 tokenId)": { "title": "Get Approved", + "signature": "getApproved(uint256 tokenId)", "description": "Check which address is approved to transfer a specific token.", "stateMutability": "view", "group": "erc721", - "params": { - "tokenId": { "label": "token ID", "type": "token-id" } - }, + "fields": [ + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ], "returns": { - "_0": { "label": "approved operator", "type": "address" } + "_0": { + "label": "approved operator", + "type": "address" + } } }, - "setApprovalForAll": { + "setApprovalForAll(address operator,bool approved)": { "title": "Set Approval for All", + "signature": "setApprovalForAll(address operator,bool approved)", "description": "Approve or revoke an operator to manage all your tokens.", "group": "erc721", + "intent": "Set NFT operator approval", + "interpolatedIntent": "Set {operator} approval for all tokens to {approved}", "warning": "Grants full control over all your tokens in this collection to this operator.", - "params": { - "operator": { "label": "operator", "type": "address" }, - "approved": { "label": "approved" } - } + "fields": [ + { + "path": "operator", + "label": "Operator", + "format": "addressName" + }, + { + "path": "approved", + "label": "Approved", + "format": "raw" + } + ] }, - "isApprovedForAll": { + "isApprovedForAll(address owner,address operator)": { "title": "Is Approved for All", + "signature": "isApprovedForAll(address owner,address operator)", "description": "Check whether an operator is approved to manage all tokens for an owner.", "stateMutability": "view", "group": "erc721", - "params": { - "owner": { "label": "owner", "type": "address" }, - "operator": { "label": "operator", "type": "address" } - } + "fields": [ + { + "path": "owner", + "label": "Owner", + "format": "addressName" + }, + { + "path": "operator", + "label": "Operator", + "format": "addressName" + } + ] }, - "transferFrom": { + "transferFrom(address from,address to,uint256 tokenId)": { "title": "Transfer", + "signature": "transferFrom(address from,address to,uint256 tokenId)", "description": "Transfer a token to another address.", "group": "erc721", - "intent": "Transfer token #{tokenId} from {from} to {to}", - "params": { - "from": { "label": "from", "type": "address" }, - "to": { "label": "to", "type": "address" }, - "tokenId": { "label": "token ID", "type": "token-id" } - } + "intent": "Transfer NFT", + "interpolatedIntent": "Transfer {tokenId} from {from} to {to}", + "fields": [ + { + "path": "from", + "label": "From", + "format": "addressName" + }, + { + "path": "to", + "label": "To", + "format": "addressName" + }, + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ] }, - "safeTransferFrom": { + "safeTransferFrom(address from,address to,uint256 tokenId)": { "title": "Safe Transfer", "description": "Transfer a token to another address, reverting if the recipient cannot receive ERC-721 tokens.", "group": "erc721", - "intent": "Transfer token #{tokenId} from {from} to {to}", - "params": { - "from": { "label": "from", "type": "address" }, - "to": { "label": "to", "type": "address" }, - "tokenId": { "label": "token ID", "type": "token-id" } - } + "intent": "Transfer NFT", + "interpolatedIntent": "Transfer {tokenId} from {from} to {to}", + "fields": [ + { + "path": "from", + "label": "From", + "format": "addressName" + }, + { + "path": "to", + "label": "To", + "format": "addressName" + }, + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + } + ] + }, + "safeTransferFrom(address from,address to,uint256 tokenId,bytes data)": { + "title": "Safe Transfer", + "description": "Transfer a token to another address with additional data, reverting if the recipient cannot receive ERC-721 tokens.", + "group": "erc721", + "intent": "Transfer NFT", + "interpolatedIntent": "Transfer {tokenId} from {from} to {to}", + "fields": [ + { + "path": "from", + "label": "From", + "format": "addressName" + }, + { + "path": "to", + "label": "To", + "format": "addressName" + }, + { + "path": "tokenId", + "label": "Token", + "format": "nftName", + "params": { + "collectionPath": "@.to" + } + }, + { + "path": "data", + "label": "Data", + "format": "raw", + "visible": "optional" + } + ] } }, "events": { "Transfer": { "description": "Emitted when a token is transferred.", "params": { - "from": { "label": "from" }, - "to": { "label": "to" }, - "tokenId": { "label": "token ID", "type": "token-id" } + "from": { + "label": "from" + }, + "to": { + "label": "to" + }, + "tokenId": { + "label": "token ID", + "type": "token-id" + } } }, "Approval": { "description": "Emitted when an approval is set for a specific token.", "params": { - "owner": { "label": "owner" }, - "approved": { "label": "approved operator" }, - "tokenId": { "label": "token ID", "type": "token-id" } + "owner": { + "label": "owner" + }, + "approved": { + "label": "approved operator" + }, + "tokenId": { + "label": "token ID", + "type": "token-id" + } } }, "ApprovalForAll": { "description": "Emitted when an operator is approved or revoked for all tokens.", "params": { - "owner": { "label": "owner" }, - "operator": { "label": "operator" }, - "approved": { "label": "approved" } + "owner": { + "label": "owner" + }, + "operator": { + "label": "operator" + }, + "approved": { + "label": "approved" + } } } } diff --git a/validate.ts b/validate.ts index f42d465..d997a7e 100644 --- a/validate.ts +++ b/validate.ts @@ -8,6 +8,7 @@ interface ContractData { includes?: string[] groups?: Record functions?: Record + messages?: Record events?: Record errors?: Record } @@ -15,6 +16,20 @@ interface ContractData { interface FunctionEntry { group?: string related?: string[] + intent?: string + interpolatedIntent?: string + fields?: FieldEntry[] | Record +} + +interface MessageEntry { + intent?: string + interpolatedIntent?: string + fields?: FieldEntry[] | Record +} + +interface FieldEntry { + path?: string + fields?: FieldEntry[] } const ajv = new Ajv({ strict: false, allErrors: true }) @@ -71,7 +86,7 @@ if (runInterfaces && existsSync('schema/interfaces')) { for (const file of files) { const path = join(interfaceDir, file) - const data = JSON.parse(readFileSync(path, 'utf8')) + const data: ContractData = JSON.parse(readFileSync(path, 'utf8')) const valid = validateInterface(data) if (valid) { @@ -83,6 +98,12 @@ if (runInterfaces && existsSync('schema/interfaces')) { console.log(` ${err.instancePath || '/'} ${err.message}`) } } + + // Additional semantic checks + const warnings = semanticChecks(data, path) + for (const w of warnings) { + console.log(` \x1b[33m⚠\x1b[0m ${w}`) + } } } @@ -110,6 +131,60 @@ function functionKeysMatch(ref: string, keys: Set): boolean { return false } +function collectFieldPaths(fields: FieldEntry[] | Record | undefined): Set { + const paths = new Set() + + if (Array.isArray(fields)) { + for (const field of fields) { + if (field.path) paths.add(field.path) + for (const childPath of collectFieldPaths(field.fields)) { + paths.add(childPath) + } + } + } else if (fields) { + for (const [key, field] of Object.entries(fields)) { + paths.add(field.path ?? key) + for (const childPath of collectFieldPaths(field.fields)) { + paths.add(childPath) + } + } + } + + return paths +} + +function extractPlaceholders(template: string): string[] { + const matches = template.matchAll(/\{([^{}]+)\}/g) + return Array.from(matches, match => match[1]) +} + +function checkInterpolatedIntent( + warnings: string[], + location: string, + entry: FunctionEntry | MessageEntry, +): void { + if (entry.intent) { + for (const placeholder of extractPlaceholders(entry.intent)) { + warnings.push(`${location}.intent contains dynamic placeholder "{${placeholder}}"; use interpolatedIntent for value-bearing templates`) + } + } + + if (!entry.interpolatedIntent) return + + const placeholders = extractPlaceholders(entry.interpolatedIntent) + const paths = collectFieldPaths(entry.fields) + if (paths.size === 0 && placeholders.length > 0) { + warnings.push(`${location}.interpolatedIntent has placeholders but no fields to resolve them`) + return + } + + for (const placeholder of placeholders) { + if (!paths.has(placeholder)) { + warnings.push(`${location}.interpolatedIntent placeholder "{${placeholder}}" has no matching fields.path`) + } + } +} + function semanticChecks(data: ContractData, path: string): string[] { const warnings: string[] = [] const groups = data.groups ? Object.keys(data.groups) : [] @@ -134,6 +209,15 @@ function semanticChecks(data: ContractData, path: string): string[] { } } } + + checkInterpolatedIntent(warnings, `functions.${key}`, fn) + } + } + + // Check message clear-signing placeholders + if (data.messages) { + for (const [key, message] of Object.entries(data.messages)) { + checkInterpolatedIntent(warnings, `messages.${key}`, message) } } @@ -160,8 +244,20 @@ function semanticChecks(data: ContractData, path: string): string[] { for (const ref of data.includes) { if (ref.startsWith('interface:')) { const name = ref.slice('interface:'.length) - if (!existsSync(join('schema', 'interfaces', `${name}.json`))) { + const interfacePath = join('schema', 'interfaces', `${name}.json`) + if (!existsSync(interfacePath)) { warnings.push(`includes references unknown interface "${ref}"`) + } else if (data.functions) { + const interfaceData: ContractData = JSON.parse(readFileSync(interfacePath, 'utf8')) + const interfaceFunctions = interfaceData.functions ? Object.keys(interfaceData.functions) : [] + for (const interfaceKey of interfaceFunctions) { + if (SIGNATURE_RE.test(interfaceKey)) { + const bareName = extractName(interfaceKey) + if (Object.prototype.hasOwnProperty.call(data.functions, bareName) && !Object.prototype.hasOwnProperty.call(data.functions, interfaceKey)) { + warnings.push(`functions.${bareName} overrides included "${interfaceKey}" using a different key form; shallow include merge will keep both entries`) + } + } + } } } }