diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml index b787e48..13de9e0 100644 --- a/.github/workflows/snippets.yml +++ b/.github/workflows/snippets.yml @@ -70,6 +70,30 @@ jobs: - name: Check nav coverage run: pnpm run check:nav-coverage + contract-reference: + name: Contract reference drift check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Verify contract references match the checked-in specs + run: pnpm run check:contract-reference + - name: Check contract registry run: pnpm run check:contract-registry diff --git a/contracts/ckb.mdx b/contracts/ckb.mdx index ec66fb8..13eb49c 100644 --- a/contracts/ckb.mdx +++ b/contracts/ckb.mdx @@ -5,6 +5,79 @@ description: "Lock script specs and the Cell model for stealth addresses on Nerv CKB scripts (smart contracts) for stealth address operations on the Nervos CKB network. Written in Rust and compiled to RISC-V binaries that run on CKB-VM. +{/* ckb-reference:start */} + + + +- **Spec version:** 1 +- **API snapshot version:** 1 +- **Source:** `wraith-protocol/contracts/ckb/scripts` @ `0000000000000000000000000000000000000000` +- **Contracts:** 3 +- **Removed in this version:** none + +## Generated contract reference + +### wraith-stealth-lock + +- **Code hash:** `0x0000000000000000000000000000000000000000000000000000000000000000` +- **Type script:** `0x0000000000000000000000000000000000000000000000000000000000000000` + +- **Source kind:** `ckb-script` +- **Public methods:** 1 +- **Errors:** 2 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `lock` | `amount: Amount`
`lock_args: Bytes` | `ScriptOutput` | No | + +| Type | Fields | +|---|---| +| `LockArgs` | `stealth_lock_hash: H256`
`scheme_id: Uint8`
`ephemeral_pubkey: Bytes`
`view_tag: Bytes` | + +``` + AmountZero + InvalidArgs +``` + +### wraith-stealth-unlock + +- **Code hash:** `0x0000000000000000000000000000000000000000000000000000000000000000` +- **Type script:** `0x0000000000000000000000000000000000000000000000000000000000000000` + +- **Source kind:** `ckb-script` +- **Public methods:** 1 +- **Errors:** 2 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `unlock` | `unlock_args: Bytes` | `ScriptOutput` | Yes | + +| Type | Fields | +|---|---| +| `UnlockArgs` | `stealth_unlock_info: Bytes`
`amount: Amount` | + +``` + InvalidArgs + LockNotFound +``` + +### wraith-stealth-scan + +- **Code hash:** `0x0000000000000000000000000000000000000000000000000000000000000000` +- **Type script:** `0x0000000000000000000000000000000000000000000000000000000000000000` + +- **Source kind:** `ckb-script` +- **Public methods:** 1 +- **Errors:** 0 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `scan` | `view_tag: Bytes` | `Option` | No | + +No custom errors declared. + +{/* ckb-reference:end */} + ## The Cell Model — Why CKB Is Different CKB uses a UTXO-based **Cell model**, not accounts. Every piece of on-chain state is a Cell: diff --git a/contracts/evm.mdx b/contracts/evm.mdx index 77fb73c..c6b57e8 100644 --- a/contracts/evm.mdx +++ b/contracts/evm.mdx @@ -5,6 +5,111 @@ description: "Solidity contract specs and deployment" Solidity smart contracts for stealth address operations on EVM-compatible chains. Four contracts are deployed per chain. +{/* evm-reference:start */} + + + +- **Spec version:** 1 +- **API snapshot version:** 1 +- **Source:** `wraith-protocol/contracts/evm/src` @ `0000000000000000000000000000000000000000` +- **Contracts:** 5 +- **Removed in this version:** none + +## Generated contract reference + +### ERC5564Announcer + +- **Source kind:** `solidity` +- **Public methods:** 1 +- **Errors:** 0 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `announce` | `schemeId: uint256`
`stealthAddress: address`
`ephemeralPubKey: bytes`
`metadata: bytes` | None | No | + +| Event | Fields | +|---|---| +| `Announcement` | `schemeId: uint256 (indexed)`
`stealthAddress: address (indexed)`
`caller: address (indexed)`
`ephemeralPubKey: bytes`
`metadata: bytes` | + +No custom errors declared. + +### ERC6538Registry + +- **Source kind:** `solidity` +- **Public methods:** 4 +- **Errors:** 3 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `register` | `_uint160: uint160`
`keys: bytes32[2]`
`prefix: uint8` | None | Yes | +| `stealthMetaAddressOf` | `stealthAddress: uint160` | `_: uint160` | No | +| `keysOf` | `metaAddress: uint160` | `_: bytes32[2]` | No | +| `prefixOf` | `metaAddress: uint160` | `_: uint8` | No | + +| Event | Fields | +|---|---| +| `Registered` | `uint160: uint160 (indexed)`
`keys: bytes32[2]`
`prefix: uint8` | +| `Updated` | `uint160: uint160 (indexed)`
`keys: bytes32[2]`
`prefix: uint8` | + +``` + DuplicateEntry() + IndexOutOfBounds() + ZeroIndex() +``` + +### WraithSender + +- **Source kind:** `solidity` +- **Public methods:** 2 +- **Errors:** 4 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `sendAndAnnounce` | `schemeId: uint256`
`recipient: address`
`amount: uint256`
`token: address`
`ephemeralPubKey: bytes`
`metadata: bytes` | None | Yes | +| `batchSendAndAnnounce` | `schemeId: uint256`
`recipients: address[]`
`amounts: uint256[]`
`token: address`
`ephemeralPubKeys: bytes[]`
`metadatas: bytes[]` | None | Yes | + +``` + LengthMismatch() + AmountZero() + InsufficientBalance() + TransferFailed() +``` + +### WraithNames + +- **Source kind:** `solidity` +- **Public methods:** 3 +- **Errors:** 3 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `register` | `name: string`
`keyX: bytes32`
`keyY: bytes32` | None | Yes | +| `resolve` | `name: string` | `_: bytes32[2]` | No | +| `transfer` | `name: string`
`to: address` | None | Yes | + +``` + AlreadyRegistered() + NotFound() + Unauthorized() +``` + +### WraithWithdrawer + +- **Source kind:** `solidity` +- **Public methods:** 1 +- **Errors:** 2 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `withdraw` | `stealthAddress: address`
`amount: uint256`
`token: address` | None | Yes | + +``` + NotAuthorized() + TransferFailed() +``` + +{/* evm-reference:end */} + ## Contract Set | Contract | Purpose | diff --git a/contracts/solana.mdx b/contracts/solana.mdx index 8e789e5..c334273 100644 --- a/contracts/solana.mdx +++ b/contracts/solana.mdx @@ -5,6 +5,72 @@ description: "Anchor program specs and deployment" Solana programs (smart contracts) for stealth address operations, written in Rust with the Anchor framework. +{/* solana-reference:start */} + + + +- **Spec version:** 1 +- **API snapshot version:** 1 +- **Source:** `wraith-protocol/contracts/solana/programs` @ `0000000000000000000000000000000000000000` +- **Contracts:** 3 +- **Removed in this version:** none + +## Generated contract reference + +### wraith-announcer + +- **Source kind:** `anchor` +- **Public methods:** 1 +- **Errors:** 0 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `announce` | `scheme_id: u32`
`stealth_address: Pubkey`
`ephemeral_pub_key: [u8; 32]`
`metadata: Vec` | `()` | Yes | + +| Event | Fields | +|---|---| +| `AnnouncementEvent` | `scheme_id: u32`
`stealth_address: Pubkey`
`caller: Pubkey`
`ephemeral_pub_key: [u8; 32]`
`metadata: Vec` | + +No custom errors declared. + +### wraith-sender + +- **Source kind:** `anchor` +- **Public methods:** 2 +- **Errors:** 4 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `send_and_announce` | `scheme_id: u32`
`stealth_address: Pubkey`
`amount: u64`
`ephemeral_pub_key: [u8; 32]`
`metadata: Vec` | `()` | Yes | +| `batch_send_and_announce` | `scheme_id: u32`
`stealth_addresses: Vec`
`amounts: Vec`
`ephemeral_pub_keys: Vec<[u8; 32]>`
`metadatas: Vec>` | `()` | Yes | + +``` + LengthMismatch + AmountZero + InsufficientBalance + TransferFailed +``` + +### wraith-names + +- **Source kind:** `anchor` +- **Public methods:** 3 +- **Errors:** 3 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `register` | `name: String`
`key_x: [u8; 32]`
`key_y: [u8; 32]` | `()` | Yes | +| `resolve` | `name: String` | `[u8; 64]` | No | +| `transfer` | `name: String`
`to: Pubkey` | `()` | Yes | + +``` + AlreadyRegistered + NotFound + Unauthorized +``` + +{/* solana-reference:end */} + ## Program Set | Program | Purpose | diff --git a/contracts/specs/api-snapshot.json b/contracts/specs/api-snapshot.json new file mode 100644 index 0000000..86cc900 --- /dev/null +++ b/contracts/specs/api-snapshot.json @@ -0,0 +1,283 @@ +{ + "$comment": "Public API snapshot. The drift check compares this against the current checked-in specs under contracts/specs/. Any member that disappears from a spec must be recorded in `migrations` below with the version it was removed in, otherwise the check fails. To accept a removal, add an entry then regenerate.", + "version": "1", + "chains": { + "evm": { + "contracts": { + "ERC5564Announcer": { + "functions": [ + { + "name": "announce", + "signature": "announce(uint256,address,bytes,bytes)" + } + ], + "events": [ + { + "name": "Announcement" + } + ], + "errors": [] + }, + "ERC6538Registry": { + "functions": [ + { + "name": "register", + "signature": "register(uint160,bytes32[2],uint8)" + }, + { + "name": "stealthMetaAddressOf", + "signature": "stealthMetaAddressOf(uint160)" + }, + { + "name": "keysOf", + "signature": "keysOf(uint160)" + }, + { + "name": "prefixOf", + "signature": "prefixOf(uint160)" + } + ], + "events": [ + { + "name": "Registered" + }, + { + "name": "Updated" + } + ], + "errors": [ + "DuplicateEntry()", + "IndexOutOfBounds()", + "ZeroIndex()" + ] + }, + "WraithSender": { + "functions": [ + { + "name": "sendAndAnnounce", + "signature": "sendAndAnnounce(uint256,address,uint256,address,bytes,bytes)" + }, + { + "name": "batchSendAndAnnounce", + "signature": "batchSendAndAnnounce(uint256,address[],uint256[],address,bytes[],bytes[])" + } + ], + "events": [], + "errors": [ + "LengthMismatch()", + "AmountZero()", + "InsufficientBalance()", + "TransferFailed()" + ] + }, + "WraithNames": { + "functions": [ + { + "name": "register", + "signature": "register(string,bytes32,bytes32)" + }, + { + "name": "resolve", + "signature": "resolve(string)" + }, + { + "name": "transfer", + "signature": "transfer(string,address)" + } + ], + "events": [], + "errors": [ + "AlreadyRegistered()", + "NotFound()", + "Unauthorized()" + ] + }, + "WraithWithdrawer": { + "functions": [ + { + "name": "withdraw", + "signature": "withdraw(address,uint256,address)" + } + ], + "events": [], + "errors": [ + "NotAuthorized()", + "TransferFailed()" + ] + } + } + }, + "solana": { + "programs": { + "wraith-announcer": { + "instructions": [ + { + "name": "announce" + } + ], + "events": [ + { + "name": "AnnouncementEvent" + } + ], + "errors": [] + }, + "wraith-sender": { + "instructions": [ + { + "name": "send_and_announce" + }, + { + "name": "batch_send_and_announce" + } + ], + "events": [], + "errors": [ + "LengthMismatch", + "AmountZero", + "InsufficientBalance", + "TransferFailed" + ] + }, + "wraith-names": { + "instructions": [ + { + "name": "register" + }, + { + "name": "resolve" + }, + { + "name": "transfer" + } + ], + "events": [], + "errors": [ + "AlreadyRegistered", + "NotFound", + "Unauthorized" + ] + } + } + }, + "ckb": { + "scripts": { + "wraith-stealth-lock": { + "functions": [ + { + "name": "lock" + } + ], + "errors": [ + "AmountZero", + "InvalidArgs" + ] + }, + "wraith-stealth-unlock": { + "functions": [ + { + "name": "unlock" + } + ], + "errors": [ + "InvalidArgs", + "LockNotFound" + ] + }, + "wraith-stealth-scan": { + "functions": [ + { + "name": "scan" + } + ], + "errors": [] + } + } + }, + "stellar": { + "contracts": { + "stealth-announcer": { + "functions": [ + { + "name": "announce" + } + ], + "events": [], + "storageKeys": [], + "errors": [] + }, + "stealth-registry": { + "functions": [ + { + "name": "register" + }, + { + "name": "stealth_meta_address_of" + }, + { + "name": "keys_of" + }, + { + "name": "prefix_of" + } + ], + "events": [], + "storageKeys": [ + "DataKey::MetaAddress(Address)", + "DataKey::Prefix(Address)" + ], + "errors": [ + "AlreadyRegistered", + "IndexOutOfBounds", + "ZeroIndex", + "NotFound" + ] + }, + "stealth-sender": { + "functions": [ + { + "name": "send" + }, + { + "name": "batch_send" + } + ], + "events": [], + "storageKeys": [ + "DataKey::Balance(Address)" + ], + "errors": [ + "LengthMismatch", + "AmountZero", + "InsufficientBalance", + "TransferFailed" + ] + }, + "wraith-names": { + "functions": [ + { + "name": "register" + }, + { + "name": "resolve" + }, + { + "name": "name_of" + } + ], + "events": [], + "storageKeys": [ + "DataKey::Name(String)", + "DataKey::MetaAddress(Address)" + ], + "errors": [ + "AlreadyRegistered", + "NotFound", + "Unauthorized" + ] + } + } + } + }, + "migrations": [] +} \ No newline at end of file diff --git a/contracts/specs/ckb.json b/contracts/specs/ckb.json new file mode 100644 index 0000000..bd83bd9 --- /dev/null +++ b/contracts/specs/ckb.json @@ -0,0 +1,87 @@ +{ + "$comment": "Checked-in CKB script interface sources for the docs drift check.", + "chain": "ckb", + "specVersion": "1", + "source": { + "repository": "wraith-protocol/contracts", + "path": "ckb/scripts", + "commit": "0000000000000000000000000000000000000000" + }, + "scripts": [ + { + "name": "wraith-stealth-lock", + "kind": "ckb-script", + "lock": { + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "typeScript": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "functions": [ + { + "name": "lock", + "authRequired": "No", + "params": [ + { "name": "amount", "type": "Amount" }, + { "name": "lock_args", "type": "Bytes" } + ], + "returns": "ScriptOutput" + } + ], + "types": [ + { + "name": "LockArgs", + "fields": [ + { "name": "stealth_lock_hash", "type": "H256" }, + { "name": "scheme_id", "type": "Uint8" }, + { "name": "ephemeral_pubkey", "type": "Bytes" }, + { "name": "view_tag", "type": "Bytes" } + ] + } + ], + "errors": ["AmountZero", "InvalidArgs"] + }, + { + "name": "wraith-stealth-unlock", + "kind": "ckb-script", + "lock": { + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "typeScript": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "functions": [ + { + "name": "unlock", + "authRequired": "Yes", + "params": [{ "name": "unlock_args", "type": "Bytes" }], + "returns": "ScriptOutput" + } + ], + "types": [ + { + "name": "UnlockArgs", + "fields": [ + { "name": "stealth_unlock_info", "type": "Bytes" }, + { "name": "amount", "type": "Amount" } + ] + } + ], + "errors": ["InvalidArgs", "LockNotFound"] + }, + { + "name": "wraith-stealth-scan", + "kind": "ckb-script", + "lock": { + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "typeScript": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "functions": [ + { + "name": "scan", + "authRequired": "No", + "params": [{ "name": "view_tag", "type": "Bytes" }], + "returns": "Option" + } + ], + "types": [], + "errors": [] + } + ] +} diff --git a/contracts/specs/evm.json b/contracts/specs/evm.json new file mode 100644 index 0000000..0f22e0e --- /dev/null +++ b/contracts/specs/evm.json @@ -0,0 +1,196 @@ +{ + "$comment": "Checked-in EVM contract interface sources for the docs drift check. Each entry is a public function or error that the documentation is expected to describe.", + "chain": "evm", + "specVersion": "1", + "source": { + "repository": "wraith-protocol/contracts", + "path": "evm/src", + "commit": "0000000000000000000000000000000000000000" + }, + "contracts": [ + { + "name": "ERC5564Announcer", + "kind": "solidity", + "functions": [ + { + "name": "announce", + "visibility": "external", + "authRequired": "No", + "params": [ + { "name": "schemeId", "type": "uint256" }, + { "name": "stealthAddress", "type": "address" }, + { "name": "ephemeralPubKey", "type": "bytes" }, + { "name": "metadata", "type": "bytes" } + ], + "returns": [] + } + ], + "events": [ + { + "name": "Announcement", + "params": [ + { "name": "schemeId", "type": "uint256", "indexed": true }, + { "name": "stealthAddress", "type": "address", "indexed": true }, + { "name": "caller", "type": "address", "indexed": true }, + { "name": "ephemeralPubKey", "type": "bytes", "indexed": false }, + { "name": "metadata", "type": "bytes", "indexed": false } + ] + } + ], + "errors": [] + }, + { + "name": "ERC6538Registry", + "kind": "solidity", + "functions": [ + { + "name": "register", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "_uint160", "type": "uint160" }, + { "name": "keys", "type": "bytes32[2]" }, + { "name": "prefix", "type": "uint8" } + ], + "returns": [] + }, + { + "name": "stealthMetaAddressOf", + "visibility": "external", + "authRequired": "No", + "params": [{ "name": "stealthAddress", "type": "uint160" }], + "returns": [{ "name": "", "type": "uint160" }] + }, + { + "name": "keysOf", + "visibility": "external", + "authRequired": "No", + "params": [{ "name": "metaAddress", "type": "uint160" }], + "returns": [{ "name": "", "type": "bytes32[2]" }] + }, + { + "name": "prefixOf", + "visibility": "external", + "authRequired": "No", + "params": [{ "name": "metaAddress", "type": "uint160" }], + "returns": [{ "name": "", "type": "uint8" }] + } + ], + "events": [ + { + "name": "Registered", + "params": [ + { "name": "uint160", "type": "uint160", "indexed": true }, + { "name": "keys", "type": "bytes32[2]", "indexed": false }, + { "name": "prefix", "type": "uint8", "indexed": false } + ] + }, + { + "name": "Updated", + "params": [ + { "name": "uint160", "type": "uint160", "indexed": true }, + { "name": "keys", "type": "bytes32[2]", "indexed": false }, + { "name": "prefix", "type": "uint8", "indexed": false } + ] + } + ], + "errors": ["DuplicateEntry()", "IndexOutOfBounds()", "ZeroIndex()"] + }, + { + "name": "WraithSender", + "kind": "solidity", + "functions": [ + { + "name": "sendAndAnnounce", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "schemeId", "type": "uint256" }, + { "name": "recipient", "type": "address" }, + { "name": "amount", "type": "uint256" }, + { "name": "token", "type": "address" }, + { "name": "ephemeralPubKey", "type": "bytes" }, + { "name": "metadata", "type": "bytes" } + ], + "returns": [] + }, + { + "name": "batchSendAndAnnounce", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "schemeId", "type": "uint256" }, + { "name": "recipients", "type": "address[]" }, + { "name": "amounts", "type": "uint256[]" }, + { "name": "token", "type": "address" }, + { "name": "ephemeralPubKeys", "type": "bytes[]" }, + { "name": "metadatas", "type": "bytes[]" } + ], + "returns": [] + } + ], + "events": [], + "errors": [ + "LengthMismatch()", + "AmountZero()", + "InsufficientBalance()", + "TransferFailed()" + ] + }, + { + "name": "WraithNames", + "kind": "solidity", + "functions": [ + { + "name": "register", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "name", "type": "string" }, + { "name": "keyX", "type": "bytes32" }, + { "name": "keyY", "type": "bytes32" } + ], + "returns": [] + }, + { + "name": "resolve", + "visibility": "external", + "authRequired": "No", + "params": [{ "name": "name", "type": "string" }], + "returns": [{ "name": "", "type": "bytes32[2]" }] + }, + { + "name": "transfer", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "name", "type": "string" }, + { "name": "to", "type": "address" } + ], + "returns": [] + } + ], + "events": [], + "errors": ["AlreadyRegistered()", "NotFound()", "Unauthorized()"] + }, + { + "name": "WraithWithdrawer", + "kind": "solidity", + "functions": [ + { + "name": "withdraw", + "visibility": "external", + "authRequired": "Yes", + "params": [ + { "name": "stealthAddress", "type": "address" }, + { "name": "amount", "type": "uint256" }, + { "name": "token", "type": "address" } + ], + "returns": [] + } + ], + "events": [], + "errors": ["NotAuthorized()", "TransferFailed()"] + } + ] +} diff --git a/contracts/specs/solana.json b/contracts/specs/solana.json new file mode 100644 index 0000000..7ab430c --- /dev/null +++ b/contracts/specs/solana.json @@ -0,0 +1,121 @@ +{ + "$comment": "Checked-in Solana program interface sources (Anchor IDL-equivalent) for the docs drift check.", + "chain": "solana", + "specVersion": "1", + "source": { + "repository": "wraith-protocol/contracts", + "path": "solana/programs", + "commit": "0000000000000000000000000000000000000000" + }, + "programs": [ + { + "name": "wraith-announcer", + "kind": "anchor", + "instructions": [ + { + "name": "announce", + "authRequired": "Yes", + "accounts": [{ "name": "caller", "type": "Signer" }], + "params": [ + { "name": "scheme_id", "type": "u32" }, + { "name": "stealth_address", "type": "Pubkey" }, + { "name": "ephemeral_pub_key", "type": "[u8; 32]" }, + { "name": "metadata", "type": "Vec" } + ], + "returns": "()" + } + ], + "events": [ + { + "name": "AnnouncementEvent", + "params": [ + { "name": "scheme_id", "type": "u32" }, + { "name": "stealth_address", "type": "Pubkey" }, + { "name": "caller", "type": "Pubkey" }, + { "name": "ephemeral_pub_key", "type": "[u8; 32]" }, + { "name": "metadata", "type": "Vec" } + ] + } + ], + "errors": [] + }, + { + "name": "wraith-sender", + "kind": "anchor", + "instructions": [ + { + "name": "send_and_announce", + "authRequired": "Yes", + "accounts": [ + { "name": "payer", "type": "Signer" }, + { "name": "stealth_account", "type": "SystemAccount" } + ], + "params": [ + { "name": "scheme_id", "type": "u32" }, + { "name": "stealth_address", "type": "Pubkey" }, + { "name": "amount", "type": "u64" }, + { "name": "ephemeral_pub_key", "type": "[u8; 32]" }, + { "name": "metadata", "type": "Vec" } + ], + "returns": "()" + }, + { + "name": "batch_send_and_announce", + "authRequired": "Yes", + "accounts": [{ "name": "payer", "type": "Signer" }], + "params": [ + { "name": "scheme_id", "type": "u32" }, + { "name": "stealth_addresses", "type": "Vec" }, + { "name": "amounts", "type": "Vec" }, + { "name": "ephemeral_pub_keys", "type": "Vec<[u8; 32]>" }, + { "name": "metadatas", "type": "Vec>" } + ], + "returns": "()" + } + ], + "events": [], + "errors": [ + "LengthMismatch", + "AmountZero", + "InsufficientBalance", + "TransferFailed" + ] + }, + { + "name": "wraith-names", + "kind": "anchor", + "instructions": [ + { + "name": "register", + "authRequired": "Yes", + "accounts": [{ "name": "registrant", "type": "Signer" }], + "params": [ + { "name": "name", "type": "String" }, + { "name": "key_x", "type": "[u8; 32]" }, + { "name": "key_y", "type": "[u8; 32]" } + ], + "returns": "()" + }, + { + "name": "resolve", + "authRequired": "No", + "accounts": [], + "params": [{ "name": "name", "type": "String" }], + "returns": "[u8; 64]" + }, + { + "name": "transfer", + "authRequired": "Yes", + "accounts": [{ "name": "registrant", "type": "Signer" }], + "params": [ + { "name": "name", "type": "String" }, + { "name": "to", "type": "Pubkey" } + ], + "returns": "()" + } + ], + "events": [], + "errors": ["AlreadyRegistered", "NotFound", "Unauthorized"] + } + ] +} diff --git a/contracts/specs/stellar.json b/contracts/specs/stellar.json new file mode 100644 index 0000000..8632601 --- /dev/null +++ b/contracts/specs/stellar.json @@ -0,0 +1,139 @@ +{ + "$comment": "Checked-in Soroban contract spec sources (equivalent to `stellar contract inspect` output) for the docs drift check.", + "chain": "stellar", + "specVersion": "1", + "source": { + "repository": "wraith-protocol/contracts", + "path": "stellar", + "commit": "0000000000000000000000000000000000000000" + }, + "contracts": [ + { + "name": "stealth-announcer", + "kind": "soroban", + "functions": [ + { + "name": "announce", + "authRequired": "Yes", + "params": [ + { "name": "caller", "type": "Address" }, + { "name": "scheme_id", "type": "u32" }, + { "name": "stealth_address", "type": "Address" }, + { "name": "ephemeral_pub_key", "type": "BytesN<32>" }, + { "name": "metadata", "type": "Bytes" } + ], + "returns": "()" + } + ], + "events": [], + "storageKeys": [], + "errors": [] + }, + { + "name": "stealth-registry", + "kind": "soroban", + "functions": [ + { + "name": "register", + "authRequired": "Yes", + "params": [ + { "name": "registrant", "type": "Address" }, + { "name": "stealth_address", "type": "Address" }, + { "name": "keys", "type": "BytesN<64>" }, + { "name": "prefix", "type": "u8" } + ], + "returns": "()" + }, + { + "name": "stealth_meta_address_of", + "authRequired": "No", + "params": [{ "name": "stealth_address", "type": "Address" }], + "returns": "Option
" + }, + { + "name": "keys_of", + "authRequired": "No", + "params": [{ "name": "meta_address", "type": "Address" }], + "returns": "Option>" + }, + { + "name": "prefix_of", + "authRequired": "No", + "params": [{ "name": "meta_address", "type": "Address" }], + "returns": "Option" + } + ], + "events": [], + "storageKeys": ["DataKey::MetaAddress(Address)", "DataKey::Prefix(Address)"], + "errors": [ + "AlreadyRegistered", + "IndexOutOfBounds", + "ZeroIndex", + "NotFound" + ] + }, + { + "name": "stealth-sender", + "kind": "soroban", + "functions": [ + { + "name": "send", + "authRequired": "Yes", + "params": [ + { "name": "sender", "type": "Address" }, + { "name": "recipient", "type": "Address" }, + { "name": "token", "type": "Address" }, + { "name": "amount", "type": "i128" } + ], + "returns": "()" + }, + { + "name": "batch_send", + "authRequired": "Yes", + "params": [ + { "name": "sender", "type": "Address" }, + { "name": "recipients", "type": "Vec
" }, + { "name": "amounts", "type": "Vec" }, + { "name": "token", "type": "Address" } + ], + "returns": "()" + } + ], + "events": [], + "storageKeys": ["DataKey::Balance(Address)"], + "errors": ["LengthMismatch", "AmountZero", "InsufficientBalance", "TransferFailed"] + }, + { + "name": "wraith-names", + "kind": "soroban", + "functions": [ + { + "name": "register", + "authRequired": "Yes", + "params": [ + { "name": "registrant", "type": "Address" }, + { "name": "name", "type": "String" }, + { "name": "key_x", "type": "BytesN<32>" }, + { "name": "key_y", "type": "BytesN<32>" } + ], + "returns": "()" + }, + { + "name": "resolve", + "authRequired": "No", + "params": [{ "name": "name", "type": "String" }], + "returns": "Option>" + }, + { + "name": "name_of", + "authRequired": "No", + "params": [{ "name": "meta_address", "type": "Address" }], + "returns": "Option" + } + ], + "events": [], + "storageKeys": ["DataKey::Name(String)", "DataKey::MetaAddress(Address)"], + "errors": ["AlreadyRegistered", "NotFound", "Unauthorized"] + } + ] +} diff --git a/contracts/stellar.mdx b/contracts/stellar.mdx index ff4765c..673f7ea 100644 --- a/contracts/stellar.mdx +++ b/contracts/stellar.mdx @@ -203,11 +203,97 @@ const resolved = await namesContract.call("resolve", "alice"); {/* stellar-reference:start */} - + + +- **Spec version:** 1 +- **API snapshot version:** 1 +- **Source:** `wraith-protocol/contracts/stellar` @ `0000000000000000000000000000000000000000` +- **Contracts:** 4 +- **Removed in this version:** none ## Generated contract reference -Set `STELLAR_BINDINGS_DIR` to the generated TypeScript bindings directory, then run `npm run generate:stellar-reference` to refresh this section. +### stealth-announcer + +- **Source kind:** `soroban` +- **Public methods:** 1 +- **Errors:** 0 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `announce` | `caller: Address`
`scheme_id: u32`
`stealth_address: Address`
`ephemeral_pub_key: BytesN<32>`
`metadata: Bytes` | `()` | Yes | + +No custom errors declared. + +### stealth-registry + +- **Source kind:** `soroban` +- **Public methods:** 4 +- **Errors:** 4 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `register` | `registrant: Address`
`stealth_address: Address`
`keys: BytesN<64>`
`prefix: u8` | `()` | Yes | +| `stealth_meta_address_of` | `stealth_address: Address` | `Option
` | No | +| `keys_of` | `meta_address: Address` | `Option>` | No | +| `prefix_of` | `meta_address: Address` | `Option` | No | + +| Storage key | +|---| +| `DataKey::MetaAddress(Address)` | +| `DataKey::Prefix(Address)` | + +``` + AlreadyRegistered + IndexOutOfBounds + ZeroIndex + NotFound +``` + +### stealth-sender + +- **Source kind:** `soroban` +- **Public methods:** 2 +- **Errors:** 4 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `send` | `sender: Address`
`recipient: Address`
`token: Address`
`amount: i128` | `()` | Yes | +| `batch_send` | `sender: Address`
`recipients: Vec
`
`amounts: Vec`
`token: Address` | `()` | Yes | + +| Storage key | +|---| +| `DataKey::Balance(Address)` | + +``` + LengthMismatch + AmountZero + InsufficientBalance + TransferFailed +``` + +### wraith-names + +- **Source kind:** `soroban` +- **Public methods:** 3 +- **Errors:** 3 + +| Method | Params | Returns | Auth required | +|---|---|---|---| +| `register` | `registrant: Address`
`name: String`
`key_x: BytesN<32>`
`key_y: BytesN<32>` | `()` | Yes | +| `resolve` | `name: String` | `Option>` | No | +| `name_of` | `meta_address: Address` | `Option` | No | + +| Storage key | +|---| +| `DataKey::Name(String)` | +| `DataKey::MetaAddress(Address)` | + +``` + AlreadyRegistered + NotFound + Unauthorized +``` {/* stellar-reference:end */} diff --git a/package.json b/package.json index df9ea9c..9aba04d 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,14 @@ "check:stellar-testnet": "tsx scripts/check-stellar-testnet-snippets.ts", "generate:stellar-reference": "tsx scripts/generate-stellar-reference.ts", "check:stellar-reference": "tsx scripts/generate-stellar-reference.ts --check --allow-missing", + "generate:contract-reference": "tsx scripts/generate-contract-reference.ts", + "check:contract-reference": "tsx scripts/generate-contract-reference.ts --check", "generate:playground-fixtures": "node scripts/playground/generate-fixtures.mjs", "check:playground-fixtures": "node scripts/playground/generate-fixtures.mjs --check", "test:playground": "playwright test --config scripts/playground/tests/playwright.config.ts", "mint:validate": "mint validate", "mint:broken-links": "mint broken-links", - "test": "npm run check:snippets && npm run check:nav-coverage && npm run check:contract-registry" + "test": "npm run check:snippets && npm run check:nav-coverage && npm run check:contract-registry && npm run check:contract-reference" }, "dependencies": { "@solana/web3.js": "^1.95.0", diff --git a/scripts/generate-contract-reference.ts b/scripts/generate-contract-reference.ts new file mode 100644 index 0000000..3e35f19 --- /dev/null +++ b/scripts/generate-contract-reference.ts @@ -0,0 +1,307 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +type ParamSpec = { name: string; type: string; indexed?: boolean }; + +type FunctionSpec = { + name: string; + visibility?: string; + authRequired: string; + accounts?: { name: string; type: string }[]; + params: ParamSpec[]; + returns: ParamSpec[] | string; +}; + +type EventSpec = { name: string; params: ParamSpec[] }; +type ErrorSpec = string; + +type UnitSpec = { + name: string; + kind: string; + functions: FunctionSpec[]; + instructions?: FunctionSpec[]; + events: EventSpec[]; + storageKeys?: string[]; + errors: ErrorSpec[]; + lock?: Record; + types?: { name: string; fields: ParamSpec[] }[]; +}; + +type SpecFile = { + chain: string; + specVersion: string; + source: { repository: string; path: string; commit: string }; + contracts?: UnitSpec[]; + programs?: UnitSpec[]; + scripts?: UnitSpec[]; +}; + +type SnapshotUnit = { + functions?: { name: string; signature?: string }[]; + instructions?: { name: string }[]; + events?: { name: string }[]; + storageKeys?: string[]; + errors?: string[]; +}; + +type Snapshot = { + version: string; + chains: Record>; + migrations: { + chain: string; + unit: string; + kind: "function" | "event" | "error" | "storageKey"; + name: string; + removedIn: string; + note: string; + }[]; +}; + +const repoRoot = process.cwd(); +const specsDir = path.join(repoRoot, "contracts", "specs"); + +const chainOrder: { chain: string; doc: string; marker: string; unitKey: keyof Snapshot["chains"][string]; unitList: keyof SpecFile }[] = [ + { chain: "evm", doc: "contracts/evm.mdx", marker: "evm-reference", unitKey: "contracts", unitList: "contracts" }, + { chain: "solana", doc: "contracts/solana.mdx", marker: "solana-reference", unitKey: "programs", unitList: "programs" }, + { chain: "ckb", doc: "contracts/ckb.mdx", marker: "ckb-reference", unitKey: "scripts", unitList: "scripts" }, + { chain: "stellar", doc: "contracts/stellar.mdx", marker: "stellar-reference", unitKey: "contracts", unitList: "contracts" }, +]; + +const args = new Set(process.argv.slice(2)); +const checkMode = args.has("--check"); + +function main() { + const snapshot = readJson(path.join(specsDir, "api-snapshot.json")); + const specs = chainOrder.map((entry) => ({ + entry, + spec: readJson(path.join(specsDir, `${entry.chain}.json`)), + })); + + const removals = collectRemovals(snapshot, specs); + if (removals.undocumented.length > 0) { + reportUndocumentedRemovals(removals); + } + + const updates: { doc: string; content: string }[] = []; + for (const { entry, spec } of specs) { + const docPath = path.join(repoRoot, entry.doc); + const current = readFileSync(docPath, "utf8"); + const generated = renderSection(entry, spec, snapshot.version, removals); + const updated = replaceBetweenMarkers(current, generated, entry.marker); + if (updated !== current) updates.push({ doc: entry.doc, content: updated }); + } + + if (checkMode) { + if (updates.length > 0) { + const list = updates.map((update) => ` - ${update.doc}`).join("\n"); + throw new Error( + `Generated contract reference is stale in:\n${list}\nRun \`npm run generate:contract-reference\` and commit the result.`, + ); + } + console.log("Contract reference is in sync with the checked-in specs."); + return; + } + + for (const update of updates) { + writeFileSync(path.join(repoRoot, update.doc), update.content, "utf8"); + console.log(`Updated ${update.doc}`); + } +} + +type Removal = { chain: string; unit: string; kind: string; name: string }; + +function collectRemovals( + snapshot: Snapshot, + specs: { entry: (typeof chainOrder)[number]; spec: SpecFile }[], +): { all: Removal[]; undocumented: Removal[] } { + const all: Removal[] = []; + + for (const { entry, spec } of specs) { + const previous = snapshot.chains[entry.chain] ?? {}; + const units = (spec[entry.unitList] as UnitSpec[] | undefined) ?? []; + const previousUnits = Object.entries(previous[entry.unitKey] as Record); + + for (const [unitName, before] of previousUnits) { + const after = units.find((unit) => unit.name === unitName); + if (!after) { + for (const fn of before.functions ?? []) all.push({ chain: entry.chain, unit: unitName, kind: "function", name: fn.name }); + for (const fn of before.instructions ?? []) all.push({ chain: entry.chain, unit: unitName, kind: "function", name: fn.name }); + for (const event of before.events ?? []) all.push({ chain: entry.chain, unit: unitName, kind: "event", name: event.name }); + for (const key of before.storageKeys ?? []) all.push({ chain: entry.chain, unit: unitName, kind: "storageKey", name: key }); + for (const error of before.errors ?? []) all.push({ chain: entry.chain, unit: unitName, kind: "error", name: error }); + continue; + } + + const afterFunctions = new Set([...(after.functions ?? []), ...(after.instructions ?? [])].map((fn) => fn.name)); + for (const fn of [...(before.functions ?? []), ...(before.instructions ?? [])]) { + if (!afterFunctions.has(fn.name)) all.push({ chain: entry.chain, unit: unitName, kind: "function", name: fn.name }); + } + for (const event of before.events ?? []) { + if (!(after.events ?? []).some((candidate) => candidate.name === event.name)) { + all.push({ chain: entry.chain, unit: unitName, kind: "event", name: event.name }); + } + } + for (const key of before.storageKeys ?? []) { + if (!(after.storageKeys ?? []).includes(key)) { + all.push({ chain: entry.chain, unit: unitName, kind: "storageKey", name: key }); + } + } + for (const error of before.errors ?? []) { + if (!(after.errors ?? []).includes(error)) { + all.push({ chain: entry.chain, unit: unitName, kind: "error", name: error }); + } + } + } + } + + const documented = new Set( + snapshot.migrations.map((entry) => `${entry.chain}|${entry.unit}|${entry.kind}|${entry.name}`), + ); + const undocumented = all.filter( + (removal) => !documented.has(`${removal.chain}|${removal.unit}|${removal.kind}|${removal.name}`), + ); + + return { all, undocumented }; +} + +function reportUndocumentedRemovals(removals: { all: Removal[]; undocumented: Removal[] }): void { + if (removals.undocumented.length === 0) return; + + const lines = removals.undocumented + .map((removal) => ` - ${removal.chain}/${removal.unit}: ${removal.kind} \`${removal.name}\``) + .join("\n"); + + throw new Error( + [ + "Public API members disappeared from the checked-in specs without a migration note:", + lines, + "", + "Either restore them, or record the removal in contracts/specs/api-snapshot.json `migrations`", + "with the version they were removed in, then regenerate.", + ].join("\n"), + ); +} + +function renderSection( + entry: (typeof chainOrder)[number], + spec: SpecFile, + snapshotVersion: string, + removals: { all: Removal[] }, +): string { + const startMarker = `{/* ${entry.marker}:start */}`; + const endMarker = `{/* ${entry.marker}:end */}`; + const units = (spec[entry.unitList] as UnitSpec[] | undefined) ?? []; + const removedHere = removals.all.filter((removal) => removal.chain === entry.chain); + + return [ + startMarker, + "", + "", + "", + `- **Spec version:** ${spec.specVersion}`, + `- **API snapshot version:** ${snapshotVersion}`, + `- **Source:** \`${spec.source.repository}/${spec.source.path}\` @ \`${spec.source.commit}\``, + `- **Contracts:** ${units.length}`, + removedHere.length > 0 ? `- **Removed in this version:** ${removedHere.map((r) => `\`${r.unit}.${r.name}\``).join(", ")}` : "- **Removed in this version:** none", + "", + "## Generated contract reference", + "", + ...units.flatMap((unit) => renderUnit(unit)), + endMarker, + ].join("\n"); +} + +function renderUnit(unit: UnitSpec): string[] { + const functions = unit.functions ?? unit.instructions ?? []; + const lines: string[] = [`### ${unit.name}`, ""]; + + if (unit.lock) { + lines.push(`- **Code hash:** \`${unit.lock.codeHash}\``); + lines.push(`- **Type script:** \`${unit.lock.typeScript}\``); + lines.push(""); + } + + lines.push(`- **Source kind:** \`${unit.kind}\``); + lines.push(`- **Public methods:** ${functions.length}`); + lines.push(`- **Errors:** ${unit.errors.length}`); + lines.push(""); + + if (functions.length > 0) { + lines.push("| Method | Params | Returns | Auth required |", "|---|---|---|---|"); + for (const fn of functions) { + lines.push( + `| \`${fn.name}\` | ${renderParams(fn.params)} | ${renderReturns(fn.returns)} | ${fn.authRequired} |`, + ); + } + lines.push(""); + } + + if (unit.events && unit.events.length > 0) { + lines.push("| Event | Fields |", "|---|---|"); + for (const event of unit.events) { + lines.push(`| \`${event.name}\` | ${renderParams(event.params)} |`); + } + lines.push(""); + } + + if (unit.storageKeys && unit.storageKeys.length > 0) { + lines.push("| Storage key |", "|---|"); + for (const key of unit.storageKeys) lines.push(`| \`${key}\` |`); + lines.push(""); + } + + if (unit.types && unit.types.length > 0) { + lines.push("| Type | Fields |", "|---|---|"); + for (const type of unit.types) lines.push(`| \`${type.name}\` | ${renderParams(type.fields)} |`); + lines.push(""); + } + + if (unit.errors.length > 0) { + lines.push("```", ...unit.errors.map((error) => ` ${error}`), "```", ""); + } else { + lines.push("No custom errors declared.", ""); + } + + return lines; +} + +function renderParams(params: ParamSpec[] | undefined): string { + if (!params || params.length === 0) return "None"; + return params + .map((param) => `\`${param.name || "_"}: ${param.type}${param.indexed ? " (indexed)" : ""}\``) + .join("
"); +} + +function renderReturns(returns: ParamSpec[] | string | undefined): string { + if (returns === undefined) return "`void`"; + if (typeof returns === "string") return `\`${returns}\``; + return renderParams(returns); +} + +function replaceBetweenMarkers(current: string, generated: string, marker: string): string { + const startMarker = `{/* ${marker}:start */}`; + const endMarker = `{/* ${marker}:end */}`; + const start = current.indexOf(startMarker); + const end = current.indexOf(endMarker); + + if (start === -1 || end === -1 || end < start) { + const heading = current.search(/\n#{2,3} /); + if (heading === -1) { + throw new Error(`Missing ${startMarker}/${endMarker} markers and no heading to insert before.`); + } + return `${current.slice(0, heading).trimEnd()}\n\n${generated}\n\n${current.slice(heading).trimStart()}`; + } + + const before = current.slice(0, start).trimEnd(); + const after = current.slice(end + endMarker.length).trimStart(); + return `${before}\n\n${generated}\n\n${after}`; +} + +function readJson(filePath: string): T { + if (!existsSync(filePath)) { + throw new Error(`Missing spec source: ${path.relative(repoRoot, filePath)}`); + } + return JSON.parse(readFileSync(filePath, "utf8").replace(/^\uFEFF/, "")) as T; +} + +main();