From 0624f2c161dc0185fa79236ec5e6d0efe978b470 Mon Sep 17 00:00:00 2001 From: Maksym H Date: Sat, 31 Oct 2026 21:31:51 +0000 Subject: [PATCH 1/4] Publish the dotNS addresses in the Trinity stack descriptor --- packages/cli/package.json | 2 +- packages/cli/src/commands/trinity-stack.ts | 15 ++++++++ packages/network-config/package.json | 2 +- packages/network-config/src/trinity-stack.ts | 36 +++++++++++++++++++ .../tests/trinity-stack.test.ts | 26 ++++++++++++++ pnpm-lock.yaml | 15 ++++---- 6 files changed, 87 insertions(+), 9 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ea3f70..b8075d4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,7 +25,7 @@ "@polkadot/util": "13.5.9", "@polkadot/util-crypto": "^13.5.9", "@parity/ppn-network-config": "workspace:*", - "@parity/trinity-stack": "^0.1.0", + "@parity/trinity-stack": "^0.2.0", "commander": "^15.0.0", "polkadot-api": "^1.23.3", "toml": "^4.2.0" diff --git a/packages/cli/src/commands/trinity-stack.ts b/packages/cli/src/commands/trinity-stack.ts index 0ff8b05..fd60b88 100644 --- a/packages/cli/src/commands/trinity-stack.ts +++ b/packages/cli/src/commands/trinity-stack.ts @@ -67,6 +67,8 @@ export interface Settings { faucet?: string; expiresAt?: number; validFor?: number; + /** The dotNS deployment's contract addresses by contract name. */ + dotnsContracts?: Record; } /** One chain's genesis, its light spec written to disk, and its specs' hashes. */ @@ -157,6 +159,7 @@ export async function writeStack( forked, forkAt: bite ? Math.floor(Date.parse(bite.at) / 1000) : undefined, faucet: s.faucet, + dotnsContracts: s.dotnsContracts, expiresAt: s.expiresAt, validUntil: s.validFor ? Math.floor(now.getTime() / 1000) + s.validFor : undefined, }); @@ -180,6 +183,15 @@ export async function writeStack( return { written, complete: specsComplete && (!eth || chainId !== undefined) }; } +/** The address list `ppn fetch` writes from the dotns release, or undefined when there is none. */ +function readDotnsAddresses(binDir: string): Record | undefined { + try { + return JSON.parse(fs.readFileSync(path.join(binDir, 'dotns-addresses.json'), 'utf-8')) as Record; + } catch { + return undefined; + } +} + function hexSecret(value: string | undefined): Uint8Array | undefined { if (!value) return undefined; if (!/^0x[0-9a-fA-F]{64}$/.test(value)) throw new Error('PPN_STACK_SECRET must be 0x and 64 hex digits'); @@ -215,6 +227,9 @@ export async function trinityStack(ctx: ServiceContext): Promise { faucet: setting('PPN_STACK_FAUCET_URL'), expiresAt: optionalNumber('PPN_STACK_EXPIRES_AT', setting('PPN_STACK_EXPIRES_AT')), validFor: optionalNumber('PPN_STACK_VALID_FOR', setting('PPN_STACK_VALID_FOR')), + // `ppn fetch` writes the release's list only for a genesis network, so on any other one a + // file in bin/ is left over from another network. + dotnsContracts: ctx.net.dotns?.addresses ?? (ctx.net.genesis ? readDotnsAddresses(ctx.binDir) : undefined), }; // A copy must be replaced before its validUntil, or readers show the stack as stale. const refresh = Math.min( diff --git a/packages/network-config/package.json b/packages/network-config/package.json index 4d1ee26..f981717 100644 --- a/packages/network-config/package.json +++ b/packages/network-config/package.json @@ -13,7 +13,7 @@ "postpack": "rm -rf config && rm -f LICENSE" }, "dependencies": { - "@parity/trinity-stack": "^0.1.0", + "@parity/trinity-stack": "^0.2.0", "@polkadot/keyring": "^13.5.9", "@polkadot/types": "^15.10.2", "@polkadot/util": "13.5.9", diff --git a/packages/network-config/src/trinity-stack.ts b/packages/network-config/src/trinity-stack.ts index 00af6b5..f9c1bb6 100644 --- a/packages/network-config/src/trinity-stack.ts +++ b/packages/network-config/src/trinity-stack.ts @@ -38,6 +38,40 @@ export interface StackFacts { faucet?: string; expiresAt?: number; validUntil?: number; + /** The dotNS deployment's contract addresses by contract name, as dotns's release lists them. */ + dotnsContracts?: Record; +} + +/** + * dotNS's `DotnsConstants` key for each contract name in a dotns release's address list, plus + * `protocolRegistry` for the registry itself. The beacons and the pricing contract have no key. + */ +const DOTNS_KEYS: Record = { + DotnsRegistrar: 'registrar', + DotnsRegistrarController: 'controller', + DotnsRegistry: 'registry', + DotnsReverseResolver: 'reverseResolver', + PopRules: 'popRules', + StoreFactory: 'storeFactory', + DotnsResolver: 'resolver', + DotnsContentResolver: 'contentResolver', + DotnsPopController: 'popController', + DotnsPopResolver: 'popResolver', + DotnsPopLens: 'popLens', + DotnsNameEscrow: 'nameEscrow', + Multicall3: 'multicall3', + Create3Factory: 'create3Factory', + DotnsNameWhitelist: 'nameWhitelist', + DotnsCostModelRegistry: 'costModel', + DotnsProtocolRegistry: 'protocolRegistry', +}; + +/** A descriptor's `dotns` from a contract-name address list; undefined when no name maps. */ +function dotnsByKey(contracts: Record | undefined): Record | undefined { + const entries = Object.entries(contracts ?? {}).flatMap(([name, address]) => + DOTNS_KEYS[name] ? [[DOTNS_KEYS[name], address.toLowerCase()] as const] : [] + ); + return entries.length > 0 ? Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b))) : undefined; } /** The chain key a dashboard chain entry belongs to: every `relay-*` validator is the relay. */ @@ -110,6 +144,7 @@ export function trinityStackModel(net: NetworkDef, model: DashboardModel, facts: const storage = service('storage-provider-node'); if (storage) services.push({ kind: 'storage-provider', urls: [withSlash(storage.url)] }); if (facts.faucet) services.push({ kind: 'faucet', urls: [withSlash(facts.faucet)] }); + const dotns = chains.some((c) => c.role === 'AssetHub') ? dotnsByKey(facts.dotnsContracts) : undefined; return { id: facts.forked ? `${net.name}-fork` : net.name, @@ -121,5 +156,6 @@ export function trinityStackModel(net: NetworkDef, model: DashboardModel, facts: ...(fork && { fork }), chains, services, + ...(dotns && { dotns }), }; } diff --git a/packages/network-config/tests/trinity-stack.test.ts b/packages/network-config/tests/trinity-stack.test.ts index 648fe69..5b7af9c 100644 --- a/packages/network-config/tests/trinity-stack.test.ts +++ b/packages/network-config/tests/trinity-stack.test.ts @@ -72,6 +72,32 @@ describe('trinityStackModel — previewnet', () => { assert.equal(configured.expiresAt, 2000000000); assert.equal(configured.validUntil, 1900000000); }); + + // Readers look a contract up by its DotnsConstants key, so a contract name left as the release + // spells it, or a beacon with no key, would be an address nobody finds. + it('states the dotNS addresses by DotnsConstants key, lowercase, without contracts that have none', () => { + const stack = trinityStackModel( + net, + model, + facts({ + dotnsContracts: { + DotnsRegistrar: '0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab', + DotnsProtocolRegistry: '0xD19e3D0C97CF501125a04A97405e3e6592fa846E', + DotnsCostModelRegistry: '0x8bfd1f0957e73716732e725802f13830B5682da4', + Multicall3: '0xB4468000abD87D3c56cbFBd153161223D7b109e5', + LabelStoreBeacon: '0x2227d9807f5a71332aaa0640643030f2a3bf84cd', + DotnsFlatPricing: '0xD839B281dF72Df44fF275305E72cAEEc0fDAA648', + }, + }) + ); + assert.deepEqual(stack.dotns, { + costModel: '0x8bfd1f0957e73716732e725802f13830b5682da4', + multicall3: '0xb4468000abd87d3c56cbfbd153161223d7b109e5', + protocolRegistry: '0xd19e3d0c97cf501125a04a97405e3e6592fa846e', + registrar: '0x4f06e818ba3d987704fd91cf3d868e4b019106ab', + }); + assert.equal(trinityStackModel(net, model, facts()).dotns, undefined); + }); }); describe('trinityStackModel — forks', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b371566..5d46ef0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: workspace:* version: link:../network-config '@parity/trinity-stack': - specifier: ^0.1.0 - version: 0.1.0 + specifier: ^0.2.0 + version: 0.2.0 '@polkadot-labs/hdkd-helpers': specifier: ^0.0.11 version: 0.0.11 @@ -100,8 +100,8 @@ importers: packages/network-config: dependencies: '@parity/trinity-stack': - specifier: ^0.1.0 - version: 0.1.0 + specifier: ^0.2.0 + version: 0.2.0 '@polkadot/keyring': specifier: ^13.5.9 version: 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9) @@ -495,9 +495,10 @@ packages: resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} engines: {node: '>= 20.19.0'} - '@parity/trinity-stack@0.1.0': - resolution: {integrity: sha512-u/8l5I64eNg4YHdKBls/96aSdTQFWFSLaRwJpxbwAFl/Snyj09LaPd9sR6nEQ8xeGa/AgpN6dABzcbTpVqn/7g==} + '@parity/trinity-stack@0.2.0': + resolution: {integrity: sha512-BGRC4Zml/KZUcFKPBsxPYx111eNj6iZb5lhTEIovu7Bq/N7RBDf0jOeEiw2Zgy6hBEOIYwvGhrucnd8tlI39zg==} engines: {node: '>=22.3'} + hasBin: true '@polkadot-api/cli@0.18.1': resolution: {integrity: sha512-jPa8WSNPZWdy372sBAUnm0nU1XX5mLbmgkOOU39+zpYPSE12mYXyM3r7JuT5IHdAccEJr6qK2DplPFTeNSyq9A==} @@ -2385,7 +2386,7 @@ snapshots: '@noble/hashes@2.3.0': {} - '@parity/trinity-stack@0.1.0': {} + '@parity/trinity-stack@0.2.0': {} '@polkadot-api/cli@0.18.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)': dependencies: From 6050e25194c9273a92ea91758d563717fd3e486d Mon Sep 17 00:00:00 2001 From: Maksym H Date: Sat, 31 Oct 2026 21:31:52 +0000 Subject: [PATCH 2/4] Document the descriptor's dotns field --- docs/TRINITY-STACK.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/TRINITY-STACK.md b/docs/TRINITY-STACK.md index 2e6c267..82cdced 100644 --- a/docs/TRINITY-STACK.md +++ b/docs/TRINITY-STACK.md @@ -45,6 +45,13 @@ The descriptor is built from the dashboard model, so its URLs are the ones the d SHA-256. - `services`: the DUB, TURN and STUN, the IPFS gateway, Bulletin as `hop`, eth-rpc with its chain id, and the storage provider. +- `dotns`: the dotNS contract addresses by `DotnsConstants` key, such as `storeFactory`, plus + `protocolRegistry` for the registry itself, so readers take them from the descriptor rather + than read them from Asset Hub. They come from `dotns.addresses` in the network file, or else + from `bin/dotns-addresses.json` on a genesis network, the address list `ppn fetch` takes from + the dotns release. + Contracts with no key, such as the store beacons, are left out, and a network with neither + list publishes no `dotns`. - `fork` and each chain's `forkedAt`: on a fork whose spawn stamp records the bite and whose network file names the source's descriptor in `stack.pointer`. `forkedAt` is the source's block, whose hash `ppn bite` reads from the source and records as `biteHashes`: the fork's From 7964c2725f94cc76d8ceacf49b77bbe82c6e711f Mon Sep 17 00:00:00 2001 From: Maksym H Date: Sun, 1 Nov 2026 16:06:07 +0000 Subject: [PATCH 3/4] Take a fork's dotNS keys from its source descriptor and addresses from its registry --- docs/TRINITY-STACK.md | 12 ++-- packages/cli/src/commands/trinity-stack.ts | 60 +++++++++++++++++-- packages/cli/tests/trinity-stack.test.ts | 49 +++++++++++++++ packages/network-config/src/index.ts | 2 +- packages/network-config/src/trinity-stack.ts | 10 ++-- .../tests/trinity-stack.test.ts | 30 ++++------ 6 files changed, 128 insertions(+), 35 deletions(-) diff --git a/docs/TRINITY-STACK.md b/docs/TRINITY-STACK.md index 82cdced..1734b09 100644 --- a/docs/TRINITY-STACK.md +++ b/docs/TRINITY-STACK.md @@ -47,11 +47,13 @@ The descriptor is built from the dashboard model, so its URLs are the ones the d chain id, and the storage provider. - `dotns`: the dotNS contract addresses by `DotnsConstants` key, such as `storeFactory`, plus `protocolRegistry` for the registry itself, so readers take them from the descriptor rather - than read them from Asset Hub. They come from `dotns.addresses` in the network file, or else - from `bin/dotns-addresses.json` on a genesis network, the address list `ppn fetch` takes from - the dotns release. - Contracts with no key, such as the store beacons, are left out, and a network with neither - list publishes no `dotns`. + than read them from Asset Hub. + - A genesis network takes them from `bin/dotns-addresses.json`, the address list `ppn fetch` + takes from the dotns release. Contracts with no key, such as the store beacons, are left out. + - A fork takes the keys from its source's descriptor, at `stack.pointer`, and each address from + its own registry's `get(key)`, since the bite copied the source's registry. A key the registry + does not hold, such as a release's `multicall3`, keeps the source's address. While the source + descriptor or eth-rpc does not answer, the fork publishes no `dotns` and tries again in 30s. - `fork` and each chain's `forkedAt`: on a fork whose spawn stamp records the bite and whose network file names the source's descriptor in `stack.pointer`. `forkedAt` is the source's block, whose hash `ppn bite` reads from the source and records as `biteHashes`: the fork's diff --git a/packages/cli/src/commands/trinity-stack.ts b/packages/cli/src/commands/trinity-stack.ts index fd60b88..40aee98 100644 --- a/packages/cli/src/commands/trinity-stack.ts +++ b/packages/cli/src/commands/trinity-stack.ts @@ -12,6 +12,7 @@ import path from 'node:path'; import { chainKey, dashboardModel, + dotnsFromContracts, trinityStackModel, trinityStackUrl, type ChainKey, @@ -19,7 +20,7 @@ import { type StackChainFacts, type StackFacts, } from '@parity/ppn-network-config'; -import { encodeLink, lightSpecGenesis, publicKey, publish } from '@parity/trinity-stack'; +import { encodeLink, lightSpecGenesis, publicKey, publish, read, type Fetch, type Pointer } from '@parity/trinity-stack'; import type { ServiceContext } from './service-context.js'; import { rpc } from '../fork/rpc.js'; import { chainSpecNames } from '../lib/chain-specs.js'; @@ -67,8 +68,10 @@ export interface Settings { faucet?: string; expiresAt?: number; validFor?: number; - /** The dotNS deployment's contract addresses by contract name. */ + /** The dotNS deployment's contract addresses by contract name, from the dotns release. */ dotnsContracts?: Record; + /** Fetches a fork's source descriptor; a fake in tests. */ + fetch: Fetch; } /** One chain's genesis, its light spec written to disk, and its specs' hashes. */ @@ -126,6 +129,44 @@ async function ethChainId(s: Settings, directUrl: string): Promise; complete: boolean }> { + let listed: Record | undefined; + try { + listed = (await read(source, { fetch: s.fetch })).descriptor.dotns; + } catch { + return { complete: false }; + } + const registry = listed?.protocolRegistry; + if (!listed || !registry) return { complete: true }; + + const dotns: Record = {}; + for (const [key, address] of Object.entries(listed)) { + const data = REGISTRY_GET + Buffer.from(key).toString('hex').padEnd(64, '0'); + let word: string; + try { + word = await s.call(ethUrl, 'eth_call', [{ to: registry, data }, 'latest']); + } catch { + return { complete: false }; + } + const registered = `0x${word.slice(-40)}`.toLowerCase(); + dotns[key] = /^0x0{40}$/.test(registered) ? address.toLowerCase() : registered; + } + return { dotns, complete: true }; +} + /** * Writes the light specs and the descriptor once. Returns what the dashboard shows, and whether * the descriptor is complete: a relay has no checkpoint until it finalizes a block, and eth-rpc @@ -152,6 +193,12 @@ export async function writeStack( } const eth = model.services.find((e) => e.id === 'eth-rpc'); const chainId = eth && (await ethChainId(s, eth.directUrl)); + // A genesis network runs the release's dotNS; a fork runs whatever its source had at the bite. + const source = ctx.net.stack?.pointer; + const dotnsFacts = + forked && source && eth + ? await forkDotns(s, source, eth.directUrl) + : { dotns: forked ? undefined : dotnsFromContracts(s.dotnsContracts), complete: true }; const now = s.now(); const stack = trinityStackModel(ctx.net, model, { chains, @@ -159,7 +206,7 @@ export async function writeStack( forked, forkAt: bite ? Math.floor(Date.parse(bite.at) / 1000) : undefined, faucet: s.faucet, - dotnsContracts: s.dotnsContracts, + dotns: dotnsFacts.dotns, expiresAt: s.expiresAt, validUntil: s.validFor ? Math.floor(now.getTime() / 1000) + s.validFor : undefined, }); @@ -180,7 +227,7 @@ export async function writeStack( const written: StackStateFile = { generation, state: published.state, pointer, link: encodeLink(pointer) }; writeIfChanged(stateFile, new TextEncoder().encode(JSON.stringify(written, null, 2) + '\n')); const specsComplete = Object.values(chains).every((c) => !c?.fullSpec || c.lightSpec); - return { written, complete: specsComplete && (!eth || chainId !== undefined) }; + return { written, complete: specsComplete && (!eth || chainId !== undefined) && dotnsFacts.complete }; } /** The address list `ppn fetch` writes from the dotns release, or undefined when there is none. */ @@ -219,6 +266,7 @@ export async function trinityStack(ctx: ServiceContext): Promise { } const s: Settings = { call: rpc, + fetch, now: () => new Date(), baseUrl: ctx.baseUrl, dataDir: ctx.dataDir, @@ -228,8 +276,8 @@ export async function trinityStack(ctx: ServiceContext): Promise { expiresAt: optionalNumber('PPN_STACK_EXPIRES_AT', setting('PPN_STACK_EXPIRES_AT')), validFor: optionalNumber('PPN_STACK_VALID_FOR', setting('PPN_STACK_VALID_FOR')), // `ppn fetch` writes the release's list only for a genesis network, so on any other one a - // file in bin/ is left over from another network. - dotnsContracts: ctx.net.dotns?.addresses ?? (ctx.net.genesis ? readDotnsAddresses(ctx.binDir) : undefined), + // file in bin/ is left over from another network. A fork reads its source's instead. + dotnsContracts: ctx.net.genesis ? readDotnsAddresses(ctx.binDir) : undefined, }; // A copy must be replaced before its validUntil, or readers show the stack as stale. const refresh = Math.min( diff --git a/packages/cli/tests/trinity-stack.test.ts b/packages/cli/tests/trinity-stack.test.ts index 88044ea..629c92e 100644 --- a/packages/cli/tests/trinity-stack.test.ts +++ b/packages/cli/tests/trinity-stack.test.ts @@ -53,6 +53,8 @@ const failing = (failed: string): Settings['call'] => async (url, method, params let dataDir: string; const settings = (extra: Partial = {}): Settings => ({ call: fakeNodes, + // No source descriptor answers unless a test serves one. + fetch: async () => new Response(null, { status: 404 }), now: () => T0, baseUrl: 'http://127.0.0.1:8090', dataDir, @@ -155,6 +157,53 @@ describe('writeStack — what it writes', () => { assert.equal(descriptor.fork, undefined); assert.ok(descriptor.chains.every((c) => c.forkedAt === undefined)); }); + + // The bite copies the source's registry, so the fork has the source's keys; a key rewired on + // either side since then is what the fork's own registry now returns. + it("publishes its source's dotNS keys with the addresses the fork's registry returns", async () => { + const registry = '0xd19e3d0c97cf501125a04a97405e3e6592fa846e'; + const sourceDir = dataDir; + await writeStack( + { net }, + settings({ + dotnsContracts: { + DotnsProtocolRegistry: registry, + DotnsRegistrar: '0x4f06e818ba3d987704fd91cf3d868e4b019106ab', + Multicall3: '0xb4468000abd87d3c56cbfbd153161223d7b109e5', + }, + }) + ); + const sourceDocument = fs.readFileSync(path.join(sourceDir, 'trinity-stack.json')); + + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ppn-stack-')); + writeSpecs('127.0.0.1'); + stamp({ mode: 'fork', bite: bite({ hashes }) }); + const rewiredRegistrar = '0x' + '12'.repeat(20); + const forkRegistry: Record = { protocolRegistry: registry, registrar: rewiredRegistrar }; + const call: Settings['call'] = async (url, method, params = []) => { + if (method !== 'eth_call') return fakeNodes(url, method, params); + const { data } = params[0] as { data: string }; + const key = Buffer.from(data.slice(10), 'hex').toString().replace(/\0+$/, ''); + return ('0x' + (forkRegistry[key] ?? '0x' + '00'.repeat(20)).slice(2).padStart(64, '0')) as never; + }; + const fetch: Settings['fetch'] = async (url) => + url === net.stack!.pointer!.url ? new Response(sourceDocument) : new Response(null, { status: 404 }); + + const { written, complete } = await writeStack({ net }, settings({ call, fetch })); + assert.equal(complete, true); + assert.deepEqual(open(read('trinity-stack.json'), written.pointer).descriptor.dotns, { + multicall3: '0xb4468000abd87d3c56cbfbd153161223d7b109e5', + protocolRegistry: registry, + registrar: rewiredRegistrar, + }); + }); + + it('publishes a fork without dotNS while its source descriptor does not answer, and tries again', async () => { + stamp({ mode: 'fork', bite: bite({ hashes }) }); + const { written, complete } = await writeStack({ net }, settings()); + assert.equal(complete, false); + assert.equal(open(read('trinity-stack.json'), written.pointer).descriptor.dotns, undefined); + }); }); describe('writeStack — generation and sequence', () => { diff --git a/packages/network-config/src/index.ts b/packages/network-config/src/index.ts index 6ae828f..7a4b9d2 100644 --- a/packages/network-config/src/index.ts +++ b/packages/network-config/src/index.ts @@ -51,7 +51,7 @@ export type { DashboardEndpoint, DashboardIce, } from './dashboard-model.js'; -export { trinityStackModel, trinityStackUrl, chainKey } from './trinity-stack.js'; +export { trinityStackModel, trinityStackUrl, chainKey, dotnsFromContracts } from './trinity-stack.js'; export type { StackFacts, StackChainFacts } from './trinity-stack.js'; export { hrmpChannels } from './toml-generator.js'; export type { HrmpChannel } from './toml-generator.js'; diff --git a/packages/network-config/src/trinity-stack.ts b/packages/network-config/src/trinity-stack.ts index f9c1bb6..bfe21ac 100644 --- a/packages/network-config/src/trinity-stack.ts +++ b/packages/network-config/src/trinity-stack.ts @@ -38,8 +38,8 @@ export interface StackFacts { faucet?: string; expiresAt?: number; validUntil?: number; - /** The dotNS deployment's contract addresses by contract name, as dotns's release lists them. */ - dotnsContracts?: Record; + /** dotNS contract addresses by protocol registry key. */ + dotns?: Record; } /** @@ -66,8 +66,8 @@ const DOTNS_KEYS: Record = { DotnsProtocolRegistry: 'protocolRegistry', }; -/** A descriptor's `dotns` from a contract-name address list; undefined when no name maps. */ -function dotnsByKey(contracts: Record | undefined): Record | undefined { +/** A descriptor's `dotns` from a dotns release's contract-name address list; undefined when no name maps. */ +export function dotnsFromContracts(contracts: Record | undefined): Record | undefined { const entries = Object.entries(contracts ?? {}).flatMap(([name, address]) => DOTNS_KEYS[name] ? [[DOTNS_KEYS[name], address.toLowerCase()] as const] : [] ); @@ -144,7 +144,7 @@ export function trinityStackModel(net: NetworkDef, model: DashboardModel, facts: const storage = service('storage-provider-node'); if (storage) services.push({ kind: 'storage-provider', urls: [withSlash(storage.url)] }); if (facts.faucet) services.push({ kind: 'faucet', urls: [withSlash(facts.faucet)] }); - const dotns = chains.some((c) => c.role === 'AssetHub') ? dotnsByKey(facts.dotnsContracts) : undefined; + const dotns = chains.some((c) => c.role === 'AssetHub') ? facts.dotns : undefined; return { id: facts.forked ? `${net.name}-fork` : net.name, diff --git a/packages/network-config/tests/trinity-stack.test.ts b/packages/network-config/tests/trinity-stack.test.ts index 5b7af9c..46e1b6c 100644 --- a/packages/network-config/tests/trinity-stack.test.ts +++ b/packages/network-config/tests/trinity-stack.test.ts @@ -9,7 +9,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { dashboardModel } from '../src/dashboard-model.js'; import { loadDescriptorOnly } from '../src/load.js'; -import { trinityStackModel, trinityStackUrl, type StackFacts } from '../src/trinity-stack.js'; +import { dotnsFromContracts, trinityStackModel, trinityStackUrl, type StackFacts } from '../src/trinity-stack.js'; const BASE = 'https://previewnet.substrate.dev'; const WSS = 'wss://previewnet.substrate.dev'; @@ -76,27 +76,21 @@ describe('trinityStackModel — previewnet', () => { // Readers look a contract up by its DotnsConstants key, so a contract name left as the release // spells it, or a beacon with no key, would be an address nobody finds. it('states the dotNS addresses by DotnsConstants key, lowercase, without contracts that have none', () => { - const stack = trinityStackModel( - net, - model, - facts({ - dotnsContracts: { - DotnsRegistrar: '0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab', - DotnsProtocolRegistry: '0xD19e3D0C97CF501125a04A97405e3e6592fa846E', - DotnsCostModelRegistry: '0x8bfd1f0957e73716732e725802f13830B5682da4', - Multicall3: '0xB4468000abD87D3c56cbFBd153161223D7b109e5', - LabelStoreBeacon: '0x2227d9807f5a71332aaa0640643030f2a3bf84cd', - DotnsFlatPricing: '0xD839B281dF72Df44fF275305E72cAEEc0fDAA648', - }, - }) - ); - assert.deepEqual(stack.dotns, { + const dotns = dotnsFromContracts({ + DotnsRegistrar: '0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab', + DotnsProtocolRegistry: '0xD19e3D0C97CF501125a04A97405e3e6592fa846E', + DotnsCostModelRegistry: '0x8bfd1f0957e73716732e725802f13830B5682da4', + Multicall3: '0xB4468000abD87D3c56cbFBd153161223D7b109e5', + LabelStoreBeacon: '0x2227d9807f5a71332aaa0640643030f2a3bf84cd', + DotnsFlatPricing: '0xD839B281dF72Df44fF275305E72cAEEc0fDAA648', + }); + assert.deepEqual(dotns, { costModel: '0x8bfd1f0957e73716732e725802f13830b5682da4', multicall3: '0xb4468000abd87d3c56cbfbd153161223d7b109e5', protocolRegistry: '0xd19e3d0c97cf501125a04a97405e3e6592fa846e', registrar: '0x4f06e818ba3d987704fd91cf3d868e4b019106ab', }); - assert.equal(trinityStackModel(net, model, facts()).dotns, undefined); + assert.equal(dotnsFromContracts({ LabelStoreBeacon: '0x2227d9807f5a71332aaa0640643030f2a3bf84cd' }), undefined); }); }); @@ -138,7 +132,7 @@ describe('trinityStackModel — forks', () => { }); it('refuses a network with no TLD', () => { - const devnet = loadDescriptorOnly('devnet'); + const devnet = { ...loadDescriptorOnly('devnet'), stack: undefined }; assert.throws(() => trinityStackModel(devnet, dashboardModel(devnet, 'http://127.0.0.1:8090'), facts()), /no stack\.tld/); }); From 5ccbbb4df8118efa4eb0e544516cbb47c6d71579 Mon Sep 17 00:00:00 2001 From: Maksym H Date: Sun, 1 Nov 2026 16:06:07 +0000 Subject: [PATCH 4/4] Point paseo-next-v2 and devnet forks at their live descriptors --- networks/README.md | 2 +- networks/devnet.json | 6 ++++++ networks/paseo-next-v2.json | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/networks/README.md b/networks/README.md index d3a2c4b..fba503c 100644 --- a/networks/README.md +++ b/networks/README.md @@ -55,7 +55,7 @@ or `PPN_NETWORK=` in the environment of the scripts. | `dotns.resolver` | DotnsContentResolver address for `pin-bulletin-products` — per network, since every DotNS deployment has its own. Omitted → read from `bin/dotns-addresses.json` (previewnet's release artifact); still absent → the import is skipped. | | `dotns.gateway` | IPFS gateway the product bytes are fetched from. Omitted → `bite.source`, which is right where the source network's front door is also its gateway (previewnet, devnet) and wrong where it is not: paseo-next-v2's `https://dot.li` is a single-page app that returns its own HTML for every path, 200 included. | | `stack.tld` | The dotNS TLD the network's Trinity stack descriptor names. Omitted → `genesisConfig.networkSuffix`; still absent → no descriptor is written. See `docs/TRINITY-STACK.md`. | -| `stack.pointer` | `{ url, key? }` where the live network publishes its own descriptor. A fork names it as `fork.of`; omitted → the fork's descriptor carries no `fork`. | +| `stack.pointer` | `{ url, key? }` where the live network publishes its own descriptor. A fork names it as `fork.of` and takes its dotNS keys from it; omitted → the fork's descriptor carries no `fork` and no `dotns`. | `_todo` fields (at any level) are blocking stubs: human notes about what still has to be confirmed. The loader ignores them; `make bite` refuses to bite a network diff --git a/networks/devnet.json b/networks/devnet.json index f204453..31db5e7 100644 --- a/networks/devnet.json +++ b/networks/devnet.json @@ -109,5 +109,11 @@ "StoreFactory": "0xD81DC23FAa69B311C1FC553Ea63798772e7D253D", "Multicall3": "0x929EdB8d61461c29d07deC834ef747EbFDcf0B74" } + }, + "stack": { + "tld": "dot", + "pointer": { + "url": "https://stack.api.polkadotcommunity.foundation/.well-known/trinity-stack.json" + } } } diff --git a/networks/paseo-next-v2.json b/networks/paseo-next-v2.json index 011bd36..d16e603 100644 --- a/networks/paseo-next-v2.json +++ b/networks/paseo-next-v2.json @@ -83,5 +83,12 @@ "_note": "Same DotNS deployment as previewnet — the factory deployer key pins these addresses, so the resolver is at the same address on both. Verified 2026-08-19 by scanning it on paseo-asset-hub-next. The gateway is named explicitly because bite.source (https://dot.li) is the app, not an IPFS gateway.", "resolver": "0x7F74D7CD50f5a834270E2ad395a01b01891AB37d", "gateway": "https://paseo-bulletin-next-ipfs.polkadot.io" + }, + "stack": { + "tld": "paseo", + "pointer": { + "url": "https://stack-paseo-next.parity-testnet.parity.io/.well-known/trinity-stack.json", + "key": "0x2b89d8ca74aa2e11ab55f5af1168cd6929b88d1998aecd979b42cfc1078d5377" + } } }