@parity/product-sdk 0.22.0 → 0.23.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.
- package/dist/chunk-BRZNHTLQ.js +89 -0
- package/dist/chunk-BRZNHTLQ.js.map +1 -0
- package/dist/{chunk-6BI66XUJ.js → chunk-DHRCWMY5.js} +9 -10
- package/dist/chunk-DHRCWMY5.js.map +1 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/dotns-bswWAmR2.d.ts +136 -0
- package/dist/identity/index.d.ts +502 -45
- package/dist/identity/index.js +800 -32
- package/dist/identity/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/{types-DTgyMIi0.d.ts → types-B5Zx4RNq.d.ts} +1 -1
- package/package.json +10 -10
- package/src/core/createApp.ts +10 -8
- package/src/identity/dotns-abis.test.ts +43 -0
- package/src/identity/dotns-abis.ts +309 -0
- package/src/identity/dotns-addresses.test.ts +536 -0
- package/src/identity/dotns-addresses.ts +320 -0
- package/src/identity/dotns-errors.ts +36 -0
- package/src/identity/dotns-namehash.test.ts +155 -0
- package/src/identity/dotns-namehash.ts +149 -0
- package/src/identity/dotns-owner.test.ts +92 -0
- package/src/identity/dotns-registry.test.ts +1309 -0
- package/src/identity/dotns-registry.ts +868 -0
- package/src/identity/dotns.test.ts +128 -0
- package/src/identity/dotns.ts +91 -78
- package/src/identity/index.test.ts +43 -0
- package/src/identity/index.ts +40 -10
- package/src/identity/types.ts +29 -25
- package/src/individuality/contract.test.ts +272 -2
- package/dist/chunk-6BI66XUJ.js.map +0 -1
- package/dist/chunk-BIKT6X54.js +0 -70
- package/dist/chunk-BIKT6X54.js.map +0 -1
- package/dist/dotns-DIaptENj.d.ts +0 -167
- package/src/identity/product-account.test.ts +0 -129
- package/src/identity/product-account.ts +0 -115
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// The gateway walk and the startup check. Contract hops run through
|
|
5
|
+
// `createFakeContractRuntime`, which uses the real codec and no chain; the
|
|
6
|
+
// pallet read is a hand-written object, since `ContractRuntime` does not
|
|
7
|
+
// declare it.
|
|
8
|
+
//
|
|
9
|
+
// Every fake here answers on `(dest, functionName)`, not on `functionName`
|
|
10
|
+
// alone. The walk calls three different contracts and two of them would
|
|
11
|
+
// otherwise answer for each other.
|
|
12
|
+
import { type AbiEntry, type ContractRuntime, createContract } from "@parity/product-sdk-contracts";
|
|
13
|
+
import { createFakeContractRuntime, fakeDryRunResult } from "@parity/product-sdk-contracts/testing";
|
|
14
|
+
import { describe, expect, test } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
DOTNS_ADDRESSES,
|
|
17
|
+
DOTNS_POP_CONTROLLER_ABI,
|
|
18
|
+
DOTNS_PROTOCOL_REGISTRY_ABI,
|
|
19
|
+
DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
|
|
20
|
+
} from "./dotns-abis.js";
|
|
21
|
+
import {
|
|
22
|
+
DOTNS_REGISTRY_KEYS,
|
|
23
|
+
sameAddress,
|
|
24
|
+
type DotNsAddresses,
|
|
25
|
+
type DotNsGatewayQueryApi,
|
|
26
|
+
discoverDotNsAddresses,
|
|
27
|
+
resolveDotNsAddresses,
|
|
28
|
+
verifyDotNsAddresses,
|
|
29
|
+
} from "./dotns-addresses.js";
|
|
30
|
+
|
|
31
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
32
|
+
|
|
33
|
+
/** A distinct H160, so a test can never pass by confusing two roles. */
|
|
34
|
+
const addr = (byte: string) => `0x${byte.repeat(20)}` as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What the fake chain reports. Deliberately disjoint from `DOTNS_ADDRESSES`:
|
|
38
|
+
* if a pinned value leaks into a discovered result, these assertions catch it.
|
|
39
|
+
*/
|
|
40
|
+
const DISCOVERED = {
|
|
41
|
+
registry: addr("a1"),
|
|
42
|
+
reverseResolver: addr("a2"),
|
|
43
|
+
resolver: addr("a3"),
|
|
44
|
+
registrarController: addr("a4"),
|
|
45
|
+
popRules: addr("a5"),
|
|
46
|
+
protocolRegistry: addr("a6"),
|
|
47
|
+
} satisfies DotNsAddresses;
|
|
48
|
+
|
|
49
|
+
const DISPATCHER = addr("b1");
|
|
50
|
+
const POP_CONTROLLER = addr("b2");
|
|
51
|
+
|
|
52
|
+
const WALK_ABIS: AbiEntry[] = [
|
|
53
|
+
...DOTNS_PROTOCOL_REGISTRY_ABI,
|
|
54
|
+
...DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
|
|
55
|
+
...DOTNS_POP_CONTROLLER_ABI,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const ROLE_BY_KEY = new Map(
|
|
59
|
+
Object.entries(DOTNS_REGISTRY_KEYS).map(([role, key]) => [key as string, role]),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
interface WalkOptions {
|
|
63
|
+
/** What the pallet reports. `undefined` models storage with no value set. */
|
|
64
|
+
dispatcher?: unknown;
|
|
65
|
+
target?: unknown;
|
|
66
|
+
protocolRegistry?: unknown;
|
|
67
|
+
get?: Partial<Record<keyof DotNsAddresses, unknown>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A chain that answers the whole walk. Every hop is overridable so a test can
|
|
72
|
+
* break exactly one of them and leave the rest working.
|
|
73
|
+
*/
|
|
74
|
+
function walkChain(options: WalkOptions = {}) {
|
|
75
|
+
const target = options.target ?? POP_CONTROLLER;
|
|
76
|
+
const registry = options.protocolRegistry ?? DISCOVERED.protocolRegistry;
|
|
77
|
+
|
|
78
|
+
const runtime = createFakeContractRuntime({
|
|
79
|
+
abi: WALK_ABIS,
|
|
80
|
+
onQuery: ({ dest, functionName, args }) => {
|
|
81
|
+
const at = dest.toLowerCase();
|
|
82
|
+
if (functionName === "TARGET" && at === DISPATCHER.toLowerCase()) return target;
|
|
83
|
+
if (functionName === "protocolRegistry" && at === POP_CONTROLLER.toLowerCase()) {
|
|
84
|
+
return registry;
|
|
85
|
+
}
|
|
86
|
+
if (functionName === "get" && at === String(registry).toLowerCase()) {
|
|
87
|
+
const role = ROLE_BY_KEY.get(String(args?.[0]));
|
|
88
|
+
if (!role) return ZERO; // an unknown key is what a wrong key looks like
|
|
89
|
+
const override = options.get?.[role as keyof DotNsAddresses];
|
|
90
|
+
return override ?? DISCOVERED[role as keyof DotNsAddresses];
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const dispatcher = "dispatcher" in options ? options.dispatcher : DISPATCHER;
|
|
97
|
+
const gatewayApi = {
|
|
98
|
+
query: {
|
|
99
|
+
DotnsGateway: { DispatcherAddress: { getValue: async () => dispatcher } },
|
|
100
|
+
},
|
|
101
|
+
} as DotNsGatewayQueryApi;
|
|
102
|
+
|
|
103
|
+
return { runtime, gatewayApi };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Addresses lowercased for comparison: viem checksums every one it decodes. */
|
|
107
|
+
const lower = (a: object): Record<string, string> =>
|
|
108
|
+
Object.fromEntries(Object.entries(a).map(([k, v]) => [k, String(v).toLowerCase()]));
|
|
109
|
+
|
|
110
|
+
function expectAddresses(actual: { ok: boolean; value?: unknown }, expected: DotNsAddresses) {
|
|
111
|
+
expect(actual.ok).toBe(true);
|
|
112
|
+
if (!actual.ok) return;
|
|
113
|
+
expect(lower(actual.value as object)).toEqual(lower(expected));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Walks performed, counted by `TARGET` since exactly one walk calls it exactly once. */
|
|
117
|
+
const walkCount = (runtime: { calls: ReadonlyArray<{ functionName?: string }> }) =>
|
|
118
|
+
runtime.calls.filter((c) => c.functionName === "TARGET").length;
|
|
119
|
+
|
|
120
|
+
describe("DOTNS_REGISTRY_KEYS", () => {
|
|
121
|
+
test("each key is the DotnsConstants literal, not a keccak hash", () => {
|
|
122
|
+
// `bytes32("registry")` is the UTF-8 bytes left-aligned and right-padded
|
|
123
|
+
// with zeros. Hardcoded rather than derived: deriving them here with the
|
|
124
|
+
// same helper the source uses would agree with any mistake it makes.
|
|
125
|
+
expect(DOTNS_REGISTRY_KEYS).toEqual({
|
|
126
|
+
registry: "0x7265676973747279000000000000000000000000000000000000000000000000",
|
|
127
|
+
registrarController:
|
|
128
|
+
"0x636f6e74726f6c6c657200000000000000000000000000000000000000000000",
|
|
129
|
+
reverseResolver: "0x726576657273655265736f6c7665720000000000000000000000000000000000",
|
|
130
|
+
resolver: "0x7265736f6c766572000000000000000000000000000000000000000000000000",
|
|
131
|
+
popRules: "0x706f7052756c6573000000000000000000000000000000000000000000000000",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the controller key is `controller`, not `registrar`", () => {
|
|
136
|
+
// The issue asked for `registrar`, which is the ERC-721 token contract,
|
|
137
|
+
// not the commit-reveal controller this field holds. Both answer `get`
|
|
138
|
+
// with a live address, so the wrong key fails silently.
|
|
139
|
+
const registrar = `0x${Buffer.from("registrar").toString("hex").padEnd(64, "0")}`;
|
|
140
|
+
expect(DOTNS_REGISTRY_KEYS.registrarController).not.toBe(registrar);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("the resolver key is `resolver`, not `contentResolver`", () => {
|
|
144
|
+
const contentResolver = `0x${Buffer.from("contentResolver").toString("hex").padEnd(64, "0")}`;
|
|
145
|
+
expect(DOTNS_REGISTRY_KEYS.resolver).not.toBe(contentResolver);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("every discoverable address has a key, and every key an address", () => {
|
|
149
|
+
// Adding a seventh address should fail here until it is discoverable.
|
|
150
|
+
// `protocolRegistry` is the exception: it has no DotnsConstants key and
|
|
151
|
+
// is reached through the pop controller instead.
|
|
152
|
+
const discoverable = Object.keys(DOTNS_ADDRESSES).filter((r) => r !== "protocolRegistry");
|
|
153
|
+
expect(Object.keys(DOTNS_REGISTRY_KEYS).sort()).toEqual(discoverable.sort());
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("no two roles share a key", () => {
|
|
157
|
+
const keys = Object.values(DOTNS_REGISTRY_KEYS);
|
|
158
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("discoverDotNsAddresses", () => {
|
|
163
|
+
test("walks pallet to dispatcher to controller to registry", async () => {
|
|
164
|
+
const { runtime, gatewayApi } = walkChain();
|
|
165
|
+
expectAddresses(await discoverDotNsAddresses({ runtime, gatewayApi }), DISCOVERED);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("reads every role from the chain, never from the pinned table", async () => {
|
|
169
|
+
// The assertion that matters: not one discovered value equals its pinned
|
|
170
|
+
// counterpart, so a fallback to DOTNS_ADDRESSES cannot pass this.
|
|
171
|
+
const { runtime, gatewayApi } = walkChain();
|
|
172
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
173
|
+
expect(r.ok).toBe(true);
|
|
174
|
+
if (!r.ok) return;
|
|
175
|
+
for (const role of Object.keys(DOTNS_ADDRESSES) as (keyof DotNsAddresses)[]) {
|
|
176
|
+
expect(r.value[role].toLowerCase()).not.toBe(DOTNS_ADDRESSES[role].toLowerCase());
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("accepts a wrapped H160 as well as a plain hex string", async () => {
|
|
181
|
+
// PAPI decodes H160 as a fixed-size binary wrapper, and the wrapper class
|
|
182
|
+
// has changed across majors. Both shapes must work.
|
|
183
|
+
const wrapped = walkChain({ dispatcher: { asHex: () => DISPATCHER } });
|
|
184
|
+
expectAddresses(await discoverDotNsAddresses(wrapped), DISCOVERED);
|
|
185
|
+
const plain = walkChain({ dispatcher: DISPATCHER });
|
|
186
|
+
expectAddresses(await discoverDotNsAddresses(plain), DISCOVERED);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("the five registry reads run concurrently, not one after another", async () => {
|
|
190
|
+
const { runtime, gatewayApi } = walkChain();
|
|
191
|
+
await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
192
|
+
expect(runtime.calls.filter((c) => c.functionName === "get")).toHaveLength(5);
|
|
193
|
+
// Two contract hops plus one batch of five. The pallet read is not here:
|
|
194
|
+
// it goes through gatewayApi, so it never reaches runtime.calls.
|
|
195
|
+
expect(runtime.calls).toHaveLength(7);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("an unset dispatcher address fails, naming the pallet", async () => {
|
|
199
|
+
const { runtime, gatewayApi } = walkChain({ dispatcher: undefined });
|
|
200
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
201
|
+
expect(r.ok).toBe(false);
|
|
202
|
+
if (r.ok) return;
|
|
203
|
+
expect(r.error.reason).toBe("AddressDiscovery");
|
|
204
|
+
expect(r.error.message).toContain("DotnsGateway.DispatcherAddress");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("a zero dispatcher address fails rather than walking into the void", async () => {
|
|
208
|
+
const { runtime, gatewayApi } = walkChain({ dispatcher: ZERO });
|
|
209
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
210
|
+
expect(r.ok).toBe(false);
|
|
211
|
+
if (!r.ok) expect(r.error.reason).toBe("AddressDiscovery");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a reverting TARGET() fails", async () => {
|
|
215
|
+
const { runtime, gatewayApi } = walkChain({ target: fakeDryRunResult({ revert: true }) });
|
|
216
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
217
|
+
expect(r.ok).toBe(false);
|
|
218
|
+
if (r.ok) return;
|
|
219
|
+
expect(r.error.reason).toBe("AddressDiscovery");
|
|
220
|
+
expect(r.error.message).toContain("TARGET");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a zero protocolRegistry() fails", async () => {
|
|
224
|
+
const { runtime, gatewayApi } = walkChain({ protocolRegistry: ZERO });
|
|
225
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
226
|
+
expect(r.ok).toBe(false);
|
|
227
|
+
if (r.ok) return;
|
|
228
|
+
expect(r.error.reason).toBe("AddressDiscovery");
|
|
229
|
+
expect(r.error.message).toContain("protocolRegistry");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("an unset role names which role is missing", async () => {
|
|
233
|
+
// A wrong key returns address(0), not an error, so the role name is the
|
|
234
|
+
// only clue the caller gets.
|
|
235
|
+
const { runtime, gatewayApi } = walkChain({ get: { popRules: ZERO } });
|
|
236
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
237
|
+
expect(r.ok).toBe(false);
|
|
238
|
+
if (r.ok) return;
|
|
239
|
+
expect(r.error.reason).toBe("AddressDiscovery");
|
|
240
|
+
expect(r.error.message).toContain("popRules");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("a failed read never becomes a pinned value", async () => {
|
|
244
|
+
const { runtime, gatewayApi } = walkChain({ get: { registry: ZERO } });
|
|
245
|
+
const r = await discoverDotNsAddresses({ runtime, gatewayApi });
|
|
246
|
+
expect(r.ok).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("falls back to the runtime's own api when no gatewayApi is given", async () => {
|
|
250
|
+
const { runtime } = walkChain();
|
|
251
|
+
const api = runtime.api as unknown as Record<string, unknown>;
|
|
252
|
+
api.query = {
|
|
253
|
+
...(api.query as object),
|
|
254
|
+
DotnsGateway: { DispatcherAddress: { getValue: async () => DISPATCHER } },
|
|
255
|
+
};
|
|
256
|
+
expectAddresses(await discoverDotNsAddresses({ runtime }), DISCOVERED);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("a runtime without the pallet fails legibly instead of throwing", async () => {
|
|
260
|
+
// Polkadot and Kusama Asset Hub have no DotnsGateway pallet at all.
|
|
261
|
+
const { runtime } = walkChain();
|
|
262
|
+
const r = await discoverDotNsAddresses({ runtime });
|
|
263
|
+
expect(r.ok).toBe(false);
|
|
264
|
+
if (r.ok) return;
|
|
265
|
+
expect(r.error.reason).toBe("AddressDiscovery");
|
|
266
|
+
expect(r.error.message).toContain("DotnsGateway");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("resolveDotNsAddresses", () => {
|
|
271
|
+
test("defaults to the pinned table and reads nothing", async () => {
|
|
272
|
+
const runtime = {} as ContractRuntime; // any read would throw
|
|
273
|
+
expect(await resolveDotNsAddresses({ runtime })).toEqual({
|
|
274
|
+
ok: true,
|
|
275
|
+
value: { ...DOTNS_ADDRESSES },
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("a per-field override wins over the pinned table", async () => {
|
|
280
|
+
const runtime = {} as ContractRuntime;
|
|
281
|
+
const r = await resolveDotNsAddresses({ runtime, registryAddress: addr("c1") });
|
|
282
|
+
expect(r).toEqual({
|
|
283
|
+
ok: true,
|
|
284
|
+
value: { ...DOTNS_ADDRESSES, registry: addr("c1") },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("addressSource discovered walks the gateway", async () => {
|
|
289
|
+
const { runtime, gatewayApi } = walkChain();
|
|
290
|
+
const r = await resolveDotNsAddresses({ runtime, gatewayApi, addressSource: "discovered" });
|
|
291
|
+
expectAddresses(r, DISCOVERED);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a per-field override wins over a discovered address too", async () => {
|
|
295
|
+
const { runtime, gatewayApi } = walkChain();
|
|
296
|
+
const r = await resolveDotNsAddresses({
|
|
297
|
+
runtime,
|
|
298
|
+
gatewayApi,
|
|
299
|
+
addressSource: "discovered",
|
|
300
|
+
registryAddress: addr("c1"),
|
|
301
|
+
});
|
|
302
|
+
expectAddresses(r, { ...DISCOVERED, registry: addr("c1") });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("the cache holds the walk, not the merged result", async () => {
|
|
306
|
+
// Two calls differing only in their override must both be right off one
|
|
307
|
+
// walk. Caching the merged set would serve the first call's override to
|
|
308
|
+
// the second.
|
|
309
|
+
const { runtime, gatewayApi } = walkChain();
|
|
310
|
+
const base = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
311
|
+
const one = await resolveDotNsAddresses({ ...base, registryAddress: addr("c1") });
|
|
312
|
+
const two = await resolveDotNsAddresses({ ...base, registryAddress: addr("c2") });
|
|
313
|
+
expectAddresses(one, { ...DISCOVERED, registry: addr("c1") });
|
|
314
|
+
expectAddresses(two, { ...DISCOVERED, registry: addr("c2") });
|
|
315
|
+
expect(walkCount(runtime)).toBe(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("caches the walk per runtime", async () => {
|
|
319
|
+
const { runtime, gatewayApi } = walkChain();
|
|
320
|
+
const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
321
|
+
await resolveDotNsAddresses(opts);
|
|
322
|
+
await resolveDotNsAddresses(opts);
|
|
323
|
+
expect(walkCount(runtime)).toBe(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("concurrent first calls share one walk", async () => {
|
|
327
|
+
const { runtime, gatewayApi } = walkChain();
|
|
328
|
+
const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
329
|
+
const results = await Promise.all([
|
|
330
|
+
resolveDotNsAddresses(opts),
|
|
331
|
+
resolveDotNsAddresses(opts),
|
|
332
|
+
resolveDotNsAddresses(opts),
|
|
333
|
+
]);
|
|
334
|
+
expect(walkCount(runtime)).toBe(1);
|
|
335
|
+
for (const r of results) expectAddresses(r, DISCOVERED);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("a failed walk is not cached, so a later call retries", async () => {
|
|
339
|
+
// A walk usually fails because the RPC blinked, not because the
|
|
340
|
+
// deployment moved. Caching that would strand the client for the life of
|
|
341
|
+
// the runtime.
|
|
342
|
+
const pallet: { value: unknown } = { value: undefined };
|
|
343
|
+
const { runtime } = walkChain();
|
|
344
|
+
const gatewayApi = {
|
|
345
|
+
query: { DotnsGateway: { DispatcherAddress: { getValue: async () => pallet.value } } },
|
|
346
|
+
} as DotNsGatewayQueryApi;
|
|
347
|
+
const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
348
|
+
|
|
349
|
+
expect((await resolveDotNsAddresses(opts)).ok).toBe(false);
|
|
350
|
+
pallet.value = DISPATCHER;
|
|
351
|
+
expectAddresses(await resolveDotNsAddresses(opts), DISCOVERED);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("pinned and discovered are cached apart on one runtime", async () => {
|
|
355
|
+
const { runtime, gatewayApi } = walkChain();
|
|
356
|
+
expect(await resolveDotNsAddresses({ runtime, gatewayApi })).toEqual({
|
|
357
|
+
ok: true,
|
|
358
|
+
value: { ...DOTNS_ADDRESSES },
|
|
359
|
+
});
|
|
360
|
+
expectAddresses(
|
|
361
|
+
await resolveDotNsAddresses({ runtime, gatewayApi, addressSource: "discovered" }),
|
|
362
|
+
DISCOVERED,
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("verifyDotNsAddresses", () => {
|
|
368
|
+
/** A chain whose walk lands exactly on the pinned table. */
|
|
369
|
+
const agreeing = (overrides: Partial<DotNsAddresses> = {}) => {
|
|
370
|
+
const answers = { ...DOTNS_ADDRESSES, ...overrides };
|
|
371
|
+
const runtime = createFakeContractRuntime({
|
|
372
|
+
abi: WALK_ABIS,
|
|
373
|
+
onQuery: ({ dest, functionName, args }) => {
|
|
374
|
+
const at = dest.toLowerCase();
|
|
375
|
+
if (functionName === "TARGET") return POP_CONTROLLER;
|
|
376
|
+
if (functionName === "protocolRegistry") return answers.protocolRegistry;
|
|
377
|
+
if (functionName === "get" && at === answers.protocolRegistry.toLowerCase()) {
|
|
378
|
+
const role = ROLE_BY_KEY.get(String(args?.[0]));
|
|
379
|
+
return role ? answers[role as keyof DotNsAddresses] : ZERO;
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const gatewayApi = {
|
|
385
|
+
query: { DotnsGateway: { DispatcherAddress: { getValue: async () => DISPATCHER } } },
|
|
386
|
+
} as DotNsGatewayQueryApi;
|
|
387
|
+
return { runtime, gatewayApi };
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
test("passes when the walk agrees with the pinned table", async () => {
|
|
391
|
+
const { runtime, gatewayApi } = agreeing();
|
|
392
|
+
expect(await verifyDotNsAddresses({ runtime, gatewayApi })).toEqual({
|
|
393
|
+
ok: true,
|
|
394
|
+
value: { ...DOTNS_ADDRESSES },
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("casing does not count as a disagreement", async () => {
|
|
399
|
+
// Chain reads come back lowercase; the pinned table is EIP-55.
|
|
400
|
+
const lowered: Partial<DotNsAddresses> = {};
|
|
401
|
+
for (const [role, address] of Object.entries(DOTNS_ADDRESSES)) {
|
|
402
|
+
lowered[role as keyof DotNsAddresses] = address.toLowerCase() as `0x${string}`;
|
|
403
|
+
}
|
|
404
|
+
const { runtime, gatewayApi } = agreeing(lowered);
|
|
405
|
+
expect((await verifyDotNsAddresses({ runtime, gatewayApi })).ok).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("fails loudly, naming the role and both addresses", async () => {
|
|
409
|
+
const { runtime, gatewayApi } = agreeing({ registry: addr("d1") });
|
|
410
|
+
const r = await verifyDotNsAddresses({ runtime, gatewayApi });
|
|
411
|
+
expect(r.ok).toBe(false);
|
|
412
|
+
if (r.ok) return;
|
|
413
|
+
expect(r.error.reason).toBe("AddressMismatch");
|
|
414
|
+
expect(r.error.message).toContain("registry");
|
|
415
|
+
expect(r.error.message.toLowerCase()).toContain(DOTNS_ADDRESSES.registry.toLowerCase());
|
|
416
|
+
expect(r.error.message.toLowerCase()).toContain(addr("d1"));
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("lists every disagreeing role, not just the first", async () => {
|
|
420
|
+
const { runtime, gatewayApi } = agreeing({
|
|
421
|
+
registry: addr("d1"),
|
|
422
|
+
popRules: addr("d2"),
|
|
423
|
+
});
|
|
424
|
+
const r = await verifyDotNsAddresses({ runtime, gatewayApi });
|
|
425
|
+
expect(r.ok).toBe(false);
|
|
426
|
+
if (r.ok) return;
|
|
427
|
+
expect(r.error.message).toContain("registry");
|
|
428
|
+
expect(r.error.message).toContain("popRules");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("checks the caller's overrides, not only the pinned table", async () => {
|
|
432
|
+
// A product that overrides an address wants that address verified. The
|
|
433
|
+
// pinned value it replaced is not what it will call.
|
|
434
|
+
const { runtime, gatewayApi } = agreeing();
|
|
435
|
+
const r = await verifyDotNsAddresses({ runtime, gatewayApi, registryAddress: addr("d1") });
|
|
436
|
+
expect(r.ok).toBe(false);
|
|
437
|
+
if (!r.ok) expect(r.error.reason).toBe("AddressMismatch");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("a discovered client has nothing to disagree with", async () => {
|
|
441
|
+
// The gap that let the false alarm through: no test passed addressSource
|
|
442
|
+
// here. A client on "discovered" calls what the walk found, so drift
|
|
443
|
+
// against the pinned table is not drift it is exposed to.
|
|
444
|
+
const moved = addr("e1");
|
|
445
|
+
const { runtime, gatewayApi } = agreeing({ registry: moved });
|
|
446
|
+
const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
447
|
+
|
|
448
|
+
const inUse = await resolveDotNsAddresses(opts);
|
|
449
|
+
expect(inUse.ok && inUse.value.registry.toLowerCase()).toBe(moved);
|
|
450
|
+
|
|
451
|
+
const v = await verifyDotNsAddresses(opts);
|
|
452
|
+
expect(v.ok).toBe(true);
|
|
453
|
+
if (v.ok) expect(v.value.registry.toLowerCase()).toBe(moved);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("a discovered client is still checked against its own overrides", async () => {
|
|
457
|
+
// The hole the first pass at this left: short circuiting for discovered
|
|
458
|
+
// clients skipped override drift, so verify reported ok while the client
|
|
459
|
+
// called an address the chain disagreed with. A false all clear is worse
|
|
460
|
+
// than the false alarm it replaced.
|
|
461
|
+
const wrong = addr("f1");
|
|
462
|
+
const { runtime, gatewayApi } = agreeing();
|
|
463
|
+
const opts = {
|
|
464
|
+
runtime,
|
|
465
|
+
gatewayApi,
|
|
466
|
+
addressSource: "discovered",
|
|
467
|
+
registryAddress: wrong,
|
|
468
|
+
} as const;
|
|
469
|
+
|
|
470
|
+
const inUse = await resolveDotNsAddresses(opts);
|
|
471
|
+
expect(inUse.ok && inUse.value.registry.toLowerCase()).toBe(wrong);
|
|
472
|
+
|
|
473
|
+
const v = await verifyDotNsAddresses(opts);
|
|
474
|
+
expect(v.ok).toBe(false);
|
|
475
|
+
if (!v.ok) {
|
|
476
|
+
expect(v.error.reason).toBe("AddressMismatch");
|
|
477
|
+
expect(v.error.message.toLowerCase()).toContain(wrong);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("verifying then using the client is one walk, not two", async () => {
|
|
482
|
+
const { runtime, gatewayApi } = agreeing();
|
|
483
|
+
const opts = { runtime, gatewayApi, addressSource: "discovered" } as const;
|
|
484
|
+
await verifyDotNsAddresses(opts);
|
|
485
|
+
await resolveDotNsAddresses(opts);
|
|
486
|
+
expect(walkCount(runtime)).toBe(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("a walk that cannot complete is a discovery failure, not a mismatch", async () => {
|
|
490
|
+
const { runtime, gatewayApi } = walkChain({ dispatcher: undefined });
|
|
491
|
+
const r = await verifyDotNsAddresses({ runtime, gatewayApi });
|
|
492
|
+
expect(r.ok).toBe(false);
|
|
493
|
+
if (!r.ok) expect(r.error.reason).toBe("AddressDiscovery");
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
describe("the new ABI fragments round-trip", () => {
|
|
498
|
+
// Decoded addresses are checksummed, so they never match a literal on casing.
|
|
499
|
+
test("get, TARGET and protocolRegistry encode and decode", async () => {
|
|
500
|
+
const { runtime } = walkChain();
|
|
501
|
+
const dispatcher = createContract(
|
|
502
|
+
runtime,
|
|
503
|
+
DISPATCHER,
|
|
504
|
+
DOTNS_ROOT_GATEWAY_DISPATCHER_ABI as never,
|
|
505
|
+
) as never as { TARGET: { query: () => Promise<{ success: boolean; value: unknown }> } };
|
|
506
|
+
expect(sameAddress((await dispatcher.TARGET.query()).value, POP_CONTROLLER)).toBe(true);
|
|
507
|
+
|
|
508
|
+
const controller = createContract(
|
|
509
|
+
runtime,
|
|
510
|
+
POP_CONTROLLER,
|
|
511
|
+
DOTNS_POP_CONTROLLER_ABI as never,
|
|
512
|
+
) as never as {
|
|
513
|
+
protocolRegistry: { query: () => Promise<{ success: boolean; value: unknown }> };
|
|
514
|
+
};
|
|
515
|
+
expect(
|
|
516
|
+
sameAddress(
|
|
517
|
+
(await controller.protocolRegistry.query()).value,
|
|
518
|
+
DISCOVERED.protocolRegistry,
|
|
519
|
+
),
|
|
520
|
+
).toBe(true);
|
|
521
|
+
|
|
522
|
+
const registry = createContract(
|
|
523
|
+
runtime,
|
|
524
|
+
DISCOVERED.protocolRegistry,
|
|
525
|
+
DOTNS_PROTOCOL_REGISTRY_ABI as never,
|
|
526
|
+
) as never as {
|
|
527
|
+
get: { query: (key: string) => Promise<{ success: boolean; value: unknown }> };
|
|
528
|
+
};
|
|
529
|
+
expect(
|
|
530
|
+
sameAddress(
|
|
531
|
+
(await registry.get.query(DOTNS_REGISTRY_KEYS.popRules)).value,
|
|
532
|
+
DISCOVERED.popRules,
|
|
533
|
+
),
|
|
534
|
+
).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
});
|