@parity/product-sdk 0.22.0 → 0.24.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-7GM3HUZF.js +89 -0
- package/dist/chunk-7GM3HUZF.js.map +1 -0
- package/dist/{chunk-6BI66XUJ.js → chunk-JIO3OZSC.js} +9 -10
- package/dist/chunk-JIO3OZSC.js.map +1 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/dotns-CtmOwVQW.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-CRYPRNYk.d.ts} +1 -1
- package/package.json +12 -12
- 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 +295 -3
- 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,1309 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Contract paths run through `createFakeContractRuntime` from
|
|
5
|
+
// `@parity/product-sdk-contracts/testing`: the real codec, no chain. What these
|
|
6
|
+
// cannot cover is whether the deployed contracts behave as modelled here.
|
|
7
|
+
import {
|
|
8
|
+
type AbiEntry,
|
|
9
|
+
type ContractRuntime,
|
|
10
|
+
QUERY_FALLBACK_ORIGIN,
|
|
11
|
+
} from "@parity/product-sdk-contracts";
|
|
12
|
+
import { createFakeContractRuntime, fakeDryRunResult } from "@parity/product-sdk-contracts/testing";
|
|
13
|
+
import { ss58ToH160 } from "@parity/product-sdk-address";
|
|
14
|
+
import type { SS58String } from "polkadot-api";
|
|
15
|
+
import { describe, expect, test } from "vitest";
|
|
16
|
+
import {
|
|
17
|
+
DOTNS_POP_CONTROLLER_ABI,
|
|
18
|
+
DOTNS_POP_RULES_ABI,
|
|
19
|
+
DOTNS_PROTOCOL_REGISTRY_ABI,
|
|
20
|
+
DOTNS_REGISTRAR_CONTROLLER_ABI,
|
|
21
|
+
DOTNS_REGISTRY_ABI,
|
|
22
|
+
DOTNS_RESOLVER_ABI,
|
|
23
|
+
DOTNS_RESOLVER_WRITE_ABI,
|
|
24
|
+
DOTNS_REVERSE_RESOLVER_ABI,
|
|
25
|
+
DOTNS_ROOT_GATEWAY_DISPATCHER_ABI,
|
|
26
|
+
DOTNS_ADDRESSES,
|
|
27
|
+
POP_STATUS,
|
|
28
|
+
} from "./dotns-abis.js";
|
|
29
|
+
import { DOTNS_REGISTRY_KEYS, type DotNsGatewayQueryApi } from "./dotns-addresses.js";
|
|
30
|
+
import { DotNsError } from "./dotns-errors.js";
|
|
31
|
+
import {
|
|
32
|
+
isDotNsAvailable,
|
|
33
|
+
prepareDotNsRegistration,
|
|
34
|
+
resolveDotNs,
|
|
35
|
+
resolveTld,
|
|
36
|
+
reverseDotNs,
|
|
37
|
+
setDotNsRecord,
|
|
38
|
+
} from "./dotns-registry.js";
|
|
39
|
+
import { DOT_TLD, dotNsTld, namehash } from "./dotns-namehash.js";
|
|
40
|
+
|
|
41
|
+
/** A second protocol-registry address, to prove the TLD cache keys on it. */
|
|
42
|
+
const OTHER_REGISTRY = "0x9999999999999999999999999999999999999999" as const;
|
|
43
|
+
|
|
44
|
+
// These suites pass `tld: DOT_TLD` explicitly so they exercise resolution,
|
|
45
|
+
// writes and pricing rather than the TLD read — that path has its own suite in
|
|
46
|
+
// `describe("resolveTld")`, and the entry-point/`.paseo` wiring has
|
|
47
|
+
// `describe("the deployment's TLD reaches every entry point")`. Supplying the
|
|
48
|
+
// TLD also keeps validation free of IO, which is what lets the cases below
|
|
49
|
+
// assert an `InvalidName` against a runtime that would throw if touched.
|
|
50
|
+
// Never reached on the validation path (the calls reject before touching it).
|
|
51
|
+
const opts = { runtime: {} as ContractRuntime, tld: DOT_TLD };
|
|
52
|
+
|
|
53
|
+
/** Any SS58 account. The writes only need one to dry-run against. */
|
|
54
|
+
const SIGNER = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" as const;
|
|
55
|
+
|
|
56
|
+
const ALL_ABIS: AbiEntry[] = [
|
|
57
|
+
...DOTNS_REGISTRY_ABI,
|
|
58
|
+
...DOTNS_RESOLVER_ABI,
|
|
59
|
+
...DOTNS_REVERSE_RESOLVER_ABI,
|
|
60
|
+
...DOTNS_RESOLVER_WRITE_ABI,
|
|
61
|
+
...DOTNS_REGISTRAR_CONTROLLER_ABI,
|
|
62
|
+
...DOTNS_POP_RULES_ABI,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Unset pointer / unregistered owner. */
|
|
66
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
67
|
+
/** Stand-in accounts. Distinct on purpose: a test must not pass by confusing them. */
|
|
68
|
+
const OWNER = "0x1111111111111111111111111111111111111111";
|
|
69
|
+
const TARGET = "0x2222222222222222222222222222222222222222";
|
|
70
|
+
|
|
71
|
+
// The real defaults, because these tests leave the addresses out of `opts` and
|
|
72
|
+
// exercise the fallback path. Arbitrary values here would stop the
|
|
73
|
+
// reverse-resolver branch firing and the tests would pass for the wrong reason.
|
|
74
|
+
// See "honours the address overrides" for the opts path.
|
|
75
|
+
const FORWARD_RESOLVER = DOTNS_ADDRESSES.resolver;
|
|
76
|
+
const REVERSE_RESOLVER = DOTNS_ADDRESSES.reverseResolver;
|
|
77
|
+
/** classifyName returns two values, so the fake encodes it positionally. */
|
|
78
|
+
const OPEN = [POP_STATUS.NoStatus, "Available to all"];
|
|
79
|
+
|
|
80
|
+
/** A runtime whose view calls answer from `answers`, keyed by function name. */
|
|
81
|
+
function runtimeWith(answers: Record<string, unknown>) {
|
|
82
|
+
return createFakeContractRuntime({
|
|
83
|
+
abi: ALL_ABIS,
|
|
84
|
+
onQuery: ({ functionName }) =>
|
|
85
|
+
functionName && functionName in answers ? answers[functionName] : undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("dotns registry surface", () => {
|
|
90
|
+
test("resolveDotNs rejects an invalid name before any registry call", async () => {
|
|
91
|
+
const r = await resolveDotNs("no", opts); // too short, no .dot
|
|
92
|
+
expect(r.ok).toBe(false);
|
|
93
|
+
if (!r.ok) {
|
|
94
|
+
expect(r.error).toBeInstanceOf(DotNsError);
|
|
95
|
+
expect(r.error.reason).toBe("InvalidName");
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("setDotNsRecord rejects an invalid name before any contract call", async () => {
|
|
100
|
+
const r = await setDotNsRecord({ name: "no", address: "0x00" }, opts);
|
|
101
|
+
expect(r.ok).toBe(false);
|
|
102
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidName");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("prepareDotNsRegistration rejects an invalid name before any contract call", async () => {
|
|
106
|
+
const r = await prepareDotNsRegistration({ name: "no", owner: "0x00" }, opts);
|
|
107
|
+
expect(r.ok).toBe(false);
|
|
108
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidName");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("DotNsError implements the SdkError marker", () => {
|
|
112
|
+
const e = new DotNsError("RegistryCall", "boom");
|
|
113
|
+
expect(e.isSdkError).toBe(true);
|
|
114
|
+
expect(e.source).toBe("dotns");
|
|
115
|
+
expect(e.reason).toBe("RegistryCall");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("resolveDotNs", () => {
|
|
120
|
+
test("resolves a name that has a forward record", async () => {
|
|
121
|
+
const runtime = runtimeWith({
|
|
122
|
+
owner: OWNER,
|
|
123
|
+
resolver: FORWARD_RESOLVER,
|
|
124
|
+
addressOf: TARGET,
|
|
125
|
+
});
|
|
126
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
127
|
+
expect(r).toEqual({
|
|
128
|
+
ok: true,
|
|
129
|
+
value: { address: TARGET, name: "alice.dot", owner: OWNER },
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a node pointed at the reverse resolver is registered with no forward record", async () => {
|
|
134
|
+
// The state of every name straight after registration.
|
|
135
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: REVERSE_RESOLVER });
|
|
136
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
137
|
+
expect(r).toEqual({ ok: true, value: { name: "alice.dot", owner: OWNER } });
|
|
138
|
+
// addressOf must not be attempted against the reverse resolver.
|
|
139
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("addressOf");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("the reverse-resolver comparison ignores address casing", async () => {
|
|
143
|
+
const runtime = runtimeWith({
|
|
144
|
+
owner: OWNER,
|
|
145
|
+
resolver: REVERSE_RESOLVER.toLowerCase(),
|
|
146
|
+
});
|
|
147
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
148
|
+
expect(r.ok && r.value?.address).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("an unset resolver pointer is also registered with no forward record", async () => {
|
|
152
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
153
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
154
|
+
expect(r).toEqual({ ok: true, value: { name: "alice.dot", owner: OWNER } });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("a forward resolver holding an empty record has no address", async () => {
|
|
158
|
+
const runtime = runtimeWith({
|
|
159
|
+
owner: OWNER,
|
|
160
|
+
resolver: FORWARD_RESOLVER,
|
|
161
|
+
addressOf: ZERO,
|
|
162
|
+
});
|
|
163
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
164
|
+
expect(r).toEqual({ ok: true, value: { name: "alice.dot", owner: OWNER } });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a zero owner means unregistered, reported as null not as a record", async () => {
|
|
168
|
+
const runtime = runtimeWith({ owner: ZERO, resolver: ZERO });
|
|
169
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
170
|
+
expect(r).toEqual({ ok: true, value: null });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("an unregistered name and a registered one are distinguishable", async () => {
|
|
174
|
+
const unregistered = await resolveDotNs("alice.dot", {
|
|
175
|
+
runtime: runtimeWith({ owner: ZERO, resolver: ZERO }),
|
|
176
|
+
tld: DOT_TLD,
|
|
177
|
+
});
|
|
178
|
+
const registered = await resolveDotNs("alice.dot", {
|
|
179
|
+
runtime: runtimeWith({ owner: OWNER, resolver: REVERSE_RESOLVER }),
|
|
180
|
+
tld: DOT_TLD,
|
|
181
|
+
});
|
|
182
|
+
expect(unregistered.ok && unregistered.value).toBeNull();
|
|
183
|
+
expect(registered.ok && registered.value).not.toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("honours the address overrides instead of the defaults", async () => {
|
|
187
|
+
// Proves the branch above keys off `opts`, not off the hardcoded table.
|
|
188
|
+
// Note the reverse-resolver override no longer decides anything here: any
|
|
189
|
+
// pointer that is not the forward resolver means "no forward record". The
|
|
190
|
+
// case is kept because it is the shape a registered name really has.
|
|
191
|
+
const customRegistry = "0x3333333333333333333333333333333333333333";
|
|
192
|
+
const customReverse = "0x4444444444444444444444444444444444444444";
|
|
193
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: customReverse });
|
|
194
|
+
const r = await resolveDotNs("alice.dot", {
|
|
195
|
+
runtime,
|
|
196
|
+
registryAddress: customRegistry,
|
|
197
|
+
reverseResolverAddress: customReverse,
|
|
198
|
+
tld: DOT_TLD,
|
|
199
|
+
});
|
|
200
|
+
// Treated as "no forward record" because it matches the override.
|
|
201
|
+
expect(r).toEqual({ ok: true, value: { name: "alice.dot", owner: OWNER } });
|
|
202
|
+
expect(runtime.calls.every((c) => c.dest === customRegistry)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("a failing owner read is an error, not a guessed owner", async () => {
|
|
206
|
+
const runtime = runtimeWith({
|
|
207
|
+
owner: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
208
|
+
resolver: FORWARD_RESOLVER,
|
|
209
|
+
addressOf: TARGET,
|
|
210
|
+
});
|
|
211
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
212
|
+
expect(r.ok).toBe(false);
|
|
213
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("isDotNsAvailable", () => {
|
|
218
|
+
test("asks the registrar controller rather than inferring from resolution", async () => {
|
|
219
|
+
const runtime = runtimeWith({ available: true, classifyName: OPEN });
|
|
220
|
+
const r = await isDotNsAvailable("alice.dot", { runtime, tld: DOT_TLD });
|
|
221
|
+
expect(r).toEqual({ ok: true, value: true });
|
|
222
|
+
expect(runtime.calls.map((c) => c.functionName).sort()).toEqual([
|
|
223
|
+
"available",
|
|
224
|
+
"classifyName",
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("passes the bare label, without the .dot suffix", async () => {
|
|
229
|
+
const runtime = runtimeWith({ available: true, classifyName: OPEN });
|
|
230
|
+
await isDotNsAvailable("alice.dot", { runtime, tld: DOT_TLD });
|
|
231
|
+
expect(runtime.calls[0]?.args).toEqual(["alice"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("an owned name with no forward record is not available", async () => {
|
|
235
|
+
// This shape used to report `true`, costing the caller a commit fee.
|
|
236
|
+
const runtime = runtimeWith({
|
|
237
|
+
available: false,
|
|
238
|
+
classifyName: OPEN,
|
|
239
|
+
owner: OWNER,
|
|
240
|
+
resolver: ZERO,
|
|
241
|
+
});
|
|
242
|
+
const r = await isDotNsAvailable("alice.dot", { runtime, tld: DOT_TLD });
|
|
243
|
+
expect(r).toEqual({ ok: true, value: false });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("an invalid name is rejected before the controller call", async () => {
|
|
247
|
+
const runtime = runtimeWith({ available: true, classifyName: OPEN });
|
|
248
|
+
const r = await isDotNsAvailable("no", { runtime, tld: DOT_TLD });
|
|
249
|
+
expect(r.ok).toBe(false);
|
|
250
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidName");
|
|
251
|
+
expect(runtime.calls).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("setDotNsRecord", () => {
|
|
256
|
+
test("moves the resolver pointer before writing the record", async () => {
|
|
257
|
+
const runtime = runtimeWith({ resolver: REVERSE_RESOLVER });
|
|
258
|
+
const r = await setDotNsRecord(
|
|
259
|
+
{ name: "alice.dot", address: TARGET },
|
|
260
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
261
|
+
);
|
|
262
|
+
expect(r.ok).toBe(true);
|
|
263
|
+
if (r.ok) expect(r.value).toHaveLength(2);
|
|
264
|
+
expect(runtime.calls.map((c) => c.functionName)).toEqual([
|
|
265
|
+
"resolver",
|
|
266
|
+
"setResolver",
|
|
267
|
+
"setAddress",
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("skips the pointer move when the node already points at the resolver", async () => {
|
|
272
|
+
const runtime = runtimeWith({ resolver: FORWARD_RESOLVER });
|
|
273
|
+
const r = await setDotNsRecord(
|
|
274
|
+
{ name: "alice.dot", address: TARGET },
|
|
275
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
276
|
+
);
|
|
277
|
+
expect(r.ok).toBe(true);
|
|
278
|
+
if (r.ok) expect(r.value).toHaveLength(1);
|
|
279
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("setResolver");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("the pointer move targets the forward resolver for the right node", async () => {
|
|
283
|
+
const runtime = runtimeWith({ resolver: ZERO });
|
|
284
|
+
await setDotNsRecord(
|
|
285
|
+
{ name: "alice.dot", address: TARGET },
|
|
286
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
287
|
+
);
|
|
288
|
+
const setResolver = runtime.calls.find((c) => c.functionName === "setResolver");
|
|
289
|
+
expect(setResolver?.args?.[1]).toBe(FORWARD_RESOLVER);
|
|
290
|
+
const setAddress = runtime.calls.find((c) => c.functionName === "setAddress");
|
|
291
|
+
expect(setAddress?.args?.[0]).toBe(setResolver?.args?.[0]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a failing resolver read is an error", async () => {
|
|
295
|
+
const runtime = runtimeWith({
|
|
296
|
+
resolver: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
297
|
+
});
|
|
298
|
+
const r = await setDotNsRecord(
|
|
299
|
+
{ name: "alice.dot", address: TARGET },
|
|
300
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
301
|
+
);
|
|
302
|
+
expect(r.ok).toBe(false);
|
|
303
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("origin", () => {
|
|
308
|
+
test("reads pass the pallet-revive fallback rather than leaving it unset", async () => {
|
|
309
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
310
|
+
await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
311
|
+
// Explicit, so the contracts layer does not warn once per query.
|
|
312
|
+
expect(runtime.calls.every((c) => c.origin === QUERY_FALLBACK_ORIGIN)).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("reads use opts.origin when given", async () => {
|
|
316
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
317
|
+
await resolveDotNs("alice.dot", { runtime, origin: SIGNER, tld: DOT_TLD });
|
|
318
|
+
expect(runtime.calls.every((c) => c.origin === SIGNER)).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("setDotNsRecord dry-runs as the caller, not the fallback account", async () => {
|
|
322
|
+
const runtime = runtimeWith({ resolver: REVERSE_RESOLVER });
|
|
323
|
+
await setDotNsRecord(
|
|
324
|
+
{ name: "alice.dot", address: TARGET },
|
|
325
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
326
|
+
);
|
|
327
|
+
const writes = runtime.calls.filter((c) =>
|
|
328
|
+
["setResolver", "setAddress"].includes(c.functionName ?? ""),
|
|
329
|
+
);
|
|
330
|
+
expect(writes).toHaveLength(2);
|
|
331
|
+
expect(writes.every((c) => c.origin === SIGNER)).toBe(true);
|
|
332
|
+
expect(writes.every((c) => c.origin !== QUERY_FALLBACK_ORIGIN)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("setDotNsRecord without an origin fails before any call", async () => {
|
|
336
|
+
const runtime = runtimeWith({ resolver: REVERSE_RESOLVER });
|
|
337
|
+
const r = await setDotNsRecord(
|
|
338
|
+
{ name: "alice.dot", address: TARGET },
|
|
339
|
+
{ runtime, tld: DOT_TLD },
|
|
340
|
+
);
|
|
341
|
+
expect(r.ok).toBe(false);
|
|
342
|
+
if (!r.ok) expect(r.error.reason).toBe("MissingOrigin");
|
|
343
|
+
expect(runtime.calls).toHaveLength(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("prepareDotNsRegistration without an origin fails before any call", async () => {
|
|
347
|
+
const runtime = runtimeWith({});
|
|
348
|
+
const r = await prepareDotNsRegistration(
|
|
349
|
+
{ name: "alice.dot", owner: OWNER },
|
|
350
|
+
{ runtime, tld: DOT_TLD },
|
|
351
|
+
);
|
|
352
|
+
expect(r.ok).toBe(false);
|
|
353
|
+
if (!r.ok) expect(r.error.reason).toBe("MissingOrigin");
|
|
354
|
+
expect(runtime.calls).toHaveLength(0);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("prepareDotNsRegistration with a malformed origin fails before any call", async () => {
|
|
358
|
+
// ss58ToH160 throws on an undecodable address. Every function here
|
|
359
|
+
// promises a Result, so the throw has to be converted, not escape.
|
|
360
|
+
const runtime = runtimeWith({});
|
|
361
|
+
const r = await prepareDotNsRegistration(
|
|
362
|
+
{ name: "alice.dot", owner: OWNER },
|
|
363
|
+
{ runtime, origin: "not-an-ss58-address" as SS58String, tld: DOT_TLD },
|
|
364
|
+
);
|
|
365
|
+
expect(r.ok).toBe(false);
|
|
366
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidOrigin");
|
|
367
|
+
expect(runtime.calls).toHaveLength(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("prepareDotNsRegistration dry-runs every call as the caller", async () => {
|
|
371
|
+
const runtime = runtimeWith({
|
|
372
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
373
|
+
price: 1000n,
|
|
374
|
+
minCommitmentAge: 60n,
|
|
375
|
+
maxCommitmentAge: 86400n,
|
|
376
|
+
});
|
|
377
|
+
await prepareDotNsRegistration(
|
|
378
|
+
{ name: "alice.dot", owner: OWNER },
|
|
379
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
380
|
+
);
|
|
381
|
+
expect(runtime.calls.length).toBeGreaterThan(0);
|
|
382
|
+
expect(runtime.calls.every((c) => c.origin === SIGNER)).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("prepareDotNsRegistration", () => {
|
|
387
|
+
/** PopRules metadata: eligible NoStatus owner unless overridden. */
|
|
388
|
+
const quote = (over: Partial<Record<string, unknown>> = {}) => ({
|
|
389
|
+
price: 1000n,
|
|
390
|
+
status: POP_STATUS.NoStatus,
|
|
391
|
+
userStatus: POP_STATUS.NoStatus,
|
|
392
|
+
message: "Available to all",
|
|
393
|
+
...over,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const happy = (over: Record<string, unknown> = {}) =>
|
|
397
|
+
runtimeWith({
|
|
398
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
399
|
+
priceWithoutCheck: quote(),
|
|
400
|
+
transferFloor: 0n,
|
|
401
|
+
minCommitmentAge: 60n,
|
|
402
|
+
maxCommitmentAge: 86400n,
|
|
403
|
+
available: true,
|
|
404
|
+
...over,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const REG = { name: "alice.dot", owner: OWNER };
|
|
408
|
+
const withSigner = (runtime: ReturnType<typeof runtimeWith>) => ({
|
|
409
|
+
runtime,
|
|
410
|
+
origin: SIGNER,
|
|
411
|
+
tld: DOT_TLD,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("does not dry-run register up front", async () => {
|
|
415
|
+
// register consumes the commitment, so preparing it before commitCall
|
|
416
|
+
// lands reverts with CommitmentNotFound.
|
|
417
|
+
const runtime = happy();
|
|
418
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
419
|
+
expect(r.ok).toBe(true);
|
|
420
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("register");
|
|
421
|
+
expect(runtime.calls.map((c) => c.functionName)).toContain("commit");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("returns a thunk that builds the register call later", async () => {
|
|
425
|
+
const runtime = happy();
|
|
426
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
427
|
+
expect(r.ok).toBe(true);
|
|
428
|
+
if (!r.ok) return;
|
|
429
|
+
runtime.reset();
|
|
430
|
+
const later = await r.value.prepareRegisterCall();
|
|
431
|
+
expect(later.ok).toBe(true);
|
|
432
|
+
expect(runtime.calls.map((c) => c.functionName)).toContain("register");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("prices from priceWithoutCheck, not from price", async () => {
|
|
436
|
+
const runtime = happy();
|
|
437
|
+
await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
438
|
+
const names = runtime.calls.map((c) => c.functionName);
|
|
439
|
+
expect(names).toContain("priceWithoutCheck");
|
|
440
|
+
expect(names).not.toContain("price");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("a governance-reserved label fails before the commit is prepared", async () => {
|
|
444
|
+
const runtime = happy({
|
|
445
|
+
priceWithoutCheck: quote({
|
|
446
|
+
status: POP_STATUS.Reserved,
|
|
447
|
+
message: "Reserved for Governance",
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
451
|
+
expect(r.ok).toBe(false);
|
|
452
|
+
if (!r.ok) expect(r.error.reason).toBe("NameReserved");
|
|
453
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("commit");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("an owner below the required tier fails before the commit is prepared", async () => {
|
|
457
|
+
const runtime = happy({
|
|
458
|
+
priceWithoutCheck: quote({
|
|
459
|
+
status: POP_STATUS.PopFull,
|
|
460
|
+
userStatus: POP_STATUS.NoStatus,
|
|
461
|
+
message: "Requires Full personhood verification",
|
|
462
|
+
}),
|
|
463
|
+
});
|
|
464
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
465
|
+
expect(r.ok).toBe(false);
|
|
466
|
+
if (!r.ok) expect(r.error.reason).toBe("OwnerStatusInsufficient");
|
|
467
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("commit");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("a verified owner meeting the tier is accepted", async () => {
|
|
471
|
+
const runtime = happy({
|
|
472
|
+
priceWithoutCheck: quote({
|
|
473
|
+
status: POP_STATUS.PopFull,
|
|
474
|
+
userStatus: POP_STATUS.PopFull,
|
|
475
|
+
price: 0n,
|
|
476
|
+
}),
|
|
477
|
+
});
|
|
478
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
479
|
+
expect(r.ok).toBe(true);
|
|
480
|
+
if (r.ok) expect(r.value.price).toBe(0n);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("the direct path skips the friction read", async () => {
|
|
484
|
+
// SIGNER derives to a different H160 than OWNER, so force the direct
|
|
485
|
+
// case by registering to the payer's own address.
|
|
486
|
+
const payer = ss58ToH160(SIGNER);
|
|
487
|
+
const runtime = happy();
|
|
488
|
+
await prepareDotNsRegistration({ name: "alice.dot", owner: payer }, withSigner(runtime));
|
|
489
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("transferFloor");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("a cross-payer registration charges the friction floor when it is higher", async () => {
|
|
493
|
+
const runtime = happy({ priceWithoutCheck: quote({ price: 10n }), transferFloor: 5000n });
|
|
494
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
495
|
+
expect(r.ok).toBe(true);
|
|
496
|
+
if (r.ok) expect(r.value.price).toBe(5000n);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("the register value is re-quoted, not the stale prepare-time price", async () => {
|
|
500
|
+
let current = 1000n;
|
|
501
|
+
const runtime = createFakeContractRuntime({
|
|
502
|
+
abi: ALL_ABIS,
|
|
503
|
+
onQuery: ({ functionName }) => {
|
|
504
|
+
if (functionName === "makeCommitment") return `0x${"ab".repeat(32)}`;
|
|
505
|
+
if (functionName === "available") return true;
|
|
506
|
+
if (functionName === "priceWithoutCheck") return quote({ price: current });
|
|
507
|
+
if (functionName === "transferFloor") return 0n;
|
|
508
|
+
if (functionName === "minCommitmentAge") return 60n;
|
|
509
|
+
if (functionName === "maxCommitmentAge") return 86400n;
|
|
510
|
+
return undefined;
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
514
|
+
expect(r.ok && r.value.price).toBe(1000n);
|
|
515
|
+
if (!r.ok) return;
|
|
516
|
+
|
|
517
|
+
current = 7777n; // price moves during the mandatory wait
|
|
518
|
+
runtime.reset();
|
|
519
|
+
await r.value.prepareRegisterCall();
|
|
520
|
+
const register = runtime.calls.find((c) => c.functionName === "register");
|
|
521
|
+
expect(register?.value).toBe(7777n);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("a failing commitment-window read is an error, not a zero window", async () => {
|
|
525
|
+
const runtime = happy({
|
|
526
|
+
minCommitmentAge: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
527
|
+
});
|
|
528
|
+
const r = await prepareDotNsRegistration(REG, withSigner(runtime));
|
|
529
|
+
expect(r.ok).toBe(false);
|
|
530
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("error causes", () => {
|
|
535
|
+
// The contracts package surfaces a dispatch payload on a failed query and a
|
|
536
|
+
// typed ContractError from a failed prepare, so callers can tell "not the
|
|
537
|
+
// owner" from "node missing" from "RPC down". Collapsing both to a bare
|
|
538
|
+
// string loses that, which is what these pin.
|
|
539
|
+
const FAILURE = { type: "ContractTrapped" } as const;
|
|
540
|
+
|
|
541
|
+
test("a failed read carries the dispatch payload", async () => {
|
|
542
|
+
const runtime = runtimeWith({ owner: fakeDryRunResult({ failure: FAILURE }) });
|
|
543
|
+
const r = await resolveDotNs("alice.dot", { runtime, tld: DOT_TLD });
|
|
544
|
+
expect(r.ok).toBe(false);
|
|
545
|
+
if (!r.ok) expect(r.error.cause).toEqual(FAILURE);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("a reverting write carries the ContractError, with its decoded reason", async () => {
|
|
549
|
+
const runtime = runtimeWith({
|
|
550
|
+
resolver: FORWARD_RESOLVER,
|
|
551
|
+
setAddress: fakeDryRunResult({ revert: "Unauthorised" }),
|
|
552
|
+
});
|
|
553
|
+
const r = await setDotNsRecord(
|
|
554
|
+
{ name: "alice.dot", address: TARGET },
|
|
555
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
556
|
+
);
|
|
557
|
+
expect(r.ok).toBe(false);
|
|
558
|
+
if (!r.ok) {
|
|
559
|
+
expect(r.error.cause).toBeDefined();
|
|
560
|
+
expect(JSON.stringify(r.error.cause)).toContain("Unauthorised");
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("a failed availability read carries its payload", async () => {
|
|
565
|
+
const runtime = runtimeWith({
|
|
566
|
+
available: fakeDryRunResult({ failure: FAILURE }),
|
|
567
|
+
classifyName: OPEN,
|
|
568
|
+
});
|
|
569
|
+
const r = await isDotNsAvailable("alice.dot", { runtime, tld: DOT_TLD });
|
|
570
|
+
expect(r.ok).toBe(false);
|
|
571
|
+
if (!r.ok) expect(r.error.cause).toEqual(FAILURE);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("a failed pricing read carries its payload", async () => {
|
|
575
|
+
const runtime = runtimeWith({
|
|
576
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
577
|
+
priceWithoutCheck: fakeDryRunResult({ failure: FAILURE }),
|
|
578
|
+
minCommitmentAge: 60n,
|
|
579
|
+
maxCommitmentAge: 86400n,
|
|
580
|
+
available: true,
|
|
581
|
+
});
|
|
582
|
+
const r = await prepareDotNsRegistration(
|
|
583
|
+
{ name: "alice.dot", owner: OWNER },
|
|
584
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
585
|
+
);
|
|
586
|
+
expect(r.ok).toBe(false);
|
|
587
|
+
if (!r.ok) expect(r.error.cause).toEqual(FAILURE);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("subnames", () => {
|
|
592
|
+
test("a subname resolves", async () => {
|
|
593
|
+
const runtime = runtimeWith({
|
|
594
|
+
owner: OWNER,
|
|
595
|
+
resolver: FORWARD_RESOLVER,
|
|
596
|
+
addressOf: TARGET,
|
|
597
|
+
});
|
|
598
|
+
const r = await resolveDotNs("bob.alice.dot", { runtime, tld: DOT_TLD });
|
|
599
|
+
expect(r).toEqual({
|
|
600
|
+
ok: true,
|
|
601
|
+
value: { address: TARGET, name: "bob.alice.dot", owner: OWNER },
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("a subname hashes to a different node than its parent", async () => {
|
|
606
|
+
const sub = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
607
|
+
await resolveDotNs("bob.alice.dot", { runtime: sub, tld: DOT_TLD });
|
|
608
|
+
const parent = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
609
|
+
await resolveDotNs("alice.dot", { runtime: parent, tld: DOT_TLD });
|
|
610
|
+
expect(sub.calls[0]?.args?.[0]).not.toBe(parent.calls[0]?.args?.[0]);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("a record can be set on a subname", async () => {
|
|
614
|
+
const runtime = runtimeWith({ resolver: FORWARD_RESOLVER });
|
|
615
|
+
const r = await setDotNsRecord(
|
|
616
|
+
{ name: "bob.alice.dot", address: TARGET },
|
|
617
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
618
|
+
);
|
|
619
|
+
expect(r.ok).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("registration still refuses a subname, since the registrar only mints single labels", async () => {
|
|
623
|
+
const runtime = runtimeWith({});
|
|
624
|
+
const r = await prepareDotNsRegistration(
|
|
625
|
+
{ name: "bob.alice.dot", owner: OWNER },
|
|
626
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
627
|
+
);
|
|
628
|
+
expect(r.ok).toBe(false);
|
|
629
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidName");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("availability still refuses a subname", async () => {
|
|
633
|
+
const runtime = runtimeWith({ available: true, classifyName: OPEN });
|
|
634
|
+
const r = await isDotNsAvailable("bob.alice.dot", { runtime, tld: DOT_TLD });
|
|
635
|
+
expect(r.ok).toBe(false);
|
|
636
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidName");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("a malformed label anywhere in the path is rejected as InvalidName", async () => {
|
|
640
|
+
// Asserting the reason, not just !ok: an empty fake runtime makes the
|
|
641
|
+
// call fail anyway, so `ok === false` alone would pass for any input.
|
|
642
|
+
const runtime = runtimeWith({});
|
|
643
|
+
for (const bad of ["a.alice.dot", "bob..alice.dot", "-bob.alice.dot", "bob.al ice.dot"]) {
|
|
644
|
+
const r = await resolveDotNs(bad, { runtime, tld: DOT_TLD });
|
|
645
|
+
expect(r.ok, bad).toBe(false);
|
|
646
|
+
if (!r.ok) expect(r.error.reason, bad).toBe("InvalidName");
|
|
647
|
+
}
|
|
648
|
+
expect(runtime.calls, "rejected before any contract call").toHaveLength(0);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("an uppercase subname is normalized rather than rejected", async () => {
|
|
652
|
+
const runtime = runtimeWith({ owner: OWNER, resolver: ZERO });
|
|
653
|
+
const r = await resolveDotNs("BOB.Alice.DOT", { runtime, tld: DOT_TLD });
|
|
654
|
+
expect(r).toEqual({ ok: true, value: { name: "bob.alice.dot", owner: OWNER } });
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("availability and registration agree", () => {
|
|
659
|
+
const OPEN_LABEL = [POP_STATUS.NoStatus, "Available to all"];
|
|
660
|
+
const GOV = [POP_STATUS.Reserved, "Reserved for Governance"];
|
|
661
|
+
|
|
662
|
+
test("a governance-reserved label is not available, even though it is unminted", async () => {
|
|
663
|
+
// available() only asks the registrar whether the token is minted, and
|
|
664
|
+
// says yes for a 3 to 5 character label that can never be claimed.
|
|
665
|
+
const runtime = runtimeWith({ available: true, classifyName: GOV });
|
|
666
|
+
const r = await isDotNsAvailable("bob.dot", { runtime, tld: DOT_TLD });
|
|
667
|
+
expect(r).toEqual({ ok: true, value: false });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("availability and registration now give the same verdict on a reserved label", async () => {
|
|
671
|
+
const avail = await isDotNsAvailable("bob.dot", {
|
|
672
|
+
runtime: runtimeWith({ available: true, classifyName: GOV }),
|
|
673
|
+
tld: DOT_TLD,
|
|
674
|
+
});
|
|
675
|
+
const reg = await prepareDotNsRegistration(
|
|
676
|
+
{ name: "bob.dot", owner: OWNER },
|
|
677
|
+
{
|
|
678
|
+
runtime: runtimeWith({
|
|
679
|
+
available: true,
|
|
680
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
681
|
+
priceWithoutCheck: {
|
|
682
|
+
price: 0n,
|
|
683
|
+
status: POP_STATUS.Reserved,
|
|
684
|
+
userStatus: POP_STATUS.NoStatus,
|
|
685
|
+
message: "Reserved for Governance",
|
|
686
|
+
},
|
|
687
|
+
minCommitmentAge: 60n,
|
|
688
|
+
maxCommitmentAge: 86400n,
|
|
689
|
+
}),
|
|
690
|
+
origin: SIGNER,
|
|
691
|
+
tld: DOT_TLD,
|
|
692
|
+
},
|
|
693
|
+
);
|
|
694
|
+
expect(avail.ok && avail.value).toBe(false);
|
|
695
|
+
expect(reg.ok).toBe(false);
|
|
696
|
+
if (!reg.ok) expect(reg.error.reason).toBe("NameReserved");
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("a failing classifyName is an error, not a false", async () => {
|
|
700
|
+
const runtime = runtimeWith({
|
|
701
|
+
available: true,
|
|
702
|
+
classifyName: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
703
|
+
});
|
|
704
|
+
const r = await isDotNsAvailable("alice.dot", { runtime, tld: DOT_TLD });
|
|
705
|
+
expect(r.ok).toBe(false);
|
|
706
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("an unminted, unrestricted label is available", async () => {
|
|
710
|
+
const runtime = runtimeWith({ available: true, classifyName: OPEN_LABEL });
|
|
711
|
+
expect(await isDotNsAvailable("longenough.dot", { runtime, tld: DOT_TLD })).toEqual({
|
|
712
|
+
ok: true,
|
|
713
|
+
value: true,
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("registration refuses a taken name before the commit", () => {
|
|
719
|
+
const base = {
|
|
720
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
721
|
+
priceWithoutCheck: {
|
|
722
|
+
price: 1000n,
|
|
723
|
+
status: POP_STATUS.NoStatus,
|
|
724
|
+
userStatus: POP_STATUS.NoStatus,
|
|
725
|
+
message: "Available to all",
|
|
726
|
+
},
|
|
727
|
+
transferFloor: 0n,
|
|
728
|
+
minCommitmentAge: 60n,
|
|
729
|
+
maxCommitmentAge: 86400n,
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
test("a taken name fails as NameUnavailable with no commit prepared", async () => {
|
|
733
|
+
// register runs _requireAvailableLabel first, so without this the caller
|
|
734
|
+
// pays for the commit and only then reverts with NameNotAvailable.
|
|
735
|
+
const runtime = runtimeWith({ ...base, available: false });
|
|
736
|
+
const r = await prepareDotNsRegistration(
|
|
737
|
+
{ name: "alice.dot", owner: OWNER },
|
|
738
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
739
|
+
);
|
|
740
|
+
expect(r.ok).toBe(false);
|
|
741
|
+
if (!r.ok) expect(r.error.reason).toBe("NameUnavailable");
|
|
742
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("commit");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("availability is actually consulted", async () => {
|
|
746
|
+
const runtime = runtimeWith({ ...base, available: true });
|
|
747
|
+
await prepareDotNsRegistration(
|
|
748
|
+
{ name: "alice.dot", owner: OWNER },
|
|
749
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
750
|
+
);
|
|
751
|
+
expect(runtime.calls.map((c) => c.functionName)).toContain("available");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("a failing availability read is an error, not an assumed yes", async () => {
|
|
755
|
+
const runtime = runtimeWith({
|
|
756
|
+
...base,
|
|
757
|
+
available: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
758
|
+
});
|
|
759
|
+
const r = await prepareDotNsRegistration(
|
|
760
|
+
{ name: "alice.dot", owner: OWNER },
|
|
761
|
+
{ runtime, origin: SIGNER, tld: DOT_TLD },
|
|
762
|
+
);
|
|
763
|
+
expect(r.ok).toBe(false);
|
|
764
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
describe("resolveTld", () => {
|
|
769
|
+
/** A runtime whose protocol registry answers `tld()` / `tldNode()` as told. */
|
|
770
|
+
function registryRuntime(answers: { tld?: unknown; tldNode?: unknown }) {
|
|
771
|
+
return createFakeContractRuntime({
|
|
772
|
+
abi: [...ALL_ABIS, ...DOTNS_PROTOCOL_REGISTRY_ABI],
|
|
773
|
+
onQuery: ({ functionName }) => {
|
|
774
|
+
if (functionName === "tld") return answers.tld;
|
|
775
|
+
if (functionName === "tldNode") return answers.tldNode;
|
|
776
|
+
return undefined;
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const PASEO = dotNsTld(".paseo");
|
|
782
|
+
/** Both getters absent, which is what a pre-b4096968 deployment looks like. */
|
|
783
|
+
const NO_GETTER = fakeDryRunResult({ revert: true });
|
|
784
|
+
|
|
785
|
+
test("reads the suffix from the chain and derives the node", async () => {
|
|
786
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
787
|
+
const r = await resolveTld({ runtime });
|
|
788
|
+
expect(r).toEqual({ ok: true, value: PASEO });
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("derives the node itself, so tldNode() is not required to answer", async () => {
|
|
792
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: NO_GETTER });
|
|
793
|
+
const r = await resolveTld({ runtime });
|
|
794
|
+
expect(r).toEqual({ ok: true, value: PASEO });
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("caches per runtime: a second call does not re-read", async () => {
|
|
798
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
799
|
+
await resolveTld({ runtime });
|
|
800
|
+
const afterFirst = runtime.calls.filter((c) => c.functionName === "tld").length;
|
|
801
|
+
await resolveTld({ runtime });
|
|
802
|
+
expect(runtime.calls.filter((c) => c.functionName === "tld").length).toBe(afterFirst);
|
|
803
|
+
expect(afterFirst).toBe(1);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("concurrent first calls share one read", async () => {
|
|
807
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
808
|
+
const [a, b, c] = await Promise.all([
|
|
809
|
+
resolveTld({ runtime }),
|
|
810
|
+
resolveTld({ runtime }),
|
|
811
|
+
resolveTld({ runtime }),
|
|
812
|
+
]);
|
|
813
|
+
expect(runtime.calls.filter((x) => x.functionName === "tld").length).toBe(1);
|
|
814
|
+
expect([a, b, c]).toEqual([
|
|
815
|
+
{ ok: true, value: PASEO },
|
|
816
|
+
{ ok: true, value: PASEO },
|
|
817
|
+
{ ok: true, value: PASEO },
|
|
818
|
+
]);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("the cache is per runtime, not global", async () => {
|
|
822
|
+
const one = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
823
|
+
const two = registryRuntime({ tld: ".dot", tldNode: DOT_TLD.node });
|
|
824
|
+
expect(await resolveTld({ runtime: one })).toEqual({ ok: true, value: PASEO });
|
|
825
|
+
expect(await resolveTld({ runtime: two })).toEqual({ ok: true, value: DOT_TLD });
|
|
826
|
+
expect(one.calls.filter((c) => c.functionName === "tld").length).toBe(1);
|
|
827
|
+
expect(two.calls.filter((c) => c.functionName === "tld").length).toBe(1);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("the cache key includes the protocol registry address", async () => {
|
|
831
|
+
// One runtime pointed at two registries has two TLDs; keying on the
|
|
832
|
+
// runtime alone would serve the first answer for both.
|
|
833
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
834
|
+
await resolveTld({ runtime });
|
|
835
|
+
await resolveTld({ runtime, protocolRegistryAddress: OTHER_REGISTRY });
|
|
836
|
+
expect(runtime.calls.filter((c) => c.functionName === "tld").length).toBe(2);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("a supplied tld skips the chain entirely", async () => {
|
|
840
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
841
|
+
const r = await resolveTld({ runtime, tld: DOT_TLD });
|
|
842
|
+
expect(r).toEqual({ ok: true, value: DOT_TLD });
|
|
843
|
+
expect(runtime.calls).toHaveLength(0);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
test("a supplied tld whose node does not match its suffix is refused", async () => {
|
|
847
|
+
// The override path re-creating the original bug: `.paseo` names rooted
|
|
848
|
+
// at the `.dot` node.
|
|
849
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: PASEO.node });
|
|
850
|
+
const r = await resolveTld({
|
|
851
|
+
runtime,
|
|
852
|
+
tld: { suffix: ".paseo", node: DOT_TLD.node },
|
|
853
|
+
});
|
|
854
|
+
expect(r.ok).toBe(false);
|
|
855
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidTld");
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("an absent getter falls back to .dot, because that TLD was compiled in", async () => {
|
|
859
|
+
// Pre-b4096968 deployments had no tld() and no way to be anything but
|
|
860
|
+
// `.dot`. No live deployment is known to still be one.
|
|
861
|
+
const runtime = registryRuntime({ tld: NO_GETTER, tldNode: NO_GETTER });
|
|
862
|
+
expect(await resolveTld({ runtime })).toEqual({ ok: true, value: DOT_TLD });
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test("a dispatch failure is an error, not a fallback", async () => {
|
|
866
|
+
const runtime = registryRuntime({
|
|
867
|
+
tld: fakeDryRunResult({ failure: { type: "ContractTrapped" } }),
|
|
868
|
+
});
|
|
869
|
+
const r = await resolveTld({ runtime });
|
|
870
|
+
expect(r.ok).toBe(false);
|
|
871
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("a revert carrying a reason is an error, not a fallback", async () => {
|
|
875
|
+
// Only an *empty* revert means "no such function". A reverting getter
|
|
876
|
+
// that has something to say is a real failure.
|
|
877
|
+
const runtime = registryRuntime({ tld: fakeDryRunResult({ revert: "nope" }) });
|
|
878
|
+
const r = await resolveTld({ runtime });
|
|
879
|
+
expect(r.ok).toBe(false);
|
|
880
|
+
if (!r.ok) expect(r.error.reason).toBe("RegistryCall");
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("a failed read is not cached, so the next call retries", async () => {
|
|
884
|
+
let firstCall = true;
|
|
885
|
+
const runtime = createFakeContractRuntime({
|
|
886
|
+
abi: [...ALL_ABIS, ...DOTNS_PROTOCOL_REGISTRY_ABI],
|
|
887
|
+
onQuery: ({ functionName }) => {
|
|
888
|
+
if (functionName === "tldNode") return PASEO.node;
|
|
889
|
+
if (functionName !== "tld") return undefined;
|
|
890
|
+
if (firstCall) {
|
|
891
|
+
firstCall = false;
|
|
892
|
+
return fakeDryRunResult({ failure: { type: "ContractTrapped" } });
|
|
893
|
+
}
|
|
894
|
+
return ".paseo";
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
expect((await resolveTld({ runtime })).ok).toBe(false);
|
|
898
|
+
expect(await resolveTld({ runtime })).toEqual({ ok: true, value: PASEO });
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("an empty suffix is refused, not treated as the ENS root", async () => {
|
|
902
|
+
// What an upgraded-but-unmigrated proxy reports: the getters succeed and
|
|
903
|
+
// return `_tld` = "" with `_tldNode` = 0, which would reroot every name.
|
|
904
|
+
const runtime = registryRuntime({ tld: "", tldNode: `0x${"00".repeat(32)}` });
|
|
905
|
+
const r = await resolveTld({ runtime });
|
|
906
|
+
expect(r.ok).toBe(false);
|
|
907
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidTld");
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test("a multi-label suffix is refused, since initialize cannot produce one", async () => {
|
|
911
|
+
const runtime = registryRuntime({ tld: ".a.b" });
|
|
912
|
+
const r = await resolveTld({ runtime });
|
|
913
|
+
expect(r.ok).toBe(false);
|
|
914
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidTld");
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("a tldNode() that contradicts tld() is refused, trusting neither", async () => {
|
|
918
|
+
const runtime = registryRuntime({ tld: ".paseo", tldNode: DOT_TLD.node });
|
|
919
|
+
const r = await resolveTld({ runtime });
|
|
920
|
+
expect(r.ok).toBe(false);
|
|
921
|
+
if (!r.ok) expect(r.error.reason).toBe("InvalidTld");
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe("the deployment's TLD reaches every entry point", () => {
|
|
926
|
+
const PASEO = dotNsTld(".paseo");
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* `dim2` under the `.paseo` root, as read from `DotnsRegistry` on
|
|
930
|
+
* paseo-asset-hub-next — the node the chain actually keys that name by.
|
|
931
|
+
*
|
|
932
|
+
* Pinned from the chain rather than computed here: it is what makes these
|
|
933
|
+
* assertions evidence about the deployment instead of a restatement of
|
|
934
|
+
* `namehash`. Under the old hardcoded `.dot` root the same name hashed to
|
|
935
|
+
* 0xec7bd203… , which is owned by nobody, which is why every lookup
|
|
936
|
+
* reported "unregistered".
|
|
937
|
+
*/
|
|
938
|
+
const DIM2_UNDER_PASEO = "0x4eeda1749326395729498c3df6e0cf87fe912297bbf35c4f2549c19d77f56dad";
|
|
939
|
+
|
|
940
|
+
/** A deployment whose protocol registry reports `.paseo`. */
|
|
941
|
+
function paseoRuntime(answers: Record<string, unknown> = {}) {
|
|
942
|
+
return createFakeContractRuntime({
|
|
943
|
+
abi: [...ALL_ABIS, ...DOTNS_PROTOCOL_REGISTRY_ABI],
|
|
944
|
+
onQuery: ({ functionName }) => {
|
|
945
|
+
if (functionName === "tld") return ".paseo";
|
|
946
|
+
if (functionName === "tldNode") return PASEO.node;
|
|
947
|
+
return answers[functionName ?? ""];
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const nodesFor = (runtime: ReturnType<typeof paseoRuntime>, fn: string) =>
|
|
953
|
+
runtime.calls.filter((c) => c.functionName === fn).map((c) => c.args?.[0]);
|
|
954
|
+
|
|
955
|
+
test("resolveDotNs hashes under the chain's TLD, not a compiled-in one", async () => {
|
|
956
|
+
const runtime = paseoRuntime({ owner: OWNER, resolver: ZERO });
|
|
957
|
+
const r = await resolveDotNs("dim2.paseo", { runtime });
|
|
958
|
+
expect(r).toEqual({ ok: true, value: { name: "dim2.paseo", owner: OWNER } });
|
|
959
|
+
// The assertion that would have caught the original bug.
|
|
960
|
+
expect(nodesFor(runtime, "owner")).toEqual([DIM2_UNDER_PASEO]);
|
|
961
|
+
expect(namehash("dim2.paseo", PASEO)).toBe(DIM2_UNDER_PASEO);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test("a bare label picks up the deployment's suffix", async () => {
|
|
965
|
+
const runtime = paseoRuntime({ owner: OWNER, resolver: ZERO });
|
|
966
|
+
const r = await resolveDotNs("dim2", { runtime });
|
|
967
|
+
expect(r).toEqual({ ok: true, value: { name: "dim2.paseo", owner: OWNER } });
|
|
968
|
+
expect(nodesFor(runtime, "owner")).toEqual([DIM2_UNDER_PASEO]);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("a name from another deployment is refused, not resolved under ours", async () => {
|
|
972
|
+
const runtime = paseoRuntime({ owner: OWNER, resolver: ZERO });
|
|
973
|
+
const r = await resolveDotNs("dim2.dot", { runtime });
|
|
974
|
+
expect(r.ok).toBe(false);
|
|
975
|
+
if (!r.ok) {
|
|
976
|
+
expect(r.error.reason).toBe("TldMismatch");
|
|
977
|
+
// The message has to name the deployment's TLD, or the caller cannot
|
|
978
|
+
// tell this apart from a typo.
|
|
979
|
+
expect(r.error.message).toContain(".paseo");
|
|
980
|
+
}
|
|
981
|
+
// Nothing beyond the TLD read: no registry lookup for a foreign name.
|
|
982
|
+
expect(runtime.calls.map((c) => c.functionName)).toEqual(["tld", "tldNode"]);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("setDotNsRecord targets the same node resolveDotNs reads", async () => {
|
|
986
|
+
const runtime = paseoRuntime({ resolver: DOTNS_ADDRESSES.resolver });
|
|
987
|
+
const r = await setDotNsRecord(
|
|
988
|
+
{ name: "dim2.paseo", address: TARGET },
|
|
989
|
+
{ runtime, origin: SIGNER },
|
|
990
|
+
);
|
|
991
|
+
expect(r.ok).toBe(true);
|
|
992
|
+
expect(nodesFor(runtime, "resolver")).toEqual([DIM2_UNDER_PASEO]);
|
|
993
|
+
expect(nodesFor(runtime, "setAddress")).toEqual([DIM2_UNDER_PASEO]);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test("isDotNsAvailable sends the whole label, not one truncated by four", async () => {
|
|
997
|
+
const runtime = paseoRuntime({
|
|
998
|
+
available: true,
|
|
999
|
+
// Multi-output returns are encoded positionally, as the suites above do.
|
|
1000
|
+
classifyName: [POP_STATUS.PopLite, ""],
|
|
1001
|
+
});
|
|
1002
|
+
const r = await isDotNsAvailable("dim2.paseo", { runtime });
|
|
1003
|
+
expect(r).toEqual({ ok: true, value: true });
|
|
1004
|
+
// `slice(0, -4)` would send "dim2.pa" here.
|
|
1005
|
+
expect(nodesFor(runtime, "available")).toEqual(["dim2"]);
|
|
1006
|
+
expect(nodesFor(runtime, "classifyName")).toEqual(["dim2"]);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test("prepareDotNsRegistration sends the whole label too", async () => {
|
|
1010
|
+
const runtime = paseoRuntime({
|
|
1011
|
+
available: true,
|
|
1012
|
+
makeCommitment: `0x${"ab".repeat(32)}`,
|
|
1013
|
+
priceWithoutCheck: [1n, POP_STATUS.NoStatus, POP_STATUS.PopFull, ""],
|
|
1014
|
+
transferFloor: 0n,
|
|
1015
|
+
minCommitmentAge: 60n,
|
|
1016
|
+
maxCommitmentAge: 86400n,
|
|
1017
|
+
});
|
|
1018
|
+
const r = await prepareDotNsRegistration(
|
|
1019
|
+
{ name: "dim2.paseo", owner: OWNER },
|
|
1020
|
+
{ runtime, origin: SIGNER },
|
|
1021
|
+
);
|
|
1022
|
+
expect(r.ok).toBe(true);
|
|
1023
|
+
expect(nodesFor(runtime, "available")).toEqual(["dim2"]);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("reverseDotNs needs no TLD, and reads none", async () => {
|
|
1027
|
+
const runtime = paseoRuntime({ nameOf: "dim2.paseo" });
|
|
1028
|
+
const r = await reverseDotNs(OWNER, { runtime });
|
|
1029
|
+
expect(r).toEqual({ ok: true, value: "dim2.paseo" });
|
|
1030
|
+
expect(runtime.calls.map((c) => c.functionName)).toEqual(["nameOf"]);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test("every entry point consults the chain when no tld is supplied", async () => {
|
|
1034
|
+
// The regression guard for the whole change: if any entry point stops
|
|
1035
|
+
// asking, it is back to assuming a root.
|
|
1036
|
+
for (const call of [
|
|
1037
|
+
(runtime: ReturnType<typeof paseoRuntime>) => resolveDotNs("dim2.paseo", { runtime }),
|
|
1038
|
+
(runtime: ReturnType<typeof paseoRuntime>) =>
|
|
1039
|
+
isDotNsAvailable("dim2.paseo", { runtime }),
|
|
1040
|
+
(runtime: ReturnType<typeof paseoRuntime>) =>
|
|
1041
|
+
setDotNsRecord(
|
|
1042
|
+
{ name: "dim2.paseo", address: TARGET },
|
|
1043
|
+
{ runtime, origin: SIGNER },
|
|
1044
|
+
),
|
|
1045
|
+
(runtime: ReturnType<typeof paseoRuntime>) =>
|
|
1046
|
+
prepareDotNsRegistration(
|
|
1047
|
+
{ name: "dim2.paseo", owner: OWNER },
|
|
1048
|
+
{ runtime, origin: SIGNER },
|
|
1049
|
+
),
|
|
1050
|
+
]) {
|
|
1051
|
+
const runtime = paseoRuntime({ owner: OWNER, resolver: ZERO, available: true });
|
|
1052
|
+
await call(runtime);
|
|
1053
|
+
expect(runtime.calls.map((c) => c.functionName)).toContain("tld");
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
test("a legacy deployment with no tld() getter still resolves .dot names", async () => {
|
|
1058
|
+
// Guards the pre-b4096968 shape, which Previewnet was thought to have
|
|
1059
|
+
// until it reported `.test`. Kept: nothing proves the shape is extinct.
|
|
1060
|
+
const runtime = createFakeContractRuntime({
|
|
1061
|
+
abi: [...ALL_ABIS, ...DOTNS_PROTOCOL_REGISTRY_ABI],
|
|
1062
|
+
onQuery: ({ functionName }) => {
|
|
1063
|
+
if (functionName === "tld" || functionName === "tldNode") {
|
|
1064
|
+
return fakeDryRunResult({ revert: true });
|
|
1065
|
+
}
|
|
1066
|
+
if (functionName === "owner") return OWNER;
|
|
1067
|
+
if (functionName === "resolver") return ZERO;
|
|
1068
|
+
return undefined;
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
const r = await resolveDotNs("dim2.dot", { runtime });
|
|
1072
|
+
expect(r).toEqual({ ok: true, value: { name: "dim2.dot", owner: OWNER } });
|
|
1073
|
+
expect(nodesFor(runtime, "owner")).toEqual([namehash("dim2.dot", DOT_TLD)]);
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
describe("the resolver pointer is an allowlist, not a denylist", () => {
|
|
1078
|
+
/**
|
|
1079
|
+
* `DotnsContentResolver` on Paseo Asset Hub Next V2, from walking
|
|
1080
|
+
* `protocolRegistry.get(bytes32("contentResolver"))`.
|
|
1081
|
+
*
|
|
1082
|
+
* The live pointer for `dim2` on both networks. It is a real contract with no
|
|
1083
|
+
* `addressOf`, so asking it for an address reverts — which is why a
|
|
1084
|
+
* registered, owned name used to come back as `err RegistryCall`.
|
|
1085
|
+
*/
|
|
1086
|
+
const CONTENT_RESOLVER = "0x7F74D7CD50f5a834270E2ad395a01b01891AB37d";
|
|
1087
|
+
/** `DotnsPopResolver` from the same walk: a fourth resolver, equally unknown here. */
|
|
1088
|
+
const POP_RESOLVER = "0xDaC984884EcA8Fc44011f1D6C49B27828390A72B";
|
|
1089
|
+
|
|
1090
|
+
/** A runtime where the node is owned and points at `resolverAddr`. */
|
|
1091
|
+
const ownedPointingAt = (resolverAddr: string) =>
|
|
1092
|
+
createFakeContractRuntime({
|
|
1093
|
+
abi: ALL_ABIS,
|
|
1094
|
+
onQuery: ({ functionName }) => {
|
|
1095
|
+
if (functionName === "owner") return OWNER;
|
|
1096
|
+
if (functionName === "resolver") return resolverAddr;
|
|
1097
|
+
// Asking any of these for an address reverts, as the real ones do.
|
|
1098
|
+
if (functionName === "addressOf") return fakeDryRunResult({ revert: true });
|
|
1099
|
+
return undefined;
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test("a resolver we do not know means no forward record, not an error", async () => {
|
|
1104
|
+
// The assertion no earlier test could make: every fake until now returned
|
|
1105
|
+
// one of the three pointers the code contemplated, so a fourth resolver
|
|
1106
|
+
// was unreachable by construction.
|
|
1107
|
+
const runtime = ownedPointingAt(CONTENT_RESOLVER);
|
|
1108
|
+
const r = await resolveDotNs("dim2.dot", { runtime, tld: DOT_TLD });
|
|
1109
|
+
expect(r).toEqual({ ok: true, value: { name: "dim2.dot", owner: OWNER } });
|
|
1110
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("addressOf");
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
test("the same holds for the PoP resolver, and for one nobody has deployed yet", async () => {
|
|
1114
|
+
for (const pointer of [POP_RESOLVER, "0x1234512345123451234512345123451234512345"]) {
|
|
1115
|
+
const runtime = ownedPointingAt(pointer);
|
|
1116
|
+
const r = await resolveDotNs("dim2.dot", { runtime, tld: DOT_TLD });
|
|
1117
|
+
expect(r).toEqual({ ok: true, value: { name: "dim2.dot", owner: OWNER } });
|
|
1118
|
+
expect(runtime.calls.map((c) => c.functionName)).not.toContain("addressOf");
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
test("the forward resolver is still asked, and still answers", async () => {
|
|
1123
|
+
const runtime = createFakeContractRuntime({
|
|
1124
|
+
abi: ALL_ABIS,
|
|
1125
|
+
onQuery: ({ functionName }) => {
|
|
1126
|
+
if (functionName === "owner") return OWNER;
|
|
1127
|
+
if (functionName === "resolver") return DOTNS_ADDRESSES.resolver;
|
|
1128
|
+
if (functionName === "addressOf") return TARGET;
|
|
1129
|
+
return undefined;
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
1132
|
+
const r = await resolveDotNs("dim2.dot", { runtime, tld: DOT_TLD });
|
|
1133
|
+
expect(r).toEqual({ ok: true, value: { address: TARGET, name: "dim2.dot", owner: OWNER } });
|
|
1134
|
+
expect(runtime.calls.map((c) => c.functionName)).toContain("addressOf");
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("an overridden forward resolver is the one that gets asked", async () => {
|
|
1138
|
+
// The allowlist has to key off `opts`, not the hardcoded table, or an
|
|
1139
|
+
// override silently turns every name into "no forward record".
|
|
1140
|
+
const custom = "0x5555555555555555555555555555555555555555";
|
|
1141
|
+
const runtime = createFakeContractRuntime({
|
|
1142
|
+
abi: ALL_ABIS,
|
|
1143
|
+
onQuery: ({ functionName }) => {
|
|
1144
|
+
if (functionName === "owner") return OWNER;
|
|
1145
|
+
if (functionName === "resolver") return custom;
|
|
1146
|
+
if (functionName === "addressOf") return TARGET;
|
|
1147
|
+
return undefined;
|
|
1148
|
+
},
|
|
1149
|
+
});
|
|
1150
|
+
const r = await resolveDotNs("dim2.dot", {
|
|
1151
|
+
runtime,
|
|
1152
|
+
tld: DOT_TLD,
|
|
1153
|
+
resolverAddress: custom,
|
|
1154
|
+
});
|
|
1155
|
+
expect(r).toEqual({ ok: true, value: { address: TARGET, name: "dim2.dot", owner: OWNER } });
|
|
1156
|
+
});
|
|
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
|
+
});
|