@parity/product-sdk 0.0.0-dev.312.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/LICENSE +201 -0
- package/dist/address/index.d.ts +9 -0
- package/dist/address/index.js +3 -0
- package/dist/address/index.js.map +1 -0
- package/dist/chain/index.d.ts +1 -0
- package/dist/chain/index.js +3 -0
- package/dist/chain/index.js.map +1 -0
- package/dist/chunk-BRZNHTLQ.js +89 -0
- package/dist/chunk-BRZNHTLQ.js.map +1 -0
- package/dist/chunk-DHRCWMY5.js +240 -0
- package/dist/chunk-DHRCWMY5.js.map +1 -0
- package/dist/chunk-JVJ552RU.js +3 -0
- package/dist/chunk-JVJ552RU.js.map +1 -0
- package/dist/cloud-storage/index.d.ts +1 -0
- package/dist/cloud-storage/index.js +3 -0
- package/dist/cloud-storage/index.js.map +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/index.js +3 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/dotns-bswWAmR2.d.ts +136 -0
- package/dist/host/index.d.ts +1 -0
- package/dist/host/index.js +3 -0
- package/dist/host/index.js.map +1 -0
- package/dist/identity/index.d.ts +491 -0
- package/dist/identity/index.js +691 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/individuality/index.d.ts +1 -0
- package/dist/individuality/index.js +3 -0
- package/dist/individuality/index.js.map +1 -0
- package/dist/local-storage/index.d.ts +1 -0
- package/dist/local-storage/index.js +3 -0
- package/dist/local-storage/index.js.map +1 -0
- package/dist/react/index.d.ts +181 -0
- package/dist/react/index.js +198 -0
- package/dist/react/index.js.map +1 -0
- package/dist/testing/index.d.ts +61 -0
- package/dist/testing/index.js +149 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-B5Zx4RNq.d.ts +211 -0
- package/dist/wallet/index.d.ts +1 -0
- package/dist/wallet/index.js +3 -0
- package/dist/wallet/index.js.map +1 -0
- package/package.json +118 -0
- package/src/address/index.ts +8 -0
- package/src/chain/index.ts +8 -0
- package/src/cloud-storage/index.ts +8 -0
- package/src/contracts/index.ts +8 -0
- package/src/core/createApp.ts +408 -0
- package/src/core/index.ts +12 -0
- package/src/core/logger.ts +15 -0
- package/src/core/types.ts +233 -0
- package/src/crypto/index.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/identity/dotns-abis.test.ts +43 -0
- package/src/identity/dotns-abis.ts +274 -0
- package/src/identity/dotns-errors.ts +34 -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 +1154 -0
- package/src/identity/dotns-registry.ts +870 -0
- package/src/identity/dotns.test.ts +128 -0
- package/src/identity/dotns.ts +221 -0
- package/src/identity/index.ts +61 -0
- package/src/identity/product-account.test.ts +130 -0
- package/src/identity/product-account.ts +115 -0
- package/src/identity/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/individuality/contract.test.ts +135 -0
- package/src/individuality/index.ts +8 -0
- package/src/local-storage/index.ts +8 -0
- package/src/react/context.ts +35 -0
- package/src/react/index.ts +43 -0
- package/src/react/provider.tsx +78 -0
- package/src/react/useChain.ts +35 -0
- package/src/react/useLocalStorage.ts +127 -0
- package/src/react/useWallet.ts +156 -0
- package/src/testing.ts +297 -0
- package/src/wallet/index.ts +11 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Guards on the transcribed deployment table. These check the shape of the
|
|
5
|
+
// table, not that it names the live deployment: only a diff against
|
|
6
|
+
// `paritytech/dotns` `deployments/paseo-assethub/420420417.json` can do that,
|
|
7
|
+
// and it is a network fetch. See the comment on DOTNS_ADDRESSES for the
|
|
8
|
+
// commit the values were copied from, and re-check it when bumping.
|
|
9
|
+
import { isValidH160 } from "@parity/product-sdk-address";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { DOTNS_ADDRESSES } from "./dotns-abis.js";
|
|
12
|
+
|
|
13
|
+
describe("DOTNS_ADDRESSES", () => {
|
|
14
|
+
test("every entry is a well-formed H160", () => {
|
|
15
|
+
for (const [name, address] of Object.entries(DOTNS_ADDRESSES)) {
|
|
16
|
+
expect(isValidH160(address), `${name} = ${address}`).toBe(true);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("no two entries share an address", () => {
|
|
21
|
+
// Six addresses transcribed by hand: a duplicated paste is the
|
|
22
|
+
// transcription error most likely to go unnoticed, because the wrong
|
|
23
|
+
// contract still answers and only the decode looks odd.
|
|
24
|
+
const entries = Object.entries(DOTNS_ADDRESSES);
|
|
25
|
+
const seen = new Map<string, string>();
|
|
26
|
+
for (const [name, address] of entries) {
|
|
27
|
+
const key = address.toLowerCase();
|
|
28
|
+
expect(seen.get(key), `${name} duplicates ${seen.get(key)}`).toBeUndefined();
|
|
29
|
+
seen.set(key, name);
|
|
30
|
+
}
|
|
31
|
+
expect(seen.size).toBe(entries.length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("the resolver and the reverse resolver are different contracts", () => {
|
|
35
|
+
// Load-bearing beyond general distinctness: resolveDotNs decides whether
|
|
36
|
+
// a node has a forward record by comparing the registry's resolver
|
|
37
|
+
// pointer against the reverse resolver, so collapsing these two would
|
|
38
|
+
// make every registered name look resolvable.
|
|
39
|
+
expect(DOTNS_ADDRESSES.resolver.toLowerCase()).not.toBe(
|
|
40
|
+
DOTNS_ADDRESSES.reverseResolver.toLowerCase(),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Minimal ABIs for the DotNS contracts: the read and write methods this module
|
|
5
|
+
* calls, and nothing else. Sourced from `paritytech/dotns` (`contracts/registry`,
|
|
6
|
+
* `contracts/resolvers`, `contracts/registrars`, `contracts/pop`).
|
|
7
|
+
*/
|
|
8
|
+
import type { AbiEntry } from "@parity/product-sdk-contracts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `DotnsRegistry` — node → resolver / owner, plus the owner-gated pointer write.
|
|
12
|
+
*
|
|
13
|
+
* `setResolver` is needed because registration parks `records[node].resolver` on
|
|
14
|
+
* the reverse resolver, so the pointer must be moved once before any address
|
|
15
|
+
* record is readable.
|
|
16
|
+
*/
|
|
17
|
+
export const DOTNS_REGISTRY_ABI: AbiEntry[] = [
|
|
18
|
+
{
|
|
19
|
+
type: "function",
|
|
20
|
+
name: "resolver",
|
|
21
|
+
inputs: [{ name: "node", type: "bytes32" }],
|
|
22
|
+
outputs: [{ name: "", type: "address" }],
|
|
23
|
+
stateMutability: "view",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "function",
|
|
27
|
+
name: "owner",
|
|
28
|
+
inputs: [{ name: "node", type: "bytes32" }],
|
|
29
|
+
outputs: [{ name: "", type: "address" }],
|
|
30
|
+
stateMutability: "view",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "function",
|
|
34
|
+
name: "setResolver",
|
|
35
|
+
inputs: [
|
|
36
|
+
{ name: "node", type: "bytes32" },
|
|
37
|
+
{ name: "resolverAddr", type: "address" },
|
|
38
|
+
],
|
|
39
|
+
outputs: [],
|
|
40
|
+
stateMutability: "nonpayable",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** `DotnsResolver` — node → resolved address. */
|
|
45
|
+
export const DOTNS_RESOLVER_ABI: AbiEntry[] = [
|
|
46
|
+
{
|
|
47
|
+
type: "function",
|
|
48
|
+
name: "addressOf",
|
|
49
|
+
inputs: [{ name: "node", type: "bytes32" }],
|
|
50
|
+
outputs: [{ name: "value", type: "address" }],
|
|
51
|
+
stateMutability: "view",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** `DotnsReverseResolver` — account → primary name. */
|
|
56
|
+
export const DOTNS_REVERSE_RESOLVER_ABI: AbiEntry[] = [
|
|
57
|
+
{
|
|
58
|
+
type: "function",
|
|
59
|
+
name: "nameOf",
|
|
60
|
+
inputs: [{ name: "addr", type: "address" }],
|
|
61
|
+
outputs: [{ name: "name", type: "string" }],
|
|
62
|
+
stateMutability: "view",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** `DotnsResolver` write — set a node's resolved address (owner-gated). */
|
|
67
|
+
export const DOTNS_RESOLVER_WRITE_ABI: AbiEntry[] = [
|
|
68
|
+
{
|
|
69
|
+
type: "function",
|
|
70
|
+
name: "setAddress",
|
|
71
|
+
inputs: [
|
|
72
|
+
{ name: "node", type: "bytes32" },
|
|
73
|
+
{ name: "value", type: "address" },
|
|
74
|
+
],
|
|
75
|
+
outputs: [],
|
|
76
|
+
stateMutability: "nonpayable",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `DotnsRegistrarController` — the commit-reveal registration flow. `register`
|
|
82
|
+
* is payable; the value is the price from {@link DOTNS_POP_RULES_ABI}.
|
|
83
|
+
* `Registration` = (label, owner, secret, reserved).
|
|
84
|
+
*/
|
|
85
|
+
export const DOTNS_REGISTRAR_CONTROLLER_ABI: AbiEntry[] = [
|
|
86
|
+
{
|
|
87
|
+
type: "function",
|
|
88
|
+
name: "available",
|
|
89
|
+
inputs: [{ name: "label", type: "string" }],
|
|
90
|
+
outputs: [{ name: "isAvailable", type: "bool" }],
|
|
91
|
+
stateMutability: "view",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "function",
|
|
95
|
+
name: "minCommitmentAge",
|
|
96
|
+
inputs: [],
|
|
97
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
98
|
+
stateMutability: "view",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "function",
|
|
102
|
+
name: "maxCommitmentAge",
|
|
103
|
+
inputs: [],
|
|
104
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
105
|
+
stateMutability: "view",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "function",
|
|
109
|
+
name: "makeCommitment",
|
|
110
|
+
inputs: [
|
|
111
|
+
{
|
|
112
|
+
name: "registration",
|
|
113
|
+
type: "tuple",
|
|
114
|
+
components: [
|
|
115
|
+
{ name: "label", type: "string" },
|
|
116
|
+
{ name: "owner", type: "address" },
|
|
117
|
+
{ name: "secret", type: "bytes32" },
|
|
118
|
+
{ name: "reserved", type: "bool" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
outputs: [{ name: "commitment", type: "bytes32" }],
|
|
123
|
+
stateMutability: "pure",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: "function",
|
|
127
|
+
name: "commit",
|
|
128
|
+
inputs: [{ name: "commitment", type: "bytes32" }],
|
|
129
|
+
outputs: [],
|
|
130
|
+
stateMutability: "nonpayable",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: "function",
|
|
134
|
+
name: "register",
|
|
135
|
+
inputs: [
|
|
136
|
+
{
|
|
137
|
+
name: "registration",
|
|
138
|
+
type: "tuple",
|
|
139
|
+
components: [
|
|
140
|
+
{ name: "label", type: "string" },
|
|
141
|
+
{ name: "owner", type: "address" },
|
|
142
|
+
{ name: "secret", type: "bytes32" },
|
|
143
|
+
{ name: "reserved", type: "bool" },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
outputs: [],
|
|
148
|
+
stateMutability: "payable",
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `PopRules` — registration pricing.
|
|
154
|
+
*
|
|
155
|
+
* `price` is the label-only cost and runs no eligibility rules, so it is not
|
|
156
|
+
* what `register` charges. `register` uses the owner-aware variants and adds
|
|
157
|
+
* `transferFloor` when the payer is not the owner:
|
|
158
|
+
* `max(priceWithoutCheck(label, owner).price, transferFloor(label, payer, owner))`.
|
|
159
|
+
*
|
|
160
|
+
* `PopStatus` encodes as `uint8`: 0 NoStatus, 1 PopLite, 2 PopFull, 3 Reserved.
|
|
161
|
+
*/
|
|
162
|
+
export const DOTNS_POP_RULES_ABI: AbiEntry[] = [
|
|
163
|
+
{
|
|
164
|
+
type: "function",
|
|
165
|
+
name: "price",
|
|
166
|
+
inputs: [{ name: "name", type: "string" }],
|
|
167
|
+
outputs: [{ name: "cost", type: "uint256" }],
|
|
168
|
+
stateMutability: "view",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "function",
|
|
172
|
+
name: "priceWithoutCheck",
|
|
173
|
+
inputs: [
|
|
174
|
+
{ name: "name", type: "string" },
|
|
175
|
+
{ name: "userAddress", type: "address" },
|
|
176
|
+
],
|
|
177
|
+
outputs: [
|
|
178
|
+
{
|
|
179
|
+
name: "metadata",
|
|
180
|
+
type: "tuple",
|
|
181
|
+
components: [
|
|
182
|
+
{ name: "price", type: "uint256" },
|
|
183
|
+
{ name: "status", type: "uint8" },
|
|
184
|
+
{ name: "userStatus", type: "uint8" },
|
|
185
|
+
{ name: "message", type: "string" },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
stateMutability: "view",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: "function",
|
|
193
|
+
name: "classifyName",
|
|
194
|
+
inputs: [{ name: "name", type: "string" }],
|
|
195
|
+
outputs: [
|
|
196
|
+
{ name: "requirement", type: "uint8" },
|
|
197
|
+
{ name: "message", type: "string" },
|
|
198
|
+
],
|
|
199
|
+
stateMutability: "pure",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: "function",
|
|
203
|
+
name: "transferFloor",
|
|
204
|
+
inputs: [
|
|
205
|
+
{ name: "name", type: "string" },
|
|
206
|
+
{ name: "from", type: "address" },
|
|
207
|
+
{ name: "to", type: "address" },
|
|
208
|
+
],
|
|
209
|
+
outputs: [{ name: "floor", type: "uint256" }],
|
|
210
|
+
stateMutability: "view",
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* `IDotnsProtocolRegistry`, the two TLD getters.
|
|
216
|
+
*
|
|
217
|
+
* The TLD is per network: `initialize(tldLabel)` fixes it once at deployment
|
|
218
|
+
* and there is no setter, so these are immutable for the life of a deployment.
|
|
219
|
+
* `tld()` returns the suffix with its leading dot (`".paseo"`); `tldNode()`
|
|
220
|
+
* returns `namehash(0, labelhash(label))`, which is derivable from the suffix
|
|
221
|
+
* and so is read only as a cross-check.
|
|
222
|
+
*
|
|
223
|
+
* Deployments predating `dotns` `b4096968` have neither getter — the TLD was a
|
|
224
|
+
* compile-time constant there — and revert with an empty payload.
|
|
225
|
+
*/
|
|
226
|
+
export const DOTNS_PROTOCOL_REGISTRY_ABI: AbiEntry[] = [
|
|
227
|
+
{
|
|
228
|
+
type: "function",
|
|
229
|
+
name: "tld",
|
|
230
|
+
inputs: [],
|
|
231
|
+
outputs: [{ name: "suffix", type: "string" }],
|
|
232
|
+
stateMutability: "view",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: "function",
|
|
236
|
+
name: "tldNode",
|
|
237
|
+
inputs: [],
|
|
238
|
+
outputs: [{ name: "node", type: "bytes32" }],
|
|
239
|
+
stateMutability: "view",
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
/** `IPopRules.PopStatus`. Ordering is meaningful: a user meets a tier when `userStatus >= status`. */
|
|
244
|
+
export const POP_STATUS = { NoStatus: 0, PopLite: 1, PopFull: 2, Reserved: 3 } as const;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The deployed DotNS addresses. The same on every network.
|
|
248
|
+
*
|
|
249
|
+
* Not a per-network table, despite what the old name (`PASEO_ASSETHUB_DOTNS`)
|
|
250
|
+
* implied: every DotNS network is deployed through the same CREATE3 factory, so
|
|
251
|
+
* the addresses are identical everywhere and **only the TLD differs**. Verified
|
|
252
|
+
* on both Paseo Asset Hub Next V2 and Previewnet — `protocolRegistry.get(...)`
|
|
253
|
+
* returns the same registry on each, and all six addresses hold contracts on
|
|
254
|
+
* both. `paritytech/dotns` `DEPLOYMENTS.md` states the mechanism, and
|
|
255
|
+
* `preview-net-v1` confirms it from the deploy side: the TLD is passed only as
|
|
256
|
+
* registry init calldata, so it moves no address.
|
|
257
|
+
*
|
|
258
|
+
* So extending this module to another network means resolving its TLD, which
|
|
259
|
+
* `resolveTld` already does — not adding a second address table.
|
|
260
|
+
*
|
|
261
|
+
* Each address answers `DotnsProtocolRegistry.get(bytes32)` under its
|
|
262
|
+
* `DotnsConstants` key (the name below, right-padded to 32 bytes) and has a
|
|
263
|
+
* `Revive.AccountInfoOf` entry. They are pinned rather than resolved at runtime,
|
|
264
|
+
* so re-verify after a redeploy; `protocolRegistry` is the address to walk from
|
|
265
|
+
* if we ever resolve the rest dynamically.
|
|
266
|
+
*/
|
|
267
|
+
export const DOTNS_ADDRESSES = {
|
|
268
|
+
registry: "0xf34054fd76BbF85f216cf9908226D5f0A72E50CA",
|
|
269
|
+
reverseResolver: "0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035",
|
|
270
|
+
resolver: "0xbd1165E549DF96F083c0A16f61590927bC187009",
|
|
271
|
+
registrarController: "0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30",
|
|
272
|
+
popRules: "0x747B456bE03aec0b42bd85C51513730FBD45DA31",
|
|
273
|
+
protocolRegistry: "0xD19e3D0C97CF501125a04A97405e3e6592fa846E",
|
|
274
|
+
} as const;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import type { SdkError } from "@parity/product-sdk-errors";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error on the `Err` channel of a DotNS registry read/write. Implements the
|
|
7
|
+
* cross-package {@link SdkError} marker so `isSdkError(e)` recognizes it.
|
|
8
|
+
*
|
|
9
|
+
* `reason` distinguishes the failure kind so callers can branch without string
|
|
10
|
+
* matching.
|
|
11
|
+
*/
|
|
12
|
+
export type DotNsErrorReason =
|
|
13
|
+
| "InvalidName" // name failed isValidDotNsName
|
|
14
|
+
| "InvalidTld" // the deployment reported a TLD we cannot use, or the caller supplied an inconsistent one
|
|
15
|
+
| "TldMismatch" // a well-formed name, but rooted at another deployment's TLD
|
|
16
|
+
| "MissingOrigin" // a write was requested without the submitting account
|
|
17
|
+
| "InvalidOrigin" // the submitting account was supplied, but is not a decodable SS58 address
|
|
18
|
+
| "NameUnavailable" // already registered
|
|
19
|
+
| "NameReserved" // governance-held, or a base stem held by another user
|
|
20
|
+
| "OwnerStatusInsufficient" // the owner lacks the personhood tier the label requires
|
|
21
|
+
| "RegistryCall" // the on-chain contract call failed
|
|
22
|
+
| "Decode"; // the contract returned something we couldn't decode
|
|
23
|
+
|
|
24
|
+
export class DotNsError extends Error implements SdkError {
|
|
25
|
+
readonly isSdkError = true as const;
|
|
26
|
+
readonly source = "dotns";
|
|
27
|
+
readonly reason: DotNsErrorReason;
|
|
28
|
+
|
|
29
|
+
constructor(reason: DotNsErrorReason, message: string, options?: ErrorOptions) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "DotNsError";
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { bytesToHex, keccak256 } from "@parity/product-sdk-crypto";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { DOT_NODE, DOT_TLD, dotNsTld, isConsistentDotNsTld, namehash } from "./dotns-namehash.js";
|
|
6
|
+
|
|
7
|
+
// Independent reference implementation of the same algorithm, so the test
|
|
8
|
+
// pins behaviour rather than restating the code.
|
|
9
|
+
function hexBytes(hex: string): Uint8Array {
|
|
10
|
+
const pairs = hex.slice(2).match(/.{2}/g) ?? [];
|
|
11
|
+
return Uint8Array.from(pairs.map((b) => Number.parseInt(b, 16)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function refNamehash(name: string, suffix: string, root: string): string {
|
|
15
|
+
const trimmed = name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
16
|
+
const labels = trimmed === "" ? [] : trimmed.split(".");
|
|
17
|
+
let node = hexBytes(root);
|
|
18
|
+
for (let i = labels.length - 1; i >= 0; i--) {
|
|
19
|
+
const lh = keccak256(new TextEncoder().encode(labels[i]));
|
|
20
|
+
const combined = new Uint8Array(64);
|
|
21
|
+
combined.set(node, 0);
|
|
22
|
+
combined.set(lh, 32);
|
|
23
|
+
node = keccak256(combined);
|
|
24
|
+
}
|
|
25
|
+
return `0x${bytesToHex(node)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The `.paseo` TLD node, as `DotnsProtocolRegistry.tldNode()` reports it on
|
|
30
|
+
* Paseo Asset Hub Next V2.
|
|
31
|
+
*
|
|
32
|
+
* Pinned as an observed literal on purpose: it is the one value that proves our
|
|
33
|
+
* derivation matches what the contract computed at initialisation. Deriving it
|
|
34
|
+
* here instead would make the test agree with the code by construction.
|
|
35
|
+
*
|
|
36
|
+
* Needs no re-verification — it is `keccak256(0x00…00 ‖ keccak256("paseo"))`,
|
|
37
|
+
* so it is fixed by the label alone, not by any deployment's current state.
|
|
38
|
+
*/
|
|
39
|
+
const PASEO_NODE_FROM_CHAIN = "0x096b436ee9a398429fe33ad4b359bad4398dd74b412ec1dd043c93dfbf581874";
|
|
40
|
+
|
|
41
|
+
const PASEO_TLD = dotNsTld(".paseo");
|
|
42
|
+
|
|
43
|
+
describe("dotns namehash", () => {
|
|
44
|
+
test("DOT_NODE is keccak256(zeros32 || keccak256('dot'))", () => {
|
|
45
|
+
// The only hardcoded value in the module: every node hash derives from
|
|
46
|
+
// it, and namehash(".dot") returning it is a tautology, since an empty
|
|
47
|
+
// label list skips the loop. Derive it instead.
|
|
48
|
+
const labelhash = keccak256(new TextEncoder().encode("dot"));
|
|
49
|
+
const combined = new Uint8Array(64); // leading 32 bytes are the zero root
|
|
50
|
+
combined.set(labelhash, 32);
|
|
51
|
+
expect(`0x${bytesToHex(keccak256(combined))}`).toBe(DOT_NODE);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("bare .dot hashes to the DOT_NODE constant", () => {
|
|
55
|
+
expect(namehash(".dot", DOT_TLD)).toBe(DOT_NODE);
|
|
56
|
+
expect(namehash("", DOT_TLD)).toBe(DOT_NODE);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("normalizes the same with or without the .dot suffix", () => {
|
|
60
|
+
expect(namehash("alice.dot", DOT_TLD)).toBe(namehash("alice", DOT_TLD));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("matches the reference algorithm for single + sub labels", () => {
|
|
64
|
+
expect(namehash("alice.dot", DOT_TLD)).toBe(refNamehash("alice.dot", ".dot", DOT_NODE));
|
|
65
|
+
expect(namehash("bob.alice.dot", DOT_TLD)).toBe(
|
|
66
|
+
refNamehash("bob.alice.dot", ".dot", DOT_NODE),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("distinct names produce distinct nodes", () => {
|
|
71
|
+
expect(namehash("alice.dot", DOT_TLD)).not.toBe(namehash("bob.dot", DOT_TLD));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("returns a 32-byte 0x-hex string", () => {
|
|
75
|
+
expect(namehash("alice.dot", DOT_TLD)).toMatch(/^0x[0-9a-f]{64}$/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("roots at the TLD it is given, matching the reference algorithm", () => {
|
|
79
|
+
expect(namehash("alice.paseo", PASEO_TLD)).toBe(
|
|
80
|
+
refNamehash("alice.paseo", ".paseo", PASEO_NODE_FROM_CHAIN),
|
|
81
|
+
);
|
|
82
|
+
expect(namehash("bob.alice.paseo", PASEO_TLD)).toBe(
|
|
83
|
+
refNamehash("bob.alice.paseo", ".paseo", PASEO_NODE_FROM_CHAIN),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the same label under two TLDs gives two different nodes", () => {
|
|
88
|
+
// The bug this whole change exists for, in one assertion: `dim2` is
|
|
89
|
+
// owned on paseo-asset-hub-next under `.paseo` and unowned under `.dot`.
|
|
90
|
+
expect(namehash("alice.paseo", PASEO_TLD)).not.toBe(namehash("alice.dot", DOT_TLD));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("strips the suffix by length, not by a hardcoded 4", () => {
|
|
94
|
+
// `slice(0, -4)` would leave the label `alice.pa`, which hashes as the
|
|
95
|
+
// subname `pa` under `alice`.
|
|
96
|
+
const wrong = refNamehash("alice.pa", ".paseo", PASEO_NODE_FROM_CHAIN);
|
|
97
|
+
expect(namehash("alice.paseo", PASEO_TLD)).not.toBe(wrong);
|
|
98
|
+
expect(namehash("alice.paseo", PASEO_TLD)).toBe(namehash("alice", PASEO_TLD));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("dotNsTld", () => {
|
|
103
|
+
test("derives the .paseo node the contract computed at initialisation", () => {
|
|
104
|
+
// `DotnsProtocolRegistry.initialize` sets
|
|
105
|
+
// `_tldNode = namehashUnder(0, labelhash(tldLabel))`, so the node is a
|
|
106
|
+
// pure function of the suffix and we never need to read `tldNode()`.
|
|
107
|
+
expect(dotNsTld(".paseo").node).toBe(PASEO_NODE_FROM_CHAIN);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("derives DOT_NODE for .dot, tying the helper to the constant", () => {
|
|
111
|
+
expect(dotNsTld(".dot").node).toBe(DOT_NODE);
|
|
112
|
+
expect(dotNsTld(".dot")).toEqual(DOT_TLD);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("keeps the suffix verbatim, including its leading dot", () => {
|
|
116
|
+
expect(dotNsTld(".paseo").suffix).toBe(".paseo");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("accepts a bare label as well as a dotted suffix", () => {
|
|
120
|
+
// `tld()` returns ".paseo" but the deploy pipeline and `initialize`
|
|
121
|
+
// both speak the bare label, so accept either spelling.
|
|
122
|
+
expect(dotNsTld("paseo")).toEqual(dotNsTld(".paseo"));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("isConsistentDotNsTld", () => {
|
|
127
|
+
test("accepts a pair whose node is the derivation of its suffix", () => {
|
|
128
|
+
expect(isConsistentDotNsTld(DOT_TLD)).toBe(true);
|
|
129
|
+
expect(isConsistentDotNsTld(PASEO_TLD)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("rejects a suffix paired with another TLD's node", () => {
|
|
133
|
+
// The override-path form of the original bug: `.paseo` names rooted at
|
|
134
|
+
// the `.dot` node resolve to nodes nobody owns.
|
|
135
|
+
expect(isConsistentDotNsTld({ suffix: ".paseo", node: DOT_NODE })).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("rejects an empty suffix and a zero node", () => {
|
|
139
|
+
// What a pre-b4096968 proxy upgraded without a reinitializer reports:
|
|
140
|
+
// `_tld` is "" and `_tldNode` is 0x00…0, and both getters succeed.
|
|
141
|
+
expect(isConsistentDotNsTld({ suffix: "", node: DOT_NODE })).toBe(false);
|
|
142
|
+
expect(isConsistentDotNsTld({ suffix: ".", node: DOT_NODE })).toBe(false);
|
|
143
|
+
expect(
|
|
144
|
+
isConsistentDotNsTld({
|
|
145
|
+
suffix: ".paseo",
|
|
146
|
+
node: `0x${"00".repeat(32)}`,
|
|
147
|
+
}),
|
|
148
|
+
).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("rejects a multi-label suffix, which no deployment can have", () => {
|
|
152
|
+
// `initialize` requires `tldLabel.isSingleLabel()`.
|
|
153
|
+
expect(isConsistentDotNsTld({ suffix: ".a.b", node: dotNsTld(".a.b").node })).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* DotNS namehash.
|
|
5
|
+
*
|
|
6
|
+
* ENS-style recursive hashing, but rooted at the network's TLD node rather than
|
|
7
|
+
* the empty root — matching `DotnsRegistry` (`contracts/registry/DotnsRegistry.sol`
|
|
8
|
+
* `_namehash`) and `LabelUtils.namehashUnder`.
|
|
9
|
+
*
|
|
10
|
+
* labelhash(l) = keccak256(utf8(l))
|
|
11
|
+
* node(parent, l) = keccak256(parent ‖ labelhash(l))
|
|
12
|
+
* node("alice.paseo") = node(tldNode, "alice")
|
|
13
|
+
* node("bob.alice.paseo") = node(node(tldNode, "alice"), "bob")
|
|
14
|
+
*
|
|
15
|
+
* The root is **per network**, not a constant. `DotnsProtocolRegistry.initialize`
|
|
16
|
+
* fixes it once from a deploy-time label — `.dot` on Paseo Asset Hub Previewnet,
|
|
17
|
+
* `.paseo` on Next V2 — and publishes it as `tld()` / `tldNode()`. Hashing under
|
|
18
|
+
* the wrong root yields a node the chain has never written to, so every lookup
|
|
19
|
+
* reports "unregistered" with no error anywhere. That is why {@link namehash}
|
|
20
|
+
* takes the root rather than assuming one; `resolveTld` in `./dotns-registry.js`
|
|
21
|
+
* is what asks the chain for it.
|
|
22
|
+
*/
|
|
23
|
+
import { bytesToHex, hexToBytes, keccak256 } from "@parity/product-sdk-crypto";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A network's DotNS top-level domain: the suffix names are spelled with, and
|
|
27
|
+
* the node every one of its names hashes onto.
|
|
28
|
+
*
|
|
29
|
+
* The two must agree — `node` is `namehash(0, labelhash(label))` of `suffix`'s
|
|
30
|
+
* label — so build one with {@link dotNsTld} rather than by hand, and check an
|
|
31
|
+
* externally supplied pair with {@link isConsistentDotNsTld}. A `.paseo` suffix
|
|
32
|
+
* carrying the `.dot` node is exactly the defect this type exists to prevent.
|
|
33
|
+
*/
|
|
34
|
+
export interface DotNsTld {
|
|
35
|
+
/** Suffix including the leading dot, as `protocolRegistry.tld()` returns it. */
|
|
36
|
+
suffix: string;
|
|
37
|
+
/** Namehash of the TLD label, as `protocolRegistry.tldNode()` returns it. */
|
|
38
|
+
node: `0x${string}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The `.dot` TLD node. Mirrors the `DotnsConstants.DOT_NODE` the contracts removed in `b4096968`. */
|
|
42
|
+
export const DOT_NODE = "0x3fce7d1364a893e213bc4212792b517ffc88f5b13b86c8ef9c8d390c3a1370ce";
|
|
43
|
+
|
|
44
|
+
const ZERO_NODE = `0x${"00".repeat(32)}`;
|
|
45
|
+
|
|
46
|
+
const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s);
|
|
47
|
+
|
|
48
|
+
/** `keccak256(parent ‖ labelhash(label))`, the one hash step every node is built from. */
|
|
49
|
+
function nodeUnder(parent: Uint8Array, label: string): Uint8Array {
|
|
50
|
+
const combined = new Uint8Array(64);
|
|
51
|
+
combined.set(parent, 0);
|
|
52
|
+
combined.set(keccak256(utf8(label)), 32);
|
|
53
|
+
return keccak256(combined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a {@link DotNsTld} from a TLD suffix, deriving the node the same way the
|
|
58
|
+
* contract does.
|
|
59
|
+
*
|
|
60
|
+
* Mirrors `DotnsProtocolRegistry.initialize`, which sets
|
|
61
|
+
* `_tldNode = namehashUnder(bytes32(0), labelhash(tldLabel))`. Because the node
|
|
62
|
+
* is a pure function of the label, a client only ever needs the suffix — reading
|
|
63
|
+
* `tldNode()` is a cross-check, never the source.
|
|
64
|
+
*
|
|
65
|
+
* Accepts `".paseo"` or `"paseo"`: `tld()` returns the dotted form while
|
|
66
|
+
* `initialize` and the deploy pipeline speak the bare label.
|
|
67
|
+
*/
|
|
68
|
+
export function dotNsTld(suffix: string): DotNsTld {
|
|
69
|
+
const label = suffix.startsWith(".") ? suffix.slice(1) : suffix;
|
|
70
|
+
return {
|
|
71
|
+
suffix: `.${label}`,
|
|
72
|
+
node: `0x${bytesToHex(nodeUnder(hexToBytes(ZERO_NODE.slice(2)), label))}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a {@link DotNsTld} could have come from a real deployment.
|
|
78
|
+
*
|
|
79
|
+
* Three ways a pair fails, all of which produce silently wrong nodes rather
|
|
80
|
+
* than errors if they reach {@link namehash}:
|
|
81
|
+
*
|
|
82
|
+
* - the node is not the derivation of the suffix (a caller mixing `.paseo`
|
|
83
|
+
* with `DOT_NODE`);
|
|
84
|
+
* - the suffix is empty, or the node is zero — what a pre-`b4096968` registry
|
|
85
|
+
* proxy reports after an upgrade with no reinitializer, since `_tld` and
|
|
86
|
+
* `_tldNode` are then still at their defaults and *both getters succeed*;
|
|
87
|
+
* - the suffix is not a single label, which `initialize` rejects outright.
|
|
88
|
+
*
|
|
89
|
+
* Returns a boolean rather than a `Result` to keep this module free of the
|
|
90
|
+
* error type; callers with an error channel (`resolveTld`) map `false` to an
|
|
91
|
+
* `err` carrying the offending value.
|
|
92
|
+
*/
|
|
93
|
+
export function isConsistentDotNsTld(tld: DotNsTld): boolean {
|
|
94
|
+
const label = tld.suffix.startsWith(".") ? tld.suffix.slice(1) : tld.suffix;
|
|
95
|
+
if (label.length === 0 || label.includes(".")) return false;
|
|
96
|
+
if (tld.node.toLowerCase() === ZERO_NODE) return false;
|
|
97
|
+
return dotNsTld(label).node.toLowerCase() === tld.node.toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The `.dot` TLD, for a deployment that uses it. Not a default: pass it explicitly. */
|
|
101
|
+
export const DOT_TLD: DotNsTld = { suffix: ".dot", node: DOT_NODE };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Drop `suffix` from the end of `name`, leaving the labels beneath it.
|
|
105
|
+
*
|
|
106
|
+
* Strips by the suffix's own length, which is the whole point: the same logic
|
|
107
|
+
* written as `slice(0, -4)` turns `alice.paseo` into `alice.pa`, and every
|
|
108
|
+
* lookup for that name then asks about a subname nobody registered. A name that
|
|
109
|
+
* does not carry the suffix is returned unchanged, so a foreign suffix stays
|
|
110
|
+
* visible to whatever validates next.
|
|
111
|
+
*
|
|
112
|
+
* Lives here rather than in `./dotns.js` so the one copy is reachable from this
|
|
113
|
+
* module without pulling its heavier imports in.
|
|
114
|
+
*/
|
|
115
|
+
export function stripSuffix(name: string, suffix: string): string {
|
|
116
|
+
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compute the DotNS node hash for a name like `"alice.paseo"` or
|
|
121
|
+
* `"bob.alice.paseo"`, rooted at `tld`.
|
|
122
|
+
*
|
|
123
|
+
* The trailing suffix is the TLD (folded into `tld.node`); remaining labels are
|
|
124
|
+
* hashed most-significant-last, so `bob.alice.paseo` layers `bob` on top of
|
|
125
|
+
* `alice` on top of the TLD. Input is used as-is — normalize with
|
|
126
|
+
* `normalizeDotNsName` first if needed.
|
|
127
|
+
*
|
|
128
|
+
* `tld` is required rather than defaulted, because a default that is correct on
|
|
129
|
+
* one deployment is the defect this module was fixed for. It is also not
|
|
130
|
+
* validated here: a name whose suffix belongs to another deployment hashes to a
|
|
131
|
+
* node under `tld` instead of being refused, since this function returns a hash
|
|
132
|
+
* and has nowhere to report a complaint. The entry points in
|
|
133
|
+
* `./dotns-registry.js` do that check, where there is an error channel.
|
|
134
|
+
*
|
|
135
|
+
* @returns the 32-byte node as a `0x`-prefixed hex string.
|
|
136
|
+
*/
|
|
137
|
+
export function namehash(name: string, tld: DotNsTld): `0x${string}` {
|
|
138
|
+
const trimmed = stripSuffix(name, tld.suffix);
|
|
139
|
+
// "" → the bare TLD node; otherwise split into labels.
|
|
140
|
+
const labels = trimmed === "" ? [] : trimmed.split(".");
|
|
141
|
+
// Layer from the TLD outward: labels are given left-most-first
|
|
142
|
+
// (bob.alice), but must be applied right-to-left onto the parent.
|
|
143
|
+
// Annotated: hexToBytes yields ArrayBufferLike, keccak256 yields ArrayBuffer.
|
|
144
|
+
let node: Uint8Array = hexToBytes(tld.node.slice(2));
|
|
145
|
+
for (let i = labels.length - 1; i >= 0; i--) {
|
|
146
|
+
node = nodeUnder(node, labels[i]);
|
|
147
|
+
}
|
|
148
|
+
return `0x${bytesToHex(node)}`;
|
|
149
|
+
}
|