@parity/product-deploy 0.7.28-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/assets/environments.json +313 -0
  4. package/bin/bulletin-bootstrap +84 -0
  5. package/bin/bulletin-deploy +429 -0
  6. package/dist/bug-report.d.ts +29 -0
  7. package/dist/bug-report.js +27 -0
  8. package/dist/chunk-2VAUMZB2.js +284 -0
  9. package/dist/chunk-43HLT335.js +232 -0
  10. package/dist/chunk-5VZQ2KSU.js +231 -0
  11. package/dist/chunk-ADNBLFDP.js +225 -0
  12. package/dist/chunk-BMAEWZYV.js +24 -0
  13. package/dist/chunk-C2TS5MER.js +64 -0
  14. package/dist/chunk-DNXH4QTI.js +2336 -0
  15. package/dist/chunk-FZWJV5AD.js +231 -0
  16. package/dist/chunk-GZD2UFLR.js +8 -0
  17. package/dist/chunk-HOTQDYHD.js +219 -0
  18. package/dist/chunk-IDYGYIMH.js +207 -0
  19. package/dist/chunk-KHVTYIIX.js +146 -0
  20. package/dist/chunk-KJH2T5TQ.js +172 -0
  21. package/dist/chunk-KOSF5FDO.js +49 -0
  22. package/dist/chunk-LZJMVPYW.js +156 -0
  23. package/dist/chunk-MFTODIIT.js +725 -0
  24. package/dist/chunk-MMAZFJDG.js +91 -0
  25. package/dist/chunk-NF2FL4ZO.js +164 -0
  26. package/dist/chunk-OITUIM2E.js +524 -0
  27. package/dist/chunk-P6CHOMN3.js +2368 -0
  28. package/dist/chunk-QMYW3D6E.js +316 -0
  29. package/dist/chunk-QTZNULSH.js +185 -0
  30. package/dist/chunk-RI3ZLNPN.js +71 -0
  31. package/dist/chunk-S7EM5VMW.js +108 -0
  32. package/dist/chunk-T7EEVWNU.js +32 -0
  33. package/dist/chunk-UPWEOGLQ.js +37 -0
  34. package/dist/chunk-ZOC4GITL.js +13 -0
  35. package/dist/chunk-ZYVGHDMU.js +117 -0
  36. package/dist/chunk-probe.d.ts +37 -0
  37. package/dist/chunk-probe.js +18 -0
  38. package/dist/chunker.d.ts +8 -0
  39. package/dist/chunker.js +10 -0
  40. package/dist/deploy.d.ts +299 -0
  41. package/dist/deploy.js +96 -0
  42. package/dist/dotns.d.ts +506 -0
  43. package/dist/dotns.js +101 -0
  44. package/dist/environments.d.ts +104 -0
  45. package/dist/environments.js +23 -0
  46. package/dist/errors.d.ts +6 -0
  47. package/dist/errors.js +8 -0
  48. package/dist/gh-pages-mirror.d.ts +76 -0
  49. package/dist/gh-pages-mirror.js +30 -0
  50. package/dist/incremental-stats.d.ts +69 -0
  51. package/dist/incremental-stats.js +10 -0
  52. package/dist/index.d.ts +23 -0
  53. package/dist/index.js +146 -0
  54. package/dist/manifest/byte-budget.d.ts +46 -0
  55. package/dist/manifest/byte-budget.js +14 -0
  56. package/dist/manifest/config-load.d.ts +36 -0
  57. package/dist/manifest/config-load.js +10 -0
  58. package/dist/manifest/publish.d.ts +54 -0
  59. package/dist/manifest/publish.js +23 -0
  60. package/dist/manifest/schema.d.ts +29 -0
  61. package/dist/manifest/schema.js +10 -0
  62. package/dist/manifest/types.d.ts +90 -0
  63. package/dist/manifest/types.js +6 -0
  64. package/dist/manifest-embed.d.ts +18 -0
  65. package/dist/manifest-embed.js +9 -0
  66. package/dist/manifest-fetch.d.ts +32 -0
  67. package/dist/manifest-fetch.js +21 -0
  68. package/dist/manifest-roundtrip.d.ts +15 -0
  69. package/dist/manifest-roundtrip.js +55 -0
  70. package/dist/manifest.d.ts +44 -0
  71. package/dist/manifest.js +20 -0
  72. package/dist/memory-report.d.ts +95 -0
  73. package/dist/memory-report.js +17 -0
  74. package/dist/merkle.d.ts +50 -0
  75. package/dist/merkle.js +33 -0
  76. package/dist/personhood/bind-paid-alias.d.ts +43 -0
  77. package/dist/personhood/bind-paid-alias.js +10 -0
  78. package/dist/personhood/bind-personal-id.d.ts +55 -0
  79. package/dist/personhood/bind-personal-id.js +12 -0
  80. package/dist/personhood/bootstrap.d.ts +85 -0
  81. package/dist/personhood/bootstrap.js +245 -0
  82. package/dist/personhood/claim-pgas.d.ts +61 -0
  83. package/dist/personhood/claim-pgas.js +12 -0
  84. package/dist/personhood/constants.d.ts +23 -0
  85. package/dist/personhood/constants.js +22 -0
  86. package/dist/personhood/encoding.d.ts +49 -0
  87. package/dist/personhood/encoding.js +24 -0
  88. package/dist/personhood/hashing.d.ts +4 -0
  89. package/dist/personhood/hashing.js +8 -0
  90. package/dist/personhood/member-key.d.ts +12 -0
  91. package/dist/personhood/member-key.js +10 -0
  92. package/dist/personhood/people-client.d.ts +14 -0
  93. package/dist/personhood/people-client.js +48 -0
  94. package/dist/personhood/reprove.d.ts +43 -0
  95. package/dist/personhood/reprove.js +225 -0
  96. package/dist/pool.d.ts +51 -0
  97. package/dist/pool.js +30 -0
  98. package/dist/run-state.d.ts +22 -0
  99. package/dist/run-state.js +20 -0
  100. package/dist/telemetry.d.ts +56 -0
  101. package/dist/telemetry.js +71 -0
  102. package/dist/version-check.d.ts +38 -0
  103. package/dist/version-check.js +30 -0
  104. package/docs/bootstrap.md +49 -0
  105. package/docs/e2e-bootstrap.md +154 -0
  106. package/docs/telemetry.md +62 -0
  107. package/docs/testing.md +44 -0
  108. package/package.json +82 -0
  109. package/tools/release-retry-wrapper.mjs +74 -0
