@parity/product-sdk 0.0.0-dev.312.0 → 0.0.0-dev.329.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/address/index.d.ts +3 -1
  2. package/dist/{chunk-BRZNHTLQ.js → chunk-7GM3HUZF.js} +2 -2
  3. package/dist/chunk-7GM3HUZF.js.map +1 -0
  4. package/dist/chunk-TUXU2VPS.js +14 -0
  5. package/dist/chunk-TUXU2VPS.js.map +1 -0
  6. package/dist/{chunk-DHRCWMY5.js → chunk-VWD6HK2C.js} +29 -20
  7. package/dist/chunk-VWD6HK2C.js.map +1 -0
  8. package/dist/core/index.d.ts +50 -3
  9. package/dist/core/index.js +3 -2
  10. package/dist/{dotns-bswWAmR2.d.ts → dotns-CtmOwVQW.d.ts} +2 -2
  11. package/dist/identity/index.d.ts +118 -89
  12. package/dist/identity/index.js +200 -78
  13. package/dist/identity/index.js.map +1 -1
  14. package/dist/index.d.ts +3 -53
  15. package/dist/index.js +3 -2
  16. package/dist/nfts/index.d.ts +1 -0
  17. package/dist/nfts/index.js +3 -0
  18. package/dist/nfts/index.js.map +1 -0
  19. package/dist/react/index.d.ts +5 -5
  20. package/dist/react/index.js +4 -3
  21. package/dist/react/index.js.map +1 -1
  22. package/dist/renderer/index.d.ts +1 -0
  23. package/dist/renderer/index.js +3 -0
  24. package/dist/renderer/index.js.map +1 -0
  25. package/dist/testing/index.d.ts +2 -2
  26. package/dist/testing/index.js +3 -5
  27. package/dist/testing/index.js.map +1 -1
  28. package/dist/{types-B5Zx4RNq.d.ts → types-BpWw-OJl.d.ts} +19 -9
  29. package/package.json +22 -11
  30. package/src/core/createApp.test.ts +194 -0
  31. package/src/core/createApp.ts +35 -22
  32. package/src/core/local-storage-api.ts +74 -0
  33. package/src/core/types.ts +18 -7
  34. package/src/identity/dotns-abis.ts +44 -9
  35. package/src/identity/dotns-addresses.test.ts +536 -0
  36. package/src/identity/dotns-addresses.ts +320 -0
  37. package/src/identity/dotns-errors.ts +2 -0
  38. package/src/identity/dotns-namehash.ts +2 -2
  39. package/src/identity/dotns-registry.test.ts +158 -3
  40. package/src/identity/dotns-registry.ts +71 -73
  41. package/src/identity/dotns.ts +2 -2
  42. package/src/identity/index.test.ts +43 -0
  43. package/src/identity/index.ts +10 -6
  44. package/src/identity/types.ts +0 -19
  45. package/src/index.ts +3 -2
  46. package/src/individuality/contract.test.ts +426 -2
  47. package/src/nfts/contract.test.ts +58 -0
  48. package/src/nfts/index.ts +8 -0
  49. package/src/react/context.ts +1 -1
  50. package/src/react/index.ts +1 -1
  51. package/src/react/provider.tsx +3 -3
  52. package/src/renderer/index.ts +10 -0
  53. package/src/testing.ts +14 -11
  54. package/dist/chunk-BRZNHTLQ.js.map +0 -1
  55. package/dist/chunk-DHRCWMY5.js.map +0 -1
  56. package/src/identity/product-account.test.ts +0 -130
  57. package/src/identity/product-account.ts +0 -115
