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