@@ -0,0 +1,2336 @@
1
+ import {
2
+ isTestnetSpecName
3
+ } from "./chunk-QMYW3D6E.js";
4
+ import {
5
+ captureWarning,
6
+ setDeployAttribute,
7
+ setDeploySentryTag,
8
+ truncateAddress,
9
+ withSpan
10
+ } from "./chunk-MFTODIIT.js";
11
+ import {
12
+ validateContractAddresses
13
+ } from "./chunk-OITUIM2E.js";
14
+ import {
15
+ NonRetryableError
16
+ } from "./chunk-ZOC4GITL.js";
17
+
18
+ // src/dotns.ts
19
+ import crypto from "crypto";
20
+ import { createClient, Enum } from "polkadot-api";
21
+ import { getPolkadotSigner } from "polkadot-api/signer";
22
+ import { getWsProvider } from "polkadot-api/ws";
23
+ import { Keyring } from "@polkadot/keyring";
24
+ import { cryptoWaitReady } from "@polkadot/util-crypto";
25
+ import { Binary } from "polkadot-api";
26
+ import {
27
+ encodeFunctionData,
28
+ decodeFunctionResult,
29
+ decodeErrorResult,
30
+ keccak256,
31
+ toBytes,
32
+ formatEther,
33
+ isAddress,
34
+ bytesToHex,
35
+ isHex,
36
+ toHex,
37
+ zeroAddress,
38
+ namehash,
39
+ concatHex
40
+ } from "viem";
41
+ import { CID } from "multiformats/cid";
42
+ var TX_KIND_HASH = "hash";
43
+ var TX_KIND_NONCE_ADVANCED = "nonce-advanced";
44
+ var ATTR_TX_RESOLUTION_KIND = "deploy.dotns.tx_resolution_kind";
45
+ var ONE_PAS = 10000000000n;
46
+ var FEE_FLOOR_OWNED = ONE_PAS / 100n;
47
+ var FEE_FLOOR_REGISTER = ONE_PAS / 10n;
48
+ var TOP_UP_TARGET = ONE_PAS / 2n;
49
+ var SOURCE_BUFFER = ONE_PAS;
50
+ var MINIMUM_REGISTER_STORAGE_DEPOSIT = 2000000000000n;
51
+ var REPROVE_FEE_ESTIMATE = ONE_PAS / 100n;
52
+ var REPROVE_FEE_SAFETY_MARGIN_PCT = 110n;
53
+ var TOP_UP_TRANSFER_TIMEOUT_MS = 6e4;
54
+ var PASEO_FAUCET_URL = "https://faucet.polkadot.io";
55
+ function fmtPas(plancks) {
56
+ return (Number(plancks) / Number(ONE_PAS)).toFixed(4);
57
+ }
58
+ function resolveNativeTokenSymbol(envId) {
59
+ if (!envId) return "PAS";
60
+ if (envId.includes("paseo")) return "PAS";
61
+ if (envId.includes("westend")) return "WND";
62
+ if (envId.includes("rococo")) return "ROC";
63
+ return "PAS";
64
+ }
65
+ function feeFloorFor(plannedAction, storageDeposit = MINIMUM_REGISTER_STORAGE_DEPOSIT) {
66
+ if (plannedAction === "already-owned-by-us") return FEE_FLOOR_OWNED;
67
+ return FEE_FLOOR_REGISTER + storageDeposit;
68
+ }
69
+ function topUpTargetFor(plannedAction, storageDeposit = MINIMUM_REGISTER_STORAGE_DEPOSIT) {
70
+ if (plannedAction === "already-owned-by-us") return TOP_UP_TARGET;
71
+ return TOP_UP_TARGET + storageDeposit;
72
+ }
73
+ var RPC_ENDPOINTS = [
74
+ "wss://asset-hub-paseo.dotters.network",
75
+ "wss://sys.ibp.network/asset-hub-paseo",
76
+ "wss://pas-rpc.stakeworld.io/assethub"
77
+ ];
78
+ var CONTRACTS = {
79
+ DOTNS_REGISTRAR: "0x329aAA5b6bEa94E750b2dacBa74Bf41291E6c2BD",
80
+ DOTNS_REGISTRAR_CONTROLLER: "0xd09e0F1c1E6CE8Cf40df929ef4FC778629573651",
81
+ DOTNS_REGISTRY: "0x4Da0d37aBe96C06ab19963F31ca2DC0412057a6f",
82
+ DOTNS_RESOLVER: "0x95645C7fD0fF38790647FE13F87Eb11c1DCc8514",
83
+ DOTNS_CONTENT_RESOLVER: "0x7756DF72CBc7f062e7403cD59e45fBc78bed1cD7",
84
+ DOTNS_REVERSE_RESOLVER: "0x95D57363B491CF743970c640fe419541386ac8BF",
85
+ STORE_FACTORY: "0x030296782F4d3046B080BcB017f01837561D9702",
86
+ POP_RULES: "0x4e8920B1E69d0cEA9b23CBFC87A17Ee6fE02d2d3"
87
+ };
88
+ var PERSONHOOD_PRECOMPILE_ADDRESS = "0x000000000000000000000000000000000a010000";
89
+ var PERSONHOOD_CONTEXT = "0x646f746e73000000000000000000000000000000000000000000000000000000";
90
+ var DECIMALS = 12n;
91
+ var NATIVE_TO_ETH_RATIO = 1000000n;
92
+ var CONNECTION_TIMEOUT_MS = 3e4;
93
+ var OPERATION_TIMEOUT_MS = 3e5;
94
+ var TX_TIMEOUT_MS = 9e4;
95
+ var TX_CHAIN_TIME_BUDGET_MS = 18e4;
96
+ var TX_WALL_CLOCK_CEILING_MS = 10 * 60 * 1e3;
97
+ var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
98
+ var DOTNS_TX_MAX_ATTEMPTS = 3;
99
+ function classifyTxRetryDecision(err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ const lower = msg.toLowerCase();
102
+ if (lower.includes("nonce-advance fallback")) return "retry";
103
+ if (/\bstale\b/.test(lower)) return "retry";
104
+ if (/"type"\s*:\s*"future"|\binvalid::future\b/.test(lower)) return "retry";
105
+ if (lower.includes("websocket") || lower.includes("connection") || lower.includes("socket closed") || lower.includes("disconnect")) return "retry";
106
+ if (lower.includes("timed out") || lower.includes("timeout")) return "retry";
107
+ return "abort";
108
+ }
109
+ var DEFAULT_MNEMONIC = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
110
+ var _rpcIdCounter = 0;
111
+ async function fetchNonceFromEndpoint(rpc, ss58Address) {
112
+ if (!globalThis.WebSocket) throw new Error("WebSocket support is required to fetch nonce");
113
+ return new Promise((resolve, reject) => {
114
+ let done = false;
115
+ const settle = (fn, ...args) => {
116
+ if (done) return;
117
+ done = true;
118
+ clearTimeout(timer);
119
+ try {
120
+ ws.close();
121
+ } catch {
122
+ }
123
+ fn(...args);
124
+ };
125
+ const timer = setTimeout(() => settle(reject, new Error(`fetchNonce timed out after 8s for ${rpc}`)), 8e3);
126
+ const ws = new WebSocket(rpc);
127
+ const id = ++_rpcIdCounter;
128
+ ws.onopen = () => ws.send(JSON.stringify({ jsonrpc: "2.0", id, method: "system_accountNextIndex", params: [ss58Address] }));
129
+ ws.onmessage = (e) => {
130
+ const d = typeof e.data === "string" ? e.data : e.data.toString();
131
+ const r = JSON.parse(d);
132
+ if (r.id === id) {
133
+ r.error ? settle(reject, new Error(r.error.message)) : settle(resolve, r.result);
134
+ }
135
+ };
136
+ ws.onerror = () => settle(reject, new Error(`WebSocket to ${rpc} failed`));
137
+ ws.onclose = () => settle(reject, new Error(`WebSocket to ${rpc} closed before response`));
138
+ });
139
+ }
140
+ async function fetchNonce(rpc, ss58Address) {
141
+ if (Array.isArray(rpc)) return Promise.any(rpc.map((ep) => fetchNonceFromEndpoint(ep, ss58Address)));
142
+ return fetchNonceFromEndpoint(rpc, ss58Address);
143
+ }
144
+ async function verifyNonceAdvanced(endpoints, ss58Address, originalNonce) {
145
+ const results = await Promise.allSettled(
146
+ endpoints.map((ep) => fetchNonceFromEndpoint(ep, ss58Address).then((n) => ({ n, ep })))
147
+ );
148
+ for (const r of results) {
149
+ if (r.status === "fulfilled" && r.value.n > originalNonce) {
150
+ return { advanced: true, witnessRpc: r.value.ep };
151
+ }
152
+ }
153
+ return { advanced: false };
154
+ }
155
+ var ProofOfPersonhoodStatus = {
156
+ NoStatus: 0,
157
+ ProofOfPersonhoodLite: 1,
158
+ ProofOfPersonhoodFull: 2,
159
+ Reserved: 3
160
+ };
161
+ var DOTNS_REGISTRAR_CONTROLLER_ABI = [
162
+ { inputs: [{ name: "registration", type: "tuple", components: [{ name: "label", type: "string" }, { name: "owner", type: "address" }, { name: "secret", type: "bytes32" }, { name: "reserved", type: "bool" }] }], name: "makeCommitment", outputs: [{ name: "", type: "bytes32" }], stateMutability: "view", type: "function" },
163
+ { inputs: [{ name: "commitment", type: "bytes32" }], name: "commit", outputs: [], stateMutability: "nonpayable", type: "function" },
164
+ { inputs: [], name: "minCommitmentAge", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
165
+ { inputs: [], name: "maxCommitmentAge", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
166
+ { inputs: [{ name: "commitment", type: "bytes32" }], name: "commitments", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
167
+ { inputs: [{ name: "registration", type: "tuple", components: [{ name: "label", type: "string" }, { name: "owner", type: "address" }, { name: "secret", type: "bytes32" }, { name: "reserved", type: "bool" }] }], name: "register", outputs: [], stateMutability: "payable", type: "function" }
168
+ ];
169
+ var DOTNS_REGISTRAR_ABI = [
170
+ { inputs: [{ name: "tokenId", type: "uint256" }], name: "ownerOf", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" }
171
+ ];
172
+ var POP_RULES_ABI = [
173
+ { inputs: [{ name: "name", type: "string" }], name: "classifyName", outputs: [{ name: "requirement", type: "uint8" }, { name: "message", type: "string" }], stateMutability: "pure", type: "function" },
174
+ { inputs: [{ name: "name", type: "string" }], name: "price", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
175
+ { inputs: [{ name: "name", type: "string" }, { name: "userAddress", type: "address" }], name: "priceWithCheck", outputs: [{ name: "metadata", type: "tuple", components: [{ name: "price", type: "uint256" }, { name: "status", type: "uint8" }, { name: "userStatus", type: "uint8" }, { name: "message", type: "string" }] }], stateMutability: "view", type: "function" },
176
+ { inputs: [{ name: "name", type: "string" }, { name: "userAddress", type: "address" }], name: "priceWithoutCheck", outputs: [{ name: "metadata", type: "tuple", components: [{ name: "price", type: "uint256" }, { name: "status", type: "uint8" }, { name: "userStatus", type: "uint8" }, { name: "message", type: "string" }] }], stateMutability: "view", type: "function" },
177
+ { inputs: [{ name: "name", type: "string" }], name: "isBaseNameReserved", outputs: [{ name: "isReserved", type: "bool" }, { name: "reservationOwner", type: "address" }, { name: "expiryTimestamp", type: "uint64" }], stateMutability: "view", type: "function" }
178
+ ];
179
+ var PERSONHOOD_ABI = [
180
+ {
181
+ type: "function",
182
+ name: "personhoodStatus",
183
+ inputs: [
184
+ { name: "account", type: "address" },
185
+ { name: "context", type: "bytes32" }
186
+ ],
187
+ outputs: [
188
+ {
189
+ name: "info",
190
+ type: "tuple",
191
+ components: [
192
+ { name: "status", type: "uint8" },
193
+ { name: "contextAlias", type: "bytes32" }
194
+ ]
195
+ }
196
+ ],
197
+ stateMutability: "view"
198
+ }
199
+ ];
200
+ var DOTNS_REGISTRY_ABI = [
201
+ { inputs: [{ name: "record", type: "tuple", components: [{ name: "parentNode", type: "bytes32" }, { name: "subLabel", type: "string" }, { name: "parentLabel", type: "string" }, { name: "owner", type: "address" }] }], name: "setSubnodeOwner", outputs: [{ name: "subnode", type: "bytes32" }], stateMutability: "nonpayable", type: "function" },
202
+ { inputs: [{ name: "node", type: "bytes32" }, { name: "newResolver", type: "address" }], name: "setResolver", outputs: [], stateMutability: "nonpayable", type: "function" },
203
+ { inputs: [{ name: "node", type: "bytes32" }], name: "owner", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" },
204
+ { inputs: [{ name: "node", type: "bytes32" }], name: "resolver", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" }
205
+ ];
206
+ var DOTNS_CONTENT_RESOLVER_ABI = [
207
+ { inputs: [{ name: "node", type: "bytes32" }, { name: "hash", type: "bytes" }], name: "setContenthash", outputs: [], stateMutability: "nonpayable", type: "function" },
208
+ { inputs: [{ name: "node", type: "bytes32" }], name: "contenthash", outputs: [{ name: "", type: "bytes" }], stateMutability: "view", type: "function" }
209
+ ];
210
+ var DOTNS_TEXT_RESOLVER_ABI = [
211
+ { inputs: [{ name: "node", type: "bytes32" }, { name: "key", type: "string" }, { name: "value", type: "string" }], name: "setText", outputs: [], stateMutability: "nonpayable", type: "function" },
212
+ { inputs: [{ name: "node", type: "bytes32" }, { name: "key", type: "string" }], name: "text", outputs: [{ name: "", type: "string" }], stateMutability: "view", type: "function" }
213
+ ];
214
+ var PUBLISHER_ABI = [
215
+ { inputs: [{ name: "label", type: "string" }], name: "publish", outputs: [], stateMutability: "nonpayable", type: "function" },
216
+ { inputs: [{ name: "label", type: "string" }], name: "unpublish", outputs: [], stateMutability: "nonpayable", type: "function" },
217
+ { inputs: [{ name: "labelhash", type: "bytes32" }], name: "isPublished", outputs: [{ name: "", type: "bool" }], stateMutability: "view", type: "function" },
218
+ { inputs: [], name: "EmptyLabel", type: "error" },
219
+ { inputs: [], name: "NoPersonhood", type: "error" },
220
+ { inputs: [{ name: "nextAllowedAt", type: "uint64" }], name: "CooldownActive", type: "error" },
221
+ { inputs: [{ name: "caller", type: "address" }, { name: "tokenId", type: "uint256" }], name: "NotOwner", type: "error" }
222
+ ];
223
+ var PublisherNotSupportedError = class extends Error {
224
+ constructor(envName) {
225
+ super(`Publisher contract is not configured for environment '${envName}'. Use an env that has a deployed Publisher (currently: paseo-next-v2).`);
226
+ this.name = "PublisherNotSupportedError";
227
+ }
228
+ };
229
+ var ContractDryRunRevertError = class extends Error {
230
+ revertData;
231
+ revertFlags;
232
+ constructor(message, revertData, revertFlags) {
233
+ super(message);
234
+ this.name = "ContractDryRunRevertError";
235
+ this.revertData = revertData;
236
+ this.revertFlags = revertFlags;
237
+ }
238
+ };
239
+ function decodePublisherRevert(source) {
240
+ const data = typeof source === "string" ? source : source?.revertData;
241
+ if (!data || data.length < 10) return null;
242
+ try {
243
+ const decoded = decodeErrorResult({ abi: PUBLISHER_ABI, data });
244
+ return { name: decoded.errorName, args: decoded.args };
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ function convertToHexString(value) {
250
+ if (!value) return "0x";
251
+ if (value instanceof Uint8Array) return bytesToHex(value);
252
+ if (typeof value === "string" && isHex(value)) return value;
253
+ try {
254
+ return toHex(value);
255
+ } catch {
256
+ return "0x";
257
+ }
258
+ }
259
+ function convertToBigInt(value, fallback = 0n) {
260
+ try {
261
+ if (typeof value === "bigint") return value;
262
+ if (typeof value === "number") return BigInt(value);
263
+ if (typeof value === "string") return BigInt(value);
264
+ if (value && typeof value.toString === "function") return BigInt(value.toString());
265
+ return fallback;
266
+ } catch {
267
+ return fallback;
268
+ }
269
+ }
270
+ function normalizeWeight(weight) {
271
+ const referenceTime = weight?.ref_time ?? weight?.refTime ?? 0;
272
+ const proofSize = weight?.proof_size ?? weight?.proofSize ?? 0;
273
+ return { referenceTime: convertToBigInt(referenceTime, 0n), proofSize: convertToBigInt(proofSize, 0n) };
274
+ }
275
+ function extractStorageDepositCharge(rawStorageDeposit) {
276
+ if (!rawStorageDeposit) return 0n;
277
+ if (typeof rawStorageDeposit?.isCharge === "boolean") {
278
+ if (rawStorageDeposit.isCharge && rawStorageDeposit.asCharge != null) return convertToBigInt(rawStorageDeposit.asCharge, 0n);
279
+ return 0n;
280
+ }
281
+ if (rawStorageDeposit.charge != null) return convertToBigInt(rawStorageDeposit.charge, 0n);
282
+ if (rawStorageDeposit.Charge != null) return convertToBigInt(rawStorageDeposit.Charge, 0n);
283
+ if (rawStorageDeposit.value != null) return convertToBigInt(rawStorageDeposit.value, 0n);
284
+ return 0n;
285
+ }
286
+ function dotnsContractName(address, contracts = CONTRACTS) {
287
+ const normalized = address.toLowerCase();
288
+ for (const [name, contractAddress] of Object.entries({ ...CONTRACTS, ...contracts })) {
289
+ if (contractAddress.toLowerCase() === normalized) return name;
290
+ }
291
+ return "unknown";
292
+ }
293
+ function stringifyDebugValue(value) {
294
+ return JSON.stringify(value, (_key, nested) => typeof nested === "bigint" ? nested.toString() : nested);
295
+ }
296
+ function dotnsTxDebugEnabled() {
297
+ return process.env.BULLETIN_DEPLOY_DOTNS_DEBUG === "1" || process.env.DOTNS_DEBUG === "1";
298
+ }
299
+ function formatWeight(weight) {
300
+ if (!weight) return "unknown";
301
+ return `ref_time=${weight.referenceTime.toString()} proof_size=${weight.proofSize.toString()}`;
302
+ }
303
+ var BARE_REVERT_DIAGNOSTIC_FUNCTIONS = /* @__PURE__ */ new Set(["register", "commit", "setContenthash", "setSubnodeOwner", "setResolver"]);
304
+ function formatContractDryRunFailure(gasEstimate, context) {
305
+ const functionName = context.functionName ?? "unknown";
306
+ const contractName = dotnsContractName(context.contractAddress, context.contracts);
307
+ const lines = [
308
+ `Contract execution would revert during ${functionName} on ${contractName}`,
309
+ ` contract: ${context.contractAddress}`,
310
+ ` signer: ${context.signerSubstrateAddress}${context.signerEvmAddress ? ` (${context.signerEvmAddress})` : ""}`,
311
+ ` value: ${context.value.toString()}`,
312
+ ` revert: flags=${gasEstimate.revertFlags?.toString() ?? "unknown"} data=${gasEstimate.revertData ?? "0x"}`,
313
+ ` gasRequired: ${formatWeight(gasEstimate.gasRequired)}`,
314
+ ` gasConsumed: ${formatWeight(gasEstimate.gasConsumed)}`,
315
+ ` storageDeposit: ${gasEstimate.storageDeposit?.toString() ?? "unknown"}`
316
+ ];
317
+ if (dotnsTxDebugEnabled()) {
318
+ lines.push(` calldata: ${context.encodedData}`);
319
+ if (context.args) lines.push(` args: ${stringifyDebugValue(context.args)}`);
320
+ }
321
+ const revertData = gasEstimate.revertData;
322
+ const isBareRevert = (revertData === void 0 || revertData.trim() === "0x") && gasEstimate.revertFlags === 1n;
323
+ if (isBareRevert && BARE_REVERT_DIAGNOSTIC_FUNCTIONS.has(functionName)) {
324
+ if (functionName === "register") {
325
+ lines.push(
326
+ ` diagnostic: bare-revert (empty 0x) during register. Most likely cause: insufficient signer balance for storage deposit.`,
327
+ ` A fresh TLD register() requires sufficient free balance to cover the chain's storage deposit (typically 200+ PAS).`,
328
+ ` Other possible causes:`,
329
+ ` 1. PoP status changed between preflight and registration (race condition).`,
330
+ ` 2. Commitment timing: the revealed commitment is still too new or already expired.`,
331
+ ` 3. Label was registered by someone else between preflight and register.`,
332
+ ` To reproduce in isolation: \`node tools/dotns-dry-run.mjs <label>\``,
333
+ ` To rule out a mapping issue: add --fresh (a brand-new unmapped origin) \u2014 if --fresh reverts but the mapped one doesn't, it's a mapping bug.`
334
+ );
335
+ } else {
336
+ lines.push(
337
+ ` diagnostic: bare-revert (empty 0x). Account mapping was verified at connect time, so the cause is likely:`,
338
+ ` 1. PoP status changed between preflight and registration (race condition).`,
339
+ ` 2. Commitment timing: the revealed commitment is still too new or already expired.`,
340
+ ` 3. Label was registered by someone else between preflight and register.`,
341
+ ` To reproduce in isolation: \`node tools/dotns-dry-run.mjs <label>\``,
342
+ ` To rule out a mapping issue: add --fresh (a brand-new unmapped origin) \u2014 if --fresh reverts but the mapped one doesn't, it's a mapping bug.`
343
+ );
344
+ }
345
+ }
346
+ return lines.join("\n");
347
+ }
348
+ function __formatContractDryRunFailureForTest(gasEstimate, context) {
349
+ return formatContractDryRunFailure(gasEstimate, context);
350
+ }
351
+ function unwrapExecutionResult(rawResult) {
352
+ if (!rawResult) return { ok: null, err: null, successFlag: null };
353
+ if (typeof rawResult.success === "boolean") {
354
+ return rawResult.success ? { ok: rawResult.value ?? null, err: null, successFlag: true } : { ok: null, err: rawResult.error ?? rawResult.value ?? null, successFlag: false };
355
+ }
356
+ if (typeof rawResult.isOk === "boolean") {
357
+ return rawResult.isOk ? { ok: rawResult.value ?? null, err: null, successFlag: true } : { ok: null, err: rawResult.value ?? null, successFlag: false };
358
+ }
359
+ if (rawResult.ok != null) return { ok: rawResult.ok, err: null, successFlag: true };
360
+ if (rawResult.err != null) return { ok: null, err: rawResult.err, successFlag: false };
361
+ return { ok: null, err: rawResult, successFlag: null };
362
+ }
363
+ function withTimeout(promise, timeoutMs, operationName) {
364
+ let timer;
365
+ const timeoutPromise = new Promise((_, reject) => {
366
+ timer = setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs);
367
+ });
368
+ return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
369
+ }
370
+ var DOT_NODE = "0x3fce7d1364a893e213bc4212792b517ffc88f5b13b86c8ef9c8d390c3a1370ce";
371
+ function convertWeiToNative(weiValue) {
372
+ return weiValue / NATIVE_TO_ETH_RATIO;
373
+ }
374
+ function computeDomainTokenId(label) {
375
+ const labelhash = keccak256(toBytes(label));
376
+ const node = keccak256(concatHex([DOT_NODE, labelhash]));
377
+ return BigInt(node);
378
+ }
379
+ function countTrailingDigits(label) {
380
+ let count = 0;
381
+ for (let i = label.length - 1; i >= 0; i--) {
382
+ const code = label.charCodeAt(i);
383
+ if (code >= 48 && code <= 57) count++;
384
+ else break;
385
+ }
386
+ return count;
387
+ }
388
+ function stripTrailingDigits(label) {
389
+ return label.replace(/\d+$/, "").replace(/-$/, "");
390
+ }
391
+ function sanitizeDomainLabel(label) {
392
+ const trailingDigitCount = countTrailingDigits(label);
393
+ if (trailingDigitCount > 2) {
394
+ const sanitized = stripTrailingDigits(label) + label.slice(-2);
395
+ console.log(` Domain label sanitized: "${label}" \u2192 "${sanitized}" (stripped excess trailing digits)`);
396
+ return sanitized;
397
+ }
398
+ return label;
399
+ }
400
+ function validateDomainLabel(label, opts = {}) {
401
+ if (!/^[a-z0-9-]{3,}$/.test(label)) throw new Error("Invalid domain label: must contain only lowercase letters, digits, and hyphens, min 3 chars");
402
+ if (label.startsWith("-") || label.endsWith("-")) throw new Error("Invalid domain label: cannot start or end with hyphen");
403
+ const sanitized = sanitizeDomainLabel(label);
404
+ if (/-\d+$/.test(sanitized)) {
405
+ const baseWithHyphen = sanitized.replace(/\d+$/, "");
406
+ const dropHyphen = sanitized.replace(/-(\d+)$/, "$1");
407
+ const insertSegment = sanitized.replace(/-(\d+)$/, "-pr$1");
408
+ throw new Error(
409
+ `Invalid domain label: "${sanitized}" \u2014 dotns base-name extraction leaves a trailing hyphen ("${baseWithHyphen}"), which the registry rejects with PopError("Name must be lowercase ASCII DNS label"). Drop the hyphen before the digits (e.g. "${dropHyphen}") or add a non-digit segment between (e.g. "${insertSegment}").`
410
+ );
411
+ }
412
+ if (opts.checkReserved !== false) {
413
+ const classification = classifyDotnsLabel(sanitized);
414
+ if (classification.status === ProofOfPersonhoodStatus.Reserved) {
415
+ const sanitizeTrail = label !== sanitized ? `Input "${label}" was sanitized to "${sanitized}" (excess trailing digits trimmed). ` : "";
416
+ throw new NonRetryableError(
417
+ `${sanitizeTrail}Invalid domain label "${sanitized}": ${classification.message}`
418
+ );
419
+ }
420
+ }
421
+ return sanitized;
422
+ }
423
+ function isCommitmentMature(chainNowSeconds, commitTimestampSeconds, minimumAgeSeconds) {
424
+ return chainNowSeconds > commitTimestampSeconds + minimumAgeSeconds;
425
+ }
426
+ function isCommitmentTimingBarerevert(msg) {
427
+ return /bare-revert.*\(empty 0x\)/i.test(msg) || /commitment.*too new.*expired/i.test(msg) || /expired.*commitment/i.test(msg);
428
+ }
429
+ function classifyDotnsLabel(label) {
430
+ const totalLength = label.length;
431
+ const trailingDigits = countTrailingDigits(label);
432
+ if (trailingDigits > 2) {
433
+ return {
434
+ status: ProofOfPersonhoodStatus.Reserved,
435
+ message: `Name has ${trailingDigits} trailing digits; DotNS allows at most 2 trailing digits. Use a base name with 0-2 trailing digits.`
436
+ };
437
+ }
438
+ const baselength = totalLength - trailingDigits;
439
+ if (baselength <= 5) {
440
+ return {
441
+ status: ProofOfPersonhoodStatus.Reserved,
442
+ message: `Base name is ${baselength} char${baselength === 1 ? "" : "s"}; DotNS reserves base names of 5 chars or fewer for governance (PopRules). Use a base name of 6+ chars \u2014 role prefixes like 'rc<N>pool' / 'rc<N>dir' / 'nightly-<role>' work well.`
443
+ };
444
+ }
445
+ if (baselength >= 6 && baselength <= 8) {
446
+ if (trailingDigits === 2) return { status: ProofOfPersonhoodStatus.ProofOfPersonhoodLite, message: "Requires Light personhood verification" };
447
+ return { status: ProofOfPersonhoodStatus.ProofOfPersonhoodFull, message: "Requires Full personhood verification" };
448
+ }
449
+ if (trailingDigits === 2) return { status: ProofOfPersonhoodStatus.NoStatus, message: "Available to all" };
450
+ return { status: ProofOfPersonhoodStatus.ProofOfPersonhoodFull, message: "Requires Full personhood verification" };
451
+ }
452
+ function canRegister(requiredStatus, userStatus, trailingDigits) {
453
+ if (requiredStatus === ProofOfPersonhoodStatus.Reserved) return false;
454
+ if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodFull) {
455
+ return userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodFull;
456
+ }
457
+ if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
458
+ return userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite || userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodFull;
459
+ }
460
+ return trailingDigits !== 0 && userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodLite;
461
+ }
462
+ function exampleNoStatusLabel(label) {
463
+ const base = stripTrailingDigits(validateDomainLabel(label, { checkReserved: false })).replace(/[^a-z0-9-]/g, "x");
464
+ return `${base.padEnd(9, "x").slice(0, 9)}00.dot`;
465
+ }
466
+ function parseDomainName(input) {
467
+ const name = input.replace(/\.dot$/, "");
468
+ const parts = name.split(".");
469
+ if (parts.length === 1) {
470
+ const sanitized = validateDomainLabel(parts[0]);
471
+ return { isSubdomain: false, label: sanitized, sublabel: null, parentLabel: null, fullName: `${sanitized}.dot` };
472
+ }
473
+ if (parts.length === 2) {
474
+ const sanitizedSub = validateDomainLabel(parts[0], { checkReserved: false });
475
+ const sanitizedParent = validateDomainLabel(parts[1]);
476
+ const fullLabel = `${sanitizedSub}.${sanitizedParent}`;
477
+ return { isSubdomain: true, label: fullLabel, sublabel: sanitizedSub, parentLabel: sanitizedParent, fullName: `${fullLabel}.dot` };
478
+ }
479
+ throw new Error(`Invalid domain: only one level of subdomains supported (got ${parts.length} labels)`);
480
+ }
481
+ function parseProofOfPersonhoodStatus(status) {
482
+ const s = (status ?? "none").toLowerCase();
483
+ if (s === "none" || s === "nostatus") return ProofOfPersonhoodStatus.NoStatus;
484
+ if (s === "lite" || s === "poplite") return ProofOfPersonhoodStatus.ProofOfPersonhoodLite;
485
+ if (s === "full" || s === "popfull") return ProofOfPersonhoodStatus.ProofOfPersonhoodFull;
486
+ throw new Error("Invalid status. Use none, lite, or full");
487
+ }
488
+ function popStatusName(status) {
489
+ return Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === status) ?? String(status);
490
+ }
491
+ function normalizeProofOfPersonhoodStatus(status) {
492
+ if (typeof status === "number") return status;
493
+ if (typeof status === "bigint") return Number(status);
494
+ if (typeof status === "string") return Number(status);
495
+ throw new Error(`Unexpected ProofOfPersonhoodStatus type: ${typeof status}`);
496
+ }
497
+ function parsePersonhoodStatusResult(result) {
498
+ const status = Array.isArray(result) ? result[0]?.status ?? result[0] : result?.status;
499
+ return normalizeProofOfPersonhoodStatus(status);
500
+ }
501
+ var ReviveClientWrapper = class _ReviveClientWrapper {
502
+ static DRY_RUN_STORAGE_LIMIT = 18446744073709551615n;
503
+ static DRY_RUN_WEIGHT_LIMIT = { ref_time: 18446744073709551615n, proof_size: 18446744073709551615n };
504
+ client;
505
+ mappedAccounts;
506
+ constructor(client) {
507
+ this.client = client;
508
+ this.mappedAccounts = /* @__PURE__ */ new Set();
509
+ }
510
+ async getEvmAddress(substrateAddress) {
511
+ if (isAddress(substrateAddress)) return substrateAddress;
512
+ const address = await this.client.apis.ReviveApi.address(substrateAddress);
513
+ return convertToHexString(address);
514
+ }
515
+ async performDryRunCall(originSubstrateAddress, contractAddress, value, encodedData) {
516
+ if (isAddress(originSubstrateAddress)) throw new Error("performDryRunCall requires SS58 Substrate address, not EVM H160 address");
517
+ const executionResults = await this.client.apis.ReviveApi.call(originSubstrateAddress, contractAddress, value, _ReviveClientWrapper.DRY_RUN_WEIGHT_LIMIT, _ReviveClientWrapper.DRY_RUN_STORAGE_LIMIT, Binary.fromHex(encodedData));
518
+ const { ok, err, successFlag } = unwrapExecutionResult(executionResults.result);
519
+ const flags = ok?.flags ? convertToBigInt(ok.flags, 0n) : 0n;
520
+ const returnData = convertToHexString(ok?.data);
521
+ const didRevert = ok ? (flags & 1n) === 1n : true;
522
+ const gasConsumed = normalizeWeight(executionResults.weight_consumed);
523
+ const gasRequired = normalizeWeight(executionResults.weight_required ?? executionResults.weight_consumed);
524
+ const storageDepositValue = extractStorageDepositCharge(executionResults.storage_deposit);
525
+ const isOk = !!ok && !didRevert;
526
+ const isErr = !ok || didRevert || !!err || (typeof successFlag === "boolean" ? !successFlag : false);
527
+ return { gasConsumed, gasRequired, storageDeposit: { value: storageDepositValue }, result: { isOk, isErr, value: { data: ok ? returnData : "0x", flags: ok ? flags : 1n } } };
528
+ }
529
+ async estimateGasForCall(originSubstrateAddress, contractAddress, value, encodedData) {
530
+ const result = await this.performDryRunCall(originSubstrateAddress, contractAddress, value, encodedData);
531
+ if (!result.result.isOk) return { success: false, gasConsumed: result.gasConsumed, storageDeposit: result.storageDeposit.value, gasRequired: result.gasRequired, revertData: result.result.value.data, revertFlags: result.result.value.flags };
532
+ return { success: true, gasConsumed: result.gasConsumed, storageDeposit: result.storageDeposit.value, gasRequired: result.gasRequired };
533
+ }
534
+ async checkIfAccountMapped(substrateAddress) {
535
+ try {
536
+ const evmAddress = await this.getEvmAddress(substrateAddress);
537
+ const mappedAccount = await this.client.query.Revive.OriginalAccount.getValue(evmAddress);
538
+ return mappedAccount !== null && mappedAccount !== void 0;
539
+ } catch {
540
+ return false;
541
+ }
542
+ }
543
+ async ensureAccountMapped(substrateAddress, signer) {
544
+ if (isAddress(substrateAddress)) throw new Error("ensureAccountMapped requires SS58 Substrate address, not EVM H160 address");
545
+ if (this.mappedAccounts.has(substrateAddress)) return;
546
+ const isMapped = await this.checkIfAccountMapped(substrateAddress);
547
+ if (isMapped) {
548
+ this.mappedAccounts.add(substrateAddress);
549
+ return;
550
+ }
551
+ try {
552
+ await this.signAndSubmitWithRetry(() => this.client.tx.Revive.map_account(), signer, () => {
553
+ }, "Revive.map_account");
554
+ this.mappedAccounts.add(substrateAddress);
555
+ } catch (error) {
556
+ const errorMessage = error?.message || String(error);
557
+ if (errorMessage.includes("AccountAlreadyMapped")) {
558
+ this.mappedAccounts.add(substrateAddress);
559
+ return;
560
+ }
561
+ throw error;
562
+ }
563
+ }
564
+ signAndSubmitExtrinsic(extrinsic, signer, statusCallback, opts = {}) {
565
+ return new Promise((resolve, reject) => {
566
+ let settled = false;
567
+ let deadlinePoller = null;
568
+ let sub;
569
+ const finish = (fn) => (...args) => {
570
+ if (!settled) {
571
+ settled = true;
572
+ if (deadlinePoller) clearTimeout(deadlinePoller);
573
+ try {
574
+ sub?.unsubscribe();
575
+ } catch {
576
+ }
577
+ fn(...args);
578
+ }
579
+ };
580
+ const startWallClockMs = Date.now();
581
+ let startChainTimeMs = null;
582
+ const poll = async () => {
583
+ if (settled) return;
584
+ try {
585
+ if (opts.nonceFallback) {
586
+ const nonce = await verifyNonceAdvanced(opts.nonceFallback.rpcs, opts.nonceFallback.senderSS58, opts.nonceFallback.expectedNonce);
587
+ if (nonce.advanced) {
588
+ if (opts.verifyEffect) {
589
+ statusCallback("verifying");
590
+ const observed = await opts.verifyEffect();
591
+ if (!observed) {
592
+ statusCallback("failed");
593
+ finish(reject)(new Error(`nonce-advance fallback: nonce moved past ${opts.nonceFallback.expectedNonce} but expected on-chain effect not observable (likely a different tx of ours consumed the nonce, or our tx was reorged out)`));
594
+ return;
595
+ }
596
+ }
597
+ statusCallback("included");
598
+ finish(resolve)({ kind: "nonce-advanced", rpc: nonce.witnessRpc });
599
+ return;
600
+ }
601
+ }
602
+ if (Date.now() - startWallClockMs > TX_WALL_CLOCK_CEILING_MS) {
603
+ statusCallback("failed");
604
+ finish(reject)(new Error(`Transaction did not settle within ${TX_WALL_CLOCK_CEILING_MS / 1e3}s wall-clock (chain may be stalled)`));
605
+ return;
606
+ }
607
+ const chainNowMs = Number(await this.client.query.Timestamp.Now.getValue());
608
+ if (startChainTimeMs === null) startChainTimeMs = chainNowMs;
609
+ const chainElapsedMs = chainNowMs - startChainTimeMs;
610
+ if (chainElapsedMs > TX_CHAIN_TIME_BUDGET_MS) {
611
+ statusCallback("failed");
612
+ finish(reject)(new Error(`Transaction not included after ${Math.floor(chainElapsedMs / 1e3)}s of chain progress (budget=${TX_CHAIN_TIME_BUDGET_MS / 1e3}s)`));
613
+ return;
614
+ }
615
+ } catch {
616
+ }
617
+ if (!settled) deadlinePoller = setTimeout(poll, 6e3);
618
+ };
619
+ deadlinePoller = setTimeout(poll, 6e3);
620
+ try {
621
+ sub = extrinsic.signSubmitAndWatch(signer, { mortality: { mortal: true, period: 256 } }).subscribe({
622
+ next: (event) => {
623
+ const transactionHash = event.txHash?.toString();
624
+ switch (event.type) {
625
+ case "signed":
626
+ statusCallback("signing");
627
+ break;
628
+ case "broadcasted":
629
+ statusCallback("broadcasting");
630
+ break;
631
+ case "txBestBlocksState":
632
+ if (event.found) statusCallback("included");
633
+ break;
634
+ case "finalized": {
635
+ if (event.dispatchError || event.ok === false) {
636
+ statusCallback("failed");
637
+ finish(reject)(new Error(`Transaction failed: ${event.dispatchError?.toString?.() ?? "dispatch error"}`));
638
+ return;
639
+ }
640
+ const block = event.block ? { hash: String(event.block.hash), number: Number(event.block.number) } : void 0;
641
+ statusCallback("finalized");
642
+ finish(resolve)({ kind: "hash", hash: transactionHash, block });
643
+ return;
644
+ }
645
+ case "invalid":
646
+ case "dropped":
647
+ statusCallback("failed");
648
+ finish(reject)(new Error(`Transaction ${event.type}`));
649
+ return;
650
+ }
651
+ },
652
+ error: (error) => {
653
+ statusCallback("failed");
654
+ finish(reject)(error);
655
+ },
656
+ complete: () => {
657
+ if (settled) return;
658
+ statusCallback("failed");
659
+ finish(reject)(new Error("transaction subscription closed before finalization"));
660
+ }
661
+ });
662
+ } catch (error) {
663
+ statusCallback("failed");
664
+ finish(reject)(error);
665
+ }
666
+ });
667
+ }
668
+ async signAndSubmitWithRetry(buildExtrinsic, signer, statusCallback, label, opts = {}) {
669
+ let lastError;
670
+ for (let attempt = 1; attempt <= DOTNS_TX_MAX_ATTEMPTS; attempt++) {
671
+ try {
672
+ return await this.signAndSubmitExtrinsic(buildExtrinsic(), signer, statusCallback, opts);
673
+ } catch (e) {
674
+ lastError = e;
675
+ const decision = classifyTxRetryDecision(e);
676
+ if (decision === "abort" || attempt === DOTNS_TX_MAX_ATTEMPTS) break;
677
+ const short = (e?.message ?? String(e)).slice(0, 80);
678
+ console.log(` ${label}: attempt ${attempt}/${DOTNS_TX_MAX_ATTEMPTS} failed (${short}), retrying...`);
679
+ }
680
+ }
681
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
682
+ }
683
+ // Dry-runs one Revive.call and returns the chain-side limits the live
684
+ // submission should use. Throws a formatted error when the call would
685
+ // revert. Shared between single-tx submitTransaction and the batched
686
+ // submitBatchedTransactions so they cannot drift.
687
+ async dryRunReviveCall(contractAddress, value, encodedData, signerSubstrateAddress, context) {
688
+ const gasEstimate = await this.estimateGasForCall(signerSubstrateAddress, contractAddress, value, encodedData);
689
+ if (!gasEstimate.success) {
690
+ const signerEvmAddress = await this.getEvmAddress(signerSubstrateAddress);
691
+ const msg = formatContractDryRunFailure(gasEstimate, {
692
+ contractAddress,
693
+ functionName: context.functionName,
694
+ signerSubstrateAddress,
695
+ signerEvmAddress,
696
+ value,
697
+ encodedData,
698
+ args: context.args,
699
+ contracts: context.contracts
700
+ });
701
+ throw new ContractDryRunRevertError(msg, gasEstimate.revertData ?? "0x", gasEstimate.revertFlags ?? 0n);
702
+ }
703
+ const minimumStorageDeposit = 2000000000000n;
704
+ let storageDepositLimit = gasEstimate.storageDeposit === 0n ? minimumStorageDeposit : gasEstimate.storageDeposit * 120n / 100n;
705
+ if (storageDepositLimit < minimumStorageDeposit) storageDepositLimit = minimumStorageDeposit;
706
+ return {
707
+ weight_limit: { ref_time: gasEstimate.gasRequired.referenceTime, proof_size: gasEstimate.gasRequired.proofSize },
708
+ storage_deposit_limit: storageDepositLimit
709
+ };
710
+ }
711
+ async submitTransaction(contractAddress, value, encodedData, signerSubstrateAddress, signer, statusCallback, { rpcs, useNoncePolling, functionName, args, contracts, verifyEffect }) {
712
+ await this.ensureAccountMapped(signerSubstrateAddress, signer);
713
+ if (functionName === "register") {
714
+ try {
715
+ const stillMapped = await this.checkIfAccountMapped(signerSubstrateAddress);
716
+ if (!stillMapped) {
717
+ captureWarning("account mapping not confirmed on-chain immediately before register dry-run", {
718
+ signer: signerSubstrateAddress
719
+ });
720
+ }
721
+ } catch {
722
+ }
723
+ }
724
+ const prep = await this.dryRunReviveCall(contractAddress, value, encodedData, signerSubstrateAddress, { functionName, args, contracts });
725
+ const buildExtrinsic = () => this.client.tx.Revive.call({ dest: contractAddress, value, weight_limit: prep.weight_limit, storage_deposit_limit: prep.storage_deposit_limit, data: Binary.fromHex(encodedData) });
726
+ let nonceFallback;
727
+ if (useNoncePolling) {
728
+ try {
729
+ const nonce = await fetchNonce(rpcs, signerSubstrateAddress);
730
+ nonceFallback = { rpcs, senderSS58: signerSubstrateAddress, expectedNonce: nonce };
731
+ } catch {
732
+ }
733
+ }
734
+ return await this.signAndSubmitWithRetry(buildExtrinsic, signer, statusCallback, "Revive.call", { nonceFallback, verifyEffect });
735
+ }
736
+ // Dry-runs each call individually, then wraps them in a single
737
+ // Utility.batch_all extrinsic. batch_all is atomic over inner calls — only
738
+ // batch operations whose rollback-together is acceptable (e.g. cosmetic
739
+ // setText writes).
740
+ async submitBatchedTransactions(calls, signerSubstrateAddress, signer, statusCallback) {
741
+ if (calls.length === 0) throw new Error("submitBatchedTransactions: at least one call required");
742
+ await this.ensureAccountMapped(signerSubstrateAddress, signer);
743
+ const preps = await Promise.all(calls.map((c) => this.dryRunReviveCall(c.contractAddress, c.value, c.encodedData, signerSubstrateAddress, { functionName: c.functionName, args: c.args })));
744
+ const buildExtrinsic = () => {
745
+ const inners = calls.map((c, i) => this.client.tx.Revive.call({
746
+ dest: c.contractAddress,
747
+ value: c.value,
748
+ weight_limit: preps[i].weight_limit,
749
+ storage_deposit_limit: preps[i].storage_deposit_limit,
750
+ data: Binary.fromHex(c.encodedData)
751
+ }).decodedCall);
752
+ return this.client.tx.Utility.batch_all({ calls: inners });
753
+ };
754
+ return await this.signAndSubmitWithRetry(buildExtrinsic, signer, statusCallback, "Utility.batch_all");
755
+ }
756
+ };
757
+ function logTxResolution(res) {
758
+ setDeployAttribute(ATTR_TX_RESOLUTION_KIND, res.kind);
759
+ if (res.kind === TX_KIND_HASH) {
760
+ console.log(` Tx: ${res.hash}`);
761
+ } else {
762
+ let rpcHost = res.rpc;
763
+ try {
764
+ rpcHost = new URL(res.rpc).host;
765
+ } catch {
766
+ }
767
+ console.log(` Tx: confirmed via nonce-advance on ${rpcHost}`);
768
+ }
769
+ }
770
+ var DOTNS_CONTEXT_HEX_LOWER = "0x646f746e73000000000000000000000000000000000000000000000000000000";
771
+ function formatPersonhoodRemediation(state, popSelfServe, environmentId) {
772
+ if (!popSelfServe?.stateAwareGuidance) {
773
+ return "Self-attestation is no longer available. Contact the DotNS team for whitelisting / Personhood status help.";
774
+ }
775
+ switch (state.state) {
776
+ case "not-bound":
777
+ return `Your account has no DotNS alias binding on ${environmentId ?? "this environment"}. On testnets you can self-serve:
778
+ 1. Fund the service account mnemonic via ${popSelfServe.faucetUrl}
779
+ 2. Go to ${popSelfServe.personhoodFaucetUrl}, pick your env (e.g. ${popSelfServe.sudoEnvLabel}), and paste the mnemonic
780
+ 3. Go to ${popSelfServe.dotnsBootstrapUrl} and follow each step (first and last can probably be skipped)`;
781
+ case "bound-likely-stale":
782
+ return `Your alias binding exists but may have a stale ring revision. Run \`node tools/reprove-alias.mjs --mnemonic <your-mnemonic> --env ${environmentId ?? "this environment"}\` to refresh the proof, then retry the registration.`;
783
+ case "wrong-context":
784
+ return "Your alias binding exists but is for a different application context" + (state.storedContextHex ? ` (context: ${state.storedContextHex})` : "") + ". Re-bind under the 'dotns' context using the bootstrap flow: " + popSelfServe.dotnsBootstrapUrl;
785
+ case "bound-fresh":
786
+ return "Self-attestation is no longer available. Contact the DotNS team for whitelisting / Personhood status help.";
787
+ }
788
+ }
789
+ function formatPopShortfallReason(opts) {
790
+ const { label, requiredName, currentName, isTestnet, environmentId, popSelfServe, aliasState, exampleNoStatusLabel: noStatusEx } = opts;
791
+ const leadIn = `${label}.dot requires ${requiredName}, but this signer is ${currentName}.`;
792
+ let testnetBlock = "";
793
+ if (isTestnet && popSelfServe != null) {
794
+ if (popSelfServe.stateAwareGuidance) {
795
+ const state = aliasState ?? { state: "not-bound" };
796
+ testnetBlock = "\n\n" + formatPersonhoodRemediation(state, popSelfServe, environmentId);
797
+ } else {
798
+ testnetBlock = `
799
+
800
+ On testnets you can self-serve:
801
+ 1. Fund the service account mnemonic via ${popSelfServe.faucetUrl}
802
+ 2. Go to ${popSelfServe.personhoodFaucetUrl}, pick your env (e.g. ${popSelfServe.sudoEnvLabel}), and paste the mnemonic
803
+ 3. Go to ${popSelfServe.dotnsBootstrapUrl} and follow each step (first and last can probably be skipped)`;
804
+ }
805
+ }
806
+ const alternativesBlock = `
807
+
808
+ Alternatively:
809
+ - Use a NoStatus-compatible label (base length >= 9 with exactly two trailing digits, e.g. ${noStatusEx})
810
+ - Raise a whitelist issue at https://github.com/paritytech/dotns/`;
811
+ return leadIn + testnetBlock + alternativesBlock;
812
+ }
813
+ var DotNS = class {
814
+ client;
815
+ clientWrapper;
816
+ rpc;
817
+ substrateAddress;
818
+ evmAddress;
819
+ signer;
820
+ connected;
821
+ // Per-instance failover list. Populated from options.assetHubEndpoints when
822
+ // bulletin-deploy resolves the asset-hub list via environments.json; falls
823
+ // back to the legacy paseo-only RPC_ENDPOINTS for direct library callers.
824
+ assetHubEndpoints;
825
+ _usesExternalSigner = false;
826
+ _localMnemonic = null;
827
+ _contracts = CONTRACTS;
828
+ _nativeToEthRatio = NATIVE_TO_ETH_RATIO;
829
+ _environmentId = null;
830
+ _popSelfServe = null;
831
+ _registerStorageDeposit = MINIMUM_REGISTER_STORAGE_DEPOSIT;
832
+ // Test-only seam: consumed once by classifyAliasAccountState() then cleared.
833
+ // Mirrors the __setDeployRootSpanForTest / __setSentryForTest pattern.
834
+ _classifyOverrideForTest = null;
835
+ /** Test-only: inject a fixed classifyAliasAccountState return value for the next call. Consumed once. */
836
+ __setClassifyOverrideForTest(state) {
837
+ this._classifyOverrideForTest = { state, revision: 0 };
838
+ }
839
+ // Test-only seam: consumed once by getUserPopStatus() then cleared.
840
+ _userPopStatusOverrideForTest = null;
841
+ /** Test-only: inject a fixed getUserPopStatus return value for the next call. Consumed once. */
842
+ __setUserPopStatusForTest(status) {
843
+ this._userPopStatusOverrideForTest = status;
844
+ }
845
+ // Test-only seam: fallback result for reprove() when the real call throws "not strictly
846
+ // greater than stored" (account already at latest revision). Any other error propagates.
847
+ _reproveFallbackForTest = null;
848
+ /** Test-only: register a fallback reprove result used only if the real reprove() throws "not strictly greater than stored". Consumed once. */
849
+ __setReproveFallbackForTest(result) {
850
+ this._reproveFallbackForTest = result;
851
+ }
852
+ constructor() {
853
+ this.client = null;
854
+ this.clientWrapper = null;
855
+ this.rpc = null;
856
+ this.substrateAddress = null;
857
+ this.evmAddress = null;
858
+ this.signer = null;
859
+ this.connected = false;
860
+ this.assetHubEndpoints = RPC_ENDPOINTS;
861
+ }
862
+ async connect(options = {}) {
863
+ if (options.assetHubEndpoints && options.assetHubEndpoints.length > 0) {
864
+ this.assetHubEndpoints = options.assetHubEndpoints;
865
+ }
866
+ if (options.contracts && Object.keys(options.contracts).length > 0) {
867
+ validateContractAddresses(options.contracts, options.environmentId ?? "unknown");
868
+ this._contracts = { ...CONTRACTS, ...options.contracts };
869
+ }
870
+ if (options.environmentId) {
871
+ this._environmentId = options.environmentId;
872
+ }
873
+ if (options.popSelfServe !== void 0) {
874
+ this._popSelfServe = options.popSelfServe ?? null;
875
+ }
876
+ if (options.registerStorageDeposit !== void 0) {
877
+ this._registerStorageDeposit = options.registerStorageDeposit;
878
+ }
879
+ const rpc = options.rpc || process.env.DOTNS_RPC || this.assetHubEndpoints[0];
880
+ this.rpc = rpc;
881
+ this._usesExternalSigner = Boolean(options.signer && options.signerAddress);
882
+ if (this._usesExternalSigner) {
883
+ this.signer = options.signer;
884
+ this.substrateAddress = options.signerAddress;
885
+ } else {
886
+ const mnemonicArg = options.mnemonic || process.env.DOTNS_MNEMONIC || process.env.MNEMONIC;
887
+ const keyUriArg = options.keyUri || process.env.DOTNS_KEY_URI;
888
+ let source = keyUriArg || mnemonicArg || DEFAULT_MNEMONIC;
889
+ const isKeyUri = Boolean(keyUriArg);
890
+ if (!isKeyUri && !options.derivationPath) {
891
+ this._localMnemonic = mnemonicArg || DEFAULT_MNEMONIC;
892
+ }
893
+ if (options.derivationPath && !isKeyUri && source) {
894
+ source = `${source}${options.derivationPath}`;
895
+ }
896
+ await cryptoWaitReady();
897
+ const keyring = new Keyring({ type: "sr25519" });
898
+ const account = isKeyUri || options.derivationPath ? keyring.addFromUri(source) : keyring.addFromMnemonic(source);
899
+ this.signer = getPolkadotSigner(account.publicKey, "Sr25519", async (input) => account.sign(input));
900
+ this.substrateAddress = account.address;
901
+ }
902
+ console.log(` SS58 Address: ${this.substrateAddress}`);
903
+ setDeployAttribute("deploy.dotns.signer", truncateAddress(this.substrateAddress));
904
+ setDeploySentryTag("deploy.dotns.signer", truncateAddress(this.substrateAddress));
905
+ return withSpan("deploy.dotns.connect", "dotns connect", {}, async () => {
906
+ try {
907
+ this.client = createClient(getWsProvider(rpc, { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS }));
908
+ const unsafeApi = this.client.getUnsafeApi();
909
+ this.clientWrapper = new ReviveClientWrapper(unsafeApi);
910
+ this.evmAddress = await withTimeout(
911
+ this.clientWrapper.getEvmAddress(this.substrateAddress),
912
+ CONNECTION_TIMEOUT_MS,
913
+ "ReviveApi.address"
914
+ );
915
+ console.log(` H160 Address: ${this.evmAddress}`);
916
+ } catch (e) {
917
+ throw new Error(`DotNS connect: failed to resolve EVM address from ${this.substrateAddress} via ReviveApi.address (${e.message?.slice(0, 200)})`);
918
+ }
919
+ setDeployAttribute("deploy.dotns.rpc_used", rpc);
920
+ setDeployAttribute("deploy.dotns.evm_address", this.evmAddress);
921
+ this.connected = true;
922
+ if (options.nativeToEthRatio) this._nativeToEthRatio = options.nativeToEthRatio;
923
+ try {
924
+ await this.ensureMappedAccountReady(options.autoAccountMapping ?? false);
925
+ } catch (e) {
926
+ this.connected = false;
927
+ throw e;
928
+ }
929
+ return this;
930
+ });
931
+ }
932
+ async ensureMappedAccountReady(autoAccountMapping = false) {
933
+ this.ensureConnected();
934
+ if (!this.clientWrapper || !this.substrateAddress || !this.signer) {
935
+ throw new Error("Account mapping unavailable before DotNS signer is initialized");
936
+ }
937
+ if (autoAccountMapping) {
938
+ setDeployAttribute("deploy.dotns.mapping_source", "auto-account-mapping");
939
+ await this.ensureAutoMappedAccountReady();
940
+ return;
941
+ }
942
+ if (await this.clientWrapper.checkIfAccountMapped(this.substrateAddress)) {
943
+ setDeployAttribute("deploy.dotns.mapping_source", "already-mapped");
944
+ console.log(` Account: mapped`);
945
+ return;
946
+ }
947
+ console.log(` Mapping account on Asset Hub Revive...`);
948
+ try {
949
+ await this.clientWrapper.ensureAccountMapped(this.substrateAddress, this.signer);
950
+ } catch (e) {
951
+ if (await this.clientWrapper.checkIfAccountMapped(this.substrateAddress)) {
952
+ setDeployAttribute("deploy.dotns.mapping_source", "direct-mapped");
953
+ console.log(` Account: mapped`);
954
+ return;
955
+ }
956
+ captureWarning("explicit account mapping failed; falling back to Revive auto-map trigger", {
957
+ signer: this.substrateAddress,
958
+ error: e?.message?.slice?.(0, 200) ?? String(e).slice(0, 200)
959
+ });
960
+ setDeployAttribute("deploy.dotns.mapping_source", "auto-map-fallback");
961
+ await this.ensureAutoMappedAccountReady();
962
+ return;
963
+ }
964
+ setDeployAttribute("deploy.dotns.mapping_source", "direct-mapped");
965
+ console.log(` Account: mapped`);
966
+ }
967
+ async ensureAutoMappedAccountReady() {
968
+ this.ensureConnected();
969
+ if (!this.clientWrapper || !this.substrateAddress || !this.signer) {
970
+ throw new Error("Account auto-mapping unavailable before DotNS signer is initialized");
971
+ }
972
+ if (await this.clientWrapper.checkIfAccountMapped(this.substrateAddress)) {
973
+ console.log(` Account: auto-mapped (Revive.OriginalAccount confirmed)`);
974
+ return;
975
+ }
976
+ if (await this.isTestnet()) {
977
+ const free = await this.readFreeBalance(this.substrateAddress);
978
+ if (free < FEE_FLOOR_REGISTER) {
979
+ console.log(` DotNS signer ${this.substrateAddress.slice(0, 8)}... balance ${fmtPas(free)} PAS before auto-map \u2014 attempting testnet auto top-up...`);
980
+ const toppedUp = await this.attemptTestnetTopUp(this.substrateAddress, TOP_UP_TARGET);
981
+ if (toppedUp) {
982
+ console.log(` Topped up ${fmtPas(toppedUp.transferred)} PAS from ${toppedUp.source} for auto-map`);
983
+ setDeployAttribute("deploy.dotns.signer_below_floor", "true");
984
+ setDeployAttribute("deploy.dotns.toppedup", "true");
985
+ setDeployAttribute("deploy.dotns.toppedup_source", toppedUp.source);
986
+ }
987
+ }
988
+ }
989
+ if (!await this.clientWrapper.checkIfAccountMapped(this.substrateAddress)) {
990
+ try {
991
+ const cd = encodeFunctionData({ abi: DOTNS_REGISTRAR_CONTROLLER_ABI, functionName: "minCommitmentAge", args: [] });
992
+ await this.clientWrapper.signAndSubmitWithRetry(
993
+ () => this.clientWrapper.client.tx.Revive.call({
994
+ dest: this._contracts.DOTNS_REGISTRAR_CONTROLLER,
995
+ value: 0n,
996
+ weight_limit: { ref_time: 10000000000n, proof_size: 131072n },
997
+ storage_deposit_limit: 5000000000000n,
998
+ data: Binary.fromHex(cd)
999
+ }),
1000
+ this.signer,
1001
+ () => {
1002
+ },
1003
+ "auto-map trigger"
1004
+ );
1005
+ } catch (e) {
1006
+ captureWarning("DotNS auto-map trigger failed", {
1007
+ signer: this.substrateAddress,
1008
+ error: e?.message?.slice?.(0, 200) ?? String(e).slice(0, 200)
1009
+ });
1010
+ }
1011
+ }
1012
+ if (!await this.clientWrapper.checkIfAccountMapped(this.substrateAddress)) {
1013
+ throw new Error(`Account auto-mapping did not take effect on-chain for ${this.substrateAddress}. The signer needs enough testnet PAS to submit the Revive auto-map trigger before DotNS preflight can run. Top up at ${PASEO_FAUCET_URL} or fund Alice/Bob so auto-top-up can help.`);
1014
+ }
1015
+ console.log(` Account: auto-mapped (Revive.OriginalAccount confirmed)`);
1016
+ }
1017
+ ensureConnected() {
1018
+ if (!this.connected) throw new Error("Not connected. Call connect() first.");
1019
+ }
1020
+ // Returns true when the DotNS chain (Asset Hub) reports a testnet spec_name.
1021
+ // Used to gate test-only behaviors like self-granting Full PoP on a Lite
1022
+ // signer for a NoStatus label.
1023
+ _testnetCache = null;
1024
+ async isTestnet() {
1025
+ if (this._testnetCache !== null) return this._testnetCache;
1026
+ this.ensureConnected();
1027
+ if (this.clientWrapper) {
1028
+ try {
1029
+ const version = await this.clientWrapper.client.constants.System.Version();
1030
+ const raw = version?.spec_name ?? version?.specName;
1031
+ const specName = typeof raw === "string" ? raw : raw?.asText?.() ?? String(raw ?? "");
1032
+ this._testnetCache = isTestnetSpecName(specName);
1033
+ return this._testnetCache;
1034
+ } catch {
1035
+ }
1036
+ }
1037
+ const rpc = this.rpc ?? "";
1038
+ this._testnetCache = isTestnetSpecName(rpc) || rpc.includes("paseo") || rpc.includes("westend") || rpc.includes("rococo");
1039
+ return this._testnetCache;
1040
+ }
1041
+ /**
1042
+ * Classify the AliasAccounts state for a substrate address.
1043
+ * Only called on paseo-next-v2 testnets inside the preflight's NoStatus branch.
1044
+ * Returns "not-bound" if the chain is unreachable (safe fallback to generic advice).
1045
+ */
1046
+ async classifyAliasAccountState(ss58) {
1047
+ if (this._classifyOverrideForTest !== null) {
1048
+ const result = this._classifyOverrideForTest;
1049
+ this._classifyOverrideForTest = null;
1050
+ return result;
1051
+ }
1052
+ if (!this.clientWrapper) return { state: "not-bound" };
1053
+ try {
1054
+ const api = this.clientWrapper.client;
1055
+ const row = await api.query.AliasAccounts.AccountToAlias.getValue(ss58, { at: "best" });
1056
+ if (!row) return { state: "not-bound" };
1057
+ const contextHex = typeof row.ca?.context === "string" ? row.ca.context.toLowerCase() : "";
1058
+ const paid = Boolean(row.paid);
1059
+ const revision = Number(row.revision ?? 0);
1060
+ if (paid && contextHex === DOTNS_CONTEXT_HEX_LOWER) {
1061
+ return { state: "bound-likely-stale", storedContextHex: contextHex, paid, revision };
1062
+ }
1063
+ if (!paid || contextHex !== DOTNS_CONTEXT_HEX_LOWER) {
1064
+ return { state: "wrong-context", storedContextHex: contextHex, paid, revision };
1065
+ }
1066
+ return { state: "bound-fresh", storedContextHex: contextHex, paid, revision };
1067
+ } catch {
1068
+ return { state: "not-bound" };
1069
+ }
1070
+ }
1071
+ // Free PAS balance for a substrate address on the connected DotNS chain.
1072
+ // Used by preflight to gate the deploy on whether the signer can pay tx
1073
+ // fees before the chunk upload runs. Returns 0n if the account doesn't
1074
+ // exist on chain.
1075
+ async readFreeBalance(ss58) {
1076
+ this.ensureConnected();
1077
+ if (!this.clientWrapper) throw new Error("readFreeBalance: polkadot-api client not available");
1078
+ const acc = await this.clientWrapper.client.query.System.Account.getValue(ss58);
1079
+ return BigInt(acc?.data?.free ?? 0n);
1080
+ }
1081
+ // Testnet-only: try to top the deploy signer up from the well-known dev
1082
+ // phrase's Alice (root) or Bob (//Bob) account. Each candidate is read,
1083
+ // skipped if it can't spare the transfer, and otherwise used to send
1084
+ // `targetAmount` to `recipientSs58`. Returns the source label + amount on
1085
+ // success, or null if neither candidate had the funds. The dev phrase is
1086
+ // hardcoded on purpose — this only fires on testnet (gated by caller) and
1087
+ // runs against accounts every Substrate-Polkadot tester can fund.
1088
+ async attemptTestnetTopUp(recipientSs58, targetAmount) {
1089
+ this.ensureConnected();
1090
+ if (!this.clientWrapper) throw new Error("attemptTestnetTopUp: polkadot-api client not available");
1091
+ await cryptoWaitReady();
1092
+ const keyring = new Keyring({ type: "sr25519" });
1093
+ const sources = [
1094
+ { label: "Alice", uri: DEFAULT_MNEMONIC },
1095
+ { label: "Bob", uri: `${DEFAULT_MNEMONIC}//Bob` }
1096
+ ];
1097
+ for (const src of sources) {
1098
+ const account = keyring.addFromUri(src.uri);
1099
+ if (account.address === recipientSs58) continue;
1100
+ const sourceBalance = await this.readFreeBalance(account.address);
1101
+ if (sourceBalance < targetAmount + SOURCE_BUFFER) {
1102
+ console.log(` ${src.label} (${account.address.slice(0, 8)}...) low: ${fmtPas(sourceBalance)} PAS \u2014 skipping`);
1103
+ captureWarning(`DotNS auto-top-up: ${src.label} insufficient`, {
1104
+ source: src.label,
1105
+ sourceAddress: account.address,
1106
+ free: sourceBalance.toString(),
1107
+ required: (targetAmount + SOURCE_BUFFER).toString()
1108
+ });
1109
+ continue;
1110
+ }
1111
+ const signer = getPolkadotSigner(
1112
+ account.publicKey,
1113
+ "Sr25519",
1114
+ async (input) => account.sign(input)
1115
+ );
1116
+ console.log(` Trying ${src.label} (${account.address.slice(0, 8)}...): transferring ${fmtPas(targetAmount)} PAS to ${recipientSs58.slice(0, 8)}...`);
1117
+ try {
1118
+ await withTimeout(
1119
+ this.submitTransfer(signer, recipientSs58, targetAmount),
1120
+ TOP_UP_TRANSFER_TIMEOUT_MS,
1121
+ `Balances.transfer_allow_death from ${src.label}`
1122
+ );
1123
+ return { source: src.label, transferred: targetAmount };
1124
+ } catch (e) {
1125
+ console.log(` Top-up via ${src.label} failed: ${e.message?.slice(0, 200)}`);
1126
+ }
1127
+ }
1128
+ return null;
1129
+ }
1130
+ // Helper: submit a Balances.transfer_allow_death tx and resolve only after
1131
+ // GRANDPA finalization (not best-block inclusion). Auto-top-up writes to
1132
+ // the deploy signer's balance; if a re-org rolls back a best-block credit
1133
+ // before the deploy's next tx, preflight reports success but setContenthash
1134
+ // / register fail with Insufficient funds again. Waiting for finalization
1135
+ // adds ~18s on Asset Hub Paseo but eliminates the re-org window.
1136
+ // Logs a per-block-progress line so the operator sees the wait isn't a hang.
1137
+ // Note: Enum("Id", ss58) is required to decode the SS58 to AccountId32;
1138
+ // the structural literal { type: "Id", value } encodes wrong.
1139
+ // Companion in tools/setup-e2e-derivation-signers.mjs uses best-block only;
1140
+ // that's a one-time setup script, not a runtime gate, so the trade-off there
1141
+ // is different.
1142
+ submitTransfer(signer, destSubstrate, valueRaw) {
1143
+ const api = this.clientWrapper.client;
1144
+ const tx = api.tx.Balances.transfer_allow_death({
1145
+ dest: Enum("Id", destSubstrate),
1146
+ value: valueRaw
1147
+ });
1148
+ return new Promise((resolve, reject) => {
1149
+ let settled = false;
1150
+ let bestBlockSeen = false;
1151
+ const sub = tx.signSubmitAndWatch(signer).subscribe({
1152
+ next: (event) => {
1153
+ if (settled) return;
1154
+ if (event.type === "txBestBlocksState" && event.found && !bestBlockSeen) {
1155
+ bestBlockSeen = true;
1156
+ if (!event.ok) {
1157
+ settled = true;
1158
+ try {
1159
+ sub.unsubscribe();
1160
+ } catch {
1161
+ }
1162
+ reject(new Error("Balances.transfer_allow_death dispatch error"));
1163
+ return;
1164
+ }
1165
+ console.log(` Tx in best block \u2014 waiting for finalization...`);
1166
+ return;
1167
+ }
1168
+ if (event.type === "finalized") {
1169
+ settled = true;
1170
+ try {
1171
+ sub.unsubscribe();
1172
+ } catch {
1173
+ }
1174
+ if (event.ok === false) reject(new Error("Balances.transfer_allow_death finalization error"));
1175
+ else resolve();
1176
+ }
1177
+ },
1178
+ error: (e) => {
1179
+ if (settled) return;
1180
+ settled = true;
1181
+ reject(e instanceof Error ? e : new Error(String(e)));
1182
+ },
1183
+ // Defensive: papi can complete the stream after a network drop without
1184
+ // emitting finalized. Without this, the promise hangs forever and
1185
+ // preflight stalls. Reject so the caller falls back to Bob.
1186
+ complete: () => {
1187
+ if (settled) return;
1188
+ settled = true;
1189
+ reject(new Error("transfer subscription closed without finalization"));
1190
+ }
1191
+ });
1192
+ });
1193
+ }
1194
+ async contractCall(contractAddress, contractAbi, functionName, args = []) {
1195
+ this.ensureConnected();
1196
+ if (!this.clientWrapper) throw new Error("contractCall: polkadot-api client not available");
1197
+ const encodedCallData = encodeFunctionData({ abi: contractAbi, functionName, args });
1198
+ const callResult = await this.clientWrapper.performDryRunCall(this.substrateAddress, contractAddress, 0n, encodedCallData);
1199
+ if (!callResult.result.isOk) {
1200
+ const errorData = callResult.result.value;
1201
+ throw new Error(formatContractDryRunFailure({
1202
+ revertData: errorData?.data ?? "0x",
1203
+ revertFlags: errorData?.flags ?? 0n,
1204
+ gasConsumed: callResult.gasConsumed,
1205
+ gasRequired: callResult.gasRequired,
1206
+ storageDeposit: callResult.storageDeposit?.value
1207
+ }, {
1208
+ contractAddress,
1209
+ functionName,
1210
+ signerSubstrateAddress: this.substrateAddress,
1211
+ signerEvmAddress: this.evmAddress ?? void 0,
1212
+ value: 0n,
1213
+ encodedData: encodedCallData,
1214
+ args,
1215
+ contracts: this._contracts
1216
+ }));
1217
+ }
1218
+ return decodeFunctionResult({ abi: contractAbi, functionName, data: callResult.result.value.data });
1219
+ }
1220
+ /**
1221
+ * Like contractCall, but returns null when the chain replies with empty data
1222
+ * ("0x"). Use this for view functions where an unset storage slot is a
1223
+ * meaningful answer (e.g. resolver(node) for a name with no resolver,
1224
+ * text records, optional ownership lookups). Use the strict contractCall
1225
+ * for read paths that must always return a value.
1226
+ */
1227
+ async contractCallNullable(contractAddress, contractAbi, functionName, args = []) {
1228
+ this.ensureConnected();
1229
+ if (!this.clientWrapper) throw new Error("contractCallNullable: polkadot-api client not available");
1230
+ const encodedCallData = encodeFunctionData({ abi: contractAbi, functionName, args });
1231
+ const callResult = await this.clientWrapper.performDryRunCall(this.substrateAddress, contractAddress, 0n, encodedCallData);
1232
+ if (!callResult.result.isOk) {
1233
+ const errorData = callResult.result.value;
1234
+ throw new Error(formatContractDryRunFailure({
1235
+ revertData: errorData?.data ?? "0x",
1236
+ revertFlags: errorData?.flags ?? 0n,
1237
+ gasConsumed: callResult.gasConsumed,
1238
+ gasRequired: callResult.gasRequired,
1239
+ storageDeposit: callResult.storageDeposit?.value
1240
+ }, {
1241
+ contractAddress,
1242
+ functionName,
1243
+ signerSubstrateAddress: this.substrateAddress,
1244
+ signerEvmAddress: this.evmAddress ?? void 0,
1245
+ value: 0n,
1246
+ encodedData: encodedCallData,
1247
+ args,
1248
+ contracts: this._contracts
1249
+ }));
1250
+ }
1251
+ const rawData = callResult.result.value.data ?? "0x";
1252
+ if (rawData === "0x" || rawData === "" || rawData.length <= 2) return null;
1253
+ return decodeFunctionResult({ abi: contractAbi, functionName, data: rawData });
1254
+ }
1255
+ async contractTransaction(contractAddress, value, contractAbi, functionName, args = [], statusCallback = () => {
1256
+ }, { useNoncePolling, verifyEffect } = {}) {
1257
+ this.ensureConnected();
1258
+ if (!this.clientWrapper) throw new Error("contractTransaction: polkadot-api client not available");
1259
+ const encodedCallData = encodeFunctionData({ abi: contractAbi, functionName, args });
1260
+ const rpcs = this.rpc ? [this.rpc, ...this.assetHubEndpoints.filter((ep) => ep !== this.rpc)] : this.assetHubEndpoints;
1261
+ return await withTimeout(
1262
+ this.clientWrapper.submitTransaction(contractAddress, value, encodedCallData, this.substrateAddress, this.signer, statusCallback, { rpcs, useNoncePolling, functionName, args, contracts: this._contracts, verifyEffect }),
1263
+ OPERATION_TIMEOUT_MS,
1264
+ functionName
1265
+ );
1266
+ }
1267
+ async checkOwnership(label, ownerAddress = null) {
1268
+ this.ensureConnected();
1269
+ const checkAddress = (ownerAddress || this.evmAddress).toLowerCase();
1270
+ const tokenId = computeDomainTokenId(label);
1271
+ try {
1272
+ const owner = await withTimeout(this.contractCallNullable(this._contracts.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 3e4, "ownerOf");
1273
+ if (owner === null) return { owned: false, owner: null };
1274
+ const owned = owner.toLowerCase() === checkAddress;
1275
+ return { owned, owner };
1276
+ } catch {
1277
+ return { owned: false, owner: null };
1278
+ }
1279
+ }
1280
+ async getUserPopStatus(ownerAddress = null) {
1281
+ if (this._userPopStatusOverrideForTest !== null) {
1282
+ const result = this._userPopStatusOverrideForTest;
1283
+ this._userPopStatusOverrideForTest = null;
1284
+ return result;
1285
+ }
1286
+ this.ensureConnected();
1287
+ const checkAddress = ownerAddress || this.evmAddress;
1288
+ try {
1289
+ const result = await withTimeout(
1290
+ this.contractCall(PERSONHOOD_PRECOMPILE_ADDRESS, PERSONHOOD_ABI, "personhoodStatus", [checkAddress, PERSONHOOD_CONTEXT]),
1291
+ 3e4,
1292
+ "personhoodStatus"
1293
+ );
1294
+ return parsePersonhoodStatusResult(result);
1295
+ } catch (e) {
1296
+ throw new Error(
1297
+ `Could not read DotNS Personhood status for ${checkAddress} from the Personhood precompile. Check the Asset Hub RPC/environment and contact the DotNS team if the signer should be whitelisted. Underlying: ${e?.message ?? String(e)}`
1298
+ );
1299
+ }
1300
+ }
1301
+ async checkSubdomainOwnership(sublabel, parentLabel) {
1302
+ this.ensureConnected();
1303
+ if (!this.clientWrapper) return { owned: false, owner: null };
1304
+ const node = namehash(`${sublabel}.${parentLabel}.dot`);
1305
+ try {
1306
+ const owner = await withTimeout(this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [node]), 3e4, "owner");
1307
+ if (!owner || owner === zeroAddress) return { owned: false, owner: null };
1308
+ const owned = owner.toLowerCase() === this.evmAddress.toLowerCase();
1309
+ return { owned, owner };
1310
+ } catch {
1311
+ return { owned: false, owner: null };
1312
+ }
1313
+ }
1314
+ async registerSubdomain(sublabel, parentLabel) {
1315
+ return withSpan("deploy.dotns.register-subdomain", `2a. register ${sublabel}.${parentLabel}.dot`, {}, async () => {
1316
+ this.ensureConnected();
1317
+ console.log(`
1318
+ Registering subdomain ${sublabel}.${parentLabel}.dot...`);
1319
+ const parentNode = namehash(`${parentLabel}.dot`);
1320
+ const subnodeNode = namehash(`${sublabel}.${parentLabel}.dot`);
1321
+ const subnodeRecord = { parentNode, subLabel: sublabel, parentLabel, owner: this.evmAddress };
1322
+ const txResolution = await this.submitBatchedContractCalls(
1323
+ [
1324
+ { contractAddress: this._contracts.DOTNS_REGISTRY, abi: DOTNS_REGISTRY_ABI, functionName: "setSubnodeOwner", args: [subnodeRecord] },
1325
+ { contractAddress: this._contracts.DOTNS_REGISTRY, abi: DOTNS_REGISTRY_ABI, functionName: "setResolver", args: [subnodeNode, this._contracts.DOTNS_CONTENT_RESOLVER] }
1326
+ ],
1327
+ (s) => console.log(` ${s}`),
1328
+ `Utility.batch_all (register ${sublabel}.${parentLabel}.dot)`
1329
+ );
1330
+ logTxResolution(txResolution);
1331
+ if (txResolution.kind === TX_KIND_HASH) {
1332
+ setDeployAttribute("deploy.subnode.tx", txResolution.hash);
1333
+ if (txResolution.block) {
1334
+ setDeployAttribute("deploy.subnode.block", txResolution.block.number);
1335
+ setDeployAttribute("deploy.subnode.block_hash", txResolution.block.hash);
1336
+ console.log(` finalised @ block ${txResolution.block.number} (tx ${txResolution.hash})`);
1337
+ } else {
1338
+ console.log(` finalised (tx ${txResolution.hash})`);
1339
+ }
1340
+ }
1341
+ console.log(` Subdomain registered!`);
1342
+ return { sublabel, parentLabel, owner: this.evmAddress };
1343
+ });
1344
+ }
1345
+ /**
1346
+ * Submit multiple contract calls as a single atomic `Utility.batch_all`
1347
+ * extrinsic.
1348
+ *
1349
+ * Each call is encoded as a `pallet-revive::call(...)` extrinsic and
1350
+ * batched into one outer dispatch. The runtime executes them in
1351
+ * sequence and rolls back the entire batch on any inner revert. Only
1352
+ * the leading call is dry-run for gas — its weight is reused as the
1353
+ * budget for every subsequent call, on the assumption sibling
1354
+ * registry/resolver writes are similarly sized.
1355
+ */
1356
+ async submitBatchedContractCalls(calls, statusCallback, label) {
1357
+ this.ensureConnected();
1358
+ if (!this.clientWrapper) throw new Error(`${label}: polkadot-api client not available`);
1359
+ if (calls.length === 0) throw new Error(`${label}: at least one inner call required`);
1360
+ await this.clientWrapper.ensureAccountMapped(this.substrateAddress, this.signer);
1361
+ const encoded = calls.map((c) => ({
1362
+ contractAddress: c.contractAddress,
1363
+ value: c.value ?? 0n,
1364
+ data: encodeFunctionData({ abi: c.abi, functionName: c.functionName, args: c.args })
1365
+ }));
1366
+ const headEstimate = await this.clientWrapper.estimateGasForCall(
1367
+ this.substrateAddress,
1368
+ encoded[0].contractAddress,
1369
+ encoded[0].value,
1370
+ encoded[0].data
1371
+ );
1372
+ if (!headEstimate.success) {
1373
+ throw new Error(formatContractDryRunFailure(headEstimate, {
1374
+ contractAddress: encoded[0].contractAddress,
1375
+ functionName: calls[0].functionName,
1376
+ signerSubstrateAddress: this.substrateAddress,
1377
+ signerEvmAddress: this.evmAddress,
1378
+ value: encoded[0].value,
1379
+ encodedData: encoded[0].data,
1380
+ args: calls[0].args,
1381
+ contracts: this._contracts
1382
+ }));
1383
+ }
1384
+ const weight_limit = {
1385
+ proof_size: headEstimate.gasRequired.proofSize,
1386
+ ref_time: headEstimate.gasRequired.referenceTime
1387
+ };
1388
+ const minimumStorageDeposit = 2000000000000n;
1389
+ let storage_deposit_limit = headEstimate.storageDeposit === 0n ? minimumStorageDeposit : headEstimate.storageDeposit * 120n / 100n;
1390
+ if (storage_deposit_limit < minimumStorageDeposit) storage_deposit_limit = minimumStorageDeposit;
1391
+ const client = this.clientWrapper.client;
1392
+ const buildBatch = () => {
1393
+ const inner = encoded.map(
1394
+ (e) => client.tx.Revive.call({
1395
+ dest: e.contractAddress,
1396
+ value: e.value,
1397
+ weight_limit,
1398
+ storage_deposit_limit,
1399
+ data: Binary.fromHex(e.data)
1400
+ })
1401
+ );
1402
+ return client.tx.Utility.batch_all({ calls: inner.map((c) => c.decodedCall) });
1403
+ };
1404
+ return await withTimeout(
1405
+ this.clientWrapper.signAndSubmitWithRetry(buildBatch, this.signer, statusCallback, label),
1406
+ OPERATION_TIMEOUT_MS,
1407
+ label
1408
+ );
1409
+ }
1410
+ async setContenthash(domainName, contenthashHex) {
1411
+ return withSpan("deploy.dotns.set-contenthash", "2b. set-contenthash", {}, async () => {
1412
+ this.ensureConnected();
1413
+ const node = namehash(`${domainName}.dot`);
1414
+ let ipfsCid = null;
1415
+ if (contenthashHex && contenthashHex !== "0x") {
1416
+ const bytes = Buffer.from(contenthashHex.slice(2), "hex");
1417
+ if (bytes[0] === 227 && bytes.length >= 4) {
1418
+ const cidBytes = bytes.slice(2);
1419
+ ipfsCid = CID.decode(cidBytes).toString();
1420
+ }
1421
+ }
1422
+ if (!ipfsCid) throw new Error(`setContenthash: cannot decode contenthash ${contenthashHex} to an IPFS CID`);
1423
+ console.log(` Setting contenthash: ${ipfsCid}`);
1424
+ const expected = contenthashHex.toLowerCase();
1425
+ try {
1426
+ const current = (await this.getContenthash(domainName) || "0x").toLowerCase();
1427
+ if (current === expected) {
1428
+ console.log(` Contenthash already set: ${ipfsCid} \u2014 skipping tx`);
1429
+ setDeployAttribute("deploy.dotns.contenthash_unchanged", "true");
1430
+ return { node };
1431
+ }
1432
+ } catch (_) {
1433
+ }
1434
+ setDeployAttribute("deploy.dotns.contenthash_unchanged", "false");
1435
+ const MAX_VERIFY_CHAIN_SECONDS = 30;
1436
+ const POLL_INTERVAL_MS = 2e3;
1437
+ const verifyEffect = async () => {
1438
+ const wrapper = this.clientWrapper;
1439
+ if (!this.connected || !wrapper) return false;
1440
+ const startChainMs = Number(await wrapper.client.query.Timestamp.Now.getValue());
1441
+ let lastPrintedElapsed = -1;
1442
+ while (true) {
1443
+ const liveWrapper = this.clientWrapper;
1444
+ if (!this.connected || !liveWrapper) return false;
1445
+ const [onChainRaw, nowChainMs] = await Promise.all([
1446
+ this.getContenthash(domainName),
1447
+ liveWrapper.client.query.Timestamp.Now.getValue().then(Number)
1448
+ ]);
1449
+ const onChain = (onChainRaw || "0x").toLowerCase();
1450
+ if (onChain === expected) return true;
1451
+ const chainElapsed = (nowChainMs - startChainMs) / 1e3;
1452
+ if (chainElapsed >= MAX_VERIFY_CHAIN_SECONDS) return false;
1453
+ const floored = Math.floor(chainElapsed);
1454
+ if (floored > lastPrintedElapsed) {
1455
+ console.log(` Awaiting finalization (chain time +${floored}s / ${MAX_VERIFY_CHAIN_SECONDS}s)...`);
1456
+ lastPrintedElapsed = floored;
1457
+ }
1458
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1459
+ }
1460
+ };
1461
+ const txRes = await this.contractTransaction(this._contracts.DOTNS_CONTENT_RESOLVER, 0n, DOTNS_CONTENT_RESOLVER_ABI, "setContenthash", [node, contenthashHex], (s) => console.log(` ${s}`), { useNoncePolling: true, verifyEffect });
1462
+ const finalOnChain = (await this.getContenthash(domainName) || "0x").toLowerCase();
1463
+ if (finalOnChain !== expected) {
1464
+ throw new Error(
1465
+ `Post-deploy verification failed for ${domainName}.dot: on-chain contenthash is ${finalOnChain}, not the ${expected} we just wrote. The setContenthash tx may have silently failed, or another party overwrote the domain. Re-run the deploy to retry.`
1466
+ );
1467
+ }
1468
+ setDeployAttribute("deploy.dotns.tx_resolution", txRes.kind);
1469
+ logTxResolution(txRes);
1470
+ if (txRes.kind === TX_KIND_HASH) {
1471
+ setDeployAttribute("deploy.contenthash.tx", txRes.hash);
1472
+ if (txRes.block) {
1473
+ setDeployAttribute("deploy.contenthash.block", txRes.block.number);
1474
+ setDeployAttribute("deploy.contenthash.block_hash", txRes.block.hash);
1475
+ console.log(` finalised @ block ${txRes.block.number} (tx ${txRes.hash})`);
1476
+ } else {
1477
+ console.log(` finalised (tx ${txRes.hash})`);
1478
+ }
1479
+ }
1480
+ console.log(` Verified on-chain: ${ipfsCid}
1481
+ `);
1482
+ return { node };
1483
+ });
1484
+ }
1485
+ /**
1486
+ * Point a node's registered resolver at `DOTNS_CONTENT_RESOLVER` (RFC §Step 3.2).
1487
+ *
1488
+ * Hosts read text records via `IDotnsRegistry.resolver(node)`, so the
1489
+ * registered slot must point at the content resolver for manifest text
1490
+ * records to be discoverable. The pre-read is best-effort: pallet-revive
1491
+ * returns `isOk=true` with empty data when a selector isn't in the
1492
+ * deployed bytecode, which makes viem's decoder throw. Any decode failure
1493
+ * is treated as "unset" and the write fires unconditionally. `setResolver`
1494
+ * is idempotent against the same target, so registries that pre-date the
1495
+ * `resolver(bytes32)` getter just pay one extra extrinsic per publish.
1496
+ */
1497
+ async ensureContentResolver(domainName) {
1498
+ this.ensureConnected();
1499
+ const node = namehash(`${domainName}.dot`);
1500
+ const target = this._contracts.DOTNS_CONTENT_RESOLVER;
1501
+ let current = null;
1502
+ try {
1503
+ current = await this.contractCall(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "resolver", [node]);
1504
+ } catch {
1505
+ }
1506
+ if (typeof current === "string" && current.toLowerCase() === target.toLowerCase()) {
1507
+ return { changed: false };
1508
+ }
1509
+ console.log(` Redirecting resolver for ${domainName}.dot to content resolver ${target}\u2026`);
1510
+ await this.contractTransaction(this._contracts.DOTNS_REGISTRY, 0n, DOTNS_REGISTRY_ABI, "setResolver", [node, target], (s) => console.log(` ${s}`), { useNoncePolling: true });
1511
+ return { changed: true };
1512
+ }
1513
+ /** Read a text record off `DOTNS_CONTENT_RESOLVER`. Returns `""` when unset. */
1514
+ async getTextRecord(domainName, key) {
1515
+ this.ensureConnected();
1516
+ const node = namehash(`${domainName}.dot`);
1517
+ const result = await this.contractCall(
1518
+ this._contracts.DOTNS_CONTENT_RESOLVER,
1519
+ DOTNS_TEXT_RESOLVER_ABI,
1520
+ "text",
1521
+ [node, key]
1522
+ );
1523
+ return typeof result === "string" ? result : "";
1524
+ }
1525
+ async setTextRecord(domainName, key, value) {
1526
+ return withSpan("deploy.dotns.set-text", `2c. set-text ${key}`, {}, async () => {
1527
+ this.ensureConnected();
1528
+ console.log(` Setting text[${key}]: ${value}`);
1529
+ const node = namehash(`${domainName}.dot`);
1530
+ const textTxRes = await this.contractTransaction(this._contracts.DOTNS_CONTENT_RESOLVER, 0n, DOTNS_TEXT_RESOLVER_ABI, "setText", [node, key, value], (s) => console.log(` ${s}`), { useNoncePolling: true });
1531
+ logTxResolution(textTxRes);
1532
+ const MAX_CHAIN_WAIT_SECONDS = 90;
1533
+ const POLL_INTERVAL_MS = 2e3;
1534
+ const startChainMs = Number(await this.clientWrapper.client.query.Timestamp.Now.getValue());
1535
+ let onChainValue = "";
1536
+ let lastPrintedElapsed = -1;
1537
+ while (true) {
1538
+ const onChain = await withTimeout(this.contractCall(this._contracts.DOTNS_CONTENT_RESOLVER, DOTNS_TEXT_RESOLVER_ABI, "text", [node, key]), 3e4, "text");
1539
+ onChainValue = onChain ?? "";
1540
+ if (onChainValue === value) break;
1541
+ const nowChainMs = Number(await this.clientWrapper.client.query.Timestamp.Now.getValue());
1542
+ const chainElapsed = (nowChainMs - startChainMs) / 1e3;
1543
+ if (chainElapsed >= MAX_CHAIN_WAIT_SECONDS) break;
1544
+ const floored = Math.floor(chainElapsed);
1545
+ if (floored > lastPrintedElapsed) {
1546
+ console.log(` Awaiting text finalization (chain time +${floored}s / ${MAX_CHAIN_WAIT_SECONDS}s)...`);
1547
+ lastPrintedElapsed = floored;
1548
+ }
1549
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1550
+ }
1551
+ if (onChainValue !== value) {
1552
+ throw new Error(
1553
+ `Post-set verification failed for text[${key}] on ${domainName}.dot: on-chain value is ${JSON.stringify(onChainValue)}, not ${JSON.stringify(value)} we just wrote. The setText tx may have silently failed, or another writer overwrote the record.`
1554
+ );
1555
+ }
1556
+ console.log(` Verified text[${key}]: ${onChainValue}
1557
+ `);
1558
+ const txHashStr = textTxRes.kind === TX_KIND_HASH ? textTxRes.hash : TX_KIND_NONCE_ADVANCED;
1559
+ return { value, txHash: txHashStr };
1560
+ });
1561
+ }
1562
+ // Atomicity boundary: setContenthash and publish stay outside this batch
1563
+ // so a cosmetic setText failure cannot roll back the on-chain CID.
1564
+ async setTextRecords(domainName, entries) {
1565
+ if (entries.length === 0) return { txHash: null, batched: false };
1566
+ if (entries.length === 1) {
1567
+ const r = await this.setTextRecord(domainName, entries[0].key, entries[0].value);
1568
+ return { txHash: r.txHash, batched: false };
1569
+ }
1570
+ return withSpan("deploy.dotns.set-text-batch", `2c. set-text batch (${entries.length})`, {}, async () => {
1571
+ this.ensureConnected();
1572
+ const node = namehash(`${domainName}.dot`);
1573
+ const calls = entries.map((e) => {
1574
+ console.log(` Setting text[${e.key}]: ${e.value}`);
1575
+ return {
1576
+ contractAddress: this._contracts.DOTNS_CONTENT_RESOLVER,
1577
+ value: 0n,
1578
+ encodedData: encodeFunctionData({ abi: DOTNS_TEXT_RESOLVER_ABI, functionName: "setText", args: [node, e.key, e.value] }),
1579
+ functionName: "setText",
1580
+ args: [node, e.key, e.value]
1581
+ };
1582
+ });
1583
+ const batchTxRes = await withTimeout(
1584
+ this.clientWrapper.submitBatchedTransactions(calls, this.substrateAddress, this.signer, (s) => console.log(` ${s}`)),
1585
+ OPERATION_TIMEOUT_MS,
1586
+ "Utility.batch_all(setText)"
1587
+ );
1588
+ logTxResolution(batchTxRes);
1589
+ const txHash = batchTxRes.kind === TX_KIND_HASH ? batchTxRes.hash : TX_KIND_NONCE_ADVANCED;
1590
+ const MAX_CHAIN_WAIT_SECONDS = 90;
1591
+ const POLL_INTERVAL_MS = 2e3;
1592
+ const startChainMs = Number(await this.clientWrapper.client.query.Timestamp.Now.getValue());
1593
+ let lastResults = [];
1594
+ while (true) {
1595
+ lastResults = await Promise.all(entries.map((e) => withTimeout(this.contractCall(this._contracts.DOTNS_CONTENT_RESOLVER, DOTNS_TEXT_RESOLVER_ABI, "text", [node, e.key]), 3e4, "text").then((onChain) => ({ key: e.key, expected: e.value, onChain: onChain ?? "" }))));
1596
+ if (lastResults.every((v) => v.onChain === v.expected)) break;
1597
+ const nowChainMs = Number(await this.clientWrapper.client.query.Timestamp.Now.getValue());
1598
+ const chainElapsed = (nowChainMs - startChainMs) / 1e3;
1599
+ if (chainElapsed >= MAX_CHAIN_WAIT_SECONDS) break;
1600
+ console.log(` Awaiting batched text finalization (chain time +${Math.floor(chainElapsed)}s / ${MAX_CHAIN_WAIT_SECONDS}s)...`);
1601
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1602
+ }
1603
+ for (const v of lastResults) {
1604
+ if (v.onChain !== v.expected) {
1605
+ throw new Error(
1606
+ `Post-set verification failed for text[${v.key}] on ${domainName}.dot: on-chain value is ${JSON.stringify(v.onChain)}, not ${JSON.stringify(v.expected)} we just wrote. The batched setText tx may have silently failed, or another writer overwrote the record.`
1607
+ );
1608
+ }
1609
+ console.log(` Verified text[${v.key}]: ${v.onChain}`);
1610
+ }
1611
+ return { txHash, batched: true };
1612
+ });
1613
+ }
1614
+ // Adds `<label>.dot` to the on-chain Publisher registry. Pre-checks
1615
+ // isPublished and returns "already-published" instead of resubmitting,
1616
+ // which is both cheaper and avoids waking the Lite cooldown. A
1617
+ // CooldownActive revert is treated as success-equivalent — the registry
1618
+ // is already in the desired state from a recent prior publish.
1619
+ async publishLabel(label) {
1620
+ return withSpan("deploy.publish", `3. publish ${label}.dot`, { "deploy.publish.label": label }, async () => {
1621
+ this.ensureConnected();
1622
+ const publisher = this._contracts.PUBLISHER;
1623
+ if (!publisher || publisher === zeroAddress) {
1624
+ throw new PublisherNotSupportedError(this.rpc ?? "unknown");
1625
+ }
1626
+ const labelhash = keccak256(toBytes(label));
1627
+ const already = await withTimeout(
1628
+ this.contractCall(publisher, PUBLISHER_ABI, "isPublished", [labelhash]),
1629
+ 3e4,
1630
+ "isPublished"
1631
+ );
1632
+ if (already === true) {
1633
+ console.log(` Already published \u2014 skipping`);
1634
+ return { status: "already-published" };
1635
+ }
1636
+ try {
1637
+ const txRes = await this.contractTransaction(publisher, 0n, PUBLISHER_ABI, "publish", [label], (s) => console.log(` ${s}`), { useNoncePolling: true });
1638
+ logTxResolution(txRes);
1639
+ const txHash = txRes.kind === TX_KIND_HASH ? txRes.hash : TX_KIND_NONCE_ADVANCED;
1640
+ return { status: "published", txHash };
1641
+ } catch (e) {
1642
+ const decoded = decodePublisherRevert(e);
1643
+ if (decoded?.name === "CooldownActive") {
1644
+ const nextAllowed = decoded.args?.[0];
1645
+ console.log(` Cooldown active (next allowed at ${nextAllowed}) \u2014 treating as already published`);
1646
+ return { status: "cooldown-skipped" };
1647
+ }
1648
+ if (decoded?.name) throw new Error(`Publisher.publish reverted: ${decoded.name}${decoded.args ? `(${decoded.args.join(", ")})` : ""}`);
1649
+ throw e;
1650
+ }
1651
+ });
1652
+ }
1653
+ // Removes `<label>.dot` from the on-chain Publisher registry. Pre-checks
1654
+ // isPublished and returns "already-unpublished" instead of resubmitting,
1655
+ // which both saves gas and avoids emitting a spurious Unpublished event
1656
+ // for a label that was never in the set.
1657
+ async unpublishLabel(label) {
1658
+ return withSpan("deploy.unpublish", `unpublish ${label}.dot`, { "deploy.unpublish.label": label }, async () => {
1659
+ this.ensureConnected();
1660
+ const publisher = this._contracts.PUBLISHER;
1661
+ if (!publisher || publisher === zeroAddress) {
1662
+ throw new PublisherNotSupportedError(this.rpc ?? "unknown");
1663
+ }
1664
+ const labelhash = keccak256(toBytes(label));
1665
+ const isPub = await withTimeout(
1666
+ this.contractCall(publisher, PUBLISHER_ABI, "isPublished", [labelhash]),
1667
+ 3e4,
1668
+ "isPublished"
1669
+ );
1670
+ if (isPub !== true) {
1671
+ console.log(` Not currently published \u2014 skipping`);
1672
+ return { status: "already-unpublished" };
1673
+ }
1674
+ try {
1675
+ const txRes = await this.contractTransaction(publisher, 0n, PUBLISHER_ABI, "unpublish", [label], (s) => console.log(` ${s}`), { useNoncePolling: true });
1676
+ logTxResolution(txRes);
1677
+ const txHash = txRes.kind === TX_KIND_HASH ? txRes.hash : TX_KIND_NONCE_ADVANCED;
1678
+ return { status: "unpublished", txHash };
1679
+ } catch (e) {
1680
+ const decoded = decodePublisherRevert(e);
1681
+ if (decoded?.name) throw new Error(`Publisher.unpublish reverted: ${decoded.name}${decoded.args ? `(${decoded.args.join(", ")})` : ""}`);
1682
+ throw e;
1683
+ }
1684
+ });
1685
+ }
1686
+ async getContenthash(domainName) {
1687
+ this.ensureConnected();
1688
+ const node = namehash(`${domainName}.dot`);
1689
+ const result = await withTimeout(
1690
+ this.contractCall(this._contracts.DOTNS_CONTENT_RESOLVER, DOTNS_CONTENT_RESOLVER_ABI, "contenthash", [node]),
1691
+ 3e4,
1692
+ "contenthash"
1693
+ );
1694
+ return typeof result === "string" ? result : result?.toString?.() ?? String(result);
1695
+ }
1696
+ async classifyName(label) {
1697
+ this.ensureConnected();
1698
+ console.log(`
1699
+ Classifying name via PopOracle...`);
1700
+ const result = await withTimeout(this.contractCall(this._contracts.POP_RULES, POP_RULES_ABI, "classifyName", [label]), 3e4, "classifyName");
1701
+ const requiredStatus = typeof result[0] === "bigint" ? Number(result[0]) : result[0];
1702
+ const message = result[1];
1703
+ console.log(` Required status: ${popStatusName(requiredStatus)}`);
1704
+ console.log(` Message: ${message}`);
1705
+ return { requiredStatus, message };
1706
+ }
1707
+ async ensureNotRegistered(label) {
1708
+ this.ensureConnected();
1709
+ console.log(`
1710
+ Checking availability of ${label}.dot...`);
1711
+ const tokenId = computeDomainTokenId(label);
1712
+ try {
1713
+ const owner = await withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 3e4, "Availability check");
1714
+ if (owner !== zeroAddress) throw new Error(`Domain ${label}.dot already owned by ${owner}`);
1715
+ } catch (error) {
1716
+ const errorMessage = error instanceof Error ? error.message : String(error);
1717
+ if (errorMessage.includes("already owned")) throw error;
1718
+ }
1719
+ console.log(` ${label}.dot is available`);
1720
+ }
1721
+ async generateCommitment(label, includeReverse = false) {
1722
+ this.ensureConnected();
1723
+ console.log(`
1724
+ Generating commitment hash...`);
1725
+ label = validateDomainLabel(label);
1726
+ const secret = `0x${crypto.randomBytes(32).toString("hex")}`;
1727
+ const registration = { label, owner: this.evmAddress, secret, reserved: includeReverse };
1728
+ const commitment = await withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "makeCommitment", [registration]), 3e4, "Commitment generation");
1729
+ console.log(` Commitment: ${commitment}`);
1730
+ return { commitment, registration };
1731
+ }
1732
+ async submitCommitment(commitment) {
1733
+ this.ensureConnected();
1734
+ console.log(`
1735
+ Submitting commitment...`);
1736
+ const commitTxRes = await this.contractTransaction(this._contracts.DOTNS_REGISTRAR_CONTROLLER, 0n, DOTNS_REGISTRAR_CONTROLLER_ABI, "commit", [commitment], (s) => console.log(` ${s}`));
1737
+ logTxResolution(commitTxRes);
1738
+ console.log(` Committed at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1739
+ }
1740
+ async waitForCommitmentAge(commitment) {
1741
+ this.ensureConnected();
1742
+ const POLL_TIMEOUT_MS = 9e4;
1743
+ const POLL_INTERVAL_MS = 3e3;
1744
+ console.log(`
1745
+ Reading minimum commitment age...`);
1746
+ const [minimumAge, maximumAge, initialCommitTimestamp] = await Promise.all([
1747
+ withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "minCommitmentAge", []), 3e4, "minCommitmentAge"),
1748
+ withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "maxCommitmentAge", []), 3e4, "maxCommitmentAge"),
1749
+ withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "commitments", [commitment]), 3e4, "commitments")
1750
+ ]);
1751
+ const minimumAgeSeconds = typeof minimumAge === "bigint" ? Number(minimumAge) : minimumAge;
1752
+ const maximumAgeSeconds = typeof maximumAge === "bigint" ? Number(maximumAge) : maximumAge ?? 86400;
1753
+ const commitTimestamp = typeof initialCommitTimestamp === "bigint" ? Number(initialCommitTimestamp) : initialCommitTimestamp;
1754
+ if (commitTimestamp === 0) {
1755
+ throw new Error("Commitment not found on-chain. It may not have been included in a block yet.");
1756
+ }
1757
+ console.log(` Minimum commitment age: ${minimumAgeSeconds}s, maximum: ${maximumAgeSeconds}s`);
1758
+ console.log(` Commitment valid window: ${commitTimestamp + minimumAgeSeconds} \u2013 ${commitTimestamp + maximumAgeSeconds}`);
1759
+ console.log(` Commitment stored on-chain (timestamp: ${commitTimestamp})`);
1760
+ console.log(` Waiting for on-chain block.timestamp > ${commitTimestamp + minimumAgeSeconds} (timeout ${POLL_TIMEOUT_MS / 1e3}s)`);
1761
+ const pollDeadline = Date.now() + POLL_TIMEOUT_MS;
1762
+ while (Date.now() < pollDeadline) {
1763
+ const nowMs = await this.clientWrapper.client.query.Timestamp.Now.getValue();
1764
+ const chainNowSeconds = Math.floor(Number(nowMs) / 1e3);
1765
+ if (isCommitmentMature(chainNowSeconds, commitTimestamp, minimumAgeSeconds)) {
1766
+ console.log(` Commitment age requirement met (chain.now=${chainNowSeconds}, target>${commitTimestamp + minimumAgeSeconds})`);
1767
+ console.log(` Buffering ${POLL_INTERVAL_MS / 1e3}s for block propagation (guard against node lag after maturity)...`);
1768
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1769
+ const nowAfterBuffer = Math.floor(Number(await this.clientWrapper.client.query.Timestamp.Now.getValue()) / 1e3);
1770
+ const expiresAt = commitTimestamp + maximumAgeSeconds;
1771
+ const remainingSecs = expiresAt - nowAfterBuffer;
1772
+ if (remainingSecs <= 0) {
1773
+ throw new Error(`Commitment has expired (chain.now=${nowAfterBuffer}, expired at=${expiresAt}). A fresh commit cycle is needed.`);
1774
+ }
1775
+ if (remainingSecs < 30) {
1776
+ console.log(` Warning: commitment expires in ${remainingSecs}s \u2014 proceeding immediately.`);
1777
+ }
1778
+ return;
1779
+ }
1780
+ const chainSecondsToTarget = Math.max(0, commitTimestamp + minimumAgeSeconds - chainNowSeconds);
1781
+ console.log(` Chain time ${chainNowSeconds} \u2014 need +${chainSecondsToTarget}s more chain progress`);
1782
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1783
+ }
1784
+ throw new Error(`Commitment still too new after ${POLL_TIMEOUT_MS / 1e3}s of polling chain time. The chain may be stalled.`);
1785
+ }
1786
+ async getPriceAndValidate(label) {
1787
+ this.ensureConnected();
1788
+ console.log(`
1789
+ Checking price and eligibility...`);
1790
+ label = validateDomainLabel(label);
1791
+ const baseName = stripTrailingDigits(label);
1792
+ const reservationInfo = await withTimeout(this.contractCall(this._contracts.POP_RULES, POP_RULES_ABI, "isBaseNameReserved", [baseName]), 3e4, "isBaseNameReserved");
1793
+ const [isReserved, reservationOwner] = reservationInfo;
1794
+ if (isReserved && reservationOwner.toLowerCase() !== this.evmAddress.toLowerCase()) throw new Error("Base name reserved for original Lite registrant");
1795
+ const classificationResult = await withTimeout(this.contractCall(this._contracts.POP_RULES, POP_RULES_ABI, "classifyName", [label]), 3e4, "classifyName");
1796
+ const requiredStatus = typeof classificationResult[0] === "bigint" ? Number(classificationResult[0]) : classificationResult[0];
1797
+ const message = classificationResult[1];
1798
+ const userStatus = await this.getUserPopStatus();
1799
+ if (requiredStatus === ProofOfPersonhoodStatus.Reserved) throw new Error(message);
1800
+ if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodFull) {
1801
+ if (userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodFull) throw new Error("Requires Full Personhood verification");
1802
+ } else if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
1803
+ if (userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodLite && userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodFull) throw new Error("Requires Personhood Lite verification");
1804
+ } else {
1805
+ const trailingDigitCount = countTrailingDigits(label);
1806
+ if (trailingDigitCount === 0 || userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
1807
+ throw new Error("Personhood Lite cannot register base names \u2014 this name class requires a Full or NoStatus signer");
1808
+ }
1809
+ }
1810
+ const priceMeta = await withTimeout(this.contractCall(this._contracts.POP_RULES, POP_RULES_ABI, "priceWithCheck", [label, this.evmAddress]), 3e4, "priceWithCheck");
1811
+ const priceRaw = priceMeta?.price;
1812
+ if (priceRaw == null) {
1813
+ throw new Error(
1814
+ `priceWithCheck returned unexpected shape (expected object with .price): ` + JSON.stringify(priceMeta, (_, v) => typeof v === "bigint" ? v.toString() : v)
1815
+ );
1816
+ }
1817
+ const priceWei = typeof priceRaw === "bigint" ? priceRaw : BigInt(priceRaw);
1818
+ console.log(` Required status: ${popStatusName(requiredStatus)}`);
1819
+ console.log(` User status: ${popStatusName(userStatus)}`);
1820
+ console.log(` Price: ${formatEther(priceWei)} PAS`);
1821
+ return { priceWei, requiredStatus, userStatus, message };
1822
+ }
1823
+ async finalizeRegistration(registration, priceWei) {
1824
+ this.ensureConnected();
1825
+ console.log(`
1826
+ Finalizing registration for ${registration.label}.dot...`);
1827
+ const bufferedPaymentWei = priceWei * 110n / 100n;
1828
+ const bufferedPaymentNative = bufferedPaymentWei / this._nativeToEthRatio;
1829
+ if (priceWei > 0n && bufferedPaymentNative === 0n) {
1830
+ throw new Error(
1831
+ `Payment conversion underflow: priceWei=${priceWei} rounds to 0 native units (nativeToEthRatio=${this._nativeToEthRatio}). Cannot call register with zero payment.`
1832
+ );
1833
+ }
1834
+ setDeployAttribute("deploy.payment_wei", priceWei.toString());
1835
+ console.log(` Oracle price: ${formatEther(priceWei)} PAS`);
1836
+ console.log(` Paying: ${formatEther(bufferedPaymentWei)} PAS`);
1837
+ const registerTxRes = await this.contractTransaction(this._contracts.DOTNS_REGISTRAR_CONTROLLER, bufferedPaymentNative, DOTNS_REGISTRAR_CONTROLLER_ABI, "register", [registration], (s) => console.log(` ${s}`));
1838
+ logTxResolution(registerTxRes);
1839
+ if (registerTxRes.kind === TX_KIND_HASH) {
1840
+ setDeployAttribute("deploy.register.tx", registerTxRes.hash);
1841
+ if (registerTxRes.block) {
1842
+ setDeployAttribute("deploy.register.block", registerTxRes.block.number);
1843
+ setDeployAttribute("deploy.register.block_hash", registerTxRes.block.hash);
1844
+ console.log(` finalised @ block ${registerTxRes.block.number} (tx ${registerTxRes.hash})`);
1845
+ } else {
1846
+ console.log(` finalised (tx ${registerTxRes.hash})`);
1847
+ }
1848
+ }
1849
+ }
1850
+ async verifyOwnership(label) {
1851
+ this.ensureConnected();
1852
+ console.log(`
1853
+ Verifying ownership...`);
1854
+ const tokenId = computeDomainTokenId(label);
1855
+ const actualOwner = await withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 3e4, "ownerOf");
1856
+ if (actualOwner.toLowerCase() !== this.evmAddress.toLowerCase()) {
1857
+ console.log(` Expected: ${this.evmAddress}`);
1858
+ console.log(` Actual: ${actualOwner}`);
1859
+ throw new Error(`Owner mismatch for ${label}.dot`);
1860
+ }
1861
+ console.log(` Owner: ${actualOwner}`);
1862
+ }
1863
+ // View-only readiness check. Runs every chain read needed to predict whether
1864
+ // `register(label)` will succeed, so the caller can fail-fast BEFORE the
1865
+ // Bulletin chunk upload. Never writes to chain. See issue #100.
1866
+ async preflight(label) {
1867
+ return this._preflightInternal(label, false);
1868
+ }
1869
+ async _preflightInternal(label, reproveAttempted) {
1870
+ return withSpan("deploy.dotns.preflight", `preflight ${label}.dot`, {}, async () => {
1871
+ setDeployAttribute("deploy.dotns.reprove.auto", "false");
1872
+ this.ensureConnected();
1873
+ const validated = validateDomainLabel(label);
1874
+ const trailingDigits = countTrailingDigits(validated);
1875
+ const baselength = validated.length - trailingDigits;
1876
+ const classification = classifyDotnsLabel(validated);
1877
+ if (classification.status === ProofOfPersonhoodStatus.Reserved) {
1878
+ const sanitizeTrail = label !== validated ? `Input "${label}" was sanitized to "${validated}" (excess trailing digits trimmed). ` : "";
1879
+ return {
1880
+ label: validated,
1881
+ classification,
1882
+ userStatus: 0,
1883
+ trailingDigits,
1884
+ baselength,
1885
+ isAvailable: false,
1886
+ existingOwner: null,
1887
+ isBaseNameReserved: false,
1888
+ reservationOwner: null,
1889
+ isTestnet: false,
1890
+ canProceed: false,
1891
+ reason: `${sanitizeTrail}${classification.message}`,
1892
+ plannedAction: "abort",
1893
+ needsPopUpgrade: false
1894
+ };
1895
+ }
1896
+ const baseName = stripTrailingDigits(validated);
1897
+ const [userStatus, baseReservation, ownership, isTestnet, signerFreeBalance] = await Promise.all([
1898
+ this.getUserPopStatus(),
1899
+ withTimeout(this.contractCall(this._contracts.POP_RULES, POP_RULES_ABI, "isBaseNameReserved", [baseName]), 3e4, "isBaseNameReserved"),
1900
+ this.checkOwnership(validated),
1901
+ this.isTestnet(),
1902
+ this.readFreeBalance(this.substrateAddress)
1903
+ ]);
1904
+ const [isReserved, reservationOwnerRaw] = baseReservation;
1905
+ const reservationOwner = isReserved ? reservationOwnerRaw.toLowerCase() : null;
1906
+ const ownerRaw = ownership.owner?.toLowerCase() ?? null;
1907
+ const existingOwner = ownerRaw && ownerRaw !== zeroAddress ? ownerRaw : null;
1908
+ const selfAddress = this.evmAddress.toLowerCase();
1909
+ if (existingOwner !== null && existingOwner !== selfAddress) {
1910
+ return {
1911
+ label: validated,
1912
+ classification,
1913
+ userStatus,
1914
+ trailingDigits,
1915
+ baselength,
1916
+ isAvailable: false,
1917
+ existingOwner,
1918
+ isBaseNameReserved: isReserved,
1919
+ reservationOwner,
1920
+ isTestnet,
1921
+ canProceed: false,
1922
+ reason: `Domain ${validated}.dot is already owned by ${existingOwner}.`,
1923
+ plannedAction: "abort",
1924
+ needsPopUpgrade: false,
1925
+ signerFreeBalance
1926
+ };
1927
+ }
1928
+ if (existingOwner !== null && existingOwner === selfAddress) {
1929
+ return await this.gateOnFeeBalance({
1930
+ label: validated,
1931
+ classification,
1932
+ userStatus,
1933
+ trailingDigits,
1934
+ baselength,
1935
+ isAvailable: true,
1936
+ existingOwner,
1937
+ isBaseNameReserved: isReserved,
1938
+ reservationOwner,
1939
+ isTestnet,
1940
+ canProceed: true,
1941
+ plannedAction: "already-owned-by-us",
1942
+ needsPopUpgrade: false
1943
+ }, signerFreeBalance, isTestnet);
1944
+ }
1945
+ if (isReserved && reservationOwner !== selfAddress) {
1946
+ return {
1947
+ label: validated,
1948
+ classification,
1949
+ userStatus,
1950
+ trailingDigits,
1951
+ baselength,
1952
+ isAvailable: true,
1953
+ existingOwner: null,
1954
+ isBaseNameReserved: true,
1955
+ reservationOwner,
1956
+ isTestnet,
1957
+ canProceed: false,
1958
+ reason: `Base name ${baseName} is reserved for ${reservationOwner}.`,
1959
+ plannedAction: "abort",
1960
+ needsPopUpgrade: false,
1961
+ signerFreeBalance
1962
+ };
1963
+ }
1964
+ const targetPopStatus = userStatus;
1965
+ if (!canRegister(classification.status, userStatus, trailingDigits)) {
1966
+ if (classification.status === ProofOfPersonhoodStatus.NoStatus && userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
1967
+ return {
1968
+ label: validated,
1969
+ classification,
1970
+ userStatus,
1971
+ trailingDigits,
1972
+ baselength,
1973
+ isAvailable: true,
1974
+ existingOwner: null,
1975
+ isBaseNameReserved: isReserved,
1976
+ reservationOwner,
1977
+ isTestnet,
1978
+ canProceed: false,
1979
+ reason: `${validated}.dot: this name class is NoStatus-compatible, but Personhood Lite signers cannot register NoStatus-class labels. Self-attestation is no longer available. Use a NoStatus or Full signer, or contact the DotNS team for whitelisting / Personhood status help.`,
1980
+ plannedAction: "abort",
1981
+ needsPopUpgrade: false,
1982
+ targetPopStatus,
1983
+ signerFreeBalance
1984
+ };
1985
+ }
1986
+ if (userStatus === ProofOfPersonhoodStatus.NoStatus && isTestnet && this._popSelfServe?.stateAwareGuidance === true && this.substrateAddress) {
1987
+ const aliasState = await this.classifyAliasAccountState(this.substrateAddress);
1988
+ if (aliasState.state === "bound-likely-stale" && !this._usesExternalSigner && this._localMnemonic && !reproveAttempted) {
1989
+ const minBalance = REPROVE_FEE_ESTIMATE * REPROVE_FEE_SAFETY_MARGIN_PCT / 100n;
1990
+ const symbol = resolveNativeTokenSymbol(this._environmentId);
1991
+ if (signerFreeBalance < minBalance) {
1992
+ setDeployAttribute("deploy.dotns.reprove.auto", "true");
1993
+ setDeployAttribute("deploy.dotns.reprove.outcome", "insufficient_funds");
1994
+ return {
1995
+ label: validated,
1996
+ classification,
1997
+ userStatus,
1998
+ trailingDigits,
1999
+ baselength,
2000
+ isAvailable: true,
2001
+ existingOwner: null,
2002
+ isBaseNameReserved: isReserved,
2003
+ reservationOwner,
2004
+ isTestnet,
2005
+ canProceed: false,
2006
+ reason: `Cannot auto-refresh: signer balance ${fmtPas(signerFreeBalance)} ${symbol} < estimated fee ${fmtPas(REPROVE_FEE_ESTIMATE)} ${symbol}. Top up via the testnet faucet or run \`tools/reprove-alias.mjs --mnemonic <your-mnemonic> --env ${this._environmentId}\` manually.`,
2007
+ plannedAction: "abort",
2008
+ needsPopUpgrade: false,
2009
+ targetPopStatus,
2010
+ signerFreeBalance
2011
+ };
2012
+ }
2013
+ console.log(` Personhood: alias revision stale (stored=${aliasState.revision ?? "unknown"}) \u2014 refreshing on testnet`);
2014
+ console.log(` Estimated fee: ${fmtPas(REPROVE_FEE_ESTIMATE)} ${symbol} (signer balance: ${fmtPas(signerFreeBalance)} ${symbol})`);
2015
+ setDeployAttribute("deploy.dotns.reprove.auto", "true");
2016
+ let reproveSucceeded = false;
2017
+ try {
2018
+ console.log(` Submitting reprove_alias_account\u2026`);
2019
+ const reproveResult = await this.reprove(this._localMnemonic);
2020
+ console.log(` Refresh complete (revision ${reproveResult.oldRevision} \u2192 ${reproveResult.newRevision}, block ${reproveResult.blockHash})`);
2021
+ setDeployAttribute("deploy.dotns.reprove.outcome", "success");
2022
+ setDeployAttribute("deploy.dotns.reprove.old_revision", String(reproveResult.oldRevision));
2023
+ setDeployAttribute("deploy.dotns.reprove.new_revision", String(reproveResult.newRevision));
2024
+ reproveSucceeded = true;
2025
+ } catch (e) {
2026
+ const msg = e?.message ?? String(e);
2027
+ console.log(` Auto-reprove failed: ${msg}`);
2028
+ setDeployAttribute("deploy.dotns.reprove.outcome", "failed_submission");
2029
+ }
2030
+ if (reproveSucceeded) {
2031
+ console.log(` Continuing with registration of ${validated}.dot.`);
2032
+ return this._preflightInternal(label, true);
2033
+ }
2034
+ }
2035
+ const remediationMessage = formatPersonhoodRemediation(aliasState, this._popSelfServe, this._environmentId);
2036
+ const currentName2 = popStatusName(userStatus);
2037
+ const requiredName2 = popStatusName(classification.status);
2038
+ return {
2039
+ label: validated,
2040
+ classification,
2041
+ userStatus,
2042
+ trailingDigits,
2043
+ baselength,
2044
+ isAvailable: true,
2045
+ existingOwner: null,
2046
+ isBaseNameReserved: isReserved,
2047
+ reservationOwner,
2048
+ isTestnet,
2049
+ canProceed: false,
2050
+ reason: `${validated}.dot requires ${requiredName2}, but this signer is ${currentName2}. ${remediationMessage}`,
2051
+ plannedAction: "abort",
2052
+ needsPopUpgrade: false,
2053
+ targetPopStatus,
2054
+ signerFreeBalance
2055
+ };
2056
+ }
2057
+ const currentName = popStatusName(userStatus);
2058
+ const requiredName = popStatusName(classification.status);
2059
+ return {
2060
+ label: validated,
2061
+ classification,
2062
+ userStatus,
2063
+ trailingDigits,
2064
+ baselength,
2065
+ isAvailable: true,
2066
+ existingOwner: null,
2067
+ isBaseNameReserved: isReserved,
2068
+ reservationOwner,
2069
+ isTestnet,
2070
+ canProceed: false,
2071
+ reason: formatPopShortfallReason({
2072
+ label: validated,
2073
+ requiredName,
2074
+ currentName,
2075
+ isTestnet,
2076
+ environmentId: this._environmentId,
2077
+ popSelfServe: this._popSelfServe,
2078
+ aliasState: null,
2079
+ exampleNoStatusLabel: exampleNoStatusLabel(validated)
2080
+ }),
2081
+ plannedAction: "abort",
2082
+ needsPopUpgrade: false,
2083
+ targetPopStatus,
2084
+ signerFreeBalance
2085
+ };
2086
+ }
2087
+ return await this.gateOnFeeBalance({
2088
+ label: validated,
2089
+ classification,
2090
+ userStatus,
2091
+ trailingDigits,
2092
+ baselength,
2093
+ isAvailable: true,
2094
+ existingOwner: null,
2095
+ isBaseNameReserved: isReserved,
2096
+ reservationOwner,
2097
+ isTestnet,
2098
+ canProceed: true,
2099
+ plannedAction: "register",
2100
+ needsPopUpgrade: false,
2101
+ targetPopStatus
2102
+ }, signerFreeBalance, isTestnet);
2103
+ });
2104
+ }
2105
+ // Final preflight stage: check the DotNS signer can pay tx fees on the
2106
+ // connected fee chain (Asset Hub Paseo for testnet, Asset Hub Polkadot for
2107
+ // mainnet). On testnet, attempts a one-shot auto-top-up from the dev
2108
+ // phrase's Alice or Bob if the signer is short. Replaces the original
2109
+ // canProceed:true result with an actionable canProceed:false when even the
2110
+ // top-up can't get the signer above the threshold.
2111
+ async gateOnFeeBalance(candidate, signerFreeBalance, isTestnet) {
2112
+ const feeFloor = feeFloorFor(candidate.plannedAction, this._registerStorageDeposit);
2113
+ let effectiveBalance = signerFreeBalance;
2114
+ let toppedUp;
2115
+ if (effectiveBalance < feeFloor && isTestnet) {
2116
+ setDeployAttribute("deploy.dotns.signer_below_floor", "true");
2117
+ console.log(` DotNS signer ${this.substrateAddress?.slice(0, 8)}... balance ${fmtPas(effectiveBalance)} PAS < ${fmtPas(feeFloor)} PAS floor \u2014 attempting testnet auto top-up...`);
2118
+ const result = await this.attemptTestnetTopUp(this.substrateAddress, topUpTargetFor(candidate.plannedAction, this._registerStorageDeposit));
2119
+ if (result) {
2120
+ console.log(` Topped up ${fmtPas(result.transferred)} PAS from ${result.source}`);
2121
+ effectiveBalance += result.transferred;
2122
+ toppedUp = result;
2123
+ setDeployAttribute("deploy.dotns.toppedup", "true");
2124
+ setDeployAttribute("deploy.dotns.toppedup_source", result.source);
2125
+ }
2126
+ } else if (effectiveBalance < feeFloor) {
2127
+ setDeployAttribute("deploy.dotns.signer_below_floor", "true");
2128
+ }
2129
+ if (effectiveBalance < feeFloor) {
2130
+ const op = candidate.plannedAction === "already-owned-by-us" ? "setContenthash" : "register";
2131
+ const tail = isTestnet ? ` Testnet auto-top-up via Alice/Bob failed (both also low). Top up at ${PASEO_FAUCET_URL}.` : ` Top up the signer's balance and re-deploy.`;
2132
+ captureWarning("DotNS preflight balance gate: signer cannot pay fees", {
2133
+ signer: this.substrateAddress,
2134
+ free: effectiveBalance.toString(),
2135
+ floor: feeFloor.toString(),
2136
+ plannedAction: candidate.plannedAction,
2137
+ isTestnet: String(isTestnet),
2138
+ autoTopUpAttempted: String(isTestnet)
2139
+ });
2140
+ return {
2141
+ ...candidate,
2142
+ canProceed: false,
2143
+ plannedAction: "abort",
2144
+ reason: `DotNS signer ${this.substrateAddress} has ${fmtPas(effectiveBalance)} PAS free; needs \u2265${fmtPas(feeFloor)} PAS for ${op}.${tail}`,
2145
+ signerFreeBalance: effectiveBalance,
2146
+ feeFloor
2147
+ };
2148
+ }
2149
+ return { ...candidate, signerFreeBalance: effectiveBalance, feeFloor, toppedUp };
2150
+ }
2151
+ async register(label, options = {}) {
2152
+ return withSpan("deploy.dotns.register", `2a. register ${label}.dot`, {}, async () => {
2153
+ if (!this.connected) await this.connect(options);
2154
+ label = validateDomainLabel(label);
2155
+ const trailingDigitCount = countTrailingDigits(label);
2156
+ const preClassification = classifyDotnsLabel(label);
2157
+ const preRequiredStatus = preClassification.status;
2158
+ if (preRequiredStatus === ProofOfPersonhoodStatus.Reserved) {
2159
+ throw new Error(preClassification.message);
2160
+ }
2161
+ const isTestnet = await this.isTestnet();
2162
+ const registerAliasState = isTestnet && this._popSelfServe?.stateAwareGuidance === true && this.substrateAddress ? await this.classifyAliasAccountState(this.substrateAddress) : null;
2163
+ const rejectIneligible = (statusRequired, userStatus2) => {
2164
+ if (statusRequired === ProofOfPersonhoodStatus.NoStatus && userStatus2 === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
2165
+ throw new Error(
2166
+ `${label}.dot: this name class is NoStatus-compatible, but Personhood Lite signers cannot register NoStatus-class labels. Self-attestation is no longer available. Use a NoStatus or Full signer, or contact the DotNS team for whitelisting / Personhood status help.`
2167
+ );
2168
+ }
2169
+ throw new Error(
2170
+ formatPopShortfallReason({
2171
+ label,
2172
+ requiredName: popStatusName(statusRequired),
2173
+ currentName: popStatusName(userStatus2),
2174
+ isTestnet,
2175
+ environmentId: this._environmentId,
2176
+ popSelfServe: this._popSelfServe,
2177
+ aliasState: registerAliasState,
2178
+ exampleNoStatusLabel: exampleNoStatusLabel(label)
2179
+ })
2180
+ );
2181
+ };
2182
+ const reverse = options.reverse ?? (process.env.DOTNS_REVERSE ?? "false").toLowerCase() === "true";
2183
+ const [classification] = await Promise.all([
2184
+ this.classifyName(label),
2185
+ this.ensureNotRegistered(label)
2186
+ ]);
2187
+ const requiredStatus = classification.requiredStatus;
2188
+ if (requiredStatus === ProofOfPersonhoodStatus.Reserved) {
2189
+ throw new Error(classification.message);
2190
+ }
2191
+ const userStatus = await this.getUserPopStatus();
2192
+ if (!canRegister(requiredStatus, userStatus, trailingDigitCount)) {
2193
+ rejectIneligible(requiredStatus, userStatus);
2194
+ }
2195
+ const doCommitAndRegister = async () => {
2196
+ const { commitment, registration } = await this.generateCommitment(label, reverse);
2197
+ await withSpan("deploy.dotns.submit-commitment", "2a-i. submit-commitment", {}, () => this.submitCommitment(commitment));
2198
+ await withSpan("deploy.dotns.wait-commitment-age", "2a-ii. wait-commitment-age", {}, () => this.waitForCommitmentAge(commitment));
2199
+ const pricing = await withSpan("deploy.dotns.price-validation", "2a-iii. price-validation", {}, () => this.getPriceAndValidate(label));
2200
+ await withSpan("deploy.dotns.finalize-registration", "2a-iv. finalize-registration", {}, () => this.finalizeRegistration(registration, pricing.priceWei));
2201
+ };
2202
+ try {
2203
+ await doCommitAndRegister();
2204
+ } catch (err) {
2205
+ const msg = err.message ?? "";
2206
+ if (!isCommitmentTimingBarerevert(msg)) throw err;
2207
+ console.log(`
2208
+ Register bare-reverted (commitment timing race \u2014 node saw a block where commitment was too new or expired).`);
2209
+ console.log(` Retrying with a fresh commitment. This usually resolves in one block.
2210
+ `);
2211
+ await doCommitAndRegister();
2212
+ }
2213
+ await this.verifyOwnership(label);
2214
+ console.log(`
2215
+ Registration complete!`);
2216
+ return { label, owner: this.evmAddress };
2217
+ });
2218
+ }
2219
+ /**
2220
+ * Reprove a stale DotNS alias binding.
2221
+ * Opens a People-chain client internally, builds the ring proof, and submits
2222
+ * reprove_alias_account on AH. Use when the alias exists but the ring root
2223
+ * has advanced past the stored revision.
2224
+ *
2225
+ * Requires a mnemonic — the DotNS instance must have been connected with one.
2226
+ */
2227
+ async reprove(mnemonic) {
2228
+ this.ensureConnected();
2229
+ if (!this.substrateAddress || !this.signer) {
2230
+ throw new Error("reprove: DotNS must be connected with a signer");
2231
+ }
2232
+ const envId = this._environmentId ?? "paseo-next-v2";
2233
+ const { connectPeopleClient } = await import("./personhood/people-client.js");
2234
+ const { reproveAliasToAccount } = await import("./personhood/reprove.js");
2235
+ const { deriveMemberEntropy, deriveMemberKey } = await import("./personhood/member-key.js");
2236
+ const verifiable = await import("verifiablejs/nodejs");
2237
+ const memberEntropy = deriveMemberEntropy(mnemonic);
2238
+ const memberKey = deriveMemberKey(mnemonic);
2239
+ const peopleConn = await connectPeopleClient(envId);
2240
+ try {
2241
+ const result = await reproveAliasToAccount({
2242
+ peopleUnsafeApi: peopleConn.unsafeApi,
2243
+ ahUnsafeApi: this.clientWrapper.client,
2244
+ account: this.substrateAddress,
2245
+ memberKey,
2246
+ signCall: this.signer,
2247
+ buildRingProof: async ({ members, context, msg }) => {
2248
+ const r = verifiable.one_shot(memberEntropy, members, context, msg);
2249
+ return { proof: r.proof, alias: r.alias };
2250
+ }
2251
+ });
2252
+ return result;
2253
+ } catch (e) {
2254
+ if (this._reproveFallbackForTest && typeof e?.message === "string" && e.message.includes("not strictly greater than stored")) {
2255
+ const fb = this._reproveFallbackForTest;
2256
+ this._reproveFallbackForTest = null;
2257
+ return fb;
2258
+ }
2259
+ throw e;
2260
+ } finally {
2261
+ peopleConn.disconnect();
2262
+ }
2263
+ }
2264
+ /**
2265
+ * Run the personhood bootstrap flow for this DotNS signer.
2266
+ * Idempotent: each step is gated on chain state being "still needs doing".
2267
+ * Does NOT auto-run from preflight — call explicitly.
2268
+ *
2269
+ * Throws RecognizeRequiredError if the account hasn't been recognized by the
2270
+ * personhood faucet (https://sudo.personhood.dev/personhood-faucet).
2271
+ */
2272
+ async bootstrap(mnemonic) {
2273
+ const { runBootstrap } = await import("./personhood/bootstrap.js");
2274
+ const envId = this._environmentId ?? "paseo-next-v2";
2275
+ return runBootstrap({ mnemonic, environmentId: envId });
2276
+ }
2277
+ disconnect() {
2278
+ if (this.client) {
2279
+ this.client.destroy();
2280
+ this.client = null;
2281
+ this.clientWrapper = null;
2282
+ this.connected = false;
2283
+ }
2284
+ this._usesExternalSigner = false;
2285
+ }
2286
+ };
2287
+ var dotns = new DotNS();
2288
+
2289
+ export {
2290
+ TX_KIND_HASH,
2291
+ TX_KIND_NONCE_ADVANCED,
2292
+ ATTR_TX_RESOLUTION_KIND,
2293
+ MINIMUM_REGISTER_STORAGE_DEPOSIT,
2294
+ fmtPas,
2295
+ feeFloorFor,
2296
+ RPC_ENDPOINTS,
2297
+ CONTRACTS,
2298
+ DECIMALS,
2299
+ NATIVE_TO_ETH_RATIO,
2300
+ CONNECTION_TIMEOUT_MS,
2301
+ OPERATION_TIMEOUT_MS,
2302
+ TX_TIMEOUT_MS,
2303
+ TX_CHAIN_TIME_BUDGET_MS,
2304
+ TX_WALL_CLOCK_CEILING_MS,
2305
+ WS_HEARTBEAT_TIMEOUT_MS,
2306
+ DOTNS_TX_MAX_ATTEMPTS,
2307
+ classifyTxRetryDecision,
2308
+ DEFAULT_MNEMONIC,
2309
+ fetchNonce,
2310
+ verifyNonceAdvanced,
2311
+ ProofOfPersonhoodStatus,
2312
+ PUBLISHER_ABI,
2313
+ PublisherNotSupportedError,
2314
+ ContractDryRunRevertError,
2315
+ decodePublisherRevert,
2316
+ convertToHexString,
2317
+ __formatContractDryRunFailureForTest,
2318
+ DOT_NODE,
2319
+ convertWeiToNative,
2320
+ computeDomainTokenId,
2321
+ countTrailingDigits,
2322
+ stripTrailingDigits,
2323
+ sanitizeDomainLabel,
2324
+ validateDomainLabel,
2325
+ isCommitmentMature,
2326
+ isCommitmentTimingBarerevert,
2327
+ classifyDotnsLabel,
2328
+ canRegister,
2329
+ parseDomainName,
2330
+ parseProofOfPersonhoodStatus,
2331
+ popStatusName,
2332
+ formatPersonhoodRemediation,
2333
+ formatPopShortfallReason,
2334
+ DotNS,
2335
+ dotns
2336
+ };