@@ -0,0 +1,320 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Where the DotNS contract addresses come from.
5
+ *
6
+ * Every entry point in `./dotns-registry.js` asks {@link resolveDotNsAddresses}
7
+ * rather than reaching for a constant, so "which address do I call?" has one
8
+ * answer and the gateway walk can plug in behind it without touching them.
9
+ */
10
+ import {
11
+ type AbiEntry,
12
+ type ContractRuntime,
13
+ QUERY_FALLBACK_ORIGIN,
14
+ createContract,
15
+ } from "@parity/product-sdk-contracts";
16
+ import { type Result, err, ok } from "@parity/result";
17
+ import {
18
+ DOTNS_ADDRESSES,
19
+ DOTNS_POP_CONTROLLER_ABI,
20
+ DOTNS_PROTOCOL_REGISTRY_ABI,
21
+ DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
22
+ } from "./dotns-abis.js";
23
+ import { DotNsError } from "./dotns-errors.js";
24
+ import type { SS58String } from "polkadot-api";
25
+ import type { DotNsClientOptions } from "./dotns-registry.js";
26
+
27
+ type HexString = `0x${string}`;
28
+
29
+ export interface DotNsAddresses {
30
+ registry: HexString;
31
+ reverseResolver: HexString;
32
+ resolver: HexString;
33
+ registrarController: HexString;
34
+ popRules: HexString;
35
+ protocolRegistry: HexString;
36
+ }
37
+
38
+ /**
39
+ * The `DotnsProtocolRegistry.get(bytes32)` key each role answers under, from
40
+ * `DotnsConstants`. Solidity's `bytes32("name")` is the UTF-8 bytes
41
+ * right-padded with zeros, which is what these literals are.
42
+ *
43
+ * Two are not the name you would guess, and guessing wrong is silent: `get`
44
+ * answers with a live address for the contract you did not want, and it
45
+ * surfaces much later as a revert. `registrarController` is the commit-reveal
46
+ * controller, key `controller` — `registrar` is the ERC-721 holding name
47
+ * ownership. `resolver` is the forward address resolver — `contentResolver`
48
+ * holds contenthash and text records, which nothing here calls.
49
+ *
50
+ * `protocolRegistry` has no key at all, so it cannot be looked up in the
51
+ * registry it is. The walk reaches it via `popController.protocolRegistry()`.
52
+ */
53
+ export const DOTNS_REGISTRY_KEYS = {
54
+ registry: "0x7265676973747279000000000000000000000000000000000000000000000000",
55
+ registrarController: "0x636f6e74726f6c6c657200000000000000000000000000000000000000000000",
56
+ reverseResolver: "0x726576657273655265736f6c7665720000000000000000000000000000000000",
57
+ resolver: "0x7265736f6c766572000000000000000000000000000000000000000000000000",
58
+ popRules: "0x706f7052756c6573000000000000000000000000000000000000000000000000",
59
+ } as const satisfies Record<Exclude<keyof DotNsAddresses, "protocolRegistry">, HexString>;
60
+
61
+ export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
62
+
63
+ export function isZero(addr: unknown): boolean {
64
+ return typeof addr === "string" && addr.toLowerCase() === ZERO_ADDRESS;
65
+ }
66
+
67
+ /** viem checksums every decoded `address`, so a read is EIP-55 whatever the chain sent. */
68
+ export function sameAddress(a: unknown, b: unknown): boolean {
69
+ return typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
70
+ }
71
+
72
+ /** A per-runtime, per-key cache of chain reads that cannot change under us. */
73
+ export type RuntimeCache<T> = WeakMap<ContractRuntime, Map<string, Promise<Result<T, DotNsError>>>>;
74
+
75
+ /**
76
+ * Read `key` once per runtime and hand every later caller the same answer.
77
+ *
78
+ * Stores the in-flight promise rather than the resolved value, so concurrent
79
+ * first calls share one read instead of racing. A failed read is dropped from
80
+ * the cache: those are usually the RPC blinking rather than the deployment
81
+ * moving, and keeping one would strand the client for the life of the runtime.
82
+ */
83
+ export function cachedPerRuntime<T>(
84
+ cache: RuntimeCache<T>,
85
+ runtime: ContractRuntime,
86
+ key: string,
87
+ load: () => Promise<Result<T, DotNsError>>,
88
+ ): Promise<Result<T, DotNsError>> {
89
+ let byKey = cache.get(runtime);
90
+ if (!byKey) {
91
+ byKey = new Map();
92
+ cache.set(runtime, byKey);
93
+ }
94
+ const cached = byKey.get(key);
95
+ if (cached) return cached;
96
+
97
+ const pending = load();
98
+ byKey.set(key, pending);
99
+ pending.then((result) => {
100
+ if (!result.ok) byKey?.delete(key);
101
+ });
102
+ return pending;
103
+ }
104
+
105
+ function withOverrides(base: DotNsAddresses, opts: DotNsClientOptions): DotNsAddresses {
106
+ return {
107
+ registry: opts.registryAddress ?? base.registry,
108
+ reverseResolver: opts.reverseResolverAddress ?? base.reverseResolver,
109
+ resolver: opts.resolverAddress ?? base.resolver,
110
+ registrarController: opts.registrarControllerAddress ?? base.registrarController,
111
+ popRules: opts.popRulesAddress ?? base.popRules,
112
+ protocolRegistry: opts.protocolRegistryAddress ?? base.protocolRegistry,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Async because the discovered source reads the chain. The pinned source does
118
+ * no IO, so a caller that only validates a name still pays nothing for it.
119
+ */
120
+ const walkCache: RuntimeCache<DotNsAddresses> = new WeakMap();
121
+
122
+ /**
123
+ * Constant, unlike the TLD cache which keys on the protocol registry address.
124
+ * Nothing else selects the deployment: `gatewayApi` is required to describe the
125
+ * same chain as `runtime`, and the walk derives every address from there.
126
+ */
127
+ const WALK_KEY = "discovered";
128
+
129
+ export async function resolveDotNsAddresses(
130
+ opts: DotNsClientOptions,
131
+ ): Promise<Result<DotNsAddresses, DotNsError>> {
132
+ if (opts.addressSource !== "discovered") return ok(withOverrides(DOTNS_ADDRESSES, opts));
133
+
134
+ // Cache the walk, not the merged set: two callers differing only in their
135
+ // overrides must share one walk and still each get their own answer.
136
+ const walked = await cachedPerRuntime(walkCache, opts.runtime, WALK_KEY, () =>
137
+ discoverDotNsAddresses(opts),
138
+ );
139
+ return walked.ok ? ok(withOverrides(walked.value, opts)) : walked;
140
+ }
141
+
142
+ /**
143
+ * Reads `DotnsGateway.DispatcherAddress`, the pallet storage the walk starts
144
+ * from. Structural because `ContractRuntime` does not declare it.
145
+ */
146
+ export type DotNsGatewayQueryApi = {
147
+ query: {
148
+ DotnsGateway: {
149
+ DispatcherAddress: {
150
+ getValue: () => Promise<{ asHex(): string } | string | undefined>;
151
+ };
152
+ };
153
+ };
154
+ };
155
+
156
+ const discoveryFailed = (step: string, cause?: unknown) =>
157
+ err(new DotNsError("AddressDiscovery", `DotNS address discovery failed: ${step}`, { cause }));
158
+
159
+ /**
160
+ * `createContract` is generic over a typed ABI def; these minimal literal ABIs
161
+ * are called by name via `.query()`, so the handle is untyped by construction.
162
+ */
163
+ export function contractOf(runtime: ContractRuntime, address: HexString, abi: AbiEntry[]) {
164
+ return createContract(runtime, address, abi as any) as any;
165
+ }
166
+
167
+ /**
168
+ * Origin for a read. Passing the fallback explicitly rather than letting the
169
+ * contracts layer substitute it keeps each query from logging a warning.
170
+ */
171
+ export function readOrigin(opts: DotNsClientOptions): SS58String {
172
+ return opts.origin ?? QUERY_FALLBACK_ORIGIN;
173
+ }
174
+
175
+ async function readAddress(
176
+ contract: {
177
+ [m: string]: { query: (o: object) => Promise<{ success: boolean; value: unknown }> };
178
+ },
179
+ method: string,
180
+ origin: string,
181
+ ): Promise<Result<HexString, DotNsError>> {
182
+ const res = await contract[method].query({ origin });
183
+ if (!res.success) return discoveryFailed(`${method}() failed`, res.value);
184
+ const value = res.value;
185
+ if (typeof value !== "string" || isZero(value)) {
186
+ return discoveryFailed(`${method}() returned no address`);
187
+ }
188
+ return ok(value as HexString);
189
+ }
190
+
191
+ /** PAPI hands back H160 as a wrapper or a hex string depending on the major. */
192
+ function asH160(value: unknown): HexString | null {
193
+ if (typeof value === "string") return value as HexString;
194
+ if (value && typeof (value as { asHex?: unknown }).asHex === "function") {
195
+ return (value as { asHex(): string }).asHex() as HexString;
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function gatewayOf(opts: DotNsClientOptions): DotNsGatewayQueryApi | null {
201
+ if (opts.gatewayApi) return opts.gatewayApi;
202
+ const api = opts.runtime?.api as unknown as Partial<DotNsGatewayQueryApi> | undefined;
203
+ const getValue = api?.query?.DotnsGateway?.DispatcherAddress?.getValue;
204
+ return typeof getValue === "function" ? (api as DotNsGatewayQueryApi) : null;
205
+ }
206
+
207
+ /**
208
+ * Locate the live deployment from chain state, trusting nothing pinned here.
209
+ *
210
+ * Pallet -> `dispatcher.TARGET()` -> `popController.protocolRegistry()` ->
211
+ * `get(key)` per role. The first three are sequential because each answer is
212
+ * the next call's address; the role reads are not, so they go together.
213
+ *
214
+ * A failed or zero read is always an error: falling back to the pinned table
215
+ * would recreate the situation this exists to detect, and hide it better.
216
+ */
217
+ export async function discoverDotNsAddresses(
218
+ opts: DotNsClientOptions,
219
+ ): Promise<Result<DotNsAddresses, DotNsError>> {
220
+ try {
221
+ const gateway = gatewayOf(opts);
222
+ if (!gateway) {
223
+ return discoveryFailed(
224
+ "no DotnsGateway.DispatcherAddress storage on this runtime; pass opts.gatewayApi",
225
+ );
226
+ }
227
+ const dispatcher = asH160(await gateway.query.DotnsGateway.DispatcherAddress.getValue());
228
+ if (!dispatcher || isZero(dispatcher)) {
229
+ return discoveryFailed("DotnsGateway.DispatcherAddress is unset");
230
+ }
231
+
232
+ const origin = readOrigin(opts);
233
+ const target = await readAddress(
234
+ contractOf(opts.runtime, dispatcher, DOTNS_ROOT_GATEWAY_DISPATCHER_ABI),
235
+ "TARGET",
236
+ origin,
237
+ );
238
+ if (!target.ok) return target;
239
+
240
+ const protocolRegistry = await readAddress(
241
+ contractOf(opts.runtime, target.value, DOTNS_POP_CONTROLLER_ABI),
242
+ "protocolRegistry",
243
+ origin,
244
+ );
245
+ if (!protocolRegistry.ok) return protocolRegistry;
246
+
247
+ const registry = contractOf(
248
+ opts.runtime,
249
+ protocolRegistry.value,
250
+ DOTNS_PROTOCOL_REGISTRY_ABI,
251
+ );
252
+ const roles = Object.entries(DOTNS_REGISTRY_KEYS) as [
253
+ keyof typeof DOTNS_REGISTRY_KEYS,
254
+ HexString,
255
+ ][];
256
+ // Not readAddress: that calls the method with no arguments, and these
257
+ // need the role name in the error rather than the method name, since
258
+ // every one of them is the same `get`.
259
+ const found = await Promise.all(
260
+ roles.map(async ([role, key]) => {
261
+ const res = await registry.get.query(key, { origin });
262
+ if (!res.success) return discoveryFailed(`get(${role}) failed`, res.value);
263
+ const value = res.value;
264
+ // A wrong key is not an error on chain: it returns address(0),
265
+ // so the role name is the only clue the caller gets.
266
+ if (typeof value !== "string" || isZero(value)) {
267
+ return discoveryFailed(`${role} is unset on the protocol registry`);
268
+ }
269
+ return ok([role, value as HexString] as const);
270
+ }),
271
+ );
272
+ const failed = found.find((r) => !r.ok);
273
+ if (failed && !failed.ok) return failed;
274
+
275
+ const byRole = Object.fromEntries(found.flatMap((r) => (r.ok ? [r.value] : []))) as Omit<
276
+ DotNsAddresses,
277
+ "protocolRegistry"
278
+ >;
279
+ return ok({ ...byRole, protocolRegistry: protocolRegistry.value });
280
+ } catch (cause) {
281
+ return discoveryFailed("unexpected error", cause);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Walk the gateway and report every role whose live address differs from the
287
+ * one this client would call. Meant for startup: it fails loudly once, rather
288
+ * than making every later read pay for the check.
289
+ */
290
+ export async function verifyDotNsAddresses(
291
+ opts: DotNsClientOptions,
292
+ ): Promise<Result<DotNsAddresses, DotNsError>> {
293
+ // Through the cache, so verifying at startup and then using the client is
294
+ // one walk rather than two.
295
+ const discovered = await cachedPerRuntime(walkCache, opts.runtime, WALK_KEY, () =>
296
+ discoverDotNsAddresses(opts),
297
+ );
298
+ if (!discovered.ok) return discovered;
299
+
300
+ // Compare what this client actually calls, which is the pinned table or the
301
+ // walk depending on the source, with the caller's overrides on top either
302
+ // way. A discovered client can still drift, through an override that
303
+ // disagrees with the chain; it is only the pinned base that cannot apply.
304
+ const inUse = withOverrides(
305
+ opts.addressSource === "discovered" ? discovered.value : DOTNS_ADDRESSES,
306
+ opts,
307
+ );
308
+ const drift = (Object.keys(inUse) as (keyof DotNsAddresses)[])
309
+ .filter((role) => !sameAddress(inUse[role], discovered.value[role]))
310
+ .map((role) => `${role}: using ${inUse[role]}, chain says ${discovered.value[role]}`);
311
+
312
+ return drift.length
313
+ ? err(
314
+ new DotNsError(
315
+ "AddressMismatch",
316
+ `DotNS addresses disagree with the deployment — ${drift.join("; ")}`,
317
+ ),
318
+ )
319
+ : ok(inUse);
320
+ }
@@ -18,6 +18,8 @@ export type DotNsErrorReason =
18
18
  | "NameUnavailable" // already registered
19
19
  | "NameReserved" // governance-held, or a base stem held by another user
20
20
  | "OwnerStatusInsufficient" // the owner lacks the personhood tier the label requires
21
+ | "AddressDiscovery" // the gateway walk could not locate the deployment
22
+ | "AddressMismatch" // a discovered address disagrees with the one in use
21
23
  | "RegistryCall" // the on-chain contract call failed
22
24
  | "Decode"; // the contract returned something we couldn't decode
23
25
 
@@ -13,8 +13,8 @@
13
13
  * node("bob.alice.paseo") = node(node(tldNode, "alice"), "bob")
14
14
  *
15
15
  * The root is **per network**, not a constant. `DotnsProtocolRegistry.initialize`
16
- * fixes it once from a deploy-time label — `.dot` on Paseo Asset Hub Previewnet,
17
- * `.paseo` on Next V2 — and publishes it as `tld()` / `tldNode()`. Hashing under
16
+ * fixes it once from a deploy-time label — `.paseo` on Paseo Asset Hub Next V2,
17
+ * `.test` on Previewnet — and publishes it as `tld()` / `tldNode()`. Hashing under
18
18
  * the wrong root yields a node the chain has never written to, so every lookup
19
19
  * reports "unregistered" with no error anywhere. That is why {@link namehash}
20
20
  * takes the root rather than assuming one; `resolveTld` in `./dotns-registry.js`
@@ -14,6 +14,7 @@ import { ss58ToH160 } from "@parity/product-sdk-address";
14
14
  import type { SS58String } from "polkadot-api";
15
15
  import { describe, expect, test } from "vitest";
16
16
  import {
17
+ DOTNS_POP_CONTROLLER_ABI,
17
18
  DOTNS_POP_RULES_ABI,
18
19
  DOTNS_PROTOCOL_REGISTRY_ABI,
19
20
  DOTNS_REGISTRAR_CONTROLLER_ABI,
@@ -21,9 +22,11 @@ import {
21
22
  DOTNS_RESOLVER_ABI,
22
23
  DOTNS_RESOLVER_WRITE_ABI,
23
24
  DOTNS_REVERSE_RESOLVER_ABI,
25
+ DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
24
26
  DOTNS_ADDRESSES,
25
27
  POP_STATUS,
26
28
  } from "./dotns-abis.js";
29
+ import { DOTNS_REGISTRY_KEYS, type DotNsGatewayQueryApi } from "./dotns-addresses.js";
27
30
  import { DotNsError } from "./dotns-errors.js";
28
31
  import {
29
32
  isDotNsAvailable,
@@ -854,7 +857,7 @@ describe("resolveTld", () => {
854
857
 
855
858
  test("an absent getter falls back to .dot, because that TLD was compiled in", async () => {
856
859
  // Pre-b4096968 deployments had no tld() and no way to be anything but
857
- // `.dot`. Verified against Paseo Asset Hub Previewnet.
860
+ // `.dot`. No live deployment is known to still be one.
858
861
  const runtime = registryRuntime({ tld: NO_GETTER, tldNode: NO_GETTER });
859
862
  expect(await resolveTld({ runtime })).toEqual({ ok: true, value: DOT_TLD });
860
863
  });
@@ -1052,8 +1055,8 @@ describe("the deployment's TLD reaches every entry point", () => {
1052
1055
  });
1053
1056
 
1054
1057
  test("a legacy deployment with no tld() getter still resolves .dot names", async () => {
1055
- // Paseo Asset Hub Previewnet: both getters revert empty, and `dim2` is
1056
- // owned there under the `.dot` root. The fix must not break it.
1058
+ // Guards the pre-b4096968 shape, which Previewnet was thought to have
1059
+ // until it reported `.test`. Kept: nothing proves the shape is extinct.
1057
1060
  const runtime = createFakeContractRuntime({
1058
1061
  abi: [...ALL_ABIS, ...DOTNS_PROTOCOL_REGISTRY_ABI],
1059
1062
  onQuery: ({ functionName }) => {
@@ -1152,3 +1155,155 @@ describe("the resolver pointer is an allowlist, not a denylist", () => {
1152
1155
  expect(r).toEqual({ ok: true, value: { address: TARGET, name: "dim2.dot", owner: OWNER } });
1153
1156
  });
1154
1157
  });
1158
+
1159
+ describe("the discovered addresses reach every entry point", () => {
1160
+ // The wiring these tests exist for: a client asking for discovery must not
1161
+ // then read the TLD, or any name, from the pinned table. Reading one from
1162
+ // each source is the same mismatch the issue is about, one layer down.
1163
+ const DISPATCHER = `0x${"b1".repeat(20)}`;
1164
+ const POP_CONTROLLER = `0x${"b2".repeat(20)}`;
1165
+ const D_REGISTRY = `0x${"a1".repeat(20)}`;
1166
+ const D_RESOLVER = `0x${"a3".repeat(20)}`;
1167
+ const D_CONTROLLER = `0x${"a4".repeat(20)}`;
1168
+ const D_POP_RULES = `0x${"a5".repeat(20)}`;
1169
+ const D_PROTOCOL_REGISTRY = `0x${"a6".repeat(20)}`;
1170
+ const D_REVERSE = `0x${"a2".repeat(20)}`;
1171
+
1172
+ const DISCOVERED: Record<string, string> = {
1173
+ [DOTNS_REGISTRY_KEYS.registry]: D_REGISTRY,
1174
+ [DOTNS_REGISTRY_KEYS.reverseResolver]: D_REVERSE,
1175
+ [DOTNS_REGISTRY_KEYS.resolver]: D_RESOLVER,
1176
+ [DOTNS_REGISTRY_KEYS.registrarController]: D_CONTROLLER,
1177
+ [DOTNS_REGISTRY_KEYS.popRules]: D_POP_RULES,
1178
+ };
1179
+
1180
+ const gatewayApi = {
1181
+ query: { DotnsGateway: { DispatcherAddress: { getValue: async () => DISPATCHER } } },
1182
+ } as DotNsGatewayQueryApi;
1183
+
1184
+ /**
1185
+ * A chain where the discovered contracts answer and the pinned ones stay
1186
+ * silent, so anything still calling a pinned address reads `undefined`.
1187
+ */
1188
+ function discoveringRuntime(answers: Record<string, unknown> = {}) {
1189
+ return createFakeContractRuntime({
1190
+ abi: [
1191
+ ...ALL_ABIS,
1192
+ ...DOTNS_PROTOCOL_REGISTRY_ABI,
1193
+ ...DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
1194
+ ...DOTNS_POP_CONTROLLER_ABI,
1195
+ ],
1196
+ onQuery: ({ dest, functionName, args }) => {
1197
+ const at = dest.toLowerCase();
1198
+ if (functionName === "TARGET") return POP_CONTROLLER;
1199
+ if (functionName === "protocolRegistry") return D_PROTOCOL_REGISTRY;
1200
+ if (functionName === "get") return DISCOVERED[String(args?.[0])] ?? ZERO;
1201
+ if (at !== D_PROTOCOL_REGISTRY.toLowerCase() && functionName === "tld") {
1202
+ return undefined; // only the discovered registry knows the TLD
1203
+ }
1204
+ if (functionName === "tld" || functionName === "tldNode") {
1205
+ return functionName === "tld" ? ".paseo" : dotNsTld(".paseo").node;
1206
+ }
1207
+ if (functionName && functionName in answers) return answers[functionName];
1208
+ return undefined;
1209
+ },
1210
+ });
1211
+ }
1212
+
1213
+ const destsFor = (runtime: ReturnType<typeof discoveringRuntime>, fn: string) =>
1214
+ runtime.calls.filter((c) => c.functionName === fn).map((c) => c.dest.toLowerCase());
1215
+
1216
+ test("resolveTld reads the TLD from the discovered protocol registry", async () => {
1217
+ const runtime = discoveringRuntime();
1218
+ const r = await resolveTld({ runtime, gatewayApi, addressSource: "discovered" });
1219
+ expect(r).toEqual({ ok: true, value: dotNsTld(".paseo") });
1220
+ expect(destsFor(runtime, "tld")).toEqual([D_PROTOCOL_REGISTRY.toLowerCase()]);
1221
+ expect(destsFor(runtime, "tld")).not.toContain(
1222
+ DOTNS_ADDRESSES.protocolRegistry.toLowerCase(),
1223
+ );
1224
+ });
1225
+
1226
+ test("resolveDotNs reads the discovered registry", async () => {
1227
+ const runtime = discoveringRuntime({ owner: OWNER, resolver: ZERO });
1228
+ const r = await resolveDotNs("dim2.paseo", {
1229
+ runtime,
1230
+ gatewayApi,
1231
+ addressSource: "discovered",
1232
+ });
1233
+ expect(r).toEqual({ ok: true, value: { name: "dim2.paseo", owner: OWNER } });
1234
+ expect(destsFor(runtime, "owner")).toEqual([D_REGISTRY.toLowerCase()]);
1235
+ });
1236
+
1237
+ test("resolveDotNs asks the discovered forward resolver", async () => {
1238
+ const runtime = discoveringRuntime({
1239
+ owner: OWNER,
1240
+ resolver: D_RESOLVER,
1241
+ addressOf: TARGET,
1242
+ });
1243
+ const r = await resolveDotNs("dim2.paseo", {
1244
+ runtime,
1245
+ gatewayApi,
1246
+ addressSource: "discovered",
1247
+ });
1248
+ expect(r).toEqual({
1249
+ ok: true,
1250
+ value: { address: TARGET, name: "dim2.paseo", owner: OWNER },
1251
+ });
1252
+ expect(destsFor(runtime, "addressOf")).toEqual([D_RESOLVER.toLowerCase()]);
1253
+ });
1254
+
1255
+ test("reverseDotNs reads the discovered reverse resolver", async () => {
1256
+ const runtime = discoveringRuntime({ nameOf: "dim2.paseo" });
1257
+ const r = await reverseDotNs(OWNER, { runtime, gatewayApi, addressSource: "discovered" });
1258
+ expect(r).toEqual({ ok: true, value: "dim2.paseo" });
1259
+ expect(destsFor(runtime, "nameOf")).toEqual([D_REVERSE.toLowerCase()]);
1260
+ });
1261
+
1262
+ test("isDotNsAvailable reads the discovered controller and pop rules", async () => {
1263
+ const runtime = discoveringRuntime({ available: true, classifyName: OPEN });
1264
+ const r = await isDotNsAvailable("dim2.paseo", {
1265
+ runtime,
1266
+ gatewayApi,
1267
+ addressSource: "discovered",
1268
+ });
1269
+ expect(r).toEqual({ ok: true, value: true });
1270
+ expect(destsFor(runtime, "available")).toEqual([D_CONTROLLER.toLowerCase()]);
1271
+ expect(destsFor(runtime, "classifyName")).toEqual([D_POP_RULES.toLowerCase()]);
1272
+ });
1273
+
1274
+ test("setDotNsRecord writes through the discovered registry and resolver", async () => {
1275
+ const runtime = discoveringRuntime({ resolver: ZERO });
1276
+ const r = await setDotNsRecord(
1277
+ { name: "dim2.paseo", address: TARGET },
1278
+ { runtime, gatewayApi, addressSource: "discovered", origin: SIGNER },
1279
+ );
1280
+ expect(r.ok).toBe(true);
1281
+ expect(destsFor(runtime, "resolver")).toEqual([D_REGISTRY.toLowerCase()]);
1282
+ });
1283
+
1284
+ test("prepareDotNsRegistration reads the discovered controller", async () => {
1285
+ const runtime = discoveringRuntime({
1286
+ available: true,
1287
+ makeCommitment: `0x${"ab".repeat(32)}`,
1288
+ priceWithoutCheck: [1n, POP_STATUS.NoStatus, POP_STATUS.PopFull, ""],
1289
+ transferFloor: 0n,
1290
+ minCommitmentAge: 60n,
1291
+ maxCommitmentAge: 86400n,
1292
+ });
1293
+ const r = await prepareDotNsRegistration(
1294
+ { name: "dim2.paseo", owner: OWNER },
1295
+ { runtime, gatewayApi, addressSource: "discovered", origin: SIGNER },
1296
+ );
1297
+ expect(r.ok).toBe(true);
1298
+ expect(destsFor(runtime, "available")).toEqual([D_CONTROLLER.toLowerCase()]);
1299
+ expect(destsFor(runtime, "priceWithoutCheck")).toEqual([D_POP_RULES.toLowerCase()]);
1300
+ });
1301
+
1302
+ test("the walk runs once, no matter how many entry points are called", async () => {
1303
+ const runtime = discoveringRuntime({ owner: OWNER, resolver: ZERO, nameOf: "dim2.paseo" });
1304
+ const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
1305
+ await resolveDotNs("dim2.paseo", opts);
1306
+ await reverseDotNs(OWNER, opts);
1307
+ expect(runtime.calls.filter((c) => c.functionName === "TARGET")).toHaveLength(1);
1308
+ });
1309
+ });