bulletin-deploy 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/benchmark.js +163 -0
- package/bin/bulletin-deploy +62 -0
- package/package.json +41 -0
- package/src/deploy.js +373 -0
- package/src/dotns.js +541 -0
- package/src/index.js +3 -0
- package/src/pool.js +165 -0
- package/src/telemetry.js +73 -0
- package/test/pool.test.js +67 -0
- package/test/test.js +114 -0
- package/workflows/deploy-on-pr.yml +82 -0
package/src/dotns.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { createClient } from "polkadot-api";
|
|
3
|
+
import { getPolkadotSigner } from "polkadot-api/signer";
|
|
4
|
+
import { getWsProvider } from "polkadot-api/ws-provider";
|
|
5
|
+
import { Keyring } from "@polkadot/keyring";
|
|
6
|
+
import { cryptoWaitReady } from "@polkadot/util-crypto";
|
|
7
|
+
import { Binary } from "polkadot-api";
|
|
8
|
+
import {
|
|
9
|
+
encodeFunctionData,
|
|
10
|
+
decodeFunctionResult,
|
|
11
|
+
keccak256,
|
|
12
|
+
toBytes,
|
|
13
|
+
formatEther,
|
|
14
|
+
isAddress,
|
|
15
|
+
bytesToHex,
|
|
16
|
+
isHex,
|
|
17
|
+
toHex,
|
|
18
|
+
zeroAddress,
|
|
19
|
+
namehash,
|
|
20
|
+
concatHex,
|
|
21
|
+
} from "viem";
|
|
22
|
+
import { CID } from "multiformats/cid";
|
|
23
|
+
import { withSpan } from "./telemetry.js";
|
|
24
|
+
|
|
25
|
+
export const RPC_ENDPOINTS = [
|
|
26
|
+
"wss://asset-hub-paseo.dotters.network",
|
|
27
|
+
"wss://sys.ibp.network/asset-hub-paseo",
|
|
28
|
+
"wss://pas-rpc.stakeworld.io/assethub",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const CONTRACTS = {
|
|
32
|
+
DOTNS_REGISTRAR: "0x329aAA5b6bEa94E750b2dacBa74Bf41291E6c2BD",
|
|
33
|
+
DOTNS_REGISTRAR_CONTROLLER: "0xd09e0F1c1E6CE8Cf40df929ef4FC778629573651",
|
|
34
|
+
DOTNS_REGISTRY: "0x4Da0d37aBe96C06ab19963F31ca2DC0412057a6f",
|
|
35
|
+
DOTNS_RESOLVER: "0x95645C7fD0fF38790647FE13F87Eb11c1DCc8514",
|
|
36
|
+
DOTNS_CONTENT_RESOLVER: "0x7756DF72CBc7f062e7403cD59e45fBc78bed1cD7",
|
|
37
|
+
DOTNS_REVERSE_RESOLVER: "0x95D57363B491CF743970c640fe419541386ac8BF",
|
|
38
|
+
STORE_FACTORY: "0x030296782F4d3046B080BcB017f01837561D9702",
|
|
39
|
+
POP_RULES: "0x4e8920B1E69d0cEA9b23CBFC87A17Ee6fE02d2d3",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const DECIMALS = 12n;
|
|
43
|
+
export const NATIVE_TO_ETH_RATIO = 1_000_000n;
|
|
44
|
+
export const OPERATION_TIMEOUT_MS = 300_000;
|
|
45
|
+
export const TX_TIMEOUT_MS = 90_000;
|
|
46
|
+
export const DEFAULT_MNEMONIC = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
|
47
|
+
|
|
48
|
+
let _rpcIdCounter = 0;
|
|
49
|
+
export async function fetchNonce(rpc, ss58Address) {
|
|
50
|
+
const WS = globalThis.WebSocket ?? (await import("ws")).default;
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const timeout = setTimeout(() => { try { ws.close(); } catch {} reject(new Error(`fetchNonce timed out after 10s for ${rpc}`)); }, 10_000);
|
|
53
|
+
const ws = new WS(rpc);
|
|
54
|
+
const id = ++_rpcIdCounter;
|
|
55
|
+
ws.onopen = () => ws.send(JSON.stringify({ jsonrpc: "2.0", id, method: "system_accountNextIndex", params: [ss58Address] }));
|
|
56
|
+
ws.onmessage = (e) => {
|
|
57
|
+
const d = typeof e.data === "string" ? e.data : e.data.toString();
|
|
58
|
+
const r = JSON.parse(d);
|
|
59
|
+
if (r.id === id) { clearTimeout(timeout); ws.close(); r.error ? reject(new Error(r.error.message)) : resolve(r.result); }
|
|
60
|
+
};
|
|
61
|
+
ws.onerror = () => { clearTimeout(timeout); ws.close(); reject(new Error(`WebSocket to ${rpc} failed`)); };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ProofOfPersonhoodStatus = {
|
|
66
|
+
NoStatus: 0,
|
|
67
|
+
ProofOfPersonhoodLite: 1,
|
|
68
|
+
ProofOfPersonhoodFull: 2,
|
|
69
|
+
Reserved: 3,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const DOTNS_REGISTRAR_CONTROLLER_ABI = [
|
|
73
|
+
{ 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" },
|
|
74
|
+
{ inputs: [{ name: "commitment", type: "bytes32" }], name: "commit", outputs: [], stateMutability: "nonpayable", type: "function" },
|
|
75
|
+
{ inputs: [], name: "minCommitmentAge", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
|
|
76
|
+
{ 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" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const DOTNS_REGISTRAR_ABI = [
|
|
80
|
+
{ inputs: [{ name: "tokenId", type: "uint256" }], name: "ownerOf", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const POP_RULES_ABI = [
|
|
84
|
+
{ inputs: [{ name: "name", type: "string" }], name: "classifyName", outputs: [{ name: "requirement", type: "uint8" }, { name: "message", type: "string" }], stateMutability: "pure", type: "function" },
|
|
85
|
+
{ inputs: [{ name: "name", type: "string" }], name: "price", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
|
|
86
|
+
{ inputs: [{ name: "", type: "address" }], name: "userPopStatus", outputs: [{ name: "", type: "uint8" }], stateMutability: "view", type: "function" },
|
|
87
|
+
{ inputs: [{ name: "status", type: "uint8" }], name: "setUserPopStatus", outputs: [], stateMutability: "nonpayable", type: "function" },
|
|
88
|
+
{ inputs: [{ name: "baseName", type: "string" }], name: "isBaseNameReserved", outputs: [{ name: "isReserved", type: "bool" }, { name: "reservationOwner", type: "address" }, { name: "expiryTimestamp", type: "uint64" }], stateMutability: "view", type: "function" },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const DOTNS_CONTENT_RESOLVER_ABI = [
|
|
92
|
+
{ inputs: [{ name: "node", type: "bytes32" }, { name: "hash", type: "bytes" }], name: "setContenthash", outputs: [], stateMutability: "nonpayable", type: "function" },
|
|
93
|
+
{ inputs: [{ name: "node", type: "bytes32" }], name: "contenthash", outputs: [{ name: "", type: "bytes" }], stateMutability: "view", type: "function" },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function convertToHexString(value) {
|
|
97
|
+
if (!value) return "0x";
|
|
98
|
+
if (typeof value?.asHex === "function") return value.asHex();
|
|
99
|
+
if (typeof value?.toHex === "function") return value.toHex();
|
|
100
|
+
if (typeof value === "string" && isHex(value)) return value;
|
|
101
|
+
if (value instanceof Uint8Array) return bytesToHex(value);
|
|
102
|
+
try { return toHex(value); } catch { return "0x"; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function convertToBigInt(value, fallback = 0n) {
|
|
106
|
+
try {
|
|
107
|
+
if (typeof value === "bigint") return value;
|
|
108
|
+
if (typeof value === "number") return BigInt(value);
|
|
109
|
+
if (typeof value === "string") return BigInt(value);
|
|
110
|
+
if (value && typeof value.toString === "function") return BigInt(value.toString());
|
|
111
|
+
return fallback;
|
|
112
|
+
} catch { return fallback; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeWeight(weight) {
|
|
116
|
+
const referenceTime = weight?.ref_time ?? weight?.refTime ?? 0;
|
|
117
|
+
const proofSize = weight?.proof_size ?? weight?.proofSize ?? 0;
|
|
118
|
+
return { referenceTime: convertToBigInt(referenceTime, 0n), proofSize: convertToBigInt(proofSize, 0n) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractStorageDepositCharge(rawStorageDeposit) {
|
|
122
|
+
if (!rawStorageDeposit) return 0n;
|
|
123
|
+
if (typeof rawStorageDeposit?.isCharge === "boolean") {
|
|
124
|
+
if (rawStorageDeposit.isCharge && rawStorageDeposit.asCharge != null) return convertToBigInt(rawStorageDeposit.asCharge, 0n);
|
|
125
|
+
return 0n;
|
|
126
|
+
}
|
|
127
|
+
if (rawStorageDeposit.charge != null) return convertToBigInt(rawStorageDeposit.charge, 0n);
|
|
128
|
+
if (rawStorageDeposit.Charge != null) return convertToBigInt(rawStorageDeposit.Charge, 0n);
|
|
129
|
+
if (rawStorageDeposit.value != null) return convertToBigInt(rawStorageDeposit.value, 0n);
|
|
130
|
+
return 0n;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function unwrapExecutionResult(rawResult) {
|
|
134
|
+
if (!rawResult) return { ok: null, err: null, successFlag: null };
|
|
135
|
+
if (typeof rawResult.success === "boolean") {
|
|
136
|
+
return rawResult.success ? { ok: rawResult.value ?? null, err: null, successFlag: true } : { ok: null, err: rawResult.error ?? rawResult.value ?? null, successFlag: false };
|
|
137
|
+
}
|
|
138
|
+
if (typeof rawResult.isOk === "boolean") {
|
|
139
|
+
return rawResult.isOk ? { ok: rawResult.value ?? null, err: null, successFlag: true } : { ok: null, err: rawResult.value ?? null, successFlag: false };
|
|
140
|
+
}
|
|
141
|
+
if (rawResult.ok != null) return { ok: rawResult.ok, err: null, successFlag: true };
|
|
142
|
+
if (rawResult.err != null) return { ok: null, err: rawResult.err, successFlag: false };
|
|
143
|
+
return { ok: null, err: rawResult, successFlag: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function withTimeout(promise, timeoutMs, operationName) {
|
|
147
|
+
let timer;
|
|
148
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
149
|
+
timer = setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
150
|
+
});
|
|
151
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const DOT_NODE = "0x3fce7d1364a893e213bc4212792b517ffc88f5b13b86c8ef9c8d390c3a1370ce";
|
|
155
|
+
|
|
156
|
+
export function convertWeiToNative(weiValue) { return weiValue / NATIVE_TO_ETH_RATIO; }
|
|
157
|
+
export function computeDomainTokenId(label) {
|
|
158
|
+
const labelhash = keccak256(toBytes(label));
|
|
159
|
+
const node = keccak256(concatHex([DOT_NODE, labelhash]));
|
|
160
|
+
return BigInt(node);
|
|
161
|
+
}
|
|
162
|
+
export function countTrailingDigits(label) { let count = 0; for (let i = label.length - 1; i >= 0; i--) { const code = label.charCodeAt(i); if (code >= 48 && code <= 57) count++; else break; } return count; }
|
|
163
|
+
export function stripTrailingDigits(label) { return label.replace(/\d+$/, ""); }
|
|
164
|
+
|
|
165
|
+
export function validateDomainLabel(label) {
|
|
166
|
+
if (!/^[a-z0-9-]{3,}$/.test(label)) throw new Error("Invalid domain label: must contain only lowercase letters, digits, and hyphens, min 3 chars");
|
|
167
|
+
if (label.startsWith("-") || label.endsWith("-")) throw new Error("Invalid domain label: cannot start or end with hyphen");
|
|
168
|
+
const trailingDigitCount = countTrailingDigits(label);
|
|
169
|
+
if (trailingDigitCount > 2) throw new Error(`Invalid domain label: max 2 trailing digits allowed, found ${trailingDigitCount}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function parseProofOfPersonhoodStatus(status) {
|
|
173
|
+
const s = (status ?? "none").toLowerCase();
|
|
174
|
+
if (s === "none" || s === "nostatus") return ProofOfPersonhoodStatus.NoStatus;
|
|
175
|
+
if (s === "lite" || s === "poplite") return ProofOfPersonhoodStatus.ProofOfPersonhoodLite;
|
|
176
|
+
if (s === "full" || s === "popfull") return ProofOfPersonhoodStatus.ProofOfPersonhoodFull;
|
|
177
|
+
throw new Error("Invalid status. Use none, lite, or full");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
class ReviveClientWrapper {
|
|
181
|
+
static DRY_RUN_STORAGE_LIMIT = 18446744073709551615n;
|
|
182
|
+
static DRY_RUN_WEIGHT_LIMIT = { ref_time: 18446744073709551615n, proof_size: 18446744073709551615n };
|
|
183
|
+
|
|
184
|
+
constructor(client) { this.client = client; this.mappedAccounts = new Set(); }
|
|
185
|
+
|
|
186
|
+
async getEvmAddress(substrateAddress) {
|
|
187
|
+
if (isAddress(substrateAddress)) return substrateAddress;
|
|
188
|
+
const address = await this.client.apis.ReviveApi.address(substrateAddress);
|
|
189
|
+
return address.asHex();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async performDryRunCall(originSubstrateAddress, contractAddress, value, encodedData) {
|
|
193
|
+
if (isAddress(originSubstrateAddress)) throw new Error("performDryRunCall requires SS58 Substrate address, not EVM H160 address");
|
|
194
|
+
const executionResults = await this.client.apis.ReviveApi.call(originSubstrateAddress, Binary.fromHex(contractAddress), value, ReviveClientWrapper.DRY_RUN_WEIGHT_LIMIT, ReviveClientWrapper.DRY_RUN_STORAGE_LIMIT, Binary.fromHex(encodedData));
|
|
195
|
+
const { ok, err, successFlag } = unwrapExecutionResult(executionResults.result);
|
|
196
|
+
const flags = ok?.flags ? convertToBigInt(ok.flags, 0n) : 0n;
|
|
197
|
+
const returnData = convertToHexString(ok?.data);
|
|
198
|
+
const didRevert = ok ? (flags & 1n) === 1n : true;
|
|
199
|
+
const gasConsumed = normalizeWeight(executionResults.weight_consumed);
|
|
200
|
+
const gasRequired = normalizeWeight(executionResults.weight_required ?? executionResults.weight_consumed);
|
|
201
|
+
const storageDepositValue = extractStorageDepositCharge(executionResults.storage_deposit);
|
|
202
|
+
const isOk = !!ok && !didRevert;
|
|
203
|
+
const isErr = !ok || didRevert || !!err || (typeof successFlag === "boolean" ? !successFlag : false);
|
|
204
|
+
return { gasConsumed, gasRequired, storageDeposit: { value: storageDepositValue }, result: { isOk, isErr, value: { data: ok ? returnData : "0x", flags: ok ? flags : 1n } } };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async estimateGasForCall(originSubstrateAddress, contractAddress, value, encodedData) {
|
|
208
|
+
const result = await this.performDryRunCall(originSubstrateAddress, contractAddress, value, encodedData);
|
|
209
|
+
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 };
|
|
210
|
+
return { success: true, gasConsumed: result.gasConsumed, storageDeposit: result.storageDeposit.value, gasRequired: result.gasRequired };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async checkIfAccountMapped(substrateAddress) {
|
|
214
|
+
try {
|
|
215
|
+
const evmAddress = await this.getEvmAddress(substrateAddress);
|
|
216
|
+
const key = Binary.fromHex(evmAddress);
|
|
217
|
+
const mappedAccount = await this.client.query.Revive.OriginalAccount.getValue(key);
|
|
218
|
+
return mappedAccount !== null && mappedAccount !== undefined;
|
|
219
|
+
} catch { return false; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async ensureAccountMapped(substrateAddress, signer) {
|
|
223
|
+
if (isAddress(substrateAddress)) throw new Error("ensureAccountMapped requires SS58 Substrate address, not EVM H160 address");
|
|
224
|
+
if (this.mappedAccounts.has(substrateAddress)) return;
|
|
225
|
+
const isMapped = await this.checkIfAccountMapped(substrateAddress);
|
|
226
|
+
if (isMapped) { this.mappedAccounts.add(substrateAddress); return; }
|
|
227
|
+
const mappingExtrinsic = this.client.tx.Revive.map_account();
|
|
228
|
+
try {
|
|
229
|
+
await this.signAndSubmitExtrinsic(mappingExtrinsic, signer, () => {});
|
|
230
|
+
this.mappedAccounts.add(substrateAddress);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const errorMessage = error?.message || String(error);
|
|
233
|
+
if (errorMessage.includes("AccountAlreadyMapped")) { this.mappedAccounts.add(substrateAddress); return; }
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
signAndSubmitExtrinsic(extrinsic, signer, statusCallback, { rpc, senderSS58, expectedNonce } = {}) {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
let settled = false;
|
|
241
|
+
const finish = (fn) => (...args) => { if (!settled) { settled = true; clearTimeout(timer); try { sub.unsubscribe(); } catch {} fn(...args); } };
|
|
242
|
+
const timer = setTimeout(async () => {
|
|
243
|
+
if (settled) return;
|
|
244
|
+
if (rpc && senderSS58 && expectedNonce != null) {
|
|
245
|
+
try {
|
|
246
|
+
const currentNonce = await fetchNonce(rpc, senderSS58);
|
|
247
|
+
if (currentNonce > expectedNonce) {
|
|
248
|
+
console.log(` signAndSubmitExtrinsic: subscription timed out but nonce advanced (${expectedNonce} -> ${currentNonce}), tx was included`);
|
|
249
|
+
statusCallback("included");
|
|
250
|
+
finish(resolve)("timeout-nonce-fallback");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
} catch (e) { console.log(` signAndSubmitExtrinsic: nonce check failed: ${e.message}`); }
|
|
254
|
+
}
|
|
255
|
+
if (!settled) { statusCallback("failed"); finish(reject)(new Error("Transaction timed out after 90s waiting for block inclusion")); }
|
|
256
|
+
}, TX_TIMEOUT_MS);
|
|
257
|
+
try {
|
|
258
|
+
var sub = extrinsic.signSubmitAndWatch(signer, { mortality: { mortal: true, period: 256 } }).subscribe({
|
|
259
|
+
next: (event) => {
|
|
260
|
+
const transactionHash = event.txHash?.toString();
|
|
261
|
+
switch (event.type) {
|
|
262
|
+
case "signed": statusCallback("signing"); break;
|
|
263
|
+
case "broadcasted": statusCallback("broadcasting"); break;
|
|
264
|
+
case "txBestBlocksState": if (event.found) statusCallback("included"); break;
|
|
265
|
+
case "finalized":
|
|
266
|
+
if (event.dispatchError) { statusCallback("failed"); finish(reject)(new Error(`Transaction failed: ${event.dispatchError.toString()}`)); return; }
|
|
267
|
+
statusCallback("finalized"); finish(resolve)(transactionHash); return;
|
|
268
|
+
case "invalid": case "dropped": statusCallback("failed"); finish(reject)(new Error(`Transaction ${event.type}`)); return;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
error: (error) => { statusCallback("failed"); finish(reject)(error); },
|
|
272
|
+
});
|
|
273
|
+
} catch (error) { statusCallback("failed"); finish(reject)(error); }
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async submitTransaction(contractAddress, value, encodedData, signerSubstrateAddress, signer, statusCallback, { rpc } = {}) {
|
|
278
|
+
await this.ensureAccountMapped(signerSubstrateAddress, signer);
|
|
279
|
+
const gasEstimate = await this.estimateGasForCall(signerSubstrateAddress, contractAddress, value, encodedData);
|
|
280
|
+
if (!gasEstimate.success) throw new Error(`Contract execution would revert: ${gasEstimate.revertData ?? "0x"}`);
|
|
281
|
+
const weightLimit = { proof_size: gasEstimate.gasRequired.proofSize, ref_time: gasEstimate.gasRequired.referenceTime };
|
|
282
|
+
const minimumStorageDeposit = 2_000_000_000_000n;
|
|
283
|
+
let storageDepositLimit = gasEstimate.storageDeposit === 0n ? minimumStorageDeposit : (gasEstimate.storageDeposit * 120n) / 100n;
|
|
284
|
+
if (storageDepositLimit < minimumStorageDeposit) storageDepositLimit = minimumStorageDeposit;
|
|
285
|
+
const callExtrinsic = this.client.tx.Revive.call({ dest: Binary.fromHex(contractAddress), value, weight_limit: weightLimit, storage_deposit_limit: storageDepositLimit, data: Binary.fromHex(encodedData) });
|
|
286
|
+
let nonceFallback = {};
|
|
287
|
+
if (rpc) {
|
|
288
|
+
try { const nonce = await fetchNonce(rpc, signerSubstrateAddress); nonceFallback = { rpc, senderSS58: signerSubstrateAddress, expectedNonce: nonce }; } catch {}
|
|
289
|
+
}
|
|
290
|
+
return await this.signAndSubmitExtrinsic(callExtrinsic, signer, statusCallback, nonceFallback);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class DotNS {
|
|
295
|
+
constructor() { this.client = null; this.clientWrapper = null; this.rpc = null; this.substrateAddress = null; this.evmAddress = null; this.signer = null; this.connected = false; }
|
|
296
|
+
|
|
297
|
+
async connect(options = {}) {
|
|
298
|
+
const endpoints = options.rpc ? [options.rpc] : process.env.DOTNS_RPC ? [process.env.DOTNS_RPC] : RPC_ENDPOINTS;
|
|
299
|
+
const source = options.keyUri || options.mnemonic || process.env.DOTNS_KEY_URI || process.env.DOTNS_MNEMONIC || process.env.MNEMONIC || DEFAULT_MNEMONIC;
|
|
300
|
+
const isKeyUri = Boolean(options.keyUri || process.env.DOTNS_KEY_URI);
|
|
301
|
+
let lastError;
|
|
302
|
+
for (const rpc of endpoints) {
|
|
303
|
+
try {
|
|
304
|
+
console.log(` Connecting to: ${rpc}`);
|
|
305
|
+
this.rpc = rpc;
|
|
306
|
+
this.client = createClient(getWsProvider(rpc));
|
|
307
|
+
const unsafeApi = this.client.getUnsafeApi();
|
|
308
|
+
this.clientWrapper = new ReviveClientWrapper(unsafeApi);
|
|
309
|
+
await cryptoWaitReady();
|
|
310
|
+
const keyring = new Keyring({ type: "sr25519" });
|
|
311
|
+
const account = isKeyUri ? keyring.addFromUri(source) : keyring.addFromMnemonic(source);
|
|
312
|
+
this.substrateAddress = account.address;
|
|
313
|
+
this.evmAddress = await this.clientWrapper.getEvmAddress(this.substrateAddress);
|
|
314
|
+
this.signer = getPolkadotSigner(account.publicKey, "Sr25519", async (input) => account.sign(input));
|
|
315
|
+
this.connected = true;
|
|
316
|
+
console.log(` SS58 Address: ${this.substrateAddress}`);
|
|
317
|
+
console.log(` H160 Address: ${this.evmAddress}`);
|
|
318
|
+
return this;
|
|
319
|
+
} catch (e) {
|
|
320
|
+
lastError = e;
|
|
321
|
+
console.log(` Failed to connect to ${rpc}: ${e.message}`);
|
|
322
|
+
if (this.client) { try { this.client.destroy(); } catch {} this.client = null; }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
throw new Error(`All RPC endpoints failed. Last error: ${lastError?.message}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
ensureConnected() { if (!this.connected) throw new Error("Not connected. Call connect() first."); }
|
|
329
|
+
|
|
330
|
+
async contractCall(contractAddress, contractAbi, functionName, args = []) {
|
|
331
|
+
this.ensureConnected();
|
|
332
|
+
const encodedCallData = encodeFunctionData({ abi: contractAbi, functionName, args });
|
|
333
|
+
const callResult = await this.clientWrapper.performDryRunCall(this.substrateAddress, contractAddress, 0n, encodedCallData);
|
|
334
|
+
if (!callResult.result.isOk) {
|
|
335
|
+
const errorData = callResult.result.value;
|
|
336
|
+
const flags = errorData?.flags ?? 0n;
|
|
337
|
+
const revertData = errorData?.data ?? "0x";
|
|
338
|
+
const isRevert = (flags & 1n) === 1n;
|
|
339
|
+
if (isRevert) throw new Error(`Contract reverted (flags=${flags}) with data: ${revertData}`);
|
|
340
|
+
throw new Error(`Contract call failed (flags=${flags}) with data: ${revertData}`);
|
|
341
|
+
}
|
|
342
|
+
return decodeFunctionResult({ abi: contractAbi, functionName, data: callResult.result.value.data });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async contractTransaction(contractAddress, value, contractAbi, functionName, args = [], statusCallback = () => {}) {
|
|
346
|
+
this.ensureConnected();
|
|
347
|
+
const encodedCallData = encodeFunctionData({ abi: contractAbi, functionName, args });
|
|
348
|
+
return await withTimeout(this.clientWrapper.submitTransaction(contractAddress, value, encodedCallData, this.substrateAddress, this.signer, statusCallback, { rpc: this.rpc }), OPERATION_TIMEOUT_MS, functionName);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async checkOwnership(label, ownerAddress = null) {
|
|
352
|
+
this.ensureConnected();
|
|
353
|
+
const checkAddress = ownerAddress || this.evmAddress;
|
|
354
|
+
const tokenId = computeDomainTokenId(label);
|
|
355
|
+
try {
|
|
356
|
+
const owner = await withTimeout(this.contractCall(CONTRACTS.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 30000, "ownerOf");
|
|
357
|
+
const owned = owner.toLowerCase() === checkAddress.toLowerCase();
|
|
358
|
+
return { owned, owner };
|
|
359
|
+
} catch { return { owned: false, owner: null }; }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async classifyName(label) {
|
|
363
|
+
this.ensureConnected();
|
|
364
|
+
console.log(`\n Classifying name via PopOracle...`);
|
|
365
|
+
const result = await withTimeout(this.contractCall(CONTRACTS.POP_RULES, POP_RULES_ABI, "classifyName", [label]), 30000, "classifyName");
|
|
366
|
+
const requiredStatus = typeof result[0] === "bigint" ? Number(result[0]) : result[0];
|
|
367
|
+
const message = result[1];
|
|
368
|
+
console.log(` Required status: ${Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === requiredStatus)}`);
|
|
369
|
+
console.log(` Message: ${message}`);
|
|
370
|
+
return { requiredStatus, message };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getUserPopStatus(ownerAddress = null) {
|
|
374
|
+
this.ensureConnected();
|
|
375
|
+
const checkAddress = ownerAddress || this.evmAddress;
|
|
376
|
+
const result = await withTimeout(this.contractCall(CONTRACTS.POP_RULES, POP_RULES_ABI, "userPopStatus", [checkAddress]), 30000, "userPopStatus");
|
|
377
|
+
return typeof result === "bigint" ? Number(result) : result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async setUserPopStatus(status) {
|
|
381
|
+
this.ensureConnected();
|
|
382
|
+
console.log(`\n Checking current PoP status...`);
|
|
383
|
+
const currentStatus = await this.getUserPopStatus();
|
|
384
|
+
const currentStatusName = Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === currentStatus);
|
|
385
|
+
const desiredStatusName = Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === status);
|
|
386
|
+
console.log(` Current: ${currentStatusName}`);
|
|
387
|
+
console.log(` Desired: ${desiredStatusName}`);
|
|
388
|
+
if (currentStatus === status) { console.log(` Status already set, skipping update`); return; }
|
|
389
|
+
console.log(` Setting PoP status to ${desiredStatusName}...`);
|
|
390
|
+
const txHash = await this.contractTransaction(CONTRACTS.POP_RULES, 0n, POP_RULES_ABI, "setUserPopStatus", [status], (s) => console.log(` ${s}`));
|
|
391
|
+
console.log(` Tx: ${txHash}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async ensureNotRegistered(label) {
|
|
395
|
+
this.ensureConnected();
|
|
396
|
+
console.log(`\n Checking availability of ${label}.dot...`);
|
|
397
|
+
const tokenId = computeDomainTokenId(label);
|
|
398
|
+
try {
|
|
399
|
+
const owner = await withTimeout(this.contractCall(CONTRACTS.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 30000, "Availability check");
|
|
400
|
+
if (owner !== zeroAddress) throw new Error(`Domain ${label}.dot already owned by ${owner}`);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
403
|
+
if (errorMessage.includes("already owned")) throw error;
|
|
404
|
+
}
|
|
405
|
+
console.log(` ${label}.dot is available`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async generateCommitment(label, includeReverse = false) {
|
|
409
|
+
this.ensureConnected();
|
|
410
|
+
console.log(`\n Generating commitment hash...`);
|
|
411
|
+
validateDomainLabel(label);
|
|
412
|
+
const secret = `0x${crypto.randomBytes(32).toString("hex")}`;
|
|
413
|
+
const registration = { label, owner: this.evmAddress, secret, reserved: includeReverse };
|
|
414
|
+
const commitment = await withTimeout(this.contractCall(CONTRACTS.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "makeCommitment", [registration]), 30000, "Commitment generation");
|
|
415
|
+
console.log(` Commitment: ${commitment}`);
|
|
416
|
+
return { commitment, registration };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async submitCommitment(commitment) {
|
|
420
|
+
this.ensureConnected();
|
|
421
|
+
console.log(`\n Submitting commitment...`);
|
|
422
|
+
const txHash = await this.contractTransaction(CONTRACTS.DOTNS_REGISTRAR_CONTROLLER, 0n, DOTNS_REGISTRAR_CONTROLLER_ABI, "commit", [commitment], (s) => console.log(` ${s}`));
|
|
423
|
+
console.log(` Tx: ${txHash}`);
|
|
424
|
+
console.log(` Committed at: ${new Date().toISOString()}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async waitForCommitmentAge() {
|
|
428
|
+
this.ensureConnected();
|
|
429
|
+
console.log(`\n Reading minimum commitment age...`);
|
|
430
|
+
const minimumAge = await withTimeout(this.contractCall(CONTRACTS.DOTNS_REGISTRAR_CONTROLLER, DOTNS_REGISTRAR_CONTROLLER_ABI, "minCommitmentAge", []), 30000, "minCommitmentAge");
|
|
431
|
+
const minimumAgeSeconds = typeof minimumAge === "bigint" ? Number(minimumAge) : minimumAge;
|
|
432
|
+
const waitSeconds = minimumAgeSeconds + 10;
|
|
433
|
+
console.log(` Minimum commitment age: ${minimumAgeSeconds}s`);
|
|
434
|
+
console.log(` Waiting ${waitSeconds}s for commitment to mature...`);
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
|
|
436
|
+
console.log(` Commitment age requirement met`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async getPriceAndValidate(label) {
|
|
440
|
+
this.ensureConnected();
|
|
441
|
+
console.log(`\n Checking price and eligibility...`);
|
|
442
|
+
validateDomainLabel(label);
|
|
443
|
+
const baseName = stripTrailingDigits(label);
|
|
444
|
+
const reservationInfo = await withTimeout(this.contractCall(CONTRACTS.POP_RULES, POP_RULES_ABI, "isBaseNameReserved", [baseName]), 30000, "isBaseNameReserved");
|
|
445
|
+
const [isReserved, reservationOwner] = reservationInfo;
|
|
446
|
+
if (isReserved && reservationOwner.toLowerCase() !== this.evmAddress.toLowerCase()) throw new Error("Base name reserved for original Lite registrant");
|
|
447
|
+
const classificationResult = await withTimeout(this.contractCall(CONTRACTS.POP_RULES, POP_RULES_ABI, "classifyName", [label]), 30000, "classifyName");
|
|
448
|
+
const requiredStatus = typeof classificationResult[0] === "bigint" ? Number(classificationResult[0]) : classificationResult[0];
|
|
449
|
+
const message = classificationResult[1];
|
|
450
|
+
const userStatus = await this.getUserPopStatus();
|
|
451
|
+
if (requiredStatus === ProofOfPersonhoodStatus.Reserved) throw new Error(message);
|
|
452
|
+
if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodFull) {
|
|
453
|
+
if (userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodFull) throw new Error("Requires Full Personhood verification");
|
|
454
|
+
} else if (requiredStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) {
|
|
455
|
+
if (userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodLite && userStatus !== ProofOfPersonhoodStatus.ProofOfPersonhoodFull) throw new Error("Requires Personhood Lite verification");
|
|
456
|
+
} else {
|
|
457
|
+
const trailingDigitCount = countTrailingDigits(label);
|
|
458
|
+
if (trailingDigitCount === 0 || userStatus === ProofOfPersonhoodStatus.ProofOfPersonhoodLite) throw new Error("Personhood Lite cannot register base names");
|
|
459
|
+
}
|
|
460
|
+
const priceRaw = await withTimeout(this.contractCall(CONTRACTS.POP_RULES, POP_RULES_ABI, "price", [label]), 30000, "price");
|
|
461
|
+
const priceWei = typeof priceRaw === "bigint" ? priceRaw : BigInt(priceRaw);
|
|
462
|
+
const requiredStatusName = Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === requiredStatus);
|
|
463
|
+
const userStatusName = Object.keys(ProofOfPersonhoodStatus).find((k) => ProofOfPersonhoodStatus[k] === userStatus);
|
|
464
|
+
console.log(` Required status: ${requiredStatusName}`);
|
|
465
|
+
console.log(` User status: ${userStatusName}`);
|
|
466
|
+
console.log(` Price: ${formatEther(priceWei)} PAS`);
|
|
467
|
+
return { priceWei, requiredStatus, userStatus, message };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async finalizeRegistration(registration, priceWei) {
|
|
471
|
+
this.ensureConnected();
|
|
472
|
+
console.log(`\n Finalizing registration for ${registration.label}.dot...`);
|
|
473
|
+
const bufferedPaymentWei = (priceWei * 110n) / 100n;
|
|
474
|
+
const bufferedPaymentNative = convertWeiToNative(bufferedPaymentWei);
|
|
475
|
+
console.log(` Oracle price: ${formatEther(priceWei)} PAS`);
|
|
476
|
+
console.log(` Paying: ${formatEther(bufferedPaymentWei)} PAS`);
|
|
477
|
+
const txHash = await this.contractTransaction(CONTRACTS.DOTNS_REGISTRAR_CONTROLLER, bufferedPaymentNative, DOTNS_REGISTRAR_CONTROLLER_ABI, "register", [registration], (s) => console.log(` ${s}`));
|
|
478
|
+
console.log(` Tx: ${txHash}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async verifyOwnership(label) {
|
|
482
|
+
this.ensureConnected();
|
|
483
|
+
console.log(`\n Verifying ownership...`);
|
|
484
|
+
const tokenId = computeDomainTokenId(label);
|
|
485
|
+
const actualOwner = await withTimeout(this.contractCall(CONTRACTS.DOTNS_REGISTRAR, DOTNS_REGISTRAR_ABI, "ownerOf", [tokenId]), 30000, "ownerOf");
|
|
486
|
+
if (actualOwner.toLowerCase() !== this.evmAddress.toLowerCase()) {
|
|
487
|
+
console.log(` Expected: ${this.evmAddress}`);
|
|
488
|
+
console.log(` Actual: ${actualOwner}`);
|
|
489
|
+
throw new Error(`Owner mismatch for ${label}.dot`);
|
|
490
|
+
}
|
|
491
|
+
console.log(` Owner: ${actualOwner}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async setContenthash(domainName, contenthashHex) {
|
|
495
|
+
return withSpan("deploy.dotns.set-contenthash", "set-contenthash", {}, async () => {
|
|
496
|
+
this.ensureConnected();
|
|
497
|
+
const node = namehash(`${domainName}.dot`);
|
|
498
|
+
let ipfsCid = null;
|
|
499
|
+
if (contenthashHex && contenthashHex !== "0x") {
|
|
500
|
+
const bytes = Buffer.from(contenthashHex.slice(2), "hex");
|
|
501
|
+
if (bytes[0] === 0xe3 && bytes.length >= 4) {
|
|
502
|
+
const cidBytes = bytes.slice(2);
|
|
503
|
+
ipfsCid = CID.decode(cidBytes).toString();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
console.log(` Setting contenthash: ${ipfsCid || contenthashHex}`);
|
|
507
|
+
const txHash = await this.contractTransaction(CONTRACTS.DOTNS_CONTENT_RESOLVER, 0n, DOTNS_CONTENT_RESOLVER_ABI, "setContenthash", [node, contenthashHex], (s) => console.log(` ${s}`));
|
|
508
|
+
console.log(` Tx: ${txHash}`);
|
|
509
|
+
console.log(` Contenthash set successfully!\n`);
|
|
510
|
+
return { node };
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async register(label, options = {}) {
|
|
515
|
+
return withSpan("deploy.dotns.register", `register ${label}.dot`, {}, async () => {
|
|
516
|
+
const status = parseProofOfPersonhoodStatus(options.status || process.env.DOTNS_STATUS);
|
|
517
|
+
const reverse = options.reverse ?? (process.env.DOTNS_REVERSE ?? "false").toLowerCase() === "true";
|
|
518
|
+
if (!this.connected) await this.connect(options);
|
|
519
|
+
validateDomainLabel(label);
|
|
520
|
+
await Promise.all([
|
|
521
|
+
this.classifyName(label),
|
|
522
|
+
this.ensureNotRegistered(label),
|
|
523
|
+
this.setUserPopStatus(status),
|
|
524
|
+
]);
|
|
525
|
+
const { commitment, registration } = await this.generateCommitment(label, reverse);
|
|
526
|
+
await withSpan("deploy.dotns.submit-commitment", "submit-commitment", {}, () => this.submitCommitment(commitment));
|
|
527
|
+
await withSpan("deploy.dotns.wait-commitment-age", "wait-commitment-age", {}, () => this.waitForCommitmentAge());
|
|
528
|
+
const pricing = await this.getPriceAndValidate(label);
|
|
529
|
+
await withSpan("deploy.dotns.finalize-registration", "finalize-registration", {}, () => this.finalizeRegistration(registration, pricing.priceWei));
|
|
530
|
+
await this.verifyOwnership(label);
|
|
531
|
+
console.log(`\n Registration complete!`);
|
|
532
|
+
return { label, owner: this.evmAddress };
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
disconnect() {
|
|
537
|
+
if (this.client) { this.client.destroy(); this.client = null; this.clientWrapper = null; this.connected = false; }
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export const dotns = new DotNS();
|
package/src/index.js
ADDED