diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml
index 772550b..d9e412a 100644
--- a/.github/workflows/snippets.yml
+++ b/.github/workflows/snippets.yml
@@ -247,3 +247,50 @@ jobs:
- name: Run playground smoke tests
run: pnpm run test:playground
+
+ check-stellar-reference:
+ name: Stellar contract reference 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 Stellar CLI
+ # Pinned to the same minor used during the last registry verification.
+ # Update this pin when the CLI interface changes in a way that affects
+ # `stellar contract inspect` output parsing.
+ run: |
+ curl -sSfL https://github.com/stellar/stellar-cli/releases/download/v28.1.0/stellar-cli-28.1.0-x86_64-unknown-linux-gnu.tar.gz \
+ | tar -xz -C /usr/local/bin stellar
+ stellar version
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ # Only the two contracts that currently have live testnet addresses are
+ # listed here (from scripts/contract-registry.json, stellar-testnet block).
+ # stealth-registry and stealth-sender are not yet deployed and are omitted
+ # until their addresses appear in the registry.
+ - name: Check Stellar generated reference
+ env:
+ STELLAR_CONTRACT_IDS: >-
+ {
+ "stealth-announcer": "CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL",
+ "wraith-names": "CDEMB3MAE62ZOCCKZPTYSXR5CS5WVENPOU5MDVK4PNKTZXFVDC74AFBV"
+ }
+ STELLAR_NETWORK: testnet
+ run: >-
+ pnpm exec tsx scripts/generate-stellar-reference.ts
+ --check
+ --require-contracts=stealth-announcer,wraith-names
diff --git a/contracts/stellar.mdx b/contracts/stellar.mdx
index 73e4a82..13cb856 100644
--- a/contracts/stellar.mdx
+++ b/contracts/stellar.mdx
@@ -207,7 +207,54 @@ const resolved = await namesContract.call("resolve", "alice");
## 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
+
+#### Addresses
+
+| Network | Contract ID |
+|---|---|
+| testnet | `CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL` |
+
+#### Functions
+
+| Function | Params | Return | Auth required |
+|---|---|---|---|
+| `announce` | `scheme_id: u32`
`stealth_address: soroban_sdk::Address`
`ephemeral_pub_key: soroban_sdk::BytesN<32>`
`metadata: soroban_sdk::Bytes` | `void` | Yes |
+
+#### Events
+
+None found in generated bindings.
+
+#### Storage keys
+
+None found in generated bindings.
+
+### Wraith names
+
+#### Addresses
+
+| Network | Contract ID |
+|---|---|
+| testnet | `CDEMB3MAE62ZOCCKZPTYSXR5CS5WVENPOU5MDVK4PNKTZXFVDC74AFBV` |
+
+#### Functions
+
+| Function | Params | Return | Auth required |
+|---|---|---|---|
+| `name_of` | `stealth_meta_address: soroban_sdk::Bytes` | `soroban_sdk::String` | No |
+| `register` | `owner: soroban_sdk::Address`
`name: soroban_sdk::String`
`stealth_meta_address: soroban_sdk::Bytes` | `()` | Yes |
+| `release` | `owner: soroban_sdk::Address`
`name: soroban_sdk::String` | `()` | Yes |
+| `resolve` | `name: soroban_sdk::String` | `soroban_sdk::Bytes` | No |
+| `update` | `owner: soroban_sdk::Address`
`name: soroban_sdk::String`
`new_meta_address: soroban_sdk::Bytes` | `()` | Yes |
+
+#### Events
+
+None found in generated bindings.
+
+#### Storage keys
+
+- `Name`
+- `Reverse`
{/* stellar-reference:end */}
diff --git a/package.json b/package.json
index 56929b7..81d81d2 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"test:preview-redirect": "node --test scripts/check-preview-redirect.test.mjs",
"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",
+ "check:stellar-reference": "tsx scripts/generate-stellar-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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7ac50b9..e132f48 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -702,8 +702,8 @@ packages:
resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
engines: {node: '>= 0.4'}
- toml@4.3.0:
- resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==}
+ toml@5.0.1:
+ resolution: {integrity: sha512-EqYNA/YvUNndrA2B3O2ojzLQoTQrBxeZ9UIjQwk8t10oe6n7dwclBwd9IdZQTOKwe6vWX+AXtl6IntXRbepdxQ==}
engines: {node: '>=20'}
tr46@0.0.3:
@@ -739,10 +739,6 @@ packages:
resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==}
engines: {node: '>=6.14.2'}
- uuid@11.1.1:
- resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
- hasBin: true
-
uuid@14.0.1:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
@@ -972,7 +968,7 @@ snapshots:
eventsource: 2.0.2
feaxios: 0.0.23
randombytes: 2.1.0
- toml: 4.3.0
+ toml: 5.0.1
urijs: 1.19.11
transitivePeerDependencies:
- bare-url
@@ -1311,7 +1307,7 @@ snapshots:
isomorphic-ws: 4.0.1(ws@7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6))
json-stringify-safe: 5.0.1
stream-json: 1.9.1
- uuid: 11.1.1
+ uuid: 14.0.1
ws: 7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6)
transitivePeerDependencies:
- bufferutil
@@ -1431,7 +1427,7 @@ snapshots:
safe-buffer: 5.2.1
typed-array-buffer: 1.0.3
- toml@4.3.0: {}
+ toml@5.0.1: {}
tr46@0.0.3: {}
@@ -1462,8 +1458,6 @@ snapshots:
node-gyp-build: 4.8.4
optional: true
- uuid@11.1.1: {}
-
uuid@14.0.1: {}
viem@2.54.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6):
diff --git a/scripts/generate-stellar-reference.ts b/scripts/generate-stellar-reference.ts
index 09f13ab..0680df5 100644
--- a/scripts/generate-stellar-reference.ts
+++ b/scripts/generate-stellar-reference.ts
@@ -37,10 +37,35 @@ const contractTitles: Record = {
const args = new Set(process.argv.slice(2));
const checkMode = args.has("--check");
-const allowMissing = args.has("--allow-missing");
+// --allow-missing is no longer accepted; the flag is rejected so stale CI
+// invocations surface immediately rather than silently passing.
+if (args.has("--allow-missing")) {
+ throw new Error(
+ "--allow-missing has been removed. Supply STELLAR_CONTRACT_IDS or STELLAR_BINDINGS_DIR so the check has real data.",
+ );
+}
+
+// --require-contracts=slug1,slug2 asserts that every listed slug appears in
+// the generated reference. Use this in CI to catch gaps when a new contract
+// is deployed but the docs section was never generated.
+const requireContractsFlag = [...args].find((a) => a.startsWith("--require-contracts="));
+const requiredContracts: string[] =
+ requireContractsFlag ? requireContractsFlag.slice("--require-contracts=".length).split(",").filter(Boolean) : [];
function main() {
const references = loadReferencesOrExit();
+
+ if (requiredContracts.length > 0) {
+ const generatedSlugs = new Set(references.map((r) => r.slug));
+ const missing = requiredContracts.filter((slug) => !generatedSlugs.has(slug));
+ if (missing.length > 0) {
+ throw new Error(
+ `Required contract(s) missing from generated reference: ${missing.join(", ")}.\n` +
+ "Add the contract to STELLAR_CONTRACT_IDS or STELLAR_BINDINGS_DIR and re-run.",
+ );
+ }
+ }
+
const generated = renderReference(references);
const current = readFileSync(stellarDocPath, "utf8");
const updated = replaceBetweenMarkers(current, generated);
@@ -61,11 +86,6 @@ function loadReferencesOrExit(): ContractReference[] {
try {
return loadReferences();
} catch (error) {
- if (checkMode && allowMissing) {
- const message = error instanceof Error ? error.message : String(error);
- console.warn(`Skipping Stellar reference check: ${message}`);
- process.exit(0);
- }
throw error;
}
}
@@ -304,18 +324,21 @@ function loadFromStellarInspect(): ContractReference[] {
return Object.entries(configured).map(([slug, entry]) => {
const contractId = typeof entry === "string" ? entry : entry[network] ?? entry.testnet ?? Object.values(entry)[0];
- const output = execFileSync(cli, ["contract", "inspect", "--id", contractId, "--network", network], {
- encoding: "utf8",
- stdio: ["ignore", "pipe", "pipe"],
- });
+ // `stellar contract info interface` replaced the old `contract inspect --id`
+ // in CLI v28+. stdout is clean Rust trait output; info lines go to stderr.
+ const output = execFileSync(
+ cli,
+ ["contract", "info", "interface", "--contract-id", contractId, "--network", network, "--output", "rust"],
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
+ );
return {
slug,
title: contractTitles[slug] ?? slug,
addresses: { [network]: contractId },
- functions: parseInspectFunctions(output),
- events: parseInspectList(output, "Events"),
- storageKeys: parseInspectList(output, "Storage"),
+ functions: parseInfoFunctions(output),
+ events: parseInfoEvents(output),
+ storageKeys: parseInfoStorageKeys(output),
};
});
}
@@ -337,6 +360,92 @@ function readJsonFile(filePath: string): T {
return JSON.parse(readFileSync(filePath, "utf8").replace(/^\uFEFF/, "")) as T;
}
+// ---------------------------------------------------------------------------
+// Parsers for `stellar contract info interface --output rust`
+//
+// The output is a Rust snippet with a `pub trait Contract { fn ... }` block
+// followed by contracttype/contracterror enum and struct definitions.
+// ---------------------------------------------------------------------------
+
+function parseInfoFunctions(output: string): FunctionReference[] {
+ // Extract the trait body between `pub trait Contract {` and the closing `}`.
+ const traitMatch = output.match(/pub\s+trait\s+Contract\s*\{([\s\S]*?)\n\}/);
+ if (!traitMatch) return [];
+
+ const functions: FunctionReference[] = [];
+ // Each function: `fn name(\n env: ...,\n param: Type,\n) -> ReturnType;`
+ // The fn body may span multiple lines; params are separated by `,\n`.
+ const fnPattern = /\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*?)\)\s*(?:->\s*([^;{]+))?;/g;
+ let match: RegExpExecArray | null;
+
+ while ((match = fnPattern.exec(traitMatch[1]))) {
+ const name = match[1];
+ const rawParams = match[2]
+ .split(",")
+ .map((p) => p.trim())
+ .filter(Boolean)
+ .filter((p) => !p.startsWith("env:") && p !== "env: soroban_sdk::Env");
+
+ const params = rawParams.map((p) => {
+ const colonIdx = p.indexOf(":");
+ if (colonIdx === -1) return { name: p, type: "unknown" };
+ return {
+ name: p.slice(0, colonIdx).trim(),
+ type: normalizeType(p.slice(colonIdx + 1).trim()),
+ };
+ });
+
+ const rawReturn = match[3]?.trim() ?? "void";
+ // Strip Result wrapper — docs show the success type only.
+ const returnType = normalizeType(rawReturn.replace(/^Result<([^,>]+),\s*[^>]+>$/, "$1"));
+
+ functions.push({ name, params, returnType, authRequired: inferAuth(name, params) });
+ }
+
+ return functions.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function parseInfoEvents(output: string): string[] {
+ // `stellar contract info interface` emits `#[soroban_sdk::contractevent]`
+ // annotated enums for contract events. Extract their variants.
+ const events = new Set();
+ const eventEnumPattern =
+ /#\[soroban_sdk::contractevent\][\s\S]*?pub\s+enum\s+[A-Za-z0-9_]+\s*\{([\s\S]*?)\n\}/g;
+ let match: RegExpExecArray | null;
+
+ while ((match = eventEnumPattern.exec(output))) {
+ match[1]
+ .split("\n")
+ .map((line) => line.trim().replace(/,$/, ""))
+ .filter((line) => line && !line.startsWith("//") && !line.startsWith("#"))
+ .forEach((variant) => events.add(toSnakeCase(variant.replace(/\s*\{[\s\S]*?\}/, "").trim())));
+ }
+
+ return [...events].sort();
+}
+
+function parseInfoStorageKeys(output: string): string[] {
+ // Storage key enums are tagged `#[soroban_sdk::contracttype]` and named
+ // DataKey or StorageKey by convention.
+ const keys = new Set();
+ const keyEnumPattern =
+ /#\[soroban_sdk::contracttype\][\s\S]*?pub\s+enum\s+(?:[A-Za-z0-9_]*(?:DataKey|StorageKey)[A-Za-z0-9_]*)\s*\{([\s\S]*?)\n\}/g;
+ let match: RegExpExecArray | null;
+
+ while ((match = keyEnumPattern.exec(output))) {
+ match[1]
+ .split("\n")
+ .map((line) => line.trim().replace(/,$/, "").replace(/\(.*\)$/, ""))
+ .filter((line) => line && !line.startsWith("//") && !line.startsWith("#"))
+ .forEach((variant) => keys.add(variant));
+ }
+
+ return [...keys].sort();
+}
+
+// Keep the old inspect parsers for any caller that may still reference them
+// (e.g. tests); they are no longer used by loadFromStellarInspect.
+/** @deprecated Use parseInfoFunctions instead. */
function parseInspectFunctions(output: string): FunctionReference[] {
const functions: FunctionReference[] = [];
const fnPattern = /(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*(?:->\s*([^\n]+))?/g;
@@ -364,6 +473,7 @@ function parseInspectFunctions(output: string): FunctionReference[] {
return functions;
}
+/** @deprecated Use parseInfoStorageKeys / parseInfoEvents instead. */
function parseInspectList(output: string, heading: string): string[] {
const match = output.match(new RegExp(`${heading}:?\\s*\\n([\\s\\S]*?)(?:\\n\\S|$)`, "i"));
if (!match) return [];
@@ -378,7 +488,7 @@ function renderReference(references: ContractReference[]): string {
return [
startMarker,
"",
- "",
+ "{/* This section is generated by scripts/generate-stellar-reference.ts. Do not edit by hand. */}",
"",
"## Generated contract reference",
"",