@unicitylabs/sphere-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.
@@ -0,0 +1,1455 @@
1
+ // l1/types.ts
2
+ function parsePathComponents(path) {
3
+ const match = path.match(/m\/\d+'\/\d+'\/\d+'\/(\d+)\/(\d+)/);
4
+ if (!match) return null;
5
+ return { chain: parseInt(match[1], 10), index: parseInt(match[2], 10) };
6
+ }
7
+ function isChangePath(path) {
8
+ const parsed = parsePathComponents(path);
9
+ return parsed?.chain === 1;
10
+ }
11
+ function getIndexFromPath(path) {
12
+ const parsed = parsePathComponents(path);
13
+ return parsed?.index ?? 0;
14
+ }
15
+ function pathToDOMId(path) {
16
+ return path.replace(/'/g, "h").replace(/\//g, "-");
17
+ }
18
+ function domIdToPath(encoded) {
19
+ const parts = encoded.split("-");
20
+ return parts.map((part, idx) => {
21
+ if (idx === 0) return part;
22
+ return part.endsWith("h") ? `${part.slice(0, -1)}'` : part;
23
+ }).join("/");
24
+ }
25
+
26
+ // core/bech32.ts
27
+ var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
28
+ var GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
29
+ function convertBits(data, fromBits, toBits, pad) {
30
+ let acc = 0;
31
+ let bits = 0;
32
+ const ret = [];
33
+ const maxv = (1 << toBits) - 1;
34
+ for (let i = 0; i < data.length; i++) {
35
+ const value = data[i];
36
+ if (value < 0 || value >> fromBits !== 0) return null;
37
+ acc = acc << fromBits | value;
38
+ bits += fromBits;
39
+ while (bits >= toBits) {
40
+ bits -= toBits;
41
+ ret.push(acc >> bits & maxv);
42
+ }
43
+ }
44
+ if (pad) {
45
+ if (bits > 0) {
46
+ ret.push(acc << toBits - bits & maxv);
47
+ }
48
+ } else if (bits >= fromBits || acc << toBits - bits & maxv) {
49
+ return null;
50
+ }
51
+ return ret;
52
+ }
53
+ function hrpExpand(hrp) {
54
+ const ret = [];
55
+ for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) >> 5);
56
+ ret.push(0);
57
+ for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) & 31);
58
+ return ret;
59
+ }
60
+ function bech32Polymod(values) {
61
+ let chk = 1;
62
+ for (let p = 0; p < values.length; p++) {
63
+ const top = chk >> 25;
64
+ chk = (chk & 33554431) << 5 ^ values[p];
65
+ for (let i = 0; i < 5; i++) {
66
+ if (top >> i & 1) chk ^= GENERATOR[i];
67
+ }
68
+ }
69
+ return chk;
70
+ }
71
+ function bech32Checksum(hrp, data) {
72
+ const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
73
+ const mod = bech32Polymod(values) ^ 1;
74
+ const ret = [];
75
+ for (let p = 0; p < 6; p++) {
76
+ ret.push(mod >> 5 * (5 - p) & 31);
77
+ }
78
+ return ret;
79
+ }
80
+ function encodeBech32(hrp, version, program) {
81
+ if (version < 0 || version > 16) {
82
+ throw new Error("Invalid witness version");
83
+ }
84
+ const converted = convertBits(Array.from(program), 8, 5, true);
85
+ if (!converted) {
86
+ throw new Error("Failed to convert bits");
87
+ }
88
+ const data = [version].concat(converted);
89
+ const checksum = bech32Checksum(hrp, data);
90
+ const combined = data.concat(checksum);
91
+ let out = hrp + "1";
92
+ for (let i = 0; i < combined.length; i++) {
93
+ out += CHARSET[combined[i]];
94
+ }
95
+ return out;
96
+ }
97
+ function decodeBech32(addr) {
98
+ addr = addr.toLowerCase();
99
+ const pos = addr.lastIndexOf("1");
100
+ if (pos < 1) return null;
101
+ const hrp = addr.substring(0, pos);
102
+ const dataStr = addr.substring(pos + 1);
103
+ const data = [];
104
+ for (let i = 0; i < dataStr.length; i++) {
105
+ const val = CHARSET.indexOf(dataStr[i]);
106
+ if (val === -1) return null;
107
+ data.push(val);
108
+ }
109
+ const checksum = bech32Checksum(hrp, data.slice(0, -6));
110
+ for (let i = 0; i < 6; i++) {
111
+ if (checksum[i] !== data[data.length - 6 + i]) {
112
+ return null;
113
+ }
114
+ }
115
+ const version = data[0];
116
+ const program = convertBits(data.slice(1, -6), 5, 8, false);
117
+ if (!program) return null;
118
+ return {
119
+ hrp,
120
+ witnessVersion: version,
121
+ data: Uint8Array.from(program)
122
+ };
123
+ }
124
+ var createBech32 = encodeBech32;
125
+
126
+ // l1/addressToScriptHash.ts
127
+ import CryptoJS from "crypto-js";
128
+ function bytesToHex(buf) {
129
+ return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
130
+ }
131
+ function addressToScriptHash(address) {
132
+ const decoded = decodeBech32(address);
133
+ if (!decoded) throw new Error("Invalid bech32 address: " + address);
134
+ const scriptHex = "0014" + bytesToHex(decoded.data);
135
+ const sha = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(scriptHex)).toString();
136
+ return sha.match(/../g).reverse().join("");
137
+ }
138
+
139
+ // core/crypto.ts
140
+ import * as bip39 from "bip39";
141
+ import CryptoJS2 from "crypto-js";
142
+ import elliptic from "elliptic";
143
+ var ec = new elliptic.ec("secp256k1");
144
+ var CURVE_ORDER = BigInt(
145
+ "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
146
+ );
147
+ function generateMasterKey(seedHex) {
148
+ const I = CryptoJS2.HmacSHA512(
149
+ CryptoJS2.enc.Hex.parse(seedHex),
150
+ CryptoJS2.enc.Utf8.parse("Bitcoin seed")
151
+ ).toString();
152
+ const IL = I.substring(0, 64);
153
+ const IR = I.substring(64);
154
+ const masterKeyBigInt = BigInt("0x" + IL);
155
+ if (masterKeyBigInt === 0n || masterKeyBigInt >= CURVE_ORDER) {
156
+ throw new Error("Invalid master key generated");
157
+ }
158
+ return {
159
+ privateKey: IL,
160
+ chainCode: IR
161
+ };
162
+ }
163
+ function deriveChildKey(parentPrivKey, parentChainCode, index) {
164
+ const isHardened = index >= 2147483648;
165
+ let data;
166
+ if (isHardened) {
167
+ const indexHex = index.toString(16).padStart(8, "0");
168
+ data = "00" + parentPrivKey + indexHex;
169
+ } else {
170
+ const keyPair = ec.keyFromPrivate(parentPrivKey, "hex");
171
+ const compressedPubKey = keyPair.getPublic(true, "hex");
172
+ const indexHex = index.toString(16).padStart(8, "0");
173
+ data = compressedPubKey + indexHex;
174
+ }
175
+ const I = CryptoJS2.HmacSHA512(
176
+ CryptoJS2.enc.Hex.parse(data),
177
+ CryptoJS2.enc.Hex.parse(parentChainCode)
178
+ ).toString();
179
+ const IL = I.substring(0, 64);
180
+ const IR = I.substring(64);
181
+ const ilBigInt = BigInt("0x" + IL);
182
+ const parentKeyBigInt = BigInt("0x" + parentPrivKey);
183
+ if (ilBigInt >= CURVE_ORDER) {
184
+ throw new Error("Invalid key: IL >= curve order");
185
+ }
186
+ const childKeyBigInt = (ilBigInt + parentKeyBigInt) % CURVE_ORDER;
187
+ if (childKeyBigInt === 0n) {
188
+ throw new Error("Invalid key: child key is zero");
189
+ }
190
+ const childPrivKey = childKeyBigInt.toString(16).padStart(64, "0");
191
+ return {
192
+ privateKey: childPrivKey,
193
+ chainCode: IR
194
+ };
195
+ }
196
+ function deriveKeyAtPath(masterPrivKey, masterChainCode, path) {
197
+ const pathParts = path.replace("m/", "").split("/");
198
+ let currentKey = masterPrivKey;
199
+ let currentChainCode = masterChainCode;
200
+ for (const part of pathParts) {
201
+ const isHardened = part.endsWith("'") || part.endsWith("h");
202
+ const indexStr = part.replace(/['h]$/, "");
203
+ let index = parseInt(indexStr, 10);
204
+ if (isHardened) {
205
+ index += 2147483648;
206
+ }
207
+ const derived = deriveChildKey(currentKey, currentChainCode, index);
208
+ currentKey = derived.privateKey;
209
+ currentChainCode = derived.chainCode;
210
+ }
211
+ return {
212
+ privateKey: currentKey,
213
+ chainCode: currentChainCode
214
+ };
215
+ }
216
+ function getPublicKey(privateKey, compressed = true) {
217
+ const keyPair = ec.keyFromPrivate(privateKey, "hex");
218
+ return keyPair.getPublic(compressed, "hex");
219
+ }
220
+ function sha256(data, inputEncoding = "hex") {
221
+ const parsed = inputEncoding === "hex" ? CryptoJS2.enc.Hex.parse(data) : CryptoJS2.enc.Utf8.parse(data);
222
+ return CryptoJS2.SHA256(parsed).toString();
223
+ }
224
+ function ripemd160(data, inputEncoding = "hex") {
225
+ const parsed = inputEncoding === "hex" ? CryptoJS2.enc.Hex.parse(data) : CryptoJS2.enc.Utf8.parse(data);
226
+ return CryptoJS2.RIPEMD160(parsed).toString();
227
+ }
228
+ function hash160(data) {
229
+ const sha = sha256(data, "hex");
230
+ return ripemd160(sha, "hex");
231
+ }
232
+ var computeHash160 = hash160;
233
+ function hash160ToBytes(hash160Hex) {
234
+ const matches = hash160Hex.match(/../g);
235
+ if (!matches) return new Uint8Array(0);
236
+ return Uint8Array.from(matches.map((x) => parseInt(x, 16)));
237
+ }
238
+ function publicKeyToAddress(publicKey, prefix = "alpha", witnessVersion = 0) {
239
+ const pubKeyHash = hash160(publicKey);
240
+ const programBytes = hash160ToBytes(pubKeyHash);
241
+ return encodeBech32(prefix, witnessVersion, programBytes);
242
+ }
243
+ function privateKeyToAddressInfo(privateKey, prefix = "alpha") {
244
+ const publicKey = getPublicKey(privateKey);
245
+ const address = publicKeyToAddress(publicKey, prefix);
246
+ return { address, publicKey };
247
+ }
248
+ function generateAddressInfo(privateKey, index, path, prefix = "alpha") {
249
+ const { address, publicKey } = privateKeyToAddressInfo(privateKey, prefix);
250
+ return {
251
+ privateKey,
252
+ publicKey,
253
+ address,
254
+ path,
255
+ index
256
+ };
257
+ }
258
+
259
+ // l1/crypto.ts
260
+ import CryptoJS3 from "crypto-js";
261
+ var SALT = "alpha_wallet_salt";
262
+ var PBKDF2_ITERATIONS = 1e5;
263
+ function encrypt(text, password) {
264
+ return CryptoJS3.AES.encrypt(text, password).toString();
265
+ }
266
+ function decrypt(encrypted, password) {
267
+ const bytes = CryptoJS3.AES.decrypt(encrypted, password);
268
+ return bytes.toString(CryptoJS3.enc.Utf8);
269
+ }
270
+ function generatePrivateKey() {
271
+ return CryptoJS3.lib.WordArray.random(32).toString();
272
+ }
273
+ function encryptWallet(masterPrivateKey, password) {
274
+ const passwordKey = CryptoJS3.PBKDF2(password, SALT, {
275
+ keySize: 256 / 32,
276
+ iterations: PBKDF2_ITERATIONS
277
+ }).toString();
278
+ const encrypted = CryptoJS3.AES.encrypt(
279
+ masterPrivateKey,
280
+ passwordKey
281
+ ).toString();
282
+ return encrypted;
283
+ }
284
+ function decryptWallet(encryptedData, password) {
285
+ const passwordKey = CryptoJS3.PBKDF2(password, SALT, {
286
+ keySize: 256 / 32,
287
+ iterations: PBKDF2_ITERATIONS
288
+ }).toString();
289
+ const decrypted = CryptoJS3.AES.decrypt(encryptedData, passwordKey);
290
+ return decrypted.toString(CryptoJS3.enc.Utf8);
291
+ }
292
+ function hexToWIF(hexKey) {
293
+ const versionByte = "80";
294
+ const extendedKey = versionByte + hexKey;
295
+ const hash1 = CryptoJS3.SHA256(CryptoJS3.enc.Hex.parse(extendedKey)).toString();
296
+ const hash2 = CryptoJS3.SHA256(CryptoJS3.enc.Hex.parse(hash1)).toString();
297
+ const checksum = hash2.substring(0, 8);
298
+ const finalHex = extendedKey + checksum;
299
+ return base58Encode(finalHex);
300
+ }
301
+ function base58Encode(hex) {
302
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
303
+ let num = BigInt("0x" + hex);
304
+ let encoded = "";
305
+ while (num > 0n) {
306
+ const remainder = Number(num % 58n);
307
+ num = num / 58n;
308
+ encoded = ALPHABET[remainder] + encoded;
309
+ }
310
+ for (let i = 0; i < hex.length && hex.substring(i, i + 2) === "00"; i += 2) {
311
+ encoded = "1" + encoded;
312
+ }
313
+ return encoded;
314
+ }
315
+
316
+ // l1/address.ts
317
+ import CryptoJS4 from "crypto-js";
318
+ var deriveChildKeyBIP32 = deriveChildKey;
319
+ var deriveKeyAtPath2 = deriveKeyAtPath;
320
+ function generateMasterKeyFromSeed(seedHex) {
321
+ const result = generateMasterKey(seedHex);
322
+ return {
323
+ masterPrivateKey: result.privateKey,
324
+ masterChainCode: result.chainCode
325
+ };
326
+ }
327
+ function generateHDAddressBIP32(masterPriv, chainCode, index, basePath = "m/44'/0'/0'", isChange = false) {
328
+ const chain = isChange ? 1 : 0;
329
+ const fullPath = `${basePath}/${chain}/${index}`;
330
+ const derived = deriveKeyAtPath(masterPriv, chainCode, fullPath);
331
+ return generateAddressInfo(derived.privateKey, index, fullPath);
332
+ }
333
+ function generateAddressFromMasterKey(masterPrivateKey, index) {
334
+ const derivationPath = `m/44'/0'/${index}'`;
335
+ const hmacInput = CryptoJS4.enc.Hex.parse(masterPrivateKey);
336
+ const hmacKey = CryptoJS4.enc.Utf8.parse(derivationPath);
337
+ const hmacOutput = CryptoJS4.HmacSHA512(hmacInput, hmacKey).toString();
338
+ const childPrivateKey = hmacOutput.substring(0, 64);
339
+ return generateAddressInfo(childPrivateKey, index, derivationPath);
340
+ }
341
+ function deriveChildKey2(masterPriv, chainCode, index) {
342
+ const data = masterPriv + index.toString(16).padStart(8, "0");
343
+ const I = CryptoJS4.HmacSHA512(
344
+ CryptoJS4.enc.Hex.parse(data),
345
+ CryptoJS4.enc.Hex.parse(chainCode)
346
+ ).toString();
347
+ return {
348
+ privateKey: I.substring(0, 64),
349
+ nextChainCode: I.substring(64)
350
+ };
351
+ }
352
+ function generateHDAddress(masterPriv, chainCode, index) {
353
+ const child = deriveChildKey2(masterPriv, chainCode, index);
354
+ const path = `m/44'/0'/0'/${index}`;
355
+ return generateAddressInfo(child.privateKey, index, path);
356
+ }
357
+
358
+ // l1/network.ts
359
+ var DEFAULT_ENDPOINT = "wss://fulcrum.unicity.network:50004";
360
+ var ws = null;
361
+ var isConnected = false;
362
+ var isConnecting = false;
363
+ var requestId = 0;
364
+ var intentionalClose = false;
365
+ var reconnectAttempts = 0;
366
+ var isBlockSubscribed = false;
367
+ var lastBlockHeader = null;
368
+ var pending = {};
369
+ var blockSubscribers = [];
370
+ var connectionCallbacks = [];
371
+ var MAX_RECONNECT_ATTEMPTS = 10;
372
+ var BASE_DELAY = 2e3;
373
+ var MAX_DELAY = 6e4;
374
+ var RPC_TIMEOUT = 3e4;
375
+ var CONNECTION_TIMEOUT = 3e4;
376
+ function isWebSocketConnected() {
377
+ return isConnected && ws !== null && ws.readyState === WebSocket.OPEN;
378
+ }
379
+ function waitForConnection() {
380
+ if (isWebSocketConnected()) {
381
+ return Promise.resolve();
382
+ }
383
+ return new Promise((resolve, reject) => {
384
+ const callback = {
385
+ resolve: () => {
386
+ if (callback.timeoutId) clearTimeout(callback.timeoutId);
387
+ resolve();
388
+ },
389
+ reject: (err) => {
390
+ if (callback.timeoutId) clearTimeout(callback.timeoutId);
391
+ reject(err);
392
+ }
393
+ };
394
+ callback.timeoutId = setTimeout(() => {
395
+ const idx = connectionCallbacks.indexOf(callback);
396
+ if (idx > -1) connectionCallbacks.splice(idx, 1);
397
+ reject(new Error("Connection timeout"));
398
+ }, CONNECTION_TIMEOUT);
399
+ connectionCallbacks.push(callback);
400
+ });
401
+ }
402
+ function connect(endpoint = DEFAULT_ENDPOINT) {
403
+ if (isConnected) {
404
+ return Promise.resolve();
405
+ }
406
+ if (isConnecting) {
407
+ return waitForConnection();
408
+ }
409
+ isConnecting = true;
410
+ return new Promise((resolve, reject) => {
411
+ let hasResolved = false;
412
+ try {
413
+ ws = new WebSocket(endpoint);
414
+ } catch (err) {
415
+ console.error("[L1] WebSocket constructor threw exception:", err);
416
+ isConnecting = false;
417
+ reject(err);
418
+ return;
419
+ }
420
+ ws.onopen = () => {
421
+ isConnected = true;
422
+ isConnecting = false;
423
+ reconnectAttempts = 0;
424
+ hasResolved = true;
425
+ resolve();
426
+ connectionCallbacks.forEach((cb) => {
427
+ if (cb.timeoutId) clearTimeout(cb.timeoutId);
428
+ cb.resolve();
429
+ });
430
+ connectionCallbacks.length = 0;
431
+ };
432
+ ws.onclose = () => {
433
+ isConnected = false;
434
+ isBlockSubscribed = false;
435
+ Object.values(pending).forEach((req) => {
436
+ if (req.timeoutId) clearTimeout(req.timeoutId);
437
+ req.reject(new Error("WebSocket connection closed"));
438
+ });
439
+ Object.keys(pending).forEach((key) => delete pending[Number(key)]);
440
+ if (intentionalClose) {
441
+ intentionalClose = false;
442
+ isConnecting = false;
443
+ reconnectAttempts = 0;
444
+ if (!hasResolved) {
445
+ hasResolved = true;
446
+ reject(new Error("WebSocket connection closed intentionally"));
447
+ }
448
+ return;
449
+ }
450
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
451
+ console.error("[L1] Max reconnect attempts reached. Giving up.");
452
+ isConnecting = false;
453
+ const error = new Error("Max reconnect attempts reached");
454
+ connectionCallbacks.forEach((cb) => {
455
+ if (cb.timeoutId) clearTimeout(cb.timeoutId);
456
+ cb.reject(error);
457
+ });
458
+ connectionCallbacks.length = 0;
459
+ if (!hasResolved) {
460
+ hasResolved = true;
461
+ reject(error);
462
+ }
463
+ return;
464
+ }
465
+ const delay = Math.min(BASE_DELAY * Math.pow(2, reconnectAttempts), MAX_DELAY);
466
+ reconnectAttempts++;
467
+ console.warn(
468
+ `[L1] WebSocket closed unexpectedly. Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`
469
+ );
470
+ setTimeout(() => {
471
+ connect(endpoint).then(() => {
472
+ if (!hasResolved) {
473
+ hasResolved = true;
474
+ resolve();
475
+ }
476
+ }).catch((err) => {
477
+ if (!hasResolved) {
478
+ hasResolved = true;
479
+ reject(err);
480
+ }
481
+ });
482
+ }, delay);
483
+ };
484
+ ws.onerror = (err) => {
485
+ console.error("[L1] WebSocket error:", err);
486
+ };
487
+ ws.onmessage = (msg) => handleMessage(msg);
488
+ });
489
+ }
490
+ function handleMessage(event) {
491
+ const data = JSON.parse(event.data);
492
+ if (data.id && pending[data.id]) {
493
+ const request = pending[data.id];
494
+ delete pending[data.id];
495
+ if (data.error) {
496
+ request.reject(data.error);
497
+ } else {
498
+ request.resolve(data.result);
499
+ }
500
+ }
501
+ if (data.method === "blockchain.headers.subscribe") {
502
+ const header = data.params[0];
503
+ lastBlockHeader = header;
504
+ blockSubscribers.forEach((cb) => cb(header));
505
+ }
506
+ }
507
+ async function rpc(method, params = []) {
508
+ if (!isConnected && !isConnecting) {
509
+ await connect();
510
+ }
511
+ if (!isWebSocketConnected()) {
512
+ await waitForConnection();
513
+ }
514
+ return new Promise((resolve, reject) => {
515
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
516
+ return reject(new Error("WebSocket not connected (OPEN)"));
517
+ }
518
+ const id = ++requestId;
519
+ const timeoutId = setTimeout(() => {
520
+ if (pending[id]) {
521
+ delete pending[id];
522
+ reject(new Error(`RPC timeout: ${method}`));
523
+ }
524
+ }, RPC_TIMEOUT);
525
+ pending[id] = {
526
+ resolve: (result) => {
527
+ clearTimeout(timeoutId);
528
+ resolve(result);
529
+ },
530
+ reject: (err) => {
531
+ clearTimeout(timeoutId);
532
+ reject(err);
533
+ },
534
+ timeoutId
535
+ };
536
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
537
+ });
538
+ }
539
+ async function getUtxo(address) {
540
+ const scripthash = addressToScriptHash(address);
541
+ const result = await rpc("blockchain.scripthash.listunspent", [scripthash]);
542
+ if (!Array.isArray(result)) {
543
+ console.warn("listunspent returned non-array:", result);
544
+ return [];
545
+ }
546
+ return result.map((u) => ({
547
+ tx_hash: u.tx_hash,
548
+ tx_pos: u.tx_pos,
549
+ value: u.value,
550
+ height: u.height,
551
+ address
552
+ }));
553
+ }
554
+ async function getBalance(address) {
555
+ const scriptHash = addressToScriptHash(address);
556
+ const result = await rpc("blockchain.scripthash.get_balance", [scriptHash]);
557
+ const confirmed = result.confirmed || 0;
558
+ const unconfirmed = result.unconfirmed || 0;
559
+ const totalSats = confirmed + unconfirmed;
560
+ const alpha = totalSats / 1e8;
561
+ return alpha;
562
+ }
563
+ async function broadcast(rawHex) {
564
+ return await rpc("blockchain.transaction.broadcast", [rawHex]);
565
+ }
566
+ async function subscribeBlocks(cb) {
567
+ if (!isConnected && !isConnecting) {
568
+ await connect();
569
+ }
570
+ if (!isWebSocketConnected()) {
571
+ await waitForConnection();
572
+ }
573
+ blockSubscribers.push(cb);
574
+ if (!isBlockSubscribed) {
575
+ isBlockSubscribed = true;
576
+ const header = await rpc("blockchain.headers.subscribe", []);
577
+ if (header) {
578
+ lastBlockHeader = header;
579
+ blockSubscribers.forEach((subscriber) => subscriber(header));
580
+ }
581
+ } else if (lastBlockHeader) {
582
+ cb(lastBlockHeader);
583
+ }
584
+ return () => {
585
+ const index = blockSubscribers.indexOf(cb);
586
+ if (index > -1) {
587
+ blockSubscribers.splice(index, 1);
588
+ }
589
+ };
590
+ }
591
+ async function getTransactionHistory(address) {
592
+ const scriptHash = addressToScriptHash(address);
593
+ const result = await rpc("blockchain.scripthash.get_history", [scriptHash]);
594
+ if (!Array.isArray(result)) {
595
+ console.warn("get_history returned non-array:", result);
596
+ return [];
597
+ }
598
+ return result;
599
+ }
600
+ async function getTransaction(txid) {
601
+ return await rpc("blockchain.transaction.get", [txid, true]);
602
+ }
603
+ async function getBlockHeader(height) {
604
+ return await rpc("blockchain.block.header", [height, height]);
605
+ }
606
+ async function getCurrentBlockHeight() {
607
+ try {
608
+ const header = await rpc("blockchain.headers.subscribe", []);
609
+ return header?.height || 0;
610
+ } catch (err) {
611
+ console.error("Error getting current block height:", err);
612
+ return 0;
613
+ }
614
+ }
615
+ function disconnect() {
616
+ if (ws) {
617
+ intentionalClose = true;
618
+ ws.close();
619
+ ws = null;
620
+ }
621
+ isConnected = false;
622
+ isConnecting = false;
623
+ reconnectAttempts = 0;
624
+ isBlockSubscribed = false;
625
+ Object.values(pending).forEach((req) => {
626
+ if (req.timeoutId) clearTimeout(req.timeoutId);
627
+ });
628
+ Object.keys(pending).forEach((key) => delete pending[Number(key)]);
629
+ connectionCallbacks.forEach((cb) => {
630
+ if (cb.timeoutId) clearTimeout(cb.timeoutId);
631
+ });
632
+ connectionCallbacks.length = 0;
633
+ }
634
+
635
+ // l1/tx.ts
636
+ import CryptoJS5 from "crypto-js";
637
+ import elliptic2 from "elliptic";
638
+
639
+ // l1/vesting.ts
640
+ var VESTING_THRESHOLD = 28e4;
641
+ var currentBlockHeight = null;
642
+ var VestingClassifier = class {
643
+ memoryCache = /* @__PURE__ */ new Map();
644
+ dbName = "SphereVestingCacheV5";
645
+ // V5 - new cache with proper null handling
646
+ storeName = "vestingCache";
647
+ db = null;
648
+ /**
649
+ * Initialize IndexedDB for persistent caching
650
+ */
651
+ async initDB() {
652
+ return new Promise((resolve, reject) => {
653
+ const request = indexedDB.open(this.dbName, 1);
654
+ request.onupgradeneeded = (event) => {
655
+ const db = event.target.result;
656
+ if (!db.objectStoreNames.contains(this.storeName)) {
657
+ db.createObjectStore(this.storeName, { keyPath: "txHash" });
658
+ }
659
+ };
660
+ request.onsuccess = (event) => {
661
+ this.db = event.target.result;
662
+ resolve();
663
+ };
664
+ request.onerror = () => reject(request.error);
665
+ });
666
+ }
667
+ /**
668
+ * Check if transaction is coinbase
669
+ */
670
+ isCoinbaseTransaction(txData) {
671
+ if (txData.vin && txData.vin.length === 1) {
672
+ const vin = txData.vin[0];
673
+ if (vin.coinbase || !vin.txid && vin.coinbase !== void 0) {
674
+ return true;
675
+ }
676
+ if (vin.txid === "0000000000000000000000000000000000000000000000000000000000000000") {
677
+ return true;
678
+ }
679
+ }
680
+ return false;
681
+ }
682
+ /**
683
+ * Load from IndexedDB cache
684
+ */
685
+ async loadFromDB(txHash) {
686
+ if (!this.db) return null;
687
+ return new Promise((resolve) => {
688
+ const tx = this.db.transaction(this.storeName, "readonly");
689
+ const store = tx.objectStore(this.storeName);
690
+ const request = store.get(txHash);
691
+ request.onsuccess = () => {
692
+ if (request.result) {
693
+ resolve({
694
+ blockHeight: request.result.blockHeight,
695
+ isCoinbase: request.result.isCoinbase,
696
+ inputTxId: request.result.inputTxId
697
+ });
698
+ } else {
699
+ resolve(null);
700
+ }
701
+ };
702
+ request.onerror = () => resolve(null);
703
+ });
704
+ }
705
+ /**
706
+ * Save to IndexedDB cache
707
+ */
708
+ async saveToDB(txHash, entry) {
709
+ if (!this.db) return;
710
+ return new Promise((resolve) => {
711
+ const tx = this.db.transaction(this.storeName, "readwrite");
712
+ const store = tx.objectStore(this.storeName);
713
+ store.put({ txHash, ...entry });
714
+ tx.oncomplete = () => resolve();
715
+ tx.onerror = () => resolve();
716
+ });
717
+ }
718
+ /**
719
+ * Trace a transaction to its coinbase origin
720
+ * Alpha blockchain has single-input transactions, making this a linear trace
721
+ */
722
+ async traceToOrigin(txHash) {
723
+ let currentTxHash = txHash;
724
+ let iterations = 0;
725
+ const MAX_ITERATIONS = 1e4;
726
+ while (iterations < MAX_ITERATIONS) {
727
+ iterations++;
728
+ const cached = this.memoryCache.get(currentTxHash);
729
+ if (cached) {
730
+ if (cached.isCoinbase) {
731
+ if (cached.blockHeight !== null && cached.blockHeight !== void 0) {
732
+ return { coinbaseHeight: cached.blockHeight };
733
+ }
734
+ } else if (cached.inputTxId) {
735
+ currentTxHash = cached.inputTxId;
736
+ continue;
737
+ }
738
+ }
739
+ const dbCached = await this.loadFromDB(currentTxHash);
740
+ if (dbCached) {
741
+ this.memoryCache.set(currentTxHash, dbCached);
742
+ if (dbCached.isCoinbase) {
743
+ if (dbCached.blockHeight !== null && dbCached.blockHeight !== void 0) {
744
+ return { coinbaseHeight: dbCached.blockHeight };
745
+ }
746
+ } else if (dbCached.inputTxId) {
747
+ currentTxHash = dbCached.inputTxId;
748
+ continue;
749
+ }
750
+ }
751
+ const txData = await getTransaction(currentTxHash);
752
+ if (!txData || !txData.txid) {
753
+ return { coinbaseHeight: null, error: `Failed to fetch tx ${currentTxHash}` };
754
+ }
755
+ const isCoinbase = this.isCoinbaseTransaction(txData);
756
+ let blockHeight = null;
757
+ if (txData.confirmations && currentBlockHeight !== null && currentBlockHeight !== void 0) {
758
+ blockHeight = currentBlockHeight - txData.confirmations + 1;
759
+ }
760
+ let inputTxId = null;
761
+ if (!isCoinbase && txData.vin && txData.vin.length > 0 && txData.vin[0].txid) {
762
+ inputTxId = txData.vin[0].txid;
763
+ }
764
+ const cacheEntry = {
765
+ blockHeight,
766
+ // Can be null if confirmations not available
767
+ isCoinbase,
768
+ inputTxId
769
+ };
770
+ this.memoryCache.set(currentTxHash, cacheEntry);
771
+ await this.saveToDB(currentTxHash, cacheEntry);
772
+ if (isCoinbase) {
773
+ return { coinbaseHeight: blockHeight };
774
+ }
775
+ if (!inputTxId) {
776
+ return { coinbaseHeight: null, error: "Could not find input transaction" };
777
+ }
778
+ currentTxHash = inputTxId;
779
+ }
780
+ return { coinbaseHeight: null, error: "Max iterations exceeded" };
781
+ }
782
+ /**
783
+ * Classify a single UTXO
784
+ */
785
+ async classifyUtxo(utxo) {
786
+ const txHash = utxo.tx_hash || utxo.txid;
787
+ if (!txHash) {
788
+ return { isVested: false, coinbaseHeight: null, error: "No transaction hash" };
789
+ }
790
+ try {
791
+ const result = await this.traceToOrigin(txHash);
792
+ if (result.error || result.coinbaseHeight === null) {
793
+ return { isVested: false, coinbaseHeight: null, error: result.error || "Could not trace to origin" };
794
+ }
795
+ return {
796
+ isVested: result.coinbaseHeight <= VESTING_THRESHOLD,
797
+ coinbaseHeight: result.coinbaseHeight
798
+ };
799
+ } catch (err) {
800
+ return {
801
+ isVested: false,
802
+ coinbaseHeight: null,
803
+ error: err instanceof Error ? err.message : String(err)
804
+ };
805
+ }
806
+ }
807
+ /**
808
+ * Classify multiple UTXOs with progress callback
809
+ */
810
+ async classifyUtxos(utxos, onProgress) {
811
+ currentBlockHeight = await getCurrentBlockHeight();
812
+ this.memoryCache.clear();
813
+ const vested = [];
814
+ const unvested = [];
815
+ const errors = [];
816
+ for (let i = 0; i < utxos.length; i++) {
817
+ const utxo = utxos[i];
818
+ const result = await this.classifyUtxo(utxo);
819
+ if (result.error) {
820
+ errors.push({ utxo, error: result.error });
821
+ unvested.push({
822
+ ...utxo,
823
+ vestingStatus: "error",
824
+ coinbaseHeight: null
825
+ });
826
+ } else if (result.isVested) {
827
+ vested.push({
828
+ ...utxo,
829
+ vestingStatus: "vested",
830
+ coinbaseHeight: result.coinbaseHeight
831
+ });
832
+ } else {
833
+ unvested.push({
834
+ ...utxo,
835
+ vestingStatus: "unvested",
836
+ coinbaseHeight: result.coinbaseHeight
837
+ });
838
+ }
839
+ if (onProgress) {
840
+ onProgress(i + 1, utxos.length);
841
+ }
842
+ if (i % 5 === 0) {
843
+ await new Promise((resolve) => setTimeout(resolve, 0));
844
+ }
845
+ }
846
+ return { vested, unvested, errors };
847
+ }
848
+ /**
849
+ * Clear all caches
850
+ */
851
+ clearCaches() {
852
+ this.memoryCache.clear();
853
+ if (this.db) {
854
+ const tx = this.db.transaction(this.storeName, "readwrite");
855
+ tx.objectStore(this.storeName).clear();
856
+ }
857
+ }
858
+ };
859
+ var vestingClassifier = new VestingClassifier();
860
+
861
+ // l1/vestingState.ts
862
+ var VestingStateManager = class {
863
+ currentMode = "all";
864
+ addressCache = /* @__PURE__ */ new Map();
865
+ classificationInProgress = false;
866
+ /**
867
+ * Set the current vesting mode
868
+ */
869
+ setMode(mode) {
870
+ if (!["all", "vested", "unvested"].includes(mode)) {
871
+ throw new Error(`Invalid vesting mode: ${mode}`);
872
+ }
873
+ this.currentMode = mode;
874
+ }
875
+ getMode() {
876
+ return this.currentMode;
877
+ }
878
+ /**
879
+ * Classify all UTXOs for an address
880
+ */
881
+ async classifyAddressUtxos(address, utxos, onProgress) {
882
+ if (this.classificationInProgress) return;
883
+ this.classificationInProgress = true;
884
+ try {
885
+ await vestingClassifier.initDB();
886
+ const result = await vestingClassifier.classifyUtxos(utxos, onProgress);
887
+ const vestedBalance = result.vested.reduce(
888
+ (sum, utxo) => sum + BigInt(utxo.value),
889
+ 0n
890
+ );
891
+ const unvestedBalance = result.unvested.reduce(
892
+ (sum, utxo) => sum + BigInt(utxo.value),
893
+ 0n
894
+ );
895
+ this.addressCache.set(address, {
896
+ classifiedUtxos: {
897
+ vested: result.vested,
898
+ unvested: result.unvested,
899
+ all: [...result.vested, ...result.unvested]
900
+ },
901
+ vestingBalances: {
902
+ vested: vestedBalance,
903
+ unvested: unvestedBalance,
904
+ all: vestedBalance + unvestedBalance
905
+ }
906
+ });
907
+ if (result.errors.length > 0) {
908
+ console.warn(`Vesting classification errors: ${result.errors.length}`);
909
+ result.errors.slice(0, 5).forEach((err) => {
910
+ const txHash = err.utxo.tx_hash || err.utxo.txid;
911
+ console.warn(` ${txHash}: ${err.error}`);
912
+ });
913
+ }
914
+ } finally {
915
+ this.classificationInProgress = false;
916
+ }
917
+ }
918
+ /**
919
+ * Get filtered UTXOs based on current vesting mode
920
+ */
921
+ getFilteredUtxos(address) {
922
+ const cache = this.addressCache.get(address);
923
+ if (!cache) return [];
924
+ switch (this.currentMode) {
925
+ case "vested":
926
+ return cache.classifiedUtxos.vested;
927
+ case "unvested":
928
+ return cache.classifiedUtxos.unvested;
929
+ default:
930
+ return cache.classifiedUtxos.all;
931
+ }
932
+ }
933
+ /**
934
+ * Get all UTXOs regardless of vesting mode (for transactions)
935
+ */
936
+ getAllUtxos(address) {
937
+ const cache = this.addressCache.get(address);
938
+ if (!cache) return [];
939
+ return cache.classifiedUtxos.all;
940
+ }
941
+ /**
942
+ * Get balance for current vesting mode (in satoshis)
943
+ */
944
+ getBalance(address) {
945
+ const cache = this.addressCache.get(address);
946
+ if (!cache) return 0n;
947
+ return cache.vestingBalances[this.currentMode];
948
+ }
949
+ /**
950
+ * Get all balances for display
951
+ */
952
+ getAllBalances(address) {
953
+ const cache = this.addressCache.get(address);
954
+ if (!cache) {
955
+ return { vested: 0n, unvested: 0n, all: 0n };
956
+ }
957
+ return cache.vestingBalances;
958
+ }
959
+ /**
960
+ * Check if address has been classified
961
+ */
962
+ hasClassifiedData(address) {
963
+ return this.addressCache.has(address);
964
+ }
965
+ /**
966
+ * Check if classification is in progress
967
+ */
968
+ isClassifying() {
969
+ return this.classificationInProgress;
970
+ }
971
+ /**
972
+ * Clear cache for an address
973
+ */
974
+ clearAddressCache(address) {
975
+ this.addressCache.delete(address);
976
+ }
977
+ /**
978
+ * Clear all caches
979
+ */
980
+ clearAllCaches() {
981
+ this.addressCache.clear();
982
+ vestingClassifier.clearCaches();
983
+ }
984
+ };
985
+ var vestingState = new VestingStateManager();
986
+
987
+ // l1/addressHelpers.ts
988
+ var WalletAddressHelper = class {
989
+ /**
990
+ * Find address by BIP32 derivation path
991
+ * @param wallet - The wallet to search
992
+ * @param path - Full BIP32 path like "m/84'/1'/0'/0/5"
993
+ * @returns The address if found, undefined otherwise
994
+ */
995
+ static findByPath(wallet, path) {
996
+ return wallet.addresses.find((a) => a.path === path);
997
+ }
998
+ /**
999
+ * Get the default address (first external/non-change address)
1000
+ * This replaces `wallet.addresses[0]` pattern for safer access
1001
+ *
1002
+ * @param wallet - The wallet
1003
+ * @returns First non-change address, or first address if all are change
1004
+ */
1005
+ static getDefault(wallet) {
1006
+ return wallet.addresses.find((a) => !a.isChange) ?? wallet.addresses[0];
1007
+ }
1008
+ /**
1009
+ * Get the default address, or undefined if wallet has no addresses
1010
+ * Safe version that doesn't throw on empty wallet
1011
+ */
1012
+ static getDefaultOrNull(wallet) {
1013
+ if (!wallet.addresses || wallet.addresses.length === 0) {
1014
+ return void 0;
1015
+ }
1016
+ return wallet.addresses.find((a) => !a.isChange) ?? wallet.addresses[0];
1017
+ }
1018
+ /**
1019
+ * Add new address to wallet (immutable operation)
1020
+ *
1021
+ * THROWS if address with same path but different address string already exists.
1022
+ * This indicates a serious derivation or data corruption issue.
1023
+ *
1024
+ * If the same path+address already exists, returns wallet unchanged (idempotent).
1025
+ *
1026
+ * @param wallet - The wallet to add to
1027
+ * @param newAddress - The address to add
1028
+ * @returns New wallet object with address added
1029
+ * @throws Error if path exists with different address (corruption indicator)
1030
+ */
1031
+ static add(wallet, newAddress) {
1032
+ if (!newAddress.path) {
1033
+ throw new Error("Cannot add address without a path");
1034
+ }
1035
+ const existing = this.findByPath(wallet, newAddress.path);
1036
+ if (existing) {
1037
+ if (existing.address !== newAddress.address) {
1038
+ throw new Error(
1039
+ `CRITICAL: Attempted to overwrite address for path ${newAddress.path}
1040
+ Existing: ${existing.address}
1041
+ New: ${newAddress.address}
1042
+ This indicates master key corruption or derivation logic error.`
1043
+ );
1044
+ }
1045
+ return wallet;
1046
+ }
1047
+ return {
1048
+ ...wallet,
1049
+ addresses: [...wallet.addresses, newAddress]
1050
+ };
1051
+ }
1052
+ /**
1053
+ * Remove address by path (immutable operation)
1054
+ * @param wallet - The wallet to modify
1055
+ * @param path - The path of the address to remove
1056
+ * @returns New wallet object with address removed
1057
+ */
1058
+ static removeByPath(wallet, path) {
1059
+ return {
1060
+ ...wallet,
1061
+ addresses: wallet.addresses.filter((a) => a.path !== path)
1062
+ };
1063
+ }
1064
+ /**
1065
+ * Get all external (non-change) addresses
1066
+ * @param wallet - The wallet
1067
+ * @returns Array of external addresses
1068
+ */
1069
+ static getExternal(wallet) {
1070
+ return wallet.addresses.filter((a) => !a.isChange);
1071
+ }
1072
+ /**
1073
+ * Get all change addresses
1074
+ * @param wallet - The wallet
1075
+ * @returns Array of change addresses
1076
+ */
1077
+ static getChange(wallet) {
1078
+ return wallet.addresses.filter((a) => a.isChange);
1079
+ }
1080
+ /**
1081
+ * Check if wallet has an address with the given path
1082
+ * @param wallet - The wallet to check
1083
+ * @param path - The path to look for
1084
+ * @returns true if path exists
1085
+ */
1086
+ static hasPath(wallet, path) {
1087
+ return wallet.addresses.some((a) => a.path === path);
1088
+ }
1089
+ /**
1090
+ * Validate wallet address array integrity
1091
+ * Checks for duplicate paths which indicate data corruption
1092
+ *
1093
+ * @param wallet - The wallet to validate
1094
+ * @throws Error if duplicate paths found
1095
+ */
1096
+ static validate(wallet) {
1097
+ const paths = wallet.addresses.map((a) => a.path).filter(Boolean);
1098
+ const uniquePaths = new Set(paths);
1099
+ if (paths.length !== uniquePaths.size) {
1100
+ const duplicates = paths.filter((p, i) => paths.indexOf(p) !== i);
1101
+ throw new Error(
1102
+ `CRITICAL: Wallet has duplicate paths: ${duplicates.join(", ")}
1103
+ This indicates data corruption. Please restore from backup.`
1104
+ );
1105
+ }
1106
+ }
1107
+ /**
1108
+ * Sort addresses with external first, then change, each sorted by index
1109
+ * Useful for display purposes
1110
+ *
1111
+ * @param wallet - The wallet
1112
+ * @returns New wallet with sorted addresses
1113
+ */
1114
+ static sortAddresses(wallet) {
1115
+ const sorted = [...wallet.addresses].sort((a, b) => {
1116
+ const aIsChange = a.isChange ? 1 : 0;
1117
+ const bIsChange = b.isChange ? 1 : 0;
1118
+ if (aIsChange !== bIsChange) return aIsChange - bIsChange;
1119
+ return a.index - b.index;
1120
+ });
1121
+ return {
1122
+ ...wallet,
1123
+ addresses: sorted
1124
+ };
1125
+ }
1126
+ };
1127
+
1128
+ // l1/tx.ts
1129
+ var ec2 = new elliptic2.ec("secp256k1");
1130
+ var FEE = 1e4;
1131
+ var DUST = 546;
1132
+ var SAT = 1e8;
1133
+ function createScriptPubKey(address) {
1134
+ if (!address || typeof address !== "string") {
1135
+ throw new Error("Invalid address: must be a string");
1136
+ }
1137
+ const decoded = decodeBech32(address);
1138
+ if (!decoded) {
1139
+ throw new Error("Invalid bech32 address: " + address);
1140
+ }
1141
+ const dataHex = Array.from(decoded.data).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1142
+ return "0014" + dataHex;
1143
+ }
1144
+ function createSignatureHash(txPlan, publicKey) {
1145
+ let preimage = "";
1146
+ preimage += "02000000";
1147
+ const txidBytes = txPlan.input.tx_hash.match(/../g).reverse().join("");
1148
+ const voutBytes = ("00000000" + txPlan.input.tx_pos.toString(16)).slice(-8).match(/../g).reverse().join("");
1149
+ const prevouts = txidBytes + voutBytes;
1150
+ const hashPrevouts = CryptoJS5.SHA256(CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(prevouts))).toString();
1151
+ preimage += hashPrevouts;
1152
+ const sequence = "feffffff";
1153
+ const hashSequence = CryptoJS5.SHA256(CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(sequence))).toString();
1154
+ preimage += hashSequence;
1155
+ preimage += txPlan.input.tx_hash.match(/../g).reverse().join("");
1156
+ preimage += ("00000000" + txPlan.input.tx_pos.toString(16)).slice(-8).match(/../g).reverse().join("");
1157
+ const pubKeyHash = CryptoJS5.RIPEMD160(CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(publicKey))).toString();
1158
+ const scriptCode = "1976a914" + pubKeyHash + "88ac";
1159
+ preimage += scriptCode;
1160
+ const amountHex = txPlan.input.value.toString(16).padStart(16, "0");
1161
+ preimage += amountHex.match(/../g).reverse().join("");
1162
+ preimage += sequence;
1163
+ let outputs = "";
1164
+ for (const output of txPlan.outputs) {
1165
+ const outAmountHex = output.value.toString(16).padStart(16, "0");
1166
+ outputs += outAmountHex.match(/../g).reverse().join("");
1167
+ const scriptPubKey = createScriptPubKey(output.address);
1168
+ const scriptLength = (scriptPubKey.length / 2).toString(16).padStart(2, "0");
1169
+ outputs += scriptLength;
1170
+ outputs += scriptPubKey;
1171
+ }
1172
+ const hashOutputs = CryptoJS5.SHA256(CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(outputs))).toString();
1173
+ preimage += hashOutputs;
1174
+ preimage += "00000000";
1175
+ preimage += "01000000";
1176
+ const hash1 = CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(preimage));
1177
+ const hash2 = CryptoJS5.SHA256(hash1);
1178
+ return hash2.toString();
1179
+ }
1180
+ function createWitnessData(txPlan, keyPair, publicKey) {
1181
+ const sigHash = createSignatureHash(txPlan, publicKey);
1182
+ const signature = keyPair.sign(sigHash);
1183
+ const halfOrder = ec2.curve.n.shrn(1);
1184
+ if (signature.s.cmp(halfOrder) > 0) {
1185
+ signature.s = ec2.curve.n.sub(signature.s);
1186
+ }
1187
+ const derSig = signature.toDER("hex") + "01";
1188
+ let witness = "";
1189
+ witness += "02";
1190
+ const sigLen = (derSig.length / 2).toString(16).padStart(2, "0");
1191
+ witness += sigLen;
1192
+ witness += derSig;
1193
+ const pubKeyLen = (publicKey.length / 2).toString(16).padStart(2, "0");
1194
+ witness += pubKeyLen;
1195
+ witness += publicKey;
1196
+ return witness;
1197
+ }
1198
+ function buildSegWitTransaction(txPlan, keyPair, publicKey) {
1199
+ let txHex = "";
1200
+ txHex += "02000000";
1201
+ txHex += "00";
1202
+ txHex += "01";
1203
+ txHex += "01";
1204
+ const prevTxHash = txPlan.input.tx_hash;
1205
+ const reversedHash = prevTxHash.match(/../g).reverse().join("");
1206
+ txHex += reversedHash;
1207
+ const vout = txPlan.input.tx_pos;
1208
+ txHex += ("00000000" + vout.toString(16)).slice(-8).match(/../g).reverse().join("");
1209
+ txHex += "00";
1210
+ txHex += "feffffff";
1211
+ const outputCount = txPlan.outputs.length;
1212
+ txHex += ("0" + outputCount.toString(16)).slice(-2);
1213
+ for (const output of txPlan.outputs) {
1214
+ const amountHex = output.value.toString(16).padStart(16, "0");
1215
+ txHex += amountHex.match(/../g).reverse().join("");
1216
+ const scriptPubKey = createScriptPubKey(output.address);
1217
+ const scriptLength = (scriptPubKey.length / 2).toString(16).padStart(2, "0");
1218
+ txHex += scriptLength;
1219
+ txHex += scriptPubKey;
1220
+ }
1221
+ const witnessData = createWitnessData(txPlan, keyPair, publicKey);
1222
+ txHex += witnessData;
1223
+ txHex += "00000000";
1224
+ let txForId = "";
1225
+ txForId += "02000000";
1226
+ txForId += "01";
1227
+ const inputTxidBytes = txPlan.input.tx_hash.match(/../g).reverse().join("");
1228
+ txForId += inputTxidBytes;
1229
+ txForId += ("00000000" + txPlan.input.tx_pos.toString(16)).slice(-8).match(/../g).reverse().join("");
1230
+ txForId += "00";
1231
+ txForId += "feffffff";
1232
+ txForId += ("0" + txPlan.outputs.length.toString(16)).slice(-2);
1233
+ for (const output of txPlan.outputs) {
1234
+ const amountHex = ("0000000000000000" + output.value.toString(16)).slice(-16);
1235
+ const amountLittleEndian = amountHex.match(/../g).reverse().join("");
1236
+ txForId += amountLittleEndian;
1237
+ const scriptPubKey = createScriptPubKey(output.address);
1238
+ const scriptLength = ("0" + (scriptPubKey.length / 2).toString(16)).slice(-2);
1239
+ txForId += scriptLength;
1240
+ txForId += scriptPubKey;
1241
+ }
1242
+ txForId += "00000000";
1243
+ const hash1 = CryptoJS5.SHA256(CryptoJS5.enc.Hex.parse(txForId));
1244
+ const hash2 = CryptoJS5.SHA256(hash1);
1245
+ const txid = hash2.toString().match(/../g).reverse().join("");
1246
+ return {
1247
+ hex: txHex,
1248
+ txid
1249
+ };
1250
+ }
1251
+ function createAndSignTransaction(wallet, txPlan) {
1252
+ const fromAddress = txPlan.input.address;
1253
+ const addressEntry = wallet.addresses.find((a) => a.address === fromAddress);
1254
+ let privateKeyHex;
1255
+ if (addressEntry?.privateKey) {
1256
+ privateKeyHex = addressEntry.privateKey;
1257
+ } else if (wallet.childPrivateKey) {
1258
+ privateKeyHex = wallet.childPrivateKey;
1259
+ } else {
1260
+ privateKeyHex = wallet.masterPrivateKey;
1261
+ }
1262
+ if (!privateKeyHex) {
1263
+ throw new Error("No private key available for address: " + fromAddress);
1264
+ }
1265
+ const keyPair = ec2.keyFromPrivate(privateKeyHex, "hex");
1266
+ const publicKey = keyPair.getPublic(true, "hex");
1267
+ const txPlanForBuild = {
1268
+ input: {
1269
+ tx_hash: txPlan.input.txid,
1270
+ tx_pos: txPlan.input.vout,
1271
+ value: txPlan.input.value
1272
+ },
1273
+ outputs: txPlan.outputs
1274
+ };
1275
+ const tx = buildSegWitTransaction(txPlanForBuild, keyPair, publicKey);
1276
+ return {
1277
+ raw: tx.hex,
1278
+ txid: tx.txid
1279
+ };
1280
+ }
1281
+ function collectUtxosForAmount(utxoList, amountSats, recipientAddress, senderAddress) {
1282
+ const totalAvailable = utxoList.reduce((sum, u) => sum + u.value, 0);
1283
+ if (totalAvailable < amountSats + FEE) {
1284
+ return {
1285
+ success: false,
1286
+ transactions: [],
1287
+ error: `Insufficient funds. Available: ${totalAvailable / SAT} ALPHA, Required: ${(amountSats + FEE) / SAT} ALPHA (including fee)`
1288
+ };
1289
+ }
1290
+ const sortedByValue = [...utxoList].sort((a, b) => a.value - b.value);
1291
+ const sufficientUtxo = sortedByValue.find((u) => u.value >= amountSats + FEE);
1292
+ if (sufficientUtxo) {
1293
+ const changeAmount = sufficientUtxo.value - amountSats - FEE;
1294
+ const tx = {
1295
+ input: {
1296
+ txid: sufficientUtxo.txid ?? sufficientUtxo.tx_hash ?? "",
1297
+ vout: sufficientUtxo.vout ?? sufficientUtxo.tx_pos ?? 0,
1298
+ value: sufficientUtxo.value,
1299
+ address: sufficientUtxo.address ?? senderAddress
1300
+ },
1301
+ outputs: [{ address: recipientAddress, value: amountSats }],
1302
+ fee: FEE,
1303
+ changeAmount,
1304
+ changeAddress: senderAddress
1305
+ };
1306
+ if (changeAmount > DUST) {
1307
+ tx.outputs.push({ value: changeAmount, address: senderAddress });
1308
+ }
1309
+ return {
1310
+ success: true,
1311
+ transactions: [tx]
1312
+ };
1313
+ }
1314
+ const sortedDescending = [...utxoList].sort((a, b) => b.value - a.value);
1315
+ const transactions = [];
1316
+ let remainingAmount = amountSats;
1317
+ for (const utxo of sortedDescending) {
1318
+ if (remainingAmount <= 0) break;
1319
+ const utxoValue = utxo.value;
1320
+ let txAmount = 0;
1321
+ let changeAmount = 0;
1322
+ if (utxoValue >= remainingAmount + FEE) {
1323
+ txAmount = remainingAmount;
1324
+ changeAmount = utxoValue - remainingAmount - FEE;
1325
+ remainingAmount = 0;
1326
+ } else {
1327
+ txAmount = utxoValue - FEE;
1328
+ if (txAmount <= 0) continue;
1329
+ remainingAmount -= txAmount;
1330
+ }
1331
+ const tx = {
1332
+ input: {
1333
+ txid: utxo.txid ?? utxo.tx_hash ?? "",
1334
+ vout: utxo.vout ?? utxo.tx_pos ?? 0,
1335
+ value: utxo.value,
1336
+ address: utxo.address ?? senderAddress
1337
+ },
1338
+ outputs: [{ address: recipientAddress, value: txAmount }],
1339
+ fee: FEE,
1340
+ changeAmount,
1341
+ changeAddress: senderAddress
1342
+ };
1343
+ if (changeAmount > DUST) {
1344
+ tx.outputs.push({ value: changeAmount, address: senderAddress });
1345
+ }
1346
+ transactions.push(tx);
1347
+ }
1348
+ if (remainingAmount > 0) {
1349
+ return {
1350
+ success: false,
1351
+ transactions: [],
1352
+ error: `Unable to collect enough UTXOs. Short by ${remainingAmount / SAT} ALPHA after fees.`
1353
+ };
1354
+ }
1355
+ return {
1356
+ success: true,
1357
+ transactions
1358
+ };
1359
+ }
1360
+ async function createTransactionPlan(wallet, toAddress, amountAlpha, fromAddress) {
1361
+ if (!decodeBech32(toAddress)) {
1362
+ throw new Error("Invalid recipient address");
1363
+ }
1364
+ const defaultAddr = WalletAddressHelper.getDefault(wallet);
1365
+ const senderAddress = fromAddress || defaultAddr.address;
1366
+ const amountSats = Math.floor(amountAlpha * SAT);
1367
+ let utxos;
1368
+ const currentMode = vestingState.getMode();
1369
+ if (vestingState.hasClassifiedData(senderAddress)) {
1370
+ utxos = vestingState.getFilteredUtxos(senderAddress);
1371
+ console.log(`Using ${utxos.length} ${currentMode} UTXOs`);
1372
+ } else {
1373
+ utxos = await getUtxo(senderAddress);
1374
+ console.log(`Using ${utxos.length} UTXOs (vesting not classified yet)`);
1375
+ }
1376
+ if (!Array.isArray(utxos) || utxos.length === 0) {
1377
+ const modeText = currentMode !== "all" ? ` (${currentMode} coins)` : "";
1378
+ throw new Error(`No UTXOs available${modeText} for address: ` + senderAddress);
1379
+ }
1380
+ return collectUtxosForAmount(utxos, amountSats, toAddress, senderAddress);
1381
+ }
1382
+ async function sendAlpha(wallet, toAddress, amountAlpha, fromAddress) {
1383
+ const plan = await createTransactionPlan(wallet, toAddress, amountAlpha, fromAddress);
1384
+ if (!plan.success) {
1385
+ throw new Error(plan.error || "Transaction planning failed");
1386
+ }
1387
+ const results = [];
1388
+ for (const tx of plan.transactions) {
1389
+ const signed = createAndSignTransaction(wallet, tx);
1390
+ const result = await broadcast(signed.raw);
1391
+ results.push({
1392
+ txid: signed.txid,
1393
+ raw: signed.raw,
1394
+ broadcastResult: result
1395
+ });
1396
+ }
1397
+ return results;
1398
+ }
1399
+ export {
1400
+ CHARSET,
1401
+ VESTING_THRESHOLD,
1402
+ WalletAddressHelper,
1403
+ addressToScriptHash,
1404
+ broadcast,
1405
+ buildSegWitTransaction,
1406
+ collectUtxosForAmount,
1407
+ computeHash160,
1408
+ connect,
1409
+ convertBits,
1410
+ createAndSignTransaction,
1411
+ createBech32,
1412
+ createScriptPubKey,
1413
+ createTransactionPlan,
1414
+ decodeBech32,
1415
+ decrypt,
1416
+ decryptWallet,
1417
+ deriveChildKey2 as deriveChildKey,
1418
+ deriveChildKeyBIP32,
1419
+ deriveKeyAtPath2 as deriveKeyAtPath,
1420
+ disconnect,
1421
+ domIdToPath,
1422
+ ec,
1423
+ encodeBech32,
1424
+ encrypt,
1425
+ encryptWallet,
1426
+ generateAddressFromMasterKey,
1427
+ generateAddressInfo,
1428
+ generateHDAddress,
1429
+ generateHDAddressBIP32,
1430
+ generateMasterKeyFromSeed,
1431
+ generatePrivateKey,
1432
+ getBalance,
1433
+ getBlockHeader,
1434
+ getCurrentBlockHeight,
1435
+ getIndexFromPath,
1436
+ getTransaction,
1437
+ getTransactionHistory,
1438
+ getUtxo,
1439
+ hash160,
1440
+ hash160ToBytes,
1441
+ hexToWIF,
1442
+ isChangePath,
1443
+ isWebSocketConnected,
1444
+ parsePathComponents,
1445
+ pathToDOMId,
1446
+ privateKeyToAddressInfo,
1447
+ publicKeyToAddress,
1448
+ rpc,
1449
+ sendAlpha,
1450
+ subscribeBlocks,
1451
+ vestingClassifier,
1452
+ vestingState,
1453
+ waitForConnection
1454
+ };
1455
+ //# sourceMappingURL=index.js.map