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