@radiant-core/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/index.cjs +1064 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +693 -0
- package/dist/index.d.ts +693 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import rjs from '@radiant-core/radiantjs';
|
|
2
|
+
import { encode } from 'cbor-x';
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var RadiantSdkError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var InsufficientFundsError = class extends RadiantSdkError {
|
|
12
|
+
constructor(requiredPhotons, availablePhotons) {
|
|
13
|
+
super(
|
|
14
|
+
`Insufficient funds: need ${requiredPhotons} photons (incl. fee) but only ${availablePhotons} available in token-free UTXOs`
|
|
15
|
+
);
|
|
16
|
+
this.requiredPhotons = requiredPhotons;
|
|
17
|
+
this.availablePhotons = availablePhotons;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var TokenBurnGuardError = class extends RadiantSdkError {
|
|
21
|
+
constructor(txid, vout) {
|
|
22
|
+
super(
|
|
23
|
+
`Refusing token-bearing UTXO ${txid}:${vout} as RXD funding (would burn the token)`
|
|
24
|
+
);
|
|
25
|
+
this.txid = txid;
|
|
26
|
+
this.vout = vout;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var ElectrumError = class extends RadiantSdkError {
|
|
30
|
+
constructor(message, code) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var ValidationError = class extends RadiantSdkError {
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/constants.ts
|
|
39
|
+
var PHOTONS_PER_RXD = 100000000n;
|
|
40
|
+
var RADIANT_COIN_TYPE = 512;
|
|
41
|
+
var MIN_RELAY_FEE_RATE = {
|
|
42
|
+
mainnet: 10000n,
|
|
43
|
+
testnet: 1000n,
|
|
44
|
+
regtest: 1000n
|
|
45
|
+
};
|
|
46
|
+
var DUST_LIMIT = 1000n;
|
|
47
|
+
var TOKEN_OUTPUT_VALUE = 1n;
|
|
48
|
+
var DEFAULT_ELECTRUM_ENDPOINT = {
|
|
49
|
+
mainnet: "wss://electrumx.radiantcore.org:443",
|
|
50
|
+
// Adjust to your own infra; no canonical public testnet/regtest endpoint.
|
|
51
|
+
testnet: "wss://electrumx.radiantcore.org:443",
|
|
52
|
+
regtest: "ws://localhost:50011"
|
|
53
|
+
};
|
|
54
|
+
var DEFAULT_REST_BASE = "https://radiantcore.org/api";
|
|
55
|
+
var GLYPH_MAGIC_BYTES = Uint8Array.from([103, 108, 121]);
|
|
56
|
+
var GLYPH_PROTOCOL = {
|
|
57
|
+
FT: 1,
|
|
58
|
+
NFT: 2,
|
|
59
|
+
MUTABLE: 5,
|
|
60
|
+
CONTAINER: 7,
|
|
61
|
+
ENCRYPTED: 8,
|
|
62
|
+
WAVE: 11
|
|
63
|
+
};
|
|
64
|
+
var INPUT_REF_OP_MIN = 208;
|
|
65
|
+
var INPUT_REF_OP_MAX = 216;
|
|
66
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
67
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
68
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
69
|
+
|
|
70
|
+
// src/units.ts
|
|
71
|
+
var RXD_DECIMALS = 8;
|
|
72
|
+
function rxdToPhotons(rxd) {
|
|
73
|
+
const str = typeof rxd === "number" ? formatNumber(rxd) : rxd.trim();
|
|
74
|
+
if (!/^-?\d*(\.\d*)?$/.test(str) || str === "" || str === "." || str === "-") {
|
|
75
|
+
throw new ValidationError(`Invalid RXD amount: ${JSON.stringify(rxd)}`);
|
|
76
|
+
}
|
|
77
|
+
const negative = str.startsWith("-");
|
|
78
|
+
const [whole, frac = ""] = (negative ? str.slice(1) : str).split(".");
|
|
79
|
+
if (frac.length > RXD_DECIMALS) {
|
|
80
|
+
throw new ValidationError(
|
|
81
|
+
`RXD amount has more than ${RXD_DECIMALS} decimal places: ${str}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const padded = frac.padEnd(RXD_DECIMALS, "0");
|
|
85
|
+
const photons = BigInt(whole || "0") * PHOTONS_PER_RXD + BigInt(padded || "0");
|
|
86
|
+
return negative ? -photons : photons;
|
|
87
|
+
}
|
|
88
|
+
function photonsToRxd(photons) {
|
|
89
|
+
const negative = photons < 0n;
|
|
90
|
+
const abs = negative ? -photons : photons;
|
|
91
|
+
const whole = abs / PHOTONS_PER_RXD;
|
|
92
|
+
const frac = abs % PHOTONS_PER_RXD;
|
|
93
|
+
const fracStr = frac.toString().padStart(RXD_DECIMALS, "0").replace(/0+$/, "");
|
|
94
|
+
const body = fracStr.length ? `${whole}.${fracStr}` : `${whole}.0`;
|
|
95
|
+
return negative ? `-${body}` : body;
|
|
96
|
+
}
|
|
97
|
+
function formatNumber(n) {
|
|
98
|
+
if (!Number.isFinite(n)) {
|
|
99
|
+
throw new ValidationError(`Invalid RXD amount: ${n}`);
|
|
100
|
+
}
|
|
101
|
+
return n.toFixed(RXD_DECIMALS);
|
|
102
|
+
}
|
|
103
|
+
var Address = rjs.Address;
|
|
104
|
+
var HDPrivateKey = rjs.HDPrivateKey;
|
|
105
|
+
rjs.HDPublicKey;
|
|
106
|
+
var PrivateKey = rjs.PrivateKey;
|
|
107
|
+
rjs.PublicKey;
|
|
108
|
+
var Script = rjs.Script;
|
|
109
|
+
var Opcode = rjs.Opcode;
|
|
110
|
+
var Transaction = rjs.Transaction;
|
|
111
|
+
var Mnemonic = rjs.Mnemonic;
|
|
112
|
+
var Networks = rjs.Networks;
|
|
113
|
+
var crypto = rjs.crypto;
|
|
114
|
+
var radiantjs = rjs;
|
|
115
|
+
function toRjsNetwork(network) {
|
|
116
|
+
switch (network) {
|
|
117
|
+
case "mainnet":
|
|
118
|
+
return Networks.livenet;
|
|
119
|
+
case "testnet":
|
|
120
|
+
return Networks.testnet;
|
|
121
|
+
case "regtest":
|
|
122
|
+
Networks.enableRegtest?.();
|
|
123
|
+
return Networks.regtest;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function sha256(data) {
|
|
127
|
+
return crypto.Hash.sha256(data);
|
|
128
|
+
}
|
|
129
|
+
function sha256d(data) {
|
|
130
|
+
return crypto.Hash.sha256sha256(data);
|
|
131
|
+
}
|
|
132
|
+
var SIGHASH_FORKID_ALL = crypto.Signature.SIGHASH_ALL | crypto.Signature.SIGHASH_FORKID;
|
|
133
|
+
|
|
134
|
+
// src/script.ts
|
|
135
|
+
var CHECKSIG_OPS = /* @__PURE__ */ new Set([
|
|
136
|
+
172,
|
|
137
|
+
// OP_CHECKSIG
|
|
138
|
+
173,
|
|
139
|
+
// OP_CHECKSIGVERIFY
|
|
140
|
+
174,
|
|
141
|
+
// OP_CHECKMULTISIG
|
|
142
|
+
175
|
|
143
|
+
// OP_CHECKMULTISIGVERIFY
|
|
144
|
+
]);
|
|
145
|
+
function scriptHash(scriptHex) {
|
|
146
|
+
if (!scriptHex) {
|
|
147
|
+
throw new ValidationError("scriptHash: cannot hash an empty script");
|
|
148
|
+
}
|
|
149
|
+
return Buffer.from(sha256(Buffer.from(scriptHex, "hex"))).reverse().toString("hex");
|
|
150
|
+
}
|
|
151
|
+
function p2pkhScript(address, _network) {
|
|
152
|
+
try {
|
|
153
|
+
return Script.buildPublicKeyHashOut(address).toHex();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new ValidationError(
|
|
156
|
+
`p2pkhScript: invalid address ${JSON.stringify(address)}: ${String(err)}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function addressToScriptHash(address, network) {
|
|
161
|
+
return scriptHash(p2pkhScript(address));
|
|
162
|
+
}
|
|
163
|
+
function isTokenBearing(scriptHex) {
|
|
164
|
+
if (!scriptHex) return false;
|
|
165
|
+
let bytes;
|
|
166
|
+
try {
|
|
167
|
+
bytes = Buffer.from(scriptHex, "hex");
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const n = bytes.length;
|
|
172
|
+
let pos = 0;
|
|
173
|
+
while (pos < n) {
|
|
174
|
+
const op = bytes[pos];
|
|
175
|
+
if (op >= INPUT_REF_OP_MIN && op <= INPUT_REF_OP_MAX) return true;
|
|
176
|
+
let next;
|
|
177
|
+
if (op >= 1 && op <= 75) {
|
|
178
|
+
next = pos + 1 + op;
|
|
179
|
+
} else if (op === 76) {
|
|
180
|
+
if (pos + 1 >= n) return false;
|
|
181
|
+
next = pos + 2 + bytes[pos + 1];
|
|
182
|
+
} else if (op === 77) {
|
|
183
|
+
if (pos + 2 >= n) return false;
|
|
184
|
+
next = pos + 3 + (bytes[pos + 1] | bytes[pos + 2] << 8);
|
|
185
|
+
} else if (op === 78) {
|
|
186
|
+
if (pos + 4 >= n) return false;
|
|
187
|
+
next = pos + 5 + ((bytes[pos + 1] | bytes[pos + 2] << 8 | bytes[pos + 3] << 16 | bytes[pos + 4] << 24) >>> 0);
|
|
188
|
+
} else {
|
|
189
|
+
next = pos + 1;
|
|
190
|
+
}
|
|
191
|
+
if (next <= pos || next > n) return false;
|
|
192
|
+
pos = next;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
function zeroRefs(scriptHex) {
|
|
197
|
+
const script = Buffer.from(scriptHex, "hex");
|
|
198
|
+
const out = Buffer.from(script);
|
|
199
|
+
let requiresSig = false;
|
|
200
|
+
let n = 0;
|
|
201
|
+
while (n < script.length) {
|
|
202
|
+
const op = script[n];
|
|
203
|
+
n += 1;
|
|
204
|
+
if (CHECKSIG_OPS.has(op)) {
|
|
205
|
+
requiresSig = true;
|
|
206
|
+
} else if (op >= INPUT_REF_OP_MIN && op <= INPUT_REF_OP_MAX) {
|
|
207
|
+
out.fill(0, n, n + 36);
|
|
208
|
+
n += 36;
|
|
209
|
+
} else if (op <= 78 && op >= 1) {
|
|
210
|
+
let dlen = op;
|
|
211
|
+
if (op === 76) {
|
|
212
|
+
dlen = script[n];
|
|
213
|
+
n += 1;
|
|
214
|
+
} else if (op === 77) {
|
|
215
|
+
dlen = script.readUInt16LE(n);
|
|
216
|
+
n += 2;
|
|
217
|
+
} else if (op === 78) {
|
|
218
|
+
dlen = script.readUInt32LE(n);
|
|
219
|
+
n += 4;
|
|
220
|
+
}
|
|
221
|
+
n += dlen;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return (requiresSig ? out : script).toString("hex");
|
|
225
|
+
}
|
|
226
|
+
function packRef(txid, vout) {
|
|
227
|
+
if (!/^[0-9a-fA-F]{64}$/.test(txid)) {
|
|
228
|
+
throw new ValidationError(`packRef: invalid txid ${JSON.stringify(txid)}`);
|
|
229
|
+
}
|
|
230
|
+
const txidLE = Buffer.from(txid, "hex").reverse();
|
|
231
|
+
const voutLE = Buffer.alloc(4);
|
|
232
|
+
voutLE.writeUInt32LE(vout >>> 0, 0);
|
|
233
|
+
return Buffer.concat([txidLE, voutLE]).toString("hex");
|
|
234
|
+
}
|
|
235
|
+
function unpackRef(ref) {
|
|
236
|
+
if (!/^[0-9a-fA-F]{72}$/.test(ref)) {
|
|
237
|
+
throw new ValidationError(`unpackRef: invalid ref ${JSON.stringify(ref)}`);
|
|
238
|
+
}
|
|
239
|
+
const buf = Buffer.from(ref, "hex");
|
|
240
|
+
const txid = Buffer.from(buf.subarray(0, 32)).reverse().toString("hex");
|
|
241
|
+
const vout = buf.readUInt32LE(32);
|
|
242
|
+
return { txid, vout };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/client.ts
|
|
246
|
+
var WS_OPEN = 1;
|
|
247
|
+
var ElectrumClient = class {
|
|
248
|
+
constructor(options = {}) {
|
|
249
|
+
this.nextId = 1;
|
|
250
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
251
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
252
|
+
this.reconnectAttempts = 0;
|
|
253
|
+
this.closedByUser = false;
|
|
254
|
+
const network = options.network ?? "mainnet";
|
|
255
|
+
this.endpoint = options.endpoint ?? DEFAULT_ELECTRUM_ENDPOINT[network];
|
|
256
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
257
|
+
this.autoReconnect = options.reconnect ?? true;
|
|
258
|
+
this.injectedCtor = options.webSocketCtor;
|
|
259
|
+
}
|
|
260
|
+
/** True if the socket is open and ready. */
|
|
261
|
+
get connected() {
|
|
262
|
+
return this.ws?.readyState === WS_OPEN;
|
|
263
|
+
}
|
|
264
|
+
/** Open the connection (idempotent). Resolves once the socket is ready. */
|
|
265
|
+
async connect() {
|
|
266
|
+
if (this.connected) return;
|
|
267
|
+
if (this.connectPromise) return this.connectPromise;
|
|
268
|
+
this.closedByUser = false;
|
|
269
|
+
this.connectPromise = this.openSocket();
|
|
270
|
+
try {
|
|
271
|
+
await this.connectPromise;
|
|
272
|
+
} finally {
|
|
273
|
+
this.connectPromise = void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async resolveCtor() {
|
|
277
|
+
if (this.ctor) return this.ctor;
|
|
278
|
+
if (this.injectedCtor) return this.ctor = this.injectedCtor;
|
|
279
|
+
const g = globalThis;
|
|
280
|
+
if (typeof g.WebSocket !== "undefined") {
|
|
281
|
+
return this.ctor = g.WebSocket;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const mod = await import('ws');
|
|
285
|
+
return this.ctor = mod.default ?? mod;
|
|
286
|
+
} catch {
|
|
287
|
+
throw new ElectrumError(
|
|
288
|
+
"No WebSocket implementation available. On Node <22, install the optional `ws` dependency."
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async openSocket() {
|
|
293
|
+
const Ctor = await this.resolveCtor();
|
|
294
|
+
await new Promise((resolve, reject) => {
|
|
295
|
+
let settled = false;
|
|
296
|
+
const ws = new Ctor(this.endpoint);
|
|
297
|
+
this.ws = ws;
|
|
298
|
+
ws.addEventListener("open", () => {
|
|
299
|
+
this.reconnectAttempts = 0;
|
|
300
|
+
settled = true;
|
|
301
|
+
resolve();
|
|
302
|
+
});
|
|
303
|
+
ws.addEventListener(
|
|
304
|
+
"message",
|
|
305
|
+
(ev) => this.onMessage(ev.data)
|
|
306
|
+
);
|
|
307
|
+
ws.addEventListener("error", (ev) => {
|
|
308
|
+
if (!settled) {
|
|
309
|
+
settled = true;
|
|
310
|
+
reject(new ElectrumError(`WebSocket error connecting to ${this.endpoint}`));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
ws.addEventListener("close", () => this.onClose());
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
onMessage(data) {
|
|
317
|
+
let msg;
|
|
318
|
+
try {
|
|
319
|
+
msg = JSON.parse(typeof data === "string" ? data : String(data));
|
|
320
|
+
} catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const handle = (m) => {
|
|
324
|
+
if (m && typeof m.id === "number") {
|
|
325
|
+
const p = this.pending.get(m.id);
|
|
326
|
+
if (!p) return;
|
|
327
|
+
this.pending.delete(m.id);
|
|
328
|
+
clearTimeout(p.timer);
|
|
329
|
+
if (m.error) {
|
|
330
|
+
const e = m.error;
|
|
331
|
+
p.reject(new ElectrumError(e?.message ?? String(e), e?.code));
|
|
332
|
+
} else {
|
|
333
|
+
p.resolve(m.result);
|
|
334
|
+
}
|
|
335
|
+
} else if (m && typeof m.method === "string") {
|
|
336
|
+
const handler = this.subscriptions.get(m.method);
|
|
337
|
+
if (handler) handler(Array.isArray(m.params) ? m.params : []);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
if (Array.isArray(msg)) msg.forEach(handle);
|
|
341
|
+
else handle(msg);
|
|
342
|
+
}
|
|
343
|
+
onClose() {
|
|
344
|
+
for (const [, p] of this.pending) {
|
|
345
|
+
clearTimeout(p.timer);
|
|
346
|
+
p.reject(new ElectrumError("Connection closed"));
|
|
347
|
+
}
|
|
348
|
+
this.pending.clear();
|
|
349
|
+
this.ws = void 0;
|
|
350
|
+
if (this.closedByUser || !this.autoReconnect) return;
|
|
351
|
+
const delay = this.nextReconnectDelay();
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
this.connect().catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}, delay);
|
|
356
|
+
}
|
|
357
|
+
/** Exponential backoff with full jitter, capped at RECONNECT_MAX_MS. */
|
|
358
|
+
nextReconnectDelay() {
|
|
359
|
+
const capped = Math.min(
|
|
360
|
+
RECONNECT_MAX_MS,
|
|
361
|
+
RECONNECT_BASE_MS * 2 ** this.reconnectAttempts
|
|
362
|
+
);
|
|
363
|
+
this.reconnectAttempts++;
|
|
364
|
+
return capped / 2 + Math.random() * (capped / 2);
|
|
365
|
+
}
|
|
366
|
+
/** Send a JSON-RPC request and await its result. */
|
|
367
|
+
async request(method, ...params) {
|
|
368
|
+
if (!this.connected) await this.connect();
|
|
369
|
+
const id = this.nextId++;
|
|
370
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const timer = setTimeout(() => {
|
|
373
|
+
this.pending.delete(id);
|
|
374
|
+
reject(new ElectrumError(`Request timed out: ${method}`));
|
|
375
|
+
}, this.requestTimeoutMs);
|
|
376
|
+
this.pending.set(id, {
|
|
377
|
+
resolve,
|
|
378
|
+
reject,
|
|
379
|
+
timer
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
this.ws.send(payload);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
this.pending.delete(id);
|
|
386
|
+
reject(new ElectrumError(`Failed to send ${method}: ${String(err)}`));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// ---- Server info ----------------------------------------------------------
|
|
391
|
+
/** Negotiate protocol version. */
|
|
392
|
+
async serverVersion(client = "@radiant-core/sdk", protocol = ["1.4", "1.4.3"]) {
|
|
393
|
+
return this.request("server.version", client, protocol);
|
|
394
|
+
}
|
|
395
|
+
// ---- Scripthash queries ---------------------------------------------------
|
|
396
|
+
toScriptHash(addressOrScriptHash) {
|
|
397
|
+
return /^[0-9a-fA-F]{64}$/.test(addressOrScriptHash) ? addressOrScriptHash.toLowerCase() : addressToScriptHash(addressOrScriptHash);
|
|
398
|
+
}
|
|
399
|
+
/** Confirmed + unconfirmed balance (photons) for an address or scripthash. */
|
|
400
|
+
async getBalance(addressOrScriptHash) {
|
|
401
|
+
const sh = this.toScriptHash(addressOrScriptHash);
|
|
402
|
+
const r = await this.request(
|
|
403
|
+
"blockchain.scripthash.get_balance",
|
|
404
|
+
sh
|
|
405
|
+
);
|
|
406
|
+
return {
|
|
407
|
+
confirmed: BigInt(r.confirmed ?? 0),
|
|
408
|
+
unconfirmed: BigInt(r.unconfirmed ?? 0)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Unspent outputs for an address or scripthash, as SDK {@link Utxo}s.
|
|
413
|
+
*
|
|
414
|
+
* NOTE: ElectrumX `listunspent` does not return the output script. When you
|
|
415
|
+
* pass a plain address we fill `script` with that address's P2PKH script
|
|
416
|
+
* (correct for ordinary wallet UTXOs). When you pass a raw scripthash we
|
|
417
|
+
* cannot reconstruct the script, so `script` is left empty — provide the
|
|
418
|
+
* address form if you intend to fund/sign from the result.
|
|
419
|
+
*/
|
|
420
|
+
async listUnspent(addressOrScriptHash) {
|
|
421
|
+
const isAddress = !/^[0-9a-fA-F]{64}$/.test(addressOrScriptHash);
|
|
422
|
+
const sh = this.toScriptHash(addressOrScriptHash);
|
|
423
|
+
const rows = await this.request(
|
|
424
|
+
"blockchain.scripthash.listunspent",
|
|
425
|
+
sh
|
|
426
|
+
);
|
|
427
|
+
const script = isAddress ? p2pkhScript(addressOrScriptHash) : "";
|
|
428
|
+
return rows.map((r) => ({
|
|
429
|
+
txid: r.tx_hash,
|
|
430
|
+
vout: r.tx_pos,
|
|
431
|
+
value: BigInt(r.value),
|
|
432
|
+
height: r.height,
|
|
433
|
+
script,
|
|
434
|
+
refs: r.refs
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
/** Raw transaction history for an address or scripthash. */
|
|
438
|
+
async getHistory(addressOrScriptHash) {
|
|
439
|
+
const sh = this.toScriptHash(addressOrScriptHash);
|
|
440
|
+
return this.request("blockchain.scripthash.get_history", sh);
|
|
441
|
+
}
|
|
442
|
+
/** Fetch a raw transaction (hex, or verbose object when verbose=true). */
|
|
443
|
+
async getTransaction(txid, verbose = false) {
|
|
444
|
+
return this.request("blockchain.transaction.get", txid, verbose);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Subscribe to an address/scripthash; `onUpdate` fires with the new status
|
|
448
|
+
* hash whenever the set of outputs changes. Returns the current status.
|
|
449
|
+
*/
|
|
450
|
+
async subscribe(addressOrScriptHash, onUpdate) {
|
|
451
|
+
const sh = this.toScriptHash(addressOrScriptHash);
|
|
452
|
+
this.subscriptions.set("blockchain.scripthash.subscribe", (params) => {
|
|
453
|
+
if (params[0] === sh) onUpdate(params[1] ?? null);
|
|
454
|
+
});
|
|
455
|
+
return this.request("blockchain.scripthash.subscribe", sh);
|
|
456
|
+
}
|
|
457
|
+
/** Stop receiving updates for a scripthash/address. */
|
|
458
|
+
async unsubscribe(addressOrScriptHash) {
|
|
459
|
+
const sh = this.toScriptHash(addressOrScriptHash);
|
|
460
|
+
this.subscriptions.delete("blockchain.scripthash.subscribe");
|
|
461
|
+
return this.request("blockchain.scripthash.unsubscribe", sh);
|
|
462
|
+
}
|
|
463
|
+
// ---- Broadcast ------------------------------------------------------------
|
|
464
|
+
/**
|
|
465
|
+
* Broadcast a raw transaction (hex). Returns the txid. Treats the node's
|
|
466
|
+
* "transaction already in blockchain" as success (returns the computed txid
|
|
467
|
+
* is the caller's job; here we surface the node's reply).
|
|
468
|
+
*/
|
|
469
|
+
async broadcastTx(rawHex) {
|
|
470
|
+
try {
|
|
471
|
+
return await this.request(
|
|
472
|
+
"blockchain.transaction.broadcast",
|
|
473
|
+
rawHex
|
|
474
|
+
);
|
|
475
|
+
} catch (err) {
|
|
476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
477
|
+
if (msg.toLowerCase().includes("transactionalreadyinblockchain")) {
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// ---- Lifecycle ------------------------------------------------------------
|
|
484
|
+
/** Close the connection and stop reconnecting. */
|
|
485
|
+
close() {
|
|
486
|
+
this.closedByUser = true;
|
|
487
|
+
this.subscriptions.clear();
|
|
488
|
+
try {
|
|
489
|
+
this.ws?.close();
|
|
490
|
+
} catch {
|
|
491
|
+
}
|
|
492
|
+
this.ws = void 0;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/wallet.ts
|
|
497
|
+
var HDWallet = class _HDWallet {
|
|
498
|
+
constructor(accountNode, network, account, coinType) {
|
|
499
|
+
this.root = accountNode;
|
|
500
|
+
this.network = network;
|
|
501
|
+
this.account = account;
|
|
502
|
+
this.coinType = coinType;
|
|
503
|
+
}
|
|
504
|
+
/** Create a wallet from a BIP39 mnemonic phrase. */
|
|
505
|
+
static fromMnemonic(phrase, options = {}) {
|
|
506
|
+
if (!Mnemonic.isValid(phrase)) {
|
|
507
|
+
throw new ValidationError("fromMnemonic: invalid BIP39 mnemonic");
|
|
508
|
+
}
|
|
509
|
+
const mnemonic = new Mnemonic(phrase);
|
|
510
|
+
const network = options.network ?? "mainnet";
|
|
511
|
+
const rjsNet = toRjsNetwork(network);
|
|
512
|
+
const seed = mnemonic.toSeed(options.passphrase ?? "");
|
|
513
|
+
const master = HDPrivateKey.fromSeed(seed, rjsNet);
|
|
514
|
+
return _HDWallet.fromMaster(master, network, options);
|
|
515
|
+
}
|
|
516
|
+
/** Create a wallet from a raw seed (Buffer or hex string). */
|
|
517
|
+
static fromSeed(seed, options = {}) {
|
|
518
|
+
const network = options.network ?? "mainnet";
|
|
519
|
+
const buf = typeof seed === "string" ? Buffer.from(seed, "hex") : seed;
|
|
520
|
+
const master = HDPrivateKey.fromSeed(buf, toRjsNetwork(network));
|
|
521
|
+
return _HDWallet.fromMaster(master, network, options);
|
|
522
|
+
}
|
|
523
|
+
/** Create a wallet from an extended private key (xprv). */
|
|
524
|
+
static fromXprv(xprv, options = {}) {
|
|
525
|
+
const network = options.network ?? "mainnet";
|
|
526
|
+
const master = new HDPrivateKey(xprv);
|
|
527
|
+
return _HDWallet.fromMaster(master, network, options);
|
|
528
|
+
}
|
|
529
|
+
/** Generate a fresh random mnemonic phrase (BIP39, 12 words). */
|
|
530
|
+
static generateMnemonic() {
|
|
531
|
+
return Mnemonic.fromRandom().toString();
|
|
532
|
+
}
|
|
533
|
+
static fromMaster(master, network, options) {
|
|
534
|
+
const account = options.account ?? 0;
|
|
535
|
+
const coinType = options.coinType ?? RADIANT_COIN_TYPE;
|
|
536
|
+
const accountNode = master.deriveChild(
|
|
537
|
+
`m/44'/${coinType}'/${account}'`
|
|
538
|
+
);
|
|
539
|
+
return new _HDWallet(accountNode, network, account, coinType);
|
|
540
|
+
}
|
|
541
|
+
/** Full derivation path for a given change/index under this account. */
|
|
542
|
+
pathFor(index, change = 0) {
|
|
543
|
+
return `m/44'/${this.coinType}'/${this.account}'/${change}/${index}`;
|
|
544
|
+
}
|
|
545
|
+
/** radiantjs PrivateKey at the given index (default external chain). */
|
|
546
|
+
privateKey(index, change = 0) {
|
|
547
|
+
const node = this.root.deriveChild(change).deriveChild(index);
|
|
548
|
+
return node.privateKey;
|
|
549
|
+
}
|
|
550
|
+
/** Derive a full key bundle (WIF, pubkey, address, scripthash) at an index. */
|
|
551
|
+
deriveKey(index, change = 0) {
|
|
552
|
+
const priv = this.privateKey(index, change);
|
|
553
|
+
const address = priv.toAddress(toRjsNetwork(this.network)).toString();
|
|
554
|
+
return {
|
|
555
|
+
index,
|
|
556
|
+
change,
|
|
557
|
+
path: this.pathFor(index, change),
|
|
558
|
+
wif: priv.toWIF(),
|
|
559
|
+
publicKey: priv.toPublicKey().toString(),
|
|
560
|
+
address,
|
|
561
|
+
scriptHash: addressToScriptHash(address)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/** Address at an index (external chain by default). */
|
|
565
|
+
address(index = 0, change = 0) {
|
|
566
|
+
return this.deriveKey(index, change).address;
|
|
567
|
+
}
|
|
568
|
+
/** WIF private key at an index. */
|
|
569
|
+
wif(index = 0, change = 0) {
|
|
570
|
+
return this.privateKey(index, change).toWIF();
|
|
571
|
+
}
|
|
572
|
+
/** Derive a batch of key bundles [start, start+count). */
|
|
573
|
+
deriveRange(start, count, change = 0) {
|
|
574
|
+
const out = [];
|
|
575
|
+
for (let i = 0; i < count; i++) out.push(this.deriveKey(start + i, change));
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var Keys = {
|
|
580
|
+
/** Address (Base58Check) for a WIF private key. */
|
|
581
|
+
addressFromWif(wif, network = "mainnet") {
|
|
582
|
+
return PrivateKey.fromWIF(wif).toAddress(toRjsNetwork(network)).toString();
|
|
583
|
+
},
|
|
584
|
+
/** Compressed public key hex for a WIF private key. */
|
|
585
|
+
publicKeyFromWif(wif) {
|
|
586
|
+
return PrivateKey.fromWIF(wif).toPublicKey().toString();
|
|
587
|
+
},
|
|
588
|
+
/** ElectrumX scripthash for a WIF private key's P2PKH address. */
|
|
589
|
+
scriptHashFromWif(wif, network = "mainnet") {
|
|
590
|
+
return addressToScriptHash(this.addressFromWif(wif, network));
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/utxo.ts
|
|
595
|
+
var TX_OVERHEAD_BYTES = 10n;
|
|
596
|
+
var P2PKH_INPUT_BYTES = 148n;
|
|
597
|
+
var P2PKH_OUTPUT_BYTES = 34n;
|
|
598
|
+
var SELECTION_HEADROOM_BYTES = 160n;
|
|
599
|
+
function isFundingSafe(utxo) {
|
|
600
|
+
if (utxo.refs && utxo.refs.length > 0) return false;
|
|
601
|
+
if (isTokenBearing(utxo.script)) return false;
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
function filterFundingCandidates(utxos) {
|
|
605
|
+
return utxos.filter(isFundingSafe);
|
|
606
|
+
}
|
|
607
|
+
function assertFundingSafe(utxos) {
|
|
608
|
+
for (const u of utxos) {
|
|
609
|
+
if (!isFundingSafe(u)) throw new TokenBurnGuardError(u.txid, u.vout);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function estimateFee(inputCount, outputCount, feeRate, extraInputBytes = 0n) {
|
|
613
|
+
const bytes = TX_OVERHEAD_BYTES + P2PKH_INPUT_BYTES * BigInt(inputCount) + P2PKH_OUTPUT_BYTES * BigInt(outputCount) + extraInputBytes;
|
|
614
|
+
return bytes * feeRate;
|
|
615
|
+
}
|
|
616
|
+
function selectRxdFunding(utxos, target, feeRate, options = {}) {
|
|
617
|
+
const baseOutputCount = options.baseOutputCount ?? 1;
|
|
618
|
+
const extraInputBytes = options.extraInputBytes ?? 0n;
|
|
619
|
+
const withChange = options.withChange ?? true;
|
|
620
|
+
const candidates = filterFundingCandidates(utxos).sort(
|
|
621
|
+
(a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0
|
|
622
|
+
);
|
|
623
|
+
const reserveBytes = extraInputBytes + SELECTION_HEADROOM_BYTES;
|
|
624
|
+
const selected = [];
|
|
625
|
+
let total = 0n;
|
|
626
|
+
for (const u of candidates) {
|
|
627
|
+
selected.push(u);
|
|
628
|
+
total += u.value;
|
|
629
|
+
const outputs = baseOutputCount + (withChange ? 1 : 0);
|
|
630
|
+
const fee = estimateFee(selected.length, outputs, feeRate, reserveBytes);
|
|
631
|
+
if (total >= target + fee) {
|
|
632
|
+
return { inputs: selected, fee, total, change: total - target - fee };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const available = candidates.reduce((s, u) => s + u.value, 0n);
|
|
636
|
+
const finalFee = estimateFee(
|
|
637
|
+
Math.max(selected.length, 1),
|
|
638
|
+
baseOutputCount + (withChange ? 1 : 0),
|
|
639
|
+
feeRate,
|
|
640
|
+
reserveBytes
|
|
641
|
+
);
|
|
642
|
+
throw new InsufficientFundsError(target + finalFee, available);
|
|
643
|
+
}
|
|
644
|
+
function sumValue(utxos) {
|
|
645
|
+
return utxos.reduce((s, u) => s + u.value, 0n);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/tx.ts
|
|
649
|
+
function bn(value) {
|
|
650
|
+
return new crypto.BN(value.toString());
|
|
651
|
+
}
|
|
652
|
+
function applyFeeRate(tx, feeRate) {
|
|
653
|
+
const rate = Number(feeRate);
|
|
654
|
+
if (typeof tx.feePerByte === "function") {
|
|
655
|
+
tx.feePerByte(rate);
|
|
656
|
+
} else if (typeof tx.feePerKb === "function") {
|
|
657
|
+
tx.feePerKb(rate * 1e3);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function buildTx(params) {
|
|
661
|
+
const {
|
|
662
|
+
address,
|
|
663
|
+
inputs,
|
|
664
|
+
outputs,
|
|
665
|
+
feeRate,
|
|
666
|
+
addChange = true,
|
|
667
|
+
network = "mainnet"
|
|
668
|
+
} = params;
|
|
669
|
+
const wifs = Array.isArray(params.wif) ? params.wif : [params.wif];
|
|
670
|
+
const privKeys = wifs.map((w) => PrivateKey.fromWIF(w));
|
|
671
|
+
const ownerP2pkh = p2pkhScript(address);
|
|
672
|
+
const tx = new Transaction();
|
|
673
|
+
applyFeeRate(tx, feeRate);
|
|
674
|
+
inputs.forEach((input, index) => {
|
|
675
|
+
const prevScript = input.script ?? ownerP2pkh;
|
|
676
|
+
tx.addInput(
|
|
677
|
+
new Transaction.Input({
|
|
678
|
+
prevTxId: input.txid,
|
|
679
|
+
outputIndex: input.vout,
|
|
680
|
+
script: new Script(),
|
|
681
|
+
output: new Transaction.Output({
|
|
682
|
+
script: Script.fromHex(prevScript),
|
|
683
|
+
satoshis: bn(input.value)
|
|
684
|
+
})
|
|
685
|
+
})
|
|
686
|
+
);
|
|
687
|
+
tx.setInputScript(index, (_tx, output) => {
|
|
688
|
+
const privKey = privKeys[index] ?? privKeys[0];
|
|
689
|
+
const sigType = SIGHASH_FORKID_ALL;
|
|
690
|
+
const sig = Transaction.Sighash.sign(
|
|
691
|
+
tx,
|
|
692
|
+
privKey,
|
|
693
|
+
sigType,
|
|
694
|
+
index,
|
|
695
|
+
output.script,
|
|
696
|
+
bn(input.value)
|
|
697
|
+
);
|
|
698
|
+
const spend = Script.empty().add(Buffer.concat([sig.toBuffer(), Buffer.from([sigType])])).add(privKey.toPublicKey().toBuffer());
|
|
699
|
+
if (params.setInputScript) {
|
|
700
|
+
const overridden = params.setInputScript(index, spend);
|
|
701
|
+
if (overridden) {
|
|
702
|
+
return typeof overridden === "string" ? overridden : overridden.toString();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return spend.toString();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
outputs.forEach(({ script, value }) => {
|
|
709
|
+
tx.addOutput(
|
|
710
|
+
new Transaction.Output({ script: Script.fromHex(script), satoshis: bn(value) })
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
if (addChange) {
|
|
714
|
+
tx.change(address);
|
|
715
|
+
const probe = tx.toString();
|
|
716
|
+
const sizeBytes = BigInt(probe.length / 2);
|
|
717
|
+
const fee = (sizeBytes + 20n) * feeRate;
|
|
718
|
+
tx.fee(Number(fee));
|
|
719
|
+
tx.change(address);
|
|
720
|
+
}
|
|
721
|
+
tx.sign(privKeys[0]);
|
|
722
|
+
if (typeof tx.seal === "function") tx.seal();
|
|
723
|
+
const hex = tx.toString();
|
|
724
|
+
const resolvedOutputs = tx.outputs.map((o) => ({
|
|
725
|
+
script: o.script.toHex(),
|
|
726
|
+
value: BigInt(o.satoshis?.toString?.() ?? o.satoshis)
|
|
727
|
+
}));
|
|
728
|
+
return { tx, hex, txid: tx.id, outputs: resolvedOutputs };
|
|
729
|
+
}
|
|
730
|
+
function buildRxdTransfer(params) {
|
|
731
|
+
const { address, wif, to, amount, utxos, feeRate, network = "mainnet" } = params;
|
|
732
|
+
if (amount <= 0n) throw new ValidationError("buildRxdTransfer: amount must be > 0");
|
|
733
|
+
const selection = selectRxdFunding(utxos, amount, feeRate, {
|
|
734
|
+
baseOutputCount: 1,
|
|
735
|
+
// the recipient output
|
|
736
|
+
withChange: true
|
|
737
|
+
});
|
|
738
|
+
const inputs = selection.inputs.map((u) => ({
|
|
739
|
+
txid: u.txid,
|
|
740
|
+
vout: u.vout,
|
|
741
|
+
value: u.value,
|
|
742
|
+
script: u.script || void 0
|
|
743
|
+
}));
|
|
744
|
+
return buildTx({
|
|
745
|
+
address,
|
|
746
|
+
wif,
|
|
747
|
+
inputs,
|
|
748
|
+
outputs: [{ script: p2pkhScript(to), value: amount }],
|
|
749
|
+
addChange: true,
|
|
750
|
+
feeRate,
|
|
751
|
+
network
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
var MAGIC = Buffer.from(GLYPH_MAGIC_BYTES);
|
|
755
|
+
function encodeGlyph(payload) {
|
|
756
|
+
const encoded = Buffer.from(encode(payload));
|
|
757
|
+
const revealScriptSig = new Script().add(MAGIC).add(encoded).toHex();
|
|
758
|
+
const payloadHash = Buffer.from(sha256d(encoded)).toString("hex");
|
|
759
|
+
return { revealScriptSig, payloadHash };
|
|
760
|
+
}
|
|
761
|
+
function commitScript(address, payloadHash, refType, network) {
|
|
762
|
+
const addr = Address.fromString(address);
|
|
763
|
+
const refOp = refType === 1 ? "OP_1" : "OP_2";
|
|
764
|
+
const script = new Script();
|
|
765
|
+
script.add(Opcode.OP_HASH256).add(Buffer.from(payloadHash, "hex")).add(Opcode.OP_EQUALVERIFY).add(MAGIC).add(Opcode.OP_EQUALVERIFY).add(
|
|
766
|
+
Script.fromASM(
|
|
767
|
+
`OP_INPUTINDEX OP_OUTPOINTTXHASH OP_INPUTINDEX OP_OUTPOINTINDEX OP_4 OP_NUM2BIN OP_CAT OP_REFTYPE_OUTPUT ${refOp} OP_NUMEQUALVERIFY`
|
|
768
|
+
)
|
|
769
|
+
).add(Script.buildPublicKeyHashOut(addr));
|
|
770
|
+
return script.toHex();
|
|
771
|
+
}
|
|
772
|
+
function ftScript(address, ref, network = "mainnet") {
|
|
773
|
+
const addr = Address.fromString(address);
|
|
774
|
+
const script = Script.buildPublicKeyHashOut(addr).add(
|
|
775
|
+
Script.fromASM(
|
|
776
|
+
`OP_STATESEPARATOR OP_PUSHINPUTREF ${ref} OP_REFOUTPUTCOUNT_OUTPUTS OP_INPUTINDEX OP_CODESCRIPTBYTECODE_UTXO OP_HASH256 OP_DUP OP_CODESCRIPTHASHVALUESUM_UTXOS OP_OVER OP_CODESCRIPTHASHVALUESUM_OUTPUTS OP_GREATERTHANOREQUAL OP_VERIFY OP_CODESCRIPTHASHOUTPUTCOUNT_OUTPUTS OP_NUMEQUALVERIFY`
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
return script.toHex();
|
|
780
|
+
}
|
|
781
|
+
function nftScript(address, ref, network = "mainnet") {
|
|
782
|
+
const addr = Address.fromString(address);
|
|
783
|
+
const script = Script.fromASM(`OP_PUSHINPUTREFSINGLETON ${ref} OP_DROP`).add(
|
|
784
|
+
Script.buildPublicKeyHashOut(addr)
|
|
785
|
+
);
|
|
786
|
+
return script.toHex();
|
|
787
|
+
}
|
|
788
|
+
function parseTokenRef(scriptHex) {
|
|
789
|
+
const bytes = Buffer.from(scriptHex, "hex");
|
|
790
|
+
let pos = 0;
|
|
791
|
+
while (pos < bytes.length) {
|
|
792
|
+
const op = bytes[pos];
|
|
793
|
+
if (op >= INPUT_REF_OP_MIN && op <= INPUT_REF_OP_MAX) {
|
|
794
|
+
if (pos + 1 + 36 > bytes.length) return null;
|
|
795
|
+
return bytes.subarray(pos + 1, pos + 1 + 36).toString("hex");
|
|
796
|
+
}
|
|
797
|
+
if (op >= 1 && op <= 75) pos += 1 + op;
|
|
798
|
+
else if (op === 76) pos += 2 + (bytes[pos + 1] ?? 0);
|
|
799
|
+
else if (op === 77) pos += 3 + ((bytes[pos + 1] ?? 0) | (bytes[pos + 2] ?? 0) << 8);
|
|
800
|
+
else if (op === 78)
|
|
801
|
+
pos += 5 + (((bytes[pos + 1] ?? 0) | (bytes[pos + 2] ?? 0) << 8 | (bytes[pos + 3] ?? 0) << 16 | (bytes[pos + 4] ?? 0) << 24) >>> 0);
|
|
802
|
+
else pos += 1;
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
function rebindOwner(scriptHex, toAddress, network) {
|
|
807
|
+
const newPkh = Address.fromString(toAddress).hashBuffer.toString("hex");
|
|
808
|
+
const re = /76a914[0-9a-f]{40}88ac/i;
|
|
809
|
+
const matches = scriptHex.match(new RegExp(re, "gi"));
|
|
810
|
+
if (!matches || matches.length !== 1) {
|
|
811
|
+
throw new ValidationError(
|
|
812
|
+
"rebindOwner: expected exactly one owner P2PKH in the token script"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return scriptHex.replace(re, `76a914${newPkh}88ac`);
|
|
816
|
+
}
|
|
817
|
+
async function commitReveal(base, payload, refType, buildRevealOutputScript, tokenOutputValue) {
|
|
818
|
+
const network = base.network ?? "mainnet";
|
|
819
|
+
const feeRate = base.feeRate ?? MIN_RELAY_FEE_RATE[network];
|
|
820
|
+
const { revealScriptSig, payloadHash } = encodeGlyph(payload);
|
|
821
|
+
const commit = commitScript(base.address, payloadHash, refType);
|
|
822
|
+
const envelopeBytes = BigInt(revealScriptSig.length / 2);
|
|
823
|
+
const revealReserve = tokenOutputValue + estimateRevealFee(feeRate, envelopeBytes);
|
|
824
|
+
const commitTarget = TOKEN_OUTPUT_VALUE + revealReserve;
|
|
825
|
+
const selection = selectRxdFunding(base.fundingUtxos, commitTarget, feeRate, {
|
|
826
|
+
baseOutputCount: 1,
|
|
827
|
+
// the commit carrier output
|
|
828
|
+
withChange: true
|
|
829
|
+
});
|
|
830
|
+
const commitInputs = selection.inputs.map((u) => ({
|
|
831
|
+
txid: u.txid,
|
|
832
|
+
vout: u.vout,
|
|
833
|
+
value: u.value,
|
|
834
|
+
script: u.script || void 0
|
|
835
|
+
}));
|
|
836
|
+
const commitTx = buildTx({
|
|
837
|
+
address: base.address,
|
|
838
|
+
wif: base.wif,
|
|
839
|
+
inputs: commitInputs,
|
|
840
|
+
outputs: [{ script: commit, value: TOKEN_OUTPUT_VALUE }],
|
|
841
|
+
addChange: true,
|
|
842
|
+
feeRate,
|
|
843
|
+
network
|
|
844
|
+
});
|
|
845
|
+
const changeIndex = commitTx.outputs.length - 1;
|
|
846
|
+
const change = commitTx.outputs[changeIndex];
|
|
847
|
+
if (change.value <= 0n) {
|
|
848
|
+
throw new ValidationError(
|
|
849
|
+
"commitReveal: commit produced no change to fund the reveal"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
await base.client.broadcastTx(commitTx.hex);
|
|
853
|
+
const ref = packRef(commitTx.txid, 0);
|
|
854
|
+
const tokenScript = buildRevealOutputScript(ref);
|
|
855
|
+
const revealTx = buildTx({
|
|
856
|
+
address: base.address,
|
|
857
|
+
wif: base.wif,
|
|
858
|
+
inputs: [
|
|
859
|
+
// input 0: the commit carrier — its scriptSig carries the envelope.
|
|
860
|
+
{ txid: commitTx.txid, vout: 0, value: TOKEN_OUTPUT_VALUE, script: commit },
|
|
861
|
+
// input 1: the commit change, funds the token output + fee.
|
|
862
|
+
{
|
|
863
|
+
txid: commitTx.txid,
|
|
864
|
+
vout: changeIndex,
|
|
865
|
+
value: change.value,
|
|
866
|
+
script: change.script
|
|
867
|
+
}
|
|
868
|
+
],
|
|
869
|
+
outputs: [{ script: tokenScript, value: tokenOutputValue }],
|
|
870
|
+
addChange: true,
|
|
871
|
+
feeRate,
|
|
872
|
+
network,
|
|
873
|
+
setInputScript: (index, defaultScript) => {
|
|
874
|
+
if (index === 0) {
|
|
875
|
+
return defaultScript.toHex() + revealScriptSig;
|
|
876
|
+
}
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
await base.client.broadcastTx(revealTx.hex);
|
|
881
|
+
return {
|
|
882
|
+
ref,
|
|
883
|
+
refDisplay: `${commitTx.txid}:0`,
|
|
884
|
+
commitTxid: commitTx.txid,
|
|
885
|
+
revealTxid: revealTx.txid
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function estimateRevealFee(feeRate, envelopeBytes = 0n) {
|
|
889
|
+
return (10n + 2n * 148n + 2n * 200n + envelopeBytes) * feeRate;
|
|
890
|
+
}
|
|
891
|
+
async function mintFT(params) {
|
|
892
|
+
if (!params.ticker) throw new ValidationError("mintFT: ticker is required");
|
|
893
|
+
if (params.supply <= 0n) throw new ValidationError("mintFT: supply must be > 0");
|
|
894
|
+
const payload = {
|
|
895
|
+
v: 2,
|
|
896
|
+
p: [GLYPH_PROTOCOL.FT],
|
|
897
|
+
ticker: params.ticker,
|
|
898
|
+
...params.metadata
|
|
899
|
+
};
|
|
900
|
+
return commitReveal(
|
|
901
|
+
params,
|
|
902
|
+
payload,
|
|
903
|
+
1,
|
|
904
|
+
(ref) => ftScript(params.address, ref, params.network ?? "mainnet"),
|
|
905
|
+
params.supply
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
async function mintNFT(params) {
|
|
909
|
+
const p = params.mutable ? [GLYPH_PROTOCOL.NFT, GLYPH_PROTOCOL.MUTABLE] : [GLYPH_PROTOCOL.NFT];
|
|
910
|
+
const payload = { v: 2, p, ...params.metadata };
|
|
911
|
+
const value = params.outputValue ?? DUST_LIMIT;
|
|
912
|
+
return commitReveal(
|
|
913
|
+
params,
|
|
914
|
+
payload,
|
|
915
|
+
2,
|
|
916
|
+
(ref) => nftScript(params.address, ref, params.network ?? "mainnet"),
|
|
917
|
+
value
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
async function transferToken(params) {
|
|
921
|
+
const network = params.network ?? "mainnet";
|
|
922
|
+
const feeRate = params.feeRate ?? MIN_RELAY_FEE_RATE[network];
|
|
923
|
+
const { tokenUtxo } = params;
|
|
924
|
+
if (!tokenUtxo.script) {
|
|
925
|
+
throw new ValidationError(
|
|
926
|
+
"transferToken: tokenUtxo.script is required to rebuild the token output"
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const newScript = rebindOwner(tokenUtxo.script, params.toAddress);
|
|
930
|
+
const ref = parseTokenRef(tokenUtxo.script);
|
|
931
|
+
const selection = selectRxdFunding(params.fundingUtxos, 0n, feeRate, {
|
|
932
|
+
baseOutputCount: 1,
|
|
933
|
+
// the token output
|
|
934
|
+
extraInputBytes: 148n,
|
|
935
|
+
// the token input we add below
|
|
936
|
+
withChange: true
|
|
937
|
+
});
|
|
938
|
+
const inputs = [
|
|
939
|
+
{
|
|
940
|
+
txid: tokenUtxo.txid,
|
|
941
|
+
vout: tokenUtxo.vout,
|
|
942
|
+
value: tokenUtxo.value,
|
|
943
|
+
script: tokenUtxo.script
|
|
944
|
+
},
|
|
945
|
+
...selection.inputs.map((u) => ({
|
|
946
|
+
txid: u.txid,
|
|
947
|
+
vout: u.vout,
|
|
948
|
+
value: u.value,
|
|
949
|
+
script: u.script || void 0
|
|
950
|
+
}))
|
|
951
|
+
];
|
|
952
|
+
const built = buildTx({
|
|
953
|
+
address: params.address,
|
|
954
|
+
wif: params.wif,
|
|
955
|
+
inputs,
|
|
956
|
+
outputs: [{ script: newScript, value: tokenUtxo.value }],
|
|
957
|
+
addChange: true,
|
|
958
|
+
feeRate,
|
|
959
|
+
network
|
|
960
|
+
});
|
|
961
|
+
await params.client.broadcastTx(built.hex);
|
|
962
|
+
return { txid: built.txid, hex: built.hex, ref };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/wave.ts
|
|
966
|
+
function waveLabel(name) {
|
|
967
|
+
const trimmed = name.trim().toLowerCase();
|
|
968
|
+
if (!trimmed || trimmed.startsWith(".") || trimmed.endsWith(".")) {
|
|
969
|
+
throw new ValidationError(`waveLabel: invalid WAVE name ${JSON.stringify(name)}`);
|
|
970
|
+
}
|
|
971
|
+
const lastDot = trimmed.lastIndexOf(".");
|
|
972
|
+
const label = lastDot > 0 ? trimmed.slice(0, lastDot) : trimmed;
|
|
973
|
+
if (!label || label.length > 253) {
|
|
974
|
+
throw new ValidationError(`waveLabel: invalid WAVE name ${JSON.stringify(name)}`);
|
|
975
|
+
}
|
|
976
|
+
return label;
|
|
977
|
+
}
|
|
978
|
+
async function waveResolve(name, options = {}) {
|
|
979
|
+
const label = waveLabel(name);
|
|
980
|
+
const base = (options.restBase ?? DEFAULT_REST_BASE).replace(/\/+$/, "");
|
|
981
|
+
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
982
|
+
if (typeof doFetch !== "function") {
|
|
983
|
+
throw new ValidationError(
|
|
984
|
+
"waveResolve: no fetch implementation available; pass options.fetchImpl"
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
const url = `${base}/wave/resolve/${encodeURIComponent(label)}`;
|
|
988
|
+
const res = await doFetch(url, { signal: options.signal });
|
|
989
|
+
if (!res.ok) {
|
|
990
|
+
throw new ValidationError(
|
|
991
|
+
`waveResolve: ${label} -> HTTP ${res.status} ${res.statusText}`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
const data = await res.json();
|
|
995
|
+
const registered = data?.available !== true && data?.resolved !== false;
|
|
996
|
+
const zone = data?.zone ?? {};
|
|
997
|
+
return {
|
|
998
|
+
name: data?.name ?? label,
|
|
999
|
+
registered,
|
|
1000
|
+
address: data?.target ?? zone?.address ?? void 0,
|
|
1001
|
+
ref: data?.ref ?? void 0,
|
|
1002
|
+
owner: data?.owner ?? void 0,
|
|
1003
|
+
expires: zone?.expires ?? void 0,
|
|
1004
|
+
records: zone?.records ?? void 0,
|
|
1005
|
+
raw: data
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
async function waveResolveAddress(name, options = {}) {
|
|
1009
|
+
const r = await waveResolve(name, options);
|
|
1010
|
+
return r.registered ? r.address ?? null : null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export { DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, ElectrumClient, ElectrumError, GLYPH_PROTOCOL, HDWallet, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, TokenBurnGuardError, ValidationError, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
|
|
1014
|
+
//# sourceMappingURL=index.js.map
|
|
1015
|
+
//# sourceMappingURL=index.js.map
|