From 44ec89a8e73694093c391d16bf75a6861f0c92c0 Mon Sep 17 00:00:00 2001
From: Isaac Menichim Isreal <194775393+big6isaac@users.noreply.github.com>
Date: Fri, 25 Sep 2026 10:50:54 +0100
Subject: [PATCH] feat(docs): add a multichain contract reference drift check
The docs generated only part of the Stellar reference, and that generator
read from an external bindings directory, so nothing caught drift in the
EVM, Solana or CKB interfaces at all. There was also no record of which
spec version the docs were built from.
- Add checked-in interface sources for all four chains under
contracts/specs/: Solidity ABIs (evm.json), Anchor IDL-equivalent
programs (solana.json), CKB scripts (ckb.json), and Soroban contract
specs (stellar.json). Each records its own specVersion and the source
repository, path, and commit it was taken from.
- Add scripts/generate-contract-reference.ts, which renders every unit's
public methods, events, storage keys and custom errors into the
matching contracts/*.mdx between chain-specific markers, and stamps the
generated block with spec version, API snapshot version, source
repository/path/commit, and a removed-in-this-version line.
- Support --check to compare generated output against the committed docs
and fail on drift, so CI can enforce it.
- Add contracts/specs/api-snapshot.json as the public API baseline. The
check diffs current specs against it and fails when a public method,
event, storage key or error disappears without a matching entry in
migrations, which must name the version it was removed in and carry a
note. Additions and signature changes pass; only removals are gated.
- Wire the check into pnpm test and add a contract-reference job to the
snippet workflow.
The Stellar page previously showed only placeholder instructions; it now
carries the generated reference for all four contracts, including the
error and storage-key tables.
Note: .github/ is CODEOWNERS-gated to @truthixify, so the workflow change
needs maintainer approval. The new job only adds a check; it does not
change or relax any existing gate.
---
.github/workflows/snippets.yml | 24 ++
contracts/ckb.mdx | 73 ++++++
contracts/evm.mdx | 105 +++++++++
contracts/solana.mdx | 66 ++++++
contracts/specs/api-snapshot.json | 283 +++++++++++++++++++++++
contracts/specs/ckb.json | 87 +++++++
contracts/specs/evm.json | 196 ++++++++++++++++
contracts/specs/solana.json | 121 ++++++++++
contracts/specs/stellar.json | 139 +++++++++++
contracts/stellar.mdx | 90 +++++++-
package.json | 4 +-
scripts/generate-contract-reference.ts | 307 +++++++++++++++++++++++++
12 files changed, 1492 insertions(+), 3 deletions(-)
create mode 100644 contracts/specs/api-snapshot.json
create mode 100644 contracts/specs/ckb.json
create mode 100644 contracts/specs/evm.json
create mode 100644 contracts/specs/solana.json
create mode 100644 contracts/specs/stellar.json
create mode 100644 scripts/generate-contract-reference.ts
diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml
index 839ab78..f844bc2 100644
--- a/.github/workflows/snippets.yml
+++ b/.github/workflows/snippets.yml
@@ -35,6 +35,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
+
stellar-testnet-snippets:
name: Stellar snippet testnet validation
runs-on: ubuntu-latest
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 373fa67..2d4877e 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 7571d05..f0bb895 100644
--- a/package.json
+++ b/package.json
@@ -8,12 +8,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"
+ "test": "npm run check:snippets && npm run check:nav-coverage && 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();