navio-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.mjs ADDED
@@ -0,0 +1,5002 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { ripemd160 } from '@noble/hashes/ripemd160';
3
+ import * as blsctModule from 'navio-blsct';
4
+ import { BlsctChain, setChain } from 'navio-blsct';
5
+ export { BlsctChain, getChain, setChain } from 'navio-blsct';
6
+ import * as net from 'net';
7
+
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var Scalar2 = blsctModule.Scalar;
15
+ var ChildKey2 = blsctModule.ChildKey;
16
+ var PublicKey2 = blsctModule.PublicKey;
17
+ var SubAddr2 = blsctModule.SubAddr;
18
+ var SubAddrId2 = blsctModule.SubAddrId;
19
+ var DoublePublicKey2 = blsctModule.DoublePublicKey;
20
+ var calcPrivSpendingKey2 = blsctModule.calcPrivSpendingKey;
21
+ var recoverAmount2 = blsctModule.recoverAmount;
22
+ var ViewTag2 = blsctModule.ViewTag;
23
+ var HashId2 = blsctModule.HashId;
24
+ var calcNonce2 = blsctModule.calcNonce;
25
+ var getAmountRecoveryResultSize2 = blsctModule.getAmountRecoveryResultSize;
26
+ var getAmountRecoveryResultIsSucc2 = blsctModule.getAmountRecoveryResultIsSucc;
27
+ var getAmountRecoveryResultAmount2 = blsctModule.getAmountRecoveryResultAmount;
28
+ var deleteAmountsRetVal2 = blsctModule.deleteAmountsRetVal;
29
+ var KeyManager = class {
30
+ constructor() {
31
+ this.hdChain = null;
32
+ this.viewKey = null;
33
+ this.spendPublicKey = null;
34
+ this.masterSeed = null;
35
+ this.spendKeyId = null;
36
+ this.viewKeyId = null;
37
+ this.tokenKeyId = null;
38
+ this.blindingKeyId = null;
39
+ this.seedId = null;
40
+ // In-memory key storage (replicates KeyRing functionality)
41
+ this.keys = /* @__PURE__ */ new Map();
42
+ // keyId (hex) -> secret key
43
+ this.outKeys = /* @__PURE__ */ new Map();
44
+ // outId (hex) -> secret key
45
+ this.cryptedKeys = /* @__PURE__ */ new Map();
46
+ // keyId -> encrypted key
47
+ this.cryptedOutKeys = /* @__PURE__ */ new Map();
48
+ // outId -> encrypted key
49
+ this.keyMetadata = /* @__PURE__ */ new Map();
50
+ // keyId -> metadata
51
+ // SubAddress management
52
+ this.subAddressCounter = /* @__PURE__ */ new Map();
53
+ this.subAddresses = /* @__PURE__ */ new Map();
54
+ // hashId (hex) -> SubAddressIdentifier
55
+ this.subAddressesStr = /* @__PURE__ */ new Map();
56
+ // SubAddress (serialized) -> hashId (hex)
57
+ this.subAddressPool = /* @__PURE__ */ new Map();
58
+ // account -> Set<address indices>
59
+ this.subAddressPoolTime = /* @__PURE__ */ new Map();
60
+ // "${account}:${address}" -> timestamp
61
+ this.timeFirstKey = null;
62
+ // Creation time of first key
63
+ // Flags
64
+ this.fViewKeyDefined = false;
65
+ this.fSpendKeyDefined = false;
66
+ }
67
+ /**
68
+ * Check if HD is enabled (has a seed)
69
+ * @returns True if HD is enabled
70
+ */
71
+ isHDEnabled() {
72
+ return this.seedId !== null;
73
+ }
74
+ /**
75
+ * Check if the wallet can generate new keys
76
+ * @returns True if HD is enabled
77
+ */
78
+ canGenerateKeys() {
79
+ return this.isHDEnabled();
80
+ }
81
+ /**
82
+ * Generate a new random seed
83
+ * @returns A new random Scalar (seed)
84
+ */
85
+ generateNewSeed() {
86
+ return new Scalar2();
87
+ }
88
+ /**
89
+ * Set the HD seed and derive all master keys
90
+ * This replicates SetHDSeed from keyman.cpp
91
+ * Uses navio-blsct API: ChildKey(seed).toTxKey().toViewKey() etc.
92
+ * @param seed - The master seed Scalar
93
+ */
94
+ setHDSeed(seed) {
95
+ this.masterSeed = seed;
96
+ const childKey = new ChildKey2(seed);
97
+ const txKey = childKey.toTxKey();
98
+ const blindingKey = childKey.toBlindingKey();
99
+ const tokenKey = childKey.toTokenKey();
100
+ const viewKey = Scalar2.deserialize(txKey.toViewKey().serialize());
101
+ const spendKey = Scalar2.deserialize(txKey.toSpendingKey().serialize());
102
+ this.viewKey = viewKey;
103
+ let spendPublicKey = PublicKey2.fromScalar(spendKey);
104
+ this.spendPublicKey = spendPublicKey;
105
+ const seedPublicKey = this.getPublicKeyFromScalar(seed);
106
+ const viewPublicKey = this.getPublicKeyFromViewKey(viewKey);
107
+ const spendPublicKeyBytes = this.getPublicKeyBytes(this.spendPublicKey);
108
+ const tokenPublicKey = this.getPublicKeyFromScalar(tokenKey);
109
+ const blindingPublicKey = this.getPublicKeyFromScalar(blindingKey);
110
+ this.seedId = this.hash160(seedPublicKey);
111
+ this.spendKeyId = this.hash160(spendPublicKeyBytes);
112
+ this.viewKeyId = this.hash160(viewPublicKey);
113
+ this.tokenKeyId = this.hash160(tokenPublicKey);
114
+ this.blindingKeyId = this.hash160(blindingPublicKey);
115
+ this.hdChain = {
116
+ version: 1,
117
+ // HDChain::VERSION_HD_BASE
118
+ seedId: this.seedId,
119
+ spendId: this.spendKeyId,
120
+ viewId: this.viewKeyId,
121
+ tokenId: this.tokenKeyId,
122
+ blindingId: this.blindingKeyId
123
+ };
124
+ this.subAddressCounter.clear();
125
+ }
126
+ /**
127
+ * Setup key generation from a seed
128
+ * Replicates SetupGeneration from keyman.cpp
129
+ * @param seedBytes - The seed bytes (32 bytes for master key, 80 bytes for view/spend keys)
130
+ * @param type - The type of seed being imported
131
+ * @param force - Force setup even if HD is already enabled
132
+ * @returns True if setup was successful
133
+ */
134
+ setupGeneration(seedBytes, type = "IMPORT_MASTER_KEY", force = false) {
135
+ if (this.canGenerateKeys() && !force) {
136
+ return false;
137
+ }
138
+ if (seedBytes.length === 32) {
139
+ if (type === "IMPORT_MASTER_KEY") {
140
+ const seed = this.createScalarFromBytes(seedBytes);
141
+ this.setHDSeed(seed);
142
+ }
143
+ } else if (seedBytes.length === 80) {
144
+ if (type === "IMPORT_VIEW_KEY") {
145
+ const viewKeyBytes = seedBytes.slice(0, 32);
146
+ const spendingKeyBytes = seedBytes.slice(32, 80);
147
+ const viewKey = this.createViewKeyFromBytes(viewKeyBytes);
148
+ const spendingPublicKey = this.createPublicKeyFromBytes(spendingKeyBytes);
149
+ this.viewKey = Scalar2.deserialize(viewKey.serialize());
150
+ this.spendPublicKey = spendingPublicKey;
151
+ }
152
+ } else {
153
+ const seed = this.generateNewSeed();
154
+ this.setHDSeed(seed);
155
+ }
156
+ this.newSubAddressPool(0);
157
+ this.newSubAddressPool(-1);
158
+ this.newSubAddressPool(-2);
159
+ return true;
160
+ }
161
+ /**
162
+ * Get the master seed key
163
+ * @returns The master seed Scalar
164
+ */
165
+ getMasterSeedKey() {
166
+ if (!this.isHDEnabled()) {
167
+ throw new Error("HD is not enabled");
168
+ }
169
+ if (!this.masterSeed) {
170
+ throw new Error("Master seed key not available");
171
+ }
172
+ return this.masterSeed;
173
+ }
174
+ /**
175
+ * Get the private view key
176
+ * @returns The view key
177
+ */
178
+ getPrivateViewKey() {
179
+ if (!this.viewKey) {
180
+ throw new Error("View key is not available");
181
+ }
182
+ return this.viewKey;
183
+ }
184
+ /**
185
+ * Get the public spending key
186
+ * @returns The public spending key
187
+ */
188
+ getPublicSpendingKey() {
189
+ if (!this.spendPublicKey) {
190
+ throw new Error("Spending key is not available");
191
+ }
192
+ return this.spendPublicKey;
193
+ }
194
+ /**
195
+ * Get a sub-address for the given identifier
196
+ * Uses navio-blsct SubAddr.generate() method
197
+ * @param id - The sub-address identifier (defaults to account 0, address 0)
198
+ * @returns The sub-address (SubAddr)
199
+ */
200
+ getSubAddress(id = { account: 0, address: 0 }) {
201
+ if (!this.viewKey || !this.spendPublicKey) {
202
+ throw new Error("View key or spending key not available");
203
+ }
204
+ const subAddrId = SubAddrId2.generate(id.account, id.address);
205
+ return SubAddr2.generate(this.viewKey, this.spendPublicKey, subAddrId);
206
+ }
207
+ /**
208
+ * Generate a new sub-address for the given account
209
+ * @param account - The account number (0 for main, -1 for change, -2 for staking)
210
+ * @returns The generated sub-address and its identifier
211
+ */
212
+ generateNewSubAddress(account) {
213
+ if (!this.canGenerateKeys()) {
214
+ throw new Error("Cannot generate keys - HD not enabled");
215
+ }
216
+ if (!this.viewKey || !this.spendPublicKey) {
217
+ throw new Error("View key or spending public key not available");
218
+ }
219
+ if (!this.subAddressCounter.has(account)) {
220
+ this.subAddressCounter.set(account, 0);
221
+ }
222
+ const addressIndex = this.subAddressCounter.get(account);
223
+ const id = { account, address: addressIndex };
224
+ this.subAddressCounter.set(account, addressIndex + 1);
225
+ const subAddress = this.getSubAddress(id);
226
+ const dpk = DoublePublicKey2.fromKeysAcctAddr(this.viewKey, this.spendPublicKey, account, addressIndex);
227
+ const serialized = dpk.serialize();
228
+ const spendingKeyHex = serialized.substring(96);
229
+ const spendingKeyBytes = Buffer.from(spendingKeyHex, "hex");
230
+ const hashIdBytes = this.hash160(spendingKeyBytes);
231
+ const hashIdHex = this.bytesToHex(hashIdBytes);
232
+ this.subAddresses.set(hashIdHex, id);
233
+ return { subAddress, id };
234
+ }
235
+ /**
236
+ * Get a new destination (sub-address) from the pool or generate one
237
+ * @param account - The account number
238
+ * @returns The sub-address destination (SubAddr)
239
+ */
240
+ getNewDestination(account = 0) {
241
+ this.topUp();
242
+ this.getSubAddressPoolSize(account);
243
+ const { subAddress } = this.generateNewSubAddress(account);
244
+ return subAddress;
245
+ }
246
+ /**
247
+ * Create a new sub-address pool for an account
248
+ * @param account - The account number
249
+ * @returns True if successful
250
+ */
251
+ newSubAddressPool(account) {
252
+ this.subAddressPool.set(account, /* @__PURE__ */ new Set());
253
+ return this.topUpAccount(account);
254
+ }
255
+ /**
256
+ * Top up all sub-address pools
257
+ * @param size - Target size for pools (0 = use default)
258
+ * @returns True if successful
259
+ */
260
+ topUp(size = 0) {
261
+ if (!this.canGenerateKeys()) {
262
+ return false;
263
+ }
264
+ const targetSize = size > 0 ? size : 100;
265
+ for (const account of Array.from(this.subAddressPool.keys())) {
266
+ if (!this.topUpAccount(account, targetSize)) {
267
+ return false;
268
+ }
269
+ }
270
+ return true;
271
+ }
272
+ /**
273
+ * Top up a specific account's sub-address pool
274
+ * @param account - The account number
275
+ * @param size - Target size (0 = use default)
276
+ * @returns True if successful
277
+ */
278
+ topUpAccount(account, size = 0) {
279
+ const targetSize = size > 0 ? size : 100;
280
+ if (!this.subAddressPool.has(account)) {
281
+ this.subAddressPool.set(account, /* @__PURE__ */ new Set());
282
+ }
283
+ const pool = this.subAddressPool.get(account);
284
+ const missing = Math.max(targetSize - pool.size, 0);
285
+ for (let i = 0; i < missing; i++) {
286
+ const { id } = this.generateNewSubAddress(account);
287
+ pool.add(id.address);
288
+ }
289
+ return true;
290
+ }
291
+ /**
292
+ * Get the size of the sub-address pool for an account
293
+ * @param account - The account number
294
+ * @returns The pool size
295
+ */
296
+ getSubAddressPoolSize(account) {
297
+ return this.subAddressPool.get(account)?.size ?? 0;
298
+ }
299
+ /**
300
+ * Calculate hash ID from blinding and spending keys
301
+ * Uses HashId.generate() from navio-blsct
302
+ * @param blindingKey - The blinding public key
303
+ * @param spendingKey - The spending public key
304
+ * @returns The hash ID (20 bytes)
305
+ */
306
+ calculateHashId(blindingKey, spendingKey) {
307
+ if (!this.viewKey) {
308
+ throw new Error("View key not available");
309
+ }
310
+ const hashId = HashId2.generate(blindingKey, spendingKey, this.viewKey);
311
+ const hashIdHex = hashId.serialize();
312
+ return Uint8Array.from(Buffer.from(hashIdHex, "hex"));
313
+ }
314
+ /**
315
+ * Calculate view tag for output detection
316
+ * Uses ViewTag from navio-blsct
317
+ * @param blindingKey - The blinding public key
318
+ * @returns The view tag (16-bit number)
319
+ */
320
+ calculateViewTag(blindingKey) {
321
+ if (!this.viewKey) {
322
+ throw new Error("View key not available");
323
+ }
324
+ const viewTagObj = new ViewTag2(blindingKey, this.viewKey);
325
+ return viewTagObj.value;
326
+ }
327
+ /**
328
+ * Calculate nonce for range proof recovery
329
+ * Uses calcNonce from navio-blsct
330
+ * @param blindingKey - The blinding public key
331
+ * @returns The nonce (Point)
332
+ */
333
+ calculateNonce(blindingKey) {
334
+ if (!this.viewKey) {
335
+ throw new Error("View key not available");
336
+ }
337
+ const result = calcNonce2(blindingKey.value(), this.viewKey.value());
338
+ const Point2 = blsctModule.Point;
339
+ return new Point2(result);
340
+ }
341
+ /**
342
+ * Get the HD chain information
343
+ * @returns The HD chain or null if not set
344
+ */
345
+ getHDChain() {
346
+ return this.hdChain;
347
+ }
348
+ // ============================================================================
349
+ // Key Loading Methods (from database - don't persist, just load into memory)
350
+ // ============================================================================
351
+ /**
352
+ * Load a key pair from storage into memory (used by LoadWallet)
353
+ * Replicates LoadKey from keyman.cpp
354
+ * @param secretKey - The secret key
355
+ * @param publicKey - The public key
356
+ * @returns True if successful
357
+ */
358
+ loadKey(secretKey, publicKey) {
359
+ return this.addKeyPubKeyInner(secretKey, publicKey);
360
+ }
361
+ /**
362
+ * Load a view key from storage into memory
363
+ * Replicates LoadViewKey from keyman.cpp
364
+ * @param viewKey - The view key
365
+ * @param publicKey - The view key's public key
366
+ * @returns True if successful
367
+ */
368
+ loadViewKey(viewKey) {
369
+ if (!this.fViewKeyDefined) {
370
+ this.viewKey = Scalar2.deserialize(viewKey.serialize());
371
+ this.fViewKeyDefined = true;
372
+ return true;
373
+ }
374
+ return true;
375
+ }
376
+ /**
377
+ * Load a spend key from storage into memory
378
+ * Replicates LoadSpendKey from keyman.cpp
379
+ * @param publicKey - The spending public key
380
+ * @returns True if successful
381
+ */
382
+ loadSpendKey(publicKey) {
383
+ if (!this.fSpendKeyDefined) {
384
+ this.spendPublicKey = publicKey;
385
+ this.fSpendKeyDefined = true;
386
+ return true;
387
+ }
388
+ return true;
389
+ }
390
+ /**
391
+ * Load an output key from storage into memory
392
+ * Replicates LoadOutKey from keyman.cpp
393
+ * @param secretKey - The secret key for the output
394
+ * @param outId - The output ID (uint256)
395
+ * @returns True if successful
396
+ */
397
+ loadOutKey(secretKey, outId) {
398
+ return this.addKeyOutKeyInner(secretKey, outId);
399
+ }
400
+ /**
401
+ * Load an encrypted key from storage into memory
402
+ * Replicates LoadCryptedKey from keyman.cpp
403
+ * @param publicKey - The public key
404
+ * @param encryptedSecret - The encrypted secret key
405
+ * @param checksumValid - Whether the checksum is valid
406
+ * @returns True if successful
407
+ */
408
+ loadCryptedKey(publicKey, encryptedSecret, checksumValid) {
409
+ return this.addCryptedKeyInner(publicKey, encryptedSecret);
410
+ }
411
+ /**
412
+ * Load an encrypted output key from storage into memory
413
+ * Replicates LoadCryptedOutKey from keyman.cpp
414
+ * @param outId - The output ID
415
+ * @param publicKey - The public key
416
+ * @param encryptedSecret - The encrypted secret key
417
+ * @param checksumValid - Whether the checksum is valid
418
+ * @returns True if successful
419
+ */
420
+ loadCryptedOutKey(outId, publicKey, encryptedSecret, checksumValid) {
421
+ return this.addCryptedOutKeyInner(outId, publicKey, encryptedSecret);
422
+ }
423
+ /**
424
+ * Load HD chain from storage
425
+ * Replicates LoadHDChain from keyman.cpp
426
+ * @param chain - The HD chain to load
427
+ */
428
+ loadHDChain(chain) {
429
+ this.hdChain = chain;
430
+ this.seedId = chain.seedId;
431
+ this.spendKeyId = chain.spendId;
432
+ this.viewKeyId = chain.viewId;
433
+ this.tokenKeyId = chain.tokenId;
434
+ this.blindingKeyId = chain.blindingId;
435
+ }
436
+ /**
437
+ * Load key metadata from storage
438
+ * Replicates LoadKeyMetadata from keyman.cpp
439
+ * @param keyId - The key ID
440
+ * @param metadata - The key metadata
441
+ */
442
+ loadKeyMetadata(keyId, metadata) {
443
+ const keyIdHex = this.bytesToHex(keyId);
444
+ this.keyMetadata.set(keyIdHex, metadata);
445
+ this.updateTimeFirstKey(metadata.nCreateTime);
446
+ }
447
+ // ============================================================================
448
+ // Key Adding Methods (add and persist to database)
449
+ // ============================================================================
450
+ /**
451
+ * Add a key pair and save to database
452
+ * Replicates AddKeyPubKey from keyman.cpp
453
+ * @param secretKey - The secret key
454
+ * @param publicKey - The public key
455
+ * @returns True if successful
456
+ */
457
+ addKeyPubKey(secretKey, publicKey) {
458
+ if (!this.addKeyPubKeyInner(secretKey, publicKey)) {
459
+ return false;
460
+ }
461
+ return true;
462
+ }
463
+ /**
464
+ * Add an output key and save to database
465
+ * Replicates AddKeyOutKey from keyman.cpp
466
+ * @param secretKey - The secret key
467
+ * @param outId - The output ID
468
+ * @returns True if successful
469
+ */
470
+ addKeyOutKey(secretKey, outId) {
471
+ if (!this.addKeyOutKeyInner(secretKey, outId)) {
472
+ return false;
473
+ }
474
+ return true;
475
+ }
476
+ /**
477
+ * Add a view key and save to database
478
+ * Replicates AddViewKey from keyman.cpp
479
+ * @param viewKey - The view key
480
+ * @param publicKey - The view key's public key
481
+ * @returns True if successful
482
+ */
483
+ addViewKey(viewKey, _publicKey) {
484
+ if (!this.fViewKeyDefined) {
485
+ this.viewKey = Scalar2.deserialize(viewKey.serialize());
486
+ this.fViewKeyDefined = true;
487
+ return true;
488
+ }
489
+ return true;
490
+ }
491
+ /**
492
+ * Add a spend key and save to database
493
+ * Replicates AddSpendKey from keyman.cpp
494
+ * @param publicKey - The spending public key
495
+ * @returns True if successful
496
+ */
497
+ addSpendKey(publicKey) {
498
+ if (!this.fSpendKeyDefined) {
499
+ this.spendPublicKey = publicKey;
500
+ this.fSpendKeyDefined = true;
501
+ return true;
502
+ }
503
+ return true;
504
+ }
505
+ /**
506
+ * Add an encrypted key and save to database
507
+ * Replicates AddCryptedKey from keyman.cpp
508
+ * @param publicKey - The public key
509
+ * @param encryptedSecret - The encrypted secret key
510
+ * @returns True if successful
511
+ */
512
+ addCryptedKey(publicKey, encryptedSecret) {
513
+ if (!this.addCryptedKeyInner(publicKey, encryptedSecret)) {
514
+ return false;
515
+ }
516
+ return true;
517
+ }
518
+ /**
519
+ * Add an encrypted output key and save to database
520
+ * Replicates AddCryptedOutKey from keyman.cpp
521
+ * @param outId - The output ID
522
+ * @param publicKey - The public key
523
+ * @param encryptedSecret - The encrypted secret key
524
+ * @returns True if successful
525
+ */
526
+ addCryptedOutKey(outId, publicKey, encryptedSecret) {
527
+ if (!this.addCryptedOutKeyInner(outId, publicKey, encryptedSecret)) {
528
+ return false;
529
+ }
530
+ return true;
531
+ }
532
+ /**
533
+ * Add HD chain and save to database
534
+ * Replicates AddHDChain from keyman.cpp
535
+ * @param chain - The HD chain to add
536
+ */
537
+ addHDChain(chain) {
538
+ this.hdChain = chain;
539
+ this.seedId = chain.seedId;
540
+ this.spendKeyId = chain.spendId;
541
+ this.viewKeyId = chain.viewId;
542
+ this.tokenKeyId = chain.tokenId;
543
+ this.blindingKeyId = chain.blindingId;
544
+ }
545
+ // ============================================================================
546
+ // Key Retrieval Methods (get keys from memory)
547
+ // ============================================================================
548
+ /**
549
+ * Check if a key exists
550
+ * Replicates HaveKey from keyman.cpp
551
+ * @param keyId - The key ID (hash160 of public key)
552
+ * @returns True if the key exists
553
+ */
554
+ haveKey(keyId) {
555
+ const keyIdHex = this.bytesToHex(keyId);
556
+ if (this.keys.has(keyIdHex)) {
557
+ return true;
558
+ }
559
+ if (this.cryptedKeys.has(keyIdHex)) {
560
+ return true;
561
+ }
562
+ return false;
563
+ }
564
+ /**
565
+ * Get a key by key ID
566
+ * Replicates GetKey from keyman.cpp
567
+ * @param keyId - The key ID
568
+ * @returns The secret key or null if not found
569
+ */
570
+ getKey(keyId) {
571
+ const keyIdHex = this.bytesToHex(keyId);
572
+ if (this.keys.has(keyIdHex)) {
573
+ return this.keys.get(keyIdHex);
574
+ }
575
+ return null;
576
+ }
577
+ /**
578
+ * Get an output key by output ID
579
+ * Replicates GetOutKey from keyman.cpp
580
+ * @param outId - The output ID
581
+ * @returns The secret key or null if not found
582
+ */
583
+ getOutKey(outId) {
584
+ const outIdHex = this.bytesToHex(outId);
585
+ if (this.outKeys.has(outIdHex)) {
586
+ return this.outKeys.get(outIdHex);
587
+ }
588
+ return null;
589
+ }
590
+ // ============================================================================
591
+ // Internal helper methods for key management
592
+ // ============================================================================
593
+ /**
594
+ * Internal method to add a key pair (used by both Load and Add methods)
595
+ */
596
+ addKeyPubKeyInner(secretKey, publicKey) {
597
+ const keyId = this.hash160(this.getPublicKeyBytes(publicKey));
598
+ const keyIdHex = this.bytesToHex(keyId);
599
+ this.keys.set(keyIdHex, secretKey);
600
+ return true;
601
+ }
602
+ /**
603
+ * Internal method to add an output key (used by both Load and Add methods)
604
+ */
605
+ addKeyOutKeyInner(secretKey, outId) {
606
+ const outIdHex = this.bytesToHex(outId);
607
+ this.outKeys.set(outIdHex, secretKey);
608
+ return true;
609
+ }
610
+ /**
611
+ * Internal method to add an encrypted key (used by both Load and Add methods)
612
+ */
613
+ addCryptedKeyInner(publicKey, encryptedSecret) {
614
+ const keyId = this.hash160(this.getPublicKeyBytes(publicKey));
615
+ const keyIdHex = this.bytesToHex(keyId);
616
+ this.cryptedKeys.set(keyIdHex, { publicKey, encryptedSecret });
617
+ return true;
618
+ }
619
+ /**
620
+ * Internal method to add an encrypted output key (used by both Load and Add methods)
621
+ */
622
+ addCryptedOutKeyInner(outId, publicKey, encryptedSecret) {
623
+ const outIdHex = this.bytesToHex(outId);
624
+ this.cryptedOutKeys.set(outIdHex, { publicKey, encryptedSecret });
625
+ return true;
626
+ }
627
+ /**
628
+ * Update the time of the first key
629
+ * Replicates UpdateTimeFirstKey from keyman.cpp
630
+ */
631
+ updateTimeFirstKey(_nCreateTime) {
632
+ }
633
+ // Helper methods for key conversion
634
+ // These methods help convert between navio-blsct types and bytes
635
+ // The actual implementation depends on navio-blsct API
636
+ getPublicKeyFromScalar(scalar) {
637
+ return Uint8Array.from(Buffer.from(PublicKey2.fromScalar(scalar).serialize(), "hex"));
638
+ }
639
+ getPublicKeyFromViewKey(viewKey) {
640
+ return this.getPublicKeyFromScalar(Scalar2.deserialize(viewKey.serialize()));
641
+ }
642
+ getPublicKeyBytes(publicKey) {
643
+ return Uint8Array.from(Buffer.from(publicKey.serialize(), "hex"));
644
+ }
645
+ createScalarFromBytes(_bytes) {
646
+ return Scalar2.deserialize(this.bytesToHex(_bytes));
647
+ }
648
+ createViewKeyFromBytes(_bytes) {
649
+ return Scalar2.deserialize(this.bytesToHex(_bytes));
650
+ }
651
+ createPublicKeyFromBytes(_bytes) {
652
+ return PublicKey2.deserialize(this.bytesToHex(_bytes));
653
+ }
654
+ /**
655
+ * Compute hash160 (SHA256 followed by RIPEMD160)
656
+ * This is the standard hash function used in Bitcoin-like systems
657
+ * @param data - The data to hash
658
+ * @returns The hash160 result (20 bytes)
659
+ */
660
+ hash160(data) {
661
+ const sha256Hash = sha256(data);
662
+ const hash160Result = ripemd160(sha256Hash);
663
+ return hash160Result;
664
+ }
665
+ bytesToHex(bytes) {
666
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
667
+ }
668
+ /**
669
+ * Get the private spending key
670
+ * Replicates GetSpendingKey from keyman.cpp
671
+ * @returns The private spending key (Scalar)
672
+ */
673
+ getSpendingKey() {
674
+ if (!this.fSpendKeyDefined) {
675
+ throw new Error("KeyManager: the wallet has no spend key available");
676
+ }
677
+ if (!this.spendKeyId) {
678
+ throw new Error("KeyManager: spend key ID not available");
679
+ }
680
+ const key = this.getKey(this.spendKeyId);
681
+ if (!key) {
682
+ throw new Error("KeyManager: could not access the spend key");
683
+ }
684
+ return key;
685
+ }
686
+ /**
687
+ * Get spending key for a transaction output
688
+ * Replicates GetSpendingKeyForOutput from keyman.cpp
689
+ * @param out - The transaction output
690
+ * @param key - Output parameter for the spending key
691
+ * @returns True if successful
692
+ */
693
+ getSpendingKeyForOutput(out, key) {
694
+ const hashId = this.getHashIdFromTxOut(out);
695
+ return this.getSpendingKeyForOutputById(out, hashId, key);
696
+ }
697
+ /**
698
+ * Get spending key for a transaction output by hash ID
699
+ * @param out - The transaction output
700
+ * @param hashId - The hash ID
701
+ * @param key - Output parameter for the spending key
702
+ * @returns True if successful
703
+ */
704
+ getSpendingKeyForOutputById(out, hashId, key) {
705
+ const id = { account: 0, address: 0 };
706
+ if (!this.getSubAddressId(hashId, id)) {
707
+ return false;
708
+ }
709
+ return this.getSpendingKeyForOutputBySubAddress(out, id, key);
710
+ }
711
+ /**
712
+ * Get spending key for a transaction output by sub-address identifier
713
+ * @param out - The transaction output
714
+ * @param id - The sub-address identifier
715
+ * @param key - Output parameter for the spending key
716
+ * @returns True if successful
717
+ */
718
+ getSpendingKeyForOutputBySubAddress(out, id, key) {
719
+ if (!this.fViewKeyDefined || !this.viewKey) {
720
+ throw new Error("KeyManager: the wallet has no view key available");
721
+ }
722
+ const sk = this.getSpendingKey();
723
+ const blindingKey = out.blsctData.blindingKey;
724
+ const viewKeyScalar = this.getScalarFromViewKey(this.viewKey);
725
+ const spendingKeyScalar = this.getScalarFromScalar(sk);
726
+ key.value = calcPrivSpendingKey2(
727
+ blindingKey,
728
+ viewKeyScalar,
729
+ spendingKeyScalar,
730
+ id.account,
731
+ id.address
732
+ );
733
+ return true;
734
+ }
735
+ /**
736
+ * Get spending key for output with caching
737
+ * Replicates GetSpendingKeyForOutputWithCache from keyman.cpp
738
+ * @param out - The transaction output
739
+ * @param key - Output parameter for the spending key
740
+ * @returns True if successful
741
+ */
742
+ getSpendingKeyForOutputWithCache(out, key) {
743
+ const hashId = this.getHashIdFromTxOut(out);
744
+ return this.getSpendingKeyForOutputWithCacheById(out, hashId, key);
745
+ }
746
+ /**
747
+ * Get spending key for output with caching by hash ID
748
+ * @param out - The transaction output
749
+ * @param hashId - The hash ID
750
+ * @param key - Output parameter for the spending key
751
+ * @returns True if successful
752
+ */
753
+ getSpendingKeyForOutputWithCacheById(out, hashId, key) {
754
+ const id = { account: 0, address: 0 };
755
+ if (!this.getSubAddressId(hashId, id)) {
756
+ return false;
757
+ }
758
+ return this.getSpendingKeyForOutputWithCacheBySubAddress(out, id, key);
759
+ }
760
+ /**
761
+ * Get spending key for output with caching by sub-address
762
+ * @param out - The transaction output
763
+ * @param id - The sub-address identifier
764
+ * @param key - Output parameter for the spending key
765
+ * @returns True if successful
766
+ */
767
+ getSpendingKeyForOutputWithCacheBySubAddress(out, id, key) {
768
+ if (!this.fViewKeyDefined || !this.viewKey) {
769
+ throw new Error("KeyManager: the wallet has no view key available");
770
+ }
771
+ const sk = this.getSpendingKey();
772
+ const blindingKey = out.blsctData.blindingKey;
773
+ const viewKeyScalar = this.getScalarFromViewKey(this.viewKey);
774
+ const spendingKeyScalar = this.getScalarFromScalar(sk);
775
+ const outIdData = new Uint8Array(
776
+ this.getPublicKeyBytes(blindingKey).length + this.getScalarBytes(viewKeyScalar).length + this.getScalarBytes(spendingKeyScalar).length + 8 + 8
777
+ // account (int64) + address (uint64)
778
+ );
779
+ let offset = 0;
780
+ outIdData.set(this.getPublicKeyBytes(blindingKey), offset);
781
+ offset += this.getPublicKeyBytes(blindingKey).length;
782
+ outIdData.set(this.getScalarBytes(viewKeyScalar), offset);
783
+ offset += this.getScalarBytes(viewKeyScalar).length;
784
+ outIdData.set(this.getScalarBytes(spendingKeyScalar), offset);
785
+ offset += this.getScalarBytes(spendingKeyScalar).length;
786
+ const accountView = new DataView(outIdData.buffer, offset, 8);
787
+ accountView.setBigInt64(0, BigInt(id.account), true);
788
+ offset += 8;
789
+ const addressView = new DataView(outIdData.buffer, offset, 8);
790
+ addressView.setBigUint64(0, BigInt(id.address), true);
791
+ const outId = sha256(outIdData);
792
+ const cachedKey = this.getOutKey(outId);
793
+ if (cachedKey) {
794
+ key.value = cachedKey;
795
+ return true;
796
+ }
797
+ const calculatedKey = calcPrivSpendingKey2(
798
+ blindingKey,
799
+ viewKeyScalar,
800
+ spendingKeyScalar,
801
+ id.account,
802
+ id.address
803
+ );
804
+ this.addKeyOutKey(calculatedKey, outId);
805
+ key.value = calculatedKey;
806
+ return true;
807
+ }
808
+ // ============================================================================
809
+ // High Priority: Output Detection
810
+ // ============================================================================
811
+ /**
812
+ * Check if a transaction output belongs to this wallet
813
+ * Replicates IsMine(txout) from keyman.cpp
814
+ * @param txout - The transaction output
815
+ * @returns True if the output belongs to this wallet
816
+ */
817
+ isMine(txout) {
818
+ const spendingKey = txout.blsctData.spendingKey;
819
+ const isZero = this.isPublicKeyZero(spendingKey);
820
+ if (isZero) {
821
+ const extractedSpendingKey = { value: null };
822
+ if (this.extractSpendingKeyFromScript(txout.scriptPubKey, extractedSpendingKey)) {
823
+ return this.isMineByKeys(
824
+ txout.blsctData.blindingKey,
825
+ extractedSpendingKey.value,
826
+ txout.blsctData.viewTag
827
+ );
828
+ }
829
+ return false;
830
+ }
831
+ return this.isMineByKeys(
832
+ txout.blsctData.blindingKey,
833
+ spendingKey,
834
+ txout.blsctData.viewTag
835
+ );
836
+ }
837
+ /**
838
+ * Check if output belongs to wallet by keys
839
+ * Replicates IsMine(blindingKey, spendingKey, viewTag) from keyman.cpp
840
+ * @param blindingKey - The blinding public key
841
+ * @param spendingKey - The spending public key
842
+ * @param viewTag - The view tag
843
+ * @returns True if the output belongs to this wallet
844
+ */
845
+ isMineByKeys(blindingKey, spendingKey, viewTag) {
846
+ if (!this.fViewKeyDefined || !this.viewKey) {
847
+ return false;
848
+ }
849
+ const calculatedViewTag = this.calculateViewTag(blindingKey);
850
+ if (viewTag !== calculatedViewTag) {
851
+ return false;
852
+ }
853
+ const hashId = this.getHashId(blindingKey, spendingKey);
854
+ return this.haveSubAddress(hashId);
855
+ }
856
+ /**
857
+ * Check if a script belongs to this wallet
858
+ * Replicates IsMine(script) from keyman.cpp
859
+ * @param script - The script
860
+ * @returns True if the script belongs to this wallet
861
+ */
862
+ isMineByScript(_script) {
863
+ return false;
864
+ }
865
+ /**
866
+ * Get hash ID from transaction output
867
+ * Replicates GetHashId(txout) from keyman.cpp
868
+ * @param txout - The transaction output
869
+ * @returns The hash ID (20 bytes) or empty if not valid
870
+ */
871
+ getHashIdFromTxOut(txout) {
872
+ if (!txout.blsctData || txout.blsctData.viewTag === 0) {
873
+ return new Uint8Array(20);
874
+ }
875
+ const spendingKey = txout.blsctData.spendingKey;
876
+ const isZero = this.isPublicKeyZero(spendingKey);
877
+ if (isZero) {
878
+ const extractedSpendingKey = { value: null };
879
+ if (this.extractSpendingKeyFromScript(txout.scriptPubKey, extractedSpendingKey)) {
880
+ return this.getHashId(
881
+ txout.blsctData.blindingKey,
882
+ extractedSpendingKey.value
883
+ );
884
+ }
885
+ return new Uint8Array(20);
886
+ }
887
+ return this.getHashId(txout.blsctData.blindingKey, spendingKey);
888
+ }
889
+ /**
890
+ * Get hash ID from keys (public API)
891
+ * Replicates GetHashId(blindingKey, spendingKey) from keyman.cpp
892
+ * @param blindingKey - The blinding public key
893
+ * @param spendingKey - The spending public key
894
+ * @returns The hash ID (20 bytes)
895
+ */
896
+ getHashId(blindingKey, spendingKey) {
897
+ if (!this.fViewKeyDefined || !this.viewKey) {
898
+ throw new Error("KeyManager: the wallet has no view key available");
899
+ }
900
+ return this.calculateHashId(blindingKey, spendingKey);
901
+ }
902
+ /**
903
+ * Get destination from transaction output
904
+ * Replicates GetDestination from keyman.cpp
905
+ * @param txout - The transaction output
906
+ * @returns The destination (SubAddress keys) or null
907
+ */
908
+ getDestination(txout) {
909
+ const hashId = this.getHashIdFromTxOut(txout);
910
+ const subAddress = { value: null };
911
+ if (!this.getSubAddressByHashId(hashId, subAddress)) {
912
+ return null;
913
+ }
914
+ return subAddress.value;
915
+ }
916
+ /**
917
+ * Check if output is a change output
918
+ * Replicates OutputIsChange from keyman.cpp
919
+ * @param out - The transaction output
920
+ * @returns True if it's a change output (account -1)
921
+ */
922
+ outputIsChange(out) {
923
+ const hashId = this.getHashIdFromTxOut(out);
924
+ const id = { account: 0, address: 0 };
925
+ if (!this.getSubAddressId(hashId, id)) {
926
+ return false;
927
+ }
928
+ return id.account === -1;
929
+ }
930
+ // ============================================================================
931
+ // Medium Priority: Token Keys
932
+ // ============================================================================
933
+ /**
934
+ * Get master token key
935
+ * Replicates GetMasterTokenKey from keyman.cpp
936
+ * @returns The master token key (Scalar)
937
+ */
938
+ getMasterTokenKey() {
939
+ if (!this.isHDEnabled()) {
940
+ throw new Error("KeyManager: the wallet has no HD enabled");
941
+ }
942
+ if (!this.tokenKeyId) {
943
+ throw new Error("KeyManager: token key ID not available");
944
+ }
945
+ const key = this.getKey(this.tokenKeyId);
946
+ if (!key) {
947
+ throw new Error("KeyManager: could not access the master token key");
948
+ }
949
+ return key;
950
+ }
951
+ /**
952
+ * Recover amounts from transaction outputs
953
+ * Replicates RecoverOutputs from keyman.cpp
954
+ * Uses navio-blsct recoverAmount function
955
+ * Reference: https://nav-io.github.io/libblsct-bindings/ts/functions/recoverAmount.html
956
+ * @param outs - Array of transaction outputs
957
+ * @returns Recovery result with amounts and indices
958
+ */
959
+ recoverOutputs(outs) {
960
+ if (!this.fViewKeyDefined || !this.viewKey) {
961
+ return { success: false, amounts: [], indices: [] };
962
+ }
963
+ const recoveryRequests = [];
964
+ for (let i = 0; i < outs.length; i++) {
965
+ const out = outs[i];
966
+ if (!out.blsctData || !out.blsctData.rangeProof) {
967
+ continue;
968
+ }
969
+ const calculatedViewTag = this.calculateViewTag(out.blsctData.blindingKey);
970
+ if (out.blsctData.viewTag !== calculatedViewTag) {
971
+ continue;
972
+ }
973
+ const nonce = this.calculateNonce(out.blsctData.blindingKey);
974
+ recoveryRequests.push({
975
+ rangeProof: out.blsctData.rangeProof,
976
+ tokenId: out.tokenId,
977
+ nonce,
978
+ index: i
979
+ });
980
+ }
981
+ if (recoveryRequests.length === 0) {
982
+ return { success: false, amounts: [], indices: [] };
983
+ }
984
+ const rv = recoverAmount2(recoveryRequests);
985
+ try {
986
+ const size = getAmountRecoveryResultSize2(rv.value);
987
+ const amounts = [];
988
+ const indices = [];
989
+ let allSuccess = true;
990
+ for (let i = 0; i < size; i++) {
991
+ const isSucc = getAmountRecoveryResultIsSucc2(rv.value, i);
992
+ if (isSucc) {
993
+ amounts.push(getAmountRecoveryResultAmount2(rv.value, i));
994
+ indices.push(i);
995
+ } else {
996
+ allSuccess = false;
997
+ }
998
+ }
999
+ return {
1000
+ success: allSuccess && amounts.length > 0,
1001
+ amounts,
1002
+ indices
1003
+ };
1004
+ } finally {
1005
+ deleteAmountsRetVal2(rv);
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Recover amounts from transaction outputs with nonce
1010
+ * Replicates RecoverOutputsWithNonce from keyman.cpp
1011
+ * @param outs - Array of transaction outputs
1012
+ * @param nonce - The nonce (PublicKey)
1013
+ * @returns Recovery result with amounts and indices
1014
+ */
1015
+ recoverOutputsWithNonce(outs, nonce) {
1016
+ if (!this.fViewKeyDefined || !this.viewKey) {
1017
+ return { success: false, amounts: [], indices: [] };
1018
+ }
1019
+ const recoveryRequests = [];
1020
+ for (let i = 0; i < outs.length; i++) {
1021
+ const out = outs[i];
1022
+ if (!out.blsctData || !out.blsctData.rangeProof) {
1023
+ continue;
1024
+ }
1025
+ recoveryRequests.push({
1026
+ rangeProof: out.blsctData.rangeProof,
1027
+ tokenId: out.tokenId,
1028
+ nonce,
1029
+ index: i
1030
+ });
1031
+ }
1032
+ if (recoveryRequests.length === 0) {
1033
+ return { success: false, amounts: [], indices: [] };
1034
+ }
1035
+ const rv = recoverAmount2(recoveryRequests);
1036
+ try {
1037
+ const size = getAmountRecoveryResultSize2(rv.value);
1038
+ const amounts = [];
1039
+ const indices = [];
1040
+ let allSuccess = true;
1041
+ for (let i = 0; i < size; i++) {
1042
+ const isSucc = getAmountRecoveryResultIsSucc2(rv.value, i);
1043
+ if (isSucc) {
1044
+ amounts.push(getAmountRecoveryResultAmount2(rv.value, i));
1045
+ indices.push(i);
1046
+ } else {
1047
+ allSuccess = false;
1048
+ }
1049
+ }
1050
+ return {
1051
+ success: allSuccess && amounts.length > 0,
1052
+ amounts,
1053
+ indices
1054
+ };
1055
+ } finally {
1056
+ deleteAmountsRetVal2(rv);
1057
+ }
1058
+ }
1059
+ // ============================================================================
1060
+ // Medium Priority: SubAddress by Hash ID
1061
+ // ============================================================================
1062
+ /**
1063
+ * Load sub-address mapping from storage
1064
+ * Replicates LoadSubAddress from keyman.cpp
1065
+ * @param hashId - The hash ID
1066
+ * @param index - The sub-address identifier
1067
+ */
1068
+ loadSubAddress(hashId, index) {
1069
+ const hashIdHex = this.bytesToHex(hashId);
1070
+ this.subAddresses.set(hashIdHex, index);
1071
+ }
1072
+ /**
1073
+ * Add sub-address mapping and save to database
1074
+ * Replicates AddSubAddress from keyman.cpp
1075
+ * @param hashId - The hash ID
1076
+ * @param index - The sub-address identifier
1077
+ * @returns True if successful
1078
+ */
1079
+ addSubAddress(hashId, index) {
1080
+ const hashIdHex = this.bytesToHex(hashId);
1081
+ this.subAddresses.set(hashIdHex, index);
1082
+ return true;
1083
+ }
1084
+ /**
1085
+ * Check if sub-address exists by hash ID
1086
+ * Replicates HaveSubAddress from keyman.cpp
1087
+ * @param hashId - The hash ID
1088
+ * @returns True if the sub-address exists
1089
+ */
1090
+ haveSubAddress(hashId) {
1091
+ const hashIdHex = this.bytesToHex(hashId);
1092
+ return this.subAddresses.has(hashIdHex);
1093
+ }
1094
+ /**
1095
+ * Get sub-address by hash ID
1096
+ * Replicates GetSubAddress(hashId) from keyman.cpp
1097
+ * @param hashId - The hash ID
1098
+ * @param address - Output parameter for the sub-address
1099
+ * @returns True if successful
1100
+ */
1101
+ getSubAddressByHashId(hashId, address) {
1102
+ if (!this.haveSubAddress(hashId)) {
1103
+ return false;
1104
+ }
1105
+ const hashIdHex = this.bytesToHex(hashId);
1106
+ const id = this.subAddresses.get(hashIdHex);
1107
+ address.value = this.getSubAddress(id);
1108
+ return true;
1109
+ }
1110
+ /**
1111
+ * Get sub-address identifier from hash ID
1112
+ * Replicates GetSubAddressId from keyman.cpp
1113
+ * @param hashId - The hash ID
1114
+ * @param id - Output parameter for the sub-address identifier
1115
+ * @returns True if successful
1116
+ */
1117
+ getSubAddressId(hashId, id) {
1118
+ if (!this.haveSubAddress(hashId)) {
1119
+ return false;
1120
+ }
1121
+ const hashIdHex = this.bytesToHex(hashId);
1122
+ const storedId = this.subAddresses.get(hashIdHex);
1123
+ id.account = storedId.account;
1124
+ id.address = storedId.address;
1125
+ return true;
1126
+ }
1127
+ /**
1128
+ * Load sub-address string mapping from storage
1129
+ * Replicates LoadSubAddressStr from keyman.cpp
1130
+ * @param subAddress - The sub-address
1131
+ * @param hashId - The hash ID
1132
+ */
1133
+ loadSubAddressStr(subAddress, hashId) {
1134
+ const subAddressStr = this.serializeSubAddress(subAddress);
1135
+ const hashIdHex = this.bytesToHex(hashId);
1136
+ this.subAddressesStr.set(subAddressStr, hashIdHex);
1137
+ }
1138
+ /**
1139
+ * Add sub-address string mapping and save to database
1140
+ * Replicates AddSubAddressStr from keyman.cpp
1141
+ * @param subAddress - The sub-address
1142
+ * @param hashId - The hash ID
1143
+ * @returns True if successful
1144
+ */
1145
+ addSubAddressStr(subAddress, hashId) {
1146
+ const subAddressStr = this.serializeSubAddress(subAddress);
1147
+ const hashIdHex = this.bytesToHex(hashId);
1148
+ this.subAddressesStr.set(subAddressStr, hashIdHex);
1149
+ return true;
1150
+ }
1151
+ /**
1152
+ * Check if sub-address string exists
1153
+ * Replicates HaveSubAddressStr from keyman.cpp
1154
+ * @param subAddress - The sub-address
1155
+ * @returns True if the sub-address string exists
1156
+ */
1157
+ haveSubAddressStr(subAddress) {
1158
+ const subAddressStr = this.serializeSubAddress(subAddress);
1159
+ return this.subAddressesStr.has(subAddressStr);
1160
+ }
1161
+ // ============================================================================
1162
+ // Medium Priority: SubAddress Pool Management
1163
+ // ============================================================================
1164
+ /**
1165
+ * Reserve sub-address from pool
1166
+ * Replicates ReserveSubAddressFromPool from keyman.cpp
1167
+ * @param account - The account number
1168
+ * @param nIndex - Output parameter for the address index
1169
+ * @param keypool - Output parameter for the keypool entry
1170
+ */
1171
+ reserveSubAddressFromPool(account, nIndex, keypool) {
1172
+ const pool = this.subAddressPool.get(account);
1173
+ if (!pool || pool.size === 0) {
1174
+ throw new Error("KeyManager: Sub-address pool is empty");
1175
+ }
1176
+ const index = Array.from(pool)[0];
1177
+ pool.delete(index);
1178
+ const id = { account, address: index };
1179
+ const subAddress = this.getSubAddress(id);
1180
+ nIndex.value = index;
1181
+ keypool.value = { id, subAddress };
1182
+ }
1183
+ /**
1184
+ * Keep a sub-address (mark as used)
1185
+ * Replicates KeepSubAddress from keyman.cpp
1186
+ * @param id - The sub-address identifier
1187
+ */
1188
+ keepSubAddress(id) {
1189
+ const pool = this.subAddressPool.get(id.account);
1190
+ if (pool) {
1191
+ pool.delete(id.address);
1192
+ }
1193
+ const key = `${id.account}:${id.address}`;
1194
+ this.subAddressPoolTime.delete(key);
1195
+ }
1196
+ /**
1197
+ * Return sub-address to pool
1198
+ * Replicates ReturnSubAddress from keyman.cpp
1199
+ * @param id - The sub-address identifier
1200
+ */
1201
+ returnSubAddress(id) {
1202
+ const pool = this.subAddressPool.get(id.account);
1203
+ if (pool) {
1204
+ pool.add(id.address);
1205
+ const key = `${id.account}:${id.address}`;
1206
+ this.subAddressPoolTime.set(key, Date.now());
1207
+ }
1208
+ }
1209
+ /**
1210
+ * Get sub-address from pool
1211
+ * Replicates GetSubAddressFromPool from keyman.cpp
1212
+ * @param account - The account number
1213
+ * @param result - Output parameter for the hash ID
1214
+ * @param id - Output parameter for the sub-address identifier
1215
+ * @returns True if successful
1216
+ */
1217
+ getSubAddressFromPool(account, result, id) {
1218
+ const pool = this.subAddressPool.get(account);
1219
+ if (!pool || pool.size === 0) {
1220
+ return false;
1221
+ }
1222
+ const index = Array.from(pool)[0];
1223
+ const subAddressId = { account, address: index };
1224
+ const subAddress = this.getSubAddress(subAddressId);
1225
+ const hashId = this.getHashIdFromSubAddress(subAddress);
1226
+ result.value = hashId;
1227
+ id.value = subAddressId;
1228
+ return true;
1229
+ }
1230
+ /**
1231
+ * Get oldest sub-address pool time
1232
+ * Replicates GetOldestSubAddressPoolTime from keyman.cpp
1233
+ * @param account - The account number
1234
+ * @returns The oldest time or 0 if pool is empty
1235
+ */
1236
+ getOldestSubAddressPoolTime(account) {
1237
+ const pool = this.subAddressPool.get(account);
1238
+ if (!pool || pool.size === 0) {
1239
+ return 0;
1240
+ }
1241
+ let oldestTime = Number.MAX_SAFE_INTEGER;
1242
+ for (const index of Array.from(pool)) {
1243
+ const key = `${account}:${index}`;
1244
+ const time = this.subAddressPoolTime.get(key) || 0;
1245
+ if (time < oldestTime && time > 0) {
1246
+ oldestTime = time;
1247
+ }
1248
+ }
1249
+ return oldestTime === Number.MAX_SAFE_INTEGER ? 0 : oldestTime;
1250
+ }
1251
+ // ============================================================================
1252
+ // Low Priority: Utilities
1253
+ // ============================================================================
1254
+ /**
1255
+ * Add inactive HD chain
1256
+ * Replicates AddInactiveHDChain from keyman.cpp
1257
+ * @param chain - The HD chain to add
1258
+ */
1259
+ addInactiveHDChain(_chain) {
1260
+ }
1261
+ /**
1262
+ * Get time of first key
1263
+ * Replicates GetTimeFirstKey from keyman.cpp
1264
+ * @returns The creation time of the first key
1265
+ */
1266
+ getTimeFirstKey() {
1267
+ return this.timeFirstKey || 0;
1268
+ }
1269
+ /**
1270
+ * Extract spending key from script
1271
+ * Replicates ExtractSpendingKeyFromScript from keyman.cpp
1272
+ * @param script - The script
1273
+ * @param spendingKey - Output parameter for the spending key
1274
+ * @returns True if successful
1275
+ */
1276
+ extractSpendingKeyFromScript(_script, _spendingKey) {
1277
+ return false;
1278
+ }
1279
+ // ============================================================================
1280
+ // Helper methods for new functionality
1281
+ // ============================================================================
1282
+ /**
1283
+ * Check if public key is zero
1284
+ */
1285
+ isPublicKeyZero(_publicKey) {
1286
+ return false;
1287
+ }
1288
+ /**
1289
+ * Get scalar from ViewKey
1290
+ */
1291
+ getScalarFromViewKey(viewKey) {
1292
+ return Scalar2.deserialize(viewKey.serialize());
1293
+ }
1294
+ /**
1295
+ * Get scalar from Scalar (identity function, but ensures type)
1296
+ */
1297
+ getScalarFromScalar(scalar) {
1298
+ return scalar;
1299
+ }
1300
+ /**
1301
+ * Get bytes from Scalar
1302
+ */
1303
+ getScalarBytes(scalar) {
1304
+ return Uint8Array.from(Buffer.from(scalar.serialize(), "hex"));
1305
+ }
1306
+ /**
1307
+ * Serialize SubAddress to string for map key
1308
+ */
1309
+ serializeSubAddress(subAddress) {
1310
+ return subAddress.serialize();
1311
+ }
1312
+ /**
1313
+ * Get hash ID from SubAddress
1314
+ * Uses DoublePublicKey.deserialize(subaddress.serialize()) to extract keys
1315
+ */
1316
+ getHashIdFromSubAddress(subAddress) {
1317
+ const serialized = subAddress.serialize();
1318
+ const doublePublicKey = DoublePublicKey2.deserialize(serialized);
1319
+ const keys = doublePublicKey.getKeys ? doublePublicKey.getKeys() : doublePublicKey;
1320
+ const spendingKey = keys.spendingKey || keys.key1 || keys;
1321
+ const blindingKey = keys.blindingKey || keys.key2 || keys;
1322
+ return this.getHashId(blindingKey, spendingKey);
1323
+ }
1324
+ };
1325
+ var initSqlJs;
1326
+ var SQL;
1327
+ async function loadSQL() {
1328
+ if (SQL) return SQL;
1329
+ try {
1330
+ if (typeof globalThis.window !== "undefined") {
1331
+ const sqlJs = await import('sql.js');
1332
+ initSqlJs = sqlJs.default;
1333
+ SQL = await initSqlJs({
1334
+ // Prefer explicit override, otherwise use CDN to avoid requiring a local wasm file
1335
+ locateFile: (file) => {
1336
+ if (file === "sql-wasm.wasm") {
1337
+ const override = globalThis.NAVIO_SQL_WASM_URL;
1338
+ if (typeof override === "string" && override.length > 0) {
1339
+ return override;
1340
+ }
1341
+ }
1342
+ return `https://sql.js.org/dist/${file}`;
1343
+ }
1344
+ });
1345
+ } else {
1346
+ const sqlJs = __require("sql.js");
1347
+ initSqlJs = sqlJs.default || sqlJs;
1348
+ const fs = __require("fs");
1349
+ SQL = await initSqlJs({
1350
+ locateFile: (file) => {
1351
+ const path = __require("path");
1352
+ const wasmPath = path.join(__dirname, "../node_modules/sql.js/dist/sql-wasm.wasm");
1353
+ if (fs.existsSync(wasmPath)) {
1354
+ return wasmPath;
1355
+ }
1356
+ return file;
1357
+ }
1358
+ });
1359
+ }
1360
+ return SQL;
1361
+ } catch (error) {
1362
+ throw new Error(`Failed to load SQL.js: ${error}. Please install sql.js: npm install sql.js`);
1363
+ }
1364
+ }
1365
+ var WalletDB = class {
1366
+ /**
1367
+ * Create a new WalletDB instance
1368
+ * @param dbPath - Path to the database file (or name for in-memory)
1369
+ * @param createIfNotExists - Create database if it doesn't exist
1370
+ */
1371
+ constructor(dbPath = ":memory:", _createIfNotExists = true) {
1372
+ this.db = null;
1373
+ this.keyManager = null;
1374
+ this.isOpen = false;
1375
+ this.dbPath = dbPath;
1376
+ }
1377
+ /**
1378
+ * Initialize the database connection and schema
1379
+ */
1380
+ async initDatabase() {
1381
+ if (this.isOpen) return;
1382
+ const SQL2 = await loadSQL();
1383
+ const fs = this.getFileSystem();
1384
+ if (this.dbPath !== ":memory:" && fs && fs.existsSync && fs.existsSync(this.dbPath)) {
1385
+ try {
1386
+ const buffer = fs.readFileSync(this.dbPath);
1387
+ this.db = new SQL2.Database(buffer);
1388
+ } catch (error) {
1389
+ this.db = new SQL2.Database();
1390
+ }
1391
+ } else {
1392
+ this.db = new SQL2.Database();
1393
+ }
1394
+ this.createSchema();
1395
+ this.isOpen = true;
1396
+ }
1397
+ /**
1398
+ * Get file system interface (Node.js only)
1399
+ */
1400
+ getFileSystem() {
1401
+ if (typeof globalThis.window === "undefined" && typeof __require !== "undefined") {
1402
+ try {
1403
+ return __require("fs");
1404
+ } catch {
1405
+ return null;
1406
+ }
1407
+ }
1408
+ return null;
1409
+ }
1410
+ /**
1411
+ * Create database schema
1412
+ * Replicates navio-core wallet database structure
1413
+ */
1414
+ createSchema() {
1415
+ this.db.run(`
1416
+ CREATE TABLE IF NOT EXISTS keys (
1417
+ key_id TEXT PRIMARY KEY,
1418
+ secret_key TEXT NOT NULL,
1419
+ public_key TEXT NOT NULL,
1420
+ create_time INTEGER NOT NULL
1421
+ )
1422
+ `);
1423
+ this.db.run(`
1424
+ CREATE TABLE IF NOT EXISTS out_keys (
1425
+ out_id TEXT PRIMARY KEY,
1426
+ secret_key TEXT NOT NULL
1427
+ )
1428
+ `);
1429
+ this.db.run(`
1430
+ CREATE TABLE IF NOT EXISTS crypted_keys (
1431
+ key_id TEXT PRIMARY KEY,
1432
+ public_key TEXT NOT NULL,
1433
+ encrypted_secret TEXT NOT NULL
1434
+ )
1435
+ `);
1436
+ this.db.run(`
1437
+ CREATE TABLE IF NOT EXISTS crypted_out_keys (
1438
+ out_id TEXT PRIMARY KEY,
1439
+ public_key TEXT NOT NULL,
1440
+ encrypted_secret TEXT NOT NULL
1441
+ )
1442
+ `);
1443
+ this.db.run(`
1444
+ CREATE TABLE IF NOT EXISTS view_key (
1445
+ public_key TEXT PRIMARY KEY,
1446
+ secret_key TEXT NOT NULL
1447
+ )
1448
+ `);
1449
+ this.db.run(`
1450
+ CREATE TABLE IF NOT EXISTS spend_key (
1451
+ public_key TEXT PRIMARY KEY
1452
+ )
1453
+ `);
1454
+ this.db.run(`
1455
+ CREATE TABLE IF NOT EXISTS hd_chain (
1456
+ id INTEGER PRIMARY KEY,
1457
+ version INTEGER NOT NULL,
1458
+ seed_id TEXT NOT NULL,
1459
+ spend_id TEXT NOT NULL,
1460
+ view_id TEXT NOT NULL,
1461
+ token_id TEXT NOT NULL,
1462
+ blinding_id TEXT NOT NULL
1463
+ )
1464
+ `);
1465
+ this.db.run(`
1466
+ CREATE TABLE IF NOT EXISTS sub_addresses (
1467
+ hash_id TEXT PRIMARY KEY,
1468
+ account INTEGER NOT NULL,
1469
+ address INTEGER NOT NULL
1470
+ )
1471
+ `);
1472
+ this.db.run(`
1473
+ CREATE TABLE IF NOT EXISTS sub_addresses_str (
1474
+ sub_address TEXT PRIMARY KEY,
1475
+ hash_id TEXT NOT NULL
1476
+ )
1477
+ `);
1478
+ this.db.run(`
1479
+ CREATE TABLE IF NOT EXISTS sub_address_pool (
1480
+ account INTEGER NOT NULL,
1481
+ address INTEGER NOT NULL,
1482
+ create_time INTEGER NOT NULL,
1483
+ PRIMARY KEY (account, address)
1484
+ )
1485
+ `);
1486
+ this.db.run(`
1487
+ CREATE TABLE IF NOT EXISTS sub_address_counter (
1488
+ account INTEGER PRIMARY KEY,
1489
+ counter INTEGER NOT NULL
1490
+ )
1491
+ `);
1492
+ this.db.run(`
1493
+ CREATE TABLE IF NOT EXISTS key_metadata (
1494
+ key_id TEXT PRIMARY KEY,
1495
+ create_time INTEGER NOT NULL
1496
+ )
1497
+ `);
1498
+ this.db.run(`
1499
+ CREATE TABLE IF NOT EXISTS tx_keys (
1500
+ tx_hash TEXT PRIMARY KEY,
1501
+ block_height INTEGER NOT NULL,
1502
+ keys_data TEXT NOT NULL
1503
+ )
1504
+ `);
1505
+ this.db.run(`
1506
+ CREATE INDEX IF NOT EXISTS idx_tx_keys_block_height ON tx_keys(block_height)
1507
+ `);
1508
+ this.db.run(`
1509
+ CREATE TABLE IF NOT EXISTS block_hashes (
1510
+ height INTEGER PRIMARY KEY,
1511
+ hash TEXT NOT NULL
1512
+ )
1513
+ `);
1514
+ this.db.run(`
1515
+ CREATE TABLE IF NOT EXISTS sync_state (
1516
+ id INTEGER PRIMARY KEY,
1517
+ last_synced_height INTEGER NOT NULL,
1518
+ last_synced_hash TEXT NOT NULL,
1519
+ total_tx_keys_synced INTEGER NOT NULL,
1520
+ last_sync_time INTEGER NOT NULL,
1521
+ chain_tip_at_last_sync INTEGER NOT NULL
1522
+ )
1523
+ `);
1524
+ this.db.run(`
1525
+ CREATE TABLE IF NOT EXISTS wallet_metadata (
1526
+ id INTEGER PRIMARY KEY,
1527
+ creation_height INTEGER NOT NULL DEFAULT 0,
1528
+ creation_time INTEGER NOT NULL,
1529
+ restored_from_seed INTEGER NOT NULL DEFAULT 0,
1530
+ version INTEGER NOT NULL DEFAULT 1
1531
+ )
1532
+ `);
1533
+ this.db.run(`
1534
+ CREATE TABLE IF NOT EXISTS wallet_outputs (
1535
+ output_hash TEXT PRIMARY KEY,
1536
+ tx_hash TEXT NOT NULL,
1537
+ output_index INTEGER NOT NULL,
1538
+ block_height INTEGER NOT NULL,
1539
+ output_data TEXT NOT NULL,
1540
+ amount INTEGER NOT NULL DEFAULT 0,
1541
+ memo TEXT,
1542
+ token_id TEXT,
1543
+ blinding_key TEXT,
1544
+ spending_key TEXT,
1545
+ is_spent INTEGER NOT NULL DEFAULT 0,
1546
+ spent_tx_hash TEXT,
1547
+ spent_block_height INTEGER,
1548
+ created_at INTEGER NOT NULL
1549
+ )
1550
+ `);
1551
+ this.db.run(`
1552
+ CREATE INDEX IF NOT EXISTS idx_wallet_outputs_tx_hash ON wallet_outputs(tx_hash)
1553
+ `);
1554
+ this.db.run(`
1555
+ CREATE INDEX IF NOT EXISTS idx_wallet_outputs_block_height ON wallet_outputs(block_height)
1556
+ `);
1557
+ this.db.run(`
1558
+ CREATE INDEX IF NOT EXISTS idx_wallet_outputs_is_spent ON wallet_outputs(is_spent)
1559
+ `);
1560
+ }
1561
+ /**
1562
+ * Load wallet from database
1563
+ * @returns The loaded KeyManager instance
1564
+ * @throws Error if no wallet exists in the database
1565
+ */
1566
+ async loadWallet() {
1567
+ await this.initDatabase();
1568
+ const keyManager = new KeyManager();
1569
+ const hdChainResult = this.db.exec("SELECT * FROM hd_chain LIMIT 1");
1570
+ if (hdChainResult.length === 0 || hdChainResult[0].values.length === 0) {
1571
+ throw new Error("No wallet found in database");
1572
+ }
1573
+ const row = hdChainResult[0].values[0];
1574
+ const hdChain = {
1575
+ version: row[1],
1576
+ seedId: this.hexToBytes(row[2]),
1577
+ spendId: this.hexToBytes(row[3]),
1578
+ viewId: this.hexToBytes(row[4]),
1579
+ tokenId: this.hexToBytes(row[5]),
1580
+ blindingId: this.hexToBytes(row[6])
1581
+ };
1582
+ keyManager.loadHDChain(hdChain);
1583
+ const viewKeyResult = this.db.exec("SELECT * FROM view_key LIMIT 1");
1584
+ if (viewKeyResult.length > 0 && viewKeyResult[0].values.length > 0) {
1585
+ const row2 = viewKeyResult[0].values[0];
1586
+ const viewKey = this.deserializeViewKey(row2[1]);
1587
+ keyManager.loadViewKey(viewKey);
1588
+ }
1589
+ const spendKeyResult = this.db.exec("SELECT * FROM spend_key LIMIT 1");
1590
+ if (spendKeyResult.length > 0 && spendKeyResult[0].values.length > 0) {
1591
+ const row2 = spendKeyResult[0].values[0];
1592
+ const publicKey = this.deserializePublicKey(row2[0]);
1593
+ keyManager.loadSpendKey(publicKey);
1594
+ }
1595
+ const keysResult = this.db.exec("SELECT * FROM keys");
1596
+ if (keysResult.length > 0) {
1597
+ for (const row2 of keysResult[0].values) {
1598
+ const secretKey = this.deserializeScalar(row2[1]);
1599
+ const publicKey = this.deserializePublicKey(row2[2]);
1600
+ keyManager.loadKey(secretKey, publicKey);
1601
+ const stmt = this.db.prepare("SELECT create_time FROM key_metadata WHERE key_id = ?");
1602
+ stmt.bind([row2[0]]);
1603
+ if (stmt.step()) {
1604
+ const metadataRow = stmt.getAsObject();
1605
+ keyManager.loadKeyMetadata(this.hexToBytes(row2[0]), {
1606
+ nCreateTime: metadataRow.create_time
1607
+ });
1608
+ }
1609
+ stmt.free();
1610
+ }
1611
+ }
1612
+ const outKeysResult = this.db.exec("SELECT * FROM out_keys");
1613
+ if (outKeysResult.length > 0) {
1614
+ for (const row2 of outKeysResult[0].values) {
1615
+ const secretKey = this.deserializeScalar(row2[1]);
1616
+ const outId = this.hexToBytes(row2[0]);
1617
+ keyManager.loadOutKey(secretKey, outId);
1618
+ }
1619
+ }
1620
+ const cryptedKeysResult = this.db.exec("SELECT * FROM crypted_keys");
1621
+ if (cryptedKeysResult.length > 0) {
1622
+ for (const row2 of cryptedKeysResult[0].values) {
1623
+ const publicKey = this.deserializePublicKey(row2[1]);
1624
+ const encryptedSecret = this.hexToBytes(row2[2]);
1625
+ keyManager.loadCryptedKey(publicKey, encryptedSecret, true);
1626
+ }
1627
+ }
1628
+ const cryptedOutKeysResult = this.db.exec("SELECT * FROM crypted_out_keys");
1629
+ if (cryptedOutKeysResult.length > 0) {
1630
+ for (const row2 of cryptedOutKeysResult[0].values) {
1631
+ const outId = this.hexToBytes(row2[0]);
1632
+ const publicKey = this.deserializePublicKey(row2[1]);
1633
+ const encryptedSecret = this.hexToBytes(row2[2]);
1634
+ keyManager.loadCryptedOutKey(outId, publicKey, encryptedSecret, true);
1635
+ }
1636
+ }
1637
+ const subAddressesResult = this.db.exec("SELECT * FROM sub_addresses");
1638
+ if (subAddressesResult.length > 0) {
1639
+ for (const row2 of subAddressesResult[0].values) {
1640
+ const hashId = this.hexToBytes(row2[0]);
1641
+ const id = {
1642
+ account: row2[1],
1643
+ address: row2[2]
1644
+ };
1645
+ keyManager.loadSubAddress(hashId, id);
1646
+ }
1647
+ }
1648
+ const counterResult = this.db.exec("SELECT * FROM sub_address_counter");
1649
+ if (counterResult.length > 0) {
1650
+ for (const _row of counterResult[0].values) {
1651
+ }
1652
+ }
1653
+ if (subAddressesResult.length === 0 || subAddressesResult[0].values.length === 0) {
1654
+ keyManager.newSubAddressPool(0);
1655
+ keyManager.newSubAddressPool(-1);
1656
+ keyManager.newSubAddressPool(-2);
1657
+ }
1658
+ this.keyManager = keyManager;
1659
+ return keyManager;
1660
+ }
1661
+ /**
1662
+ * Create a new wallet
1663
+ * @param creationHeight - Optional block height when wallet was created (for sync optimization)
1664
+ * @returns The new KeyManager instance
1665
+ */
1666
+ async createWallet(creationHeight) {
1667
+ await this.initDatabase();
1668
+ const keyManager = new KeyManager();
1669
+ const seed = keyManager.generateNewSeed();
1670
+ keyManager.setHDSeed(seed);
1671
+ keyManager.newSubAddressPool(0);
1672
+ keyManager.newSubAddressPool(-1);
1673
+ keyManager.newSubAddressPool(-2);
1674
+ await this.saveWallet(keyManager);
1675
+ await this.saveWalletMetadata({
1676
+ creationHeight: creationHeight ?? 0,
1677
+ creationTime: Date.now(),
1678
+ restoredFromSeed: false
1679
+ });
1680
+ this.keyManager = keyManager;
1681
+ return keyManager;
1682
+ }
1683
+ /**
1684
+ * Restore wallet from seed
1685
+ * @param seedHex - The seed as hex string
1686
+ * @param creationHeight - Optional block height to start scanning from (for faster restore)
1687
+ * @returns The restored KeyManager instance
1688
+ */
1689
+ async restoreWallet(seedHex, creationHeight) {
1690
+ await this.initDatabase();
1691
+ const keyManager = new KeyManager();
1692
+ const seed = this.deserializeScalar(seedHex);
1693
+ keyManager.setHDSeed(seed);
1694
+ keyManager.newSubAddressPool(0);
1695
+ keyManager.newSubAddressPool(-1);
1696
+ keyManager.newSubAddressPool(-2);
1697
+ await this.saveWallet(keyManager);
1698
+ await this.saveWalletMetadata({
1699
+ creationHeight: creationHeight ?? 0,
1700
+ creationTime: Date.now(),
1701
+ restoredFromSeed: true
1702
+ });
1703
+ this.keyManager = keyManager;
1704
+ return keyManager;
1705
+ }
1706
+ /**
1707
+ * Save wallet metadata to database
1708
+ */
1709
+ async saveWalletMetadata(metadata) {
1710
+ if (!this.isOpen) {
1711
+ await this.initDatabase();
1712
+ }
1713
+ const stmt = this.db.prepare(`
1714
+ INSERT OR REPLACE INTO wallet_metadata (id, creation_height, creation_time, restored_from_seed, version)
1715
+ VALUES (0, ?, ?, ?, 1)
1716
+ `);
1717
+ stmt.run([metadata.creationHeight, metadata.creationTime, metadata.restoredFromSeed ? 1 : 0]);
1718
+ stmt.free();
1719
+ this.persistToDisk();
1720
+ }
1721
+ /**
1722
+ * Persist database to disk (if not in-memory)
1723
+ */
1724
+ persistToDisk() {
1725
+ if (this.dbPath !== ":memory:" && this.db) {
1726
+ const fs = this.getFileSystem();
1727
+ if (fs) {
1728
+ const data = this.db.export();
1729
+ const buffer = Buffer.from(data);
1730
+ fs.writeFileSync(this.dbPath, buffer);
1731
+ }
1732
+ }
1733
+ }
1734
+ /**
1735
+ * Get wallet metadata from database
1736
+ * @returns Wallet metadata or null if not found
1737
+ */
1738
+ async getWalletMetadata() {
1739
+ if (!this.isOpen) {
1740
+ await this.initDatabase();
1741
+ }
1742
+ const result = this.db.exec("SELECT creation_height, creation_time, restored_from_seed, version FROM wallet_metadata WHERE id = 0");
1743
+ if (result.length === 0 || result[0].values.length === 0) {
1744
+ return null;
1745
+ }
1746
+ const [creationHeight, creationTime, restoredFromSeed, version] = result[0].values[0];
1747
+ return {
1748
+ creationHeight,
1749
+ creationTime,
1750
+ restoredFromSeed: restoredFromSeed === 1,
1751
+ version
1752
+ };
1753
+ }
1754
+ /**
1755
+ * Get the wallet creation height (block height to start scanning from)
1756
+ * @returns Creation height or 0 if not set
1757
+ */
1758
+ async getCreationHeight() {
1759
+ const metadata = await this.getWalletMetadata();
1760
+ return metadata?.creationHeight ?? 0;
1761
+ }
1762
+ /**
1763
+ * Set the wallet creation height
1764
+ * @param height - Block height
1765
+ */
1766
+ async setCreationHeight(height) {
1767
+ if (!this.isOpen) {
1768
+ await this.initDatabase();
1769
+ }
1770
+ const existing = await this.getWalletMetadata();
1771
+ if (existing) {
1772
+ const stmt = this.db.prepare("UPDATE wallet_metadata SET creation_height = ? WHERE id = 0");
1773
+ stmt.run([height]);
1774
+ stmt.free();
1775
+ this.persistToDisk();
1776
+ } else {
1777
+ await this.saveWalletMetadata({
1778
+ creationHeight: height,
1779
+ creationTime: Date.now(),
1780
+ restoredFromSeed: false
1781
+ });
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Save wallet to database
1786
+ * @param keyManager - The KeyManager instance to save (optional, uses stored instance if not provided)
1787
+ */
1788
+ async saveWallet(keyManager) {
1789
+ if (!this.isOpen) {
1790
+ await this.initDatabase();
1791
+ }
1792
+ const km = keyManager || this.keyManager;
1793
+ if (!km) {
1794
+ throw new Error("No KeyManager instance to save");
1795
+ }
1796
+ this.db.run("BEGIN TRANSACTION");
1797
+ try {
1798
+ this.db.run("DELETE FROM keys");
1799
+ this.db.run("DELETE FROM out_keys");
1800
+ this.db.run("DELETE FROM crypted_keys");
1801
+ this.db.run("DELETE FROM crypted_out_keys");
1802
+ this.db.run("DELETE FROM view_key");
1803
+ this.db.run("DELETE FROM spend_key");
1804
+ this.db.run("DELETE FROM hd_chain");
1805
+ this.db.run("DELETE FROM sub_addresses");
1806
+ this.db.run("DELETE FROM sub_addresses_str");
1807
+ this.db.run("DELETE FROM sub_address_pool");
1808
+ this.db.run("DELETE FROM sub_address_counter");
1809
+ this.db.run("DELETE FROM key_metadata");
1810
+ const hdChain = km.getHDChain();
1811
+ if (hdChain) {
1812
+ const stmt = this.db.prepare(
1813
+ `INSERT INTO hd_chain (version, seed_id, spend_id, view_id, token_id, blinding_id)
1814
+ VALUES (?, ?, ?, ?, ?, ?)`
1815
+ );
1816
+ stmt.run([
1817
+ hdChain.version,
1818
+ this.bytesToHex(hdChain.seedId),
1819
+ this.bytesToHex(hdChain.spendId),
1820
+ this.bytesToHex(hdChain.viewId),
1821
+ this.bytesToHex(hdChain.tokenId),
1822
+ this.bytesToHex(hdChain.blindingId)
1823
+ ]);
1824
+ stmt.free();
1825
+ }
1826
+ try {
1827
+ const viewKey = km.getPrivateViewKey();
1828
+ const viewPublicKey = this.getPublicKeyFromViewKey(viewKey);
1829
+ const stmt = this.db.prepare("INSERT INTO view_key (public_key, secret_key) VALUES (?, ?)");
1830
+ stmt.run([this.serializePublicKey(viewPublicKey), this.serializeViewKey(viewKey)]);
1831
+ stmt.free();
1832
+ } catch {
1833
+ }
1834
+ try {
1835
+ const spendPublicKey = km.getPublicSpendingKey();
1836
+ const stmt = this.db.prepare("INSERT INTO spend_key (public_key) VALUES (?)");
1837
+ stmt.run([this.serializePublicKey(spendPublicKey)]);
1838
+ stmt.free();
1839
+ } catch {
1840
+ }
1841
+ this.db.run("COMMIT");
1842
+ this.persistToDisk();
1843
+ } catch (error) {
1844
+ this.db.run("ROLLBACK");
1845
+ throw error;
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Get the current KeyManager instance
1850
+ */
1851
+ getKeyManager() {
1852
+ return this.keyManager;
1853
+ }
1854
+ /**
1855
+ * Get the database instance (for use by sync modules)
1856
+ * @internal
1857
+ */
1858
+ getDatabase() {
1859
+ if (!this.isOpen) {
1860
+ throw new Error(
1861
+ "Database not initialized. Call loadWallet(), createWallet(), or restoreWallet() first."
1862
+ );
1863
+ }
1864
+ return this.db;
1865
+ }
1866
+ /**
1867
+ * Get the database path
1868
+ * @internal
1869
+ */
1870
+ getDatabasePath() {
1871
+ return this.dbPath;
1872
+ }
1873
+ /**
1874
+ * Save database to disk (if not in-memory)
1875
+ */
1876
+ async saveDatabase() {
1877
+ if (this.dbPath === ":memory:" || !this.isOpen) {
1878
+ return;
1879
+ }
1880
+ this.persistToDisk();
1881
+ }
1882
+ /**
1883
+ * Close the database connection
1884
+ */
1885
+ close() {
1886
+ if (this.db) {
1887
+ this.db.close();
1888
+ this.db = null;
1889
+ this.isOpen = false;
1890
+ }
1891
+ }
1892
+ // ============================================================================
1893
+ // Serialization helpers
1894
+ // ============================================================================
1895
+ deserializeScalar(hex) {
1896
+ const Scalar4 = blsctModule.Scalar;
1897
+ return Scalar4.deserialize(hex);
1898
+ }
1899
+ serializeViewKey(viewKey) {
1900
+ return viewKey.serialize();
1901
+ }
1902
+ deserializeViewKey(hex) {
1903
+ const Scalar4 = blsctModule.Scalar;
1904
+ return Scalar4.deserialize(hex);
1905
+ }
1906
+ serializePublicKey(publicKey) {
1907
+ return publicKey.serialize();
1908
+ }
1909
+ deserializePublicKey(hex) {
1910
+ const PublicKey5 = blsctModule.PublicKey;
1911
+ return PublicKey5.deserialize(hex);
1912
+ }
1913
+ getPublicKeyFromViewKey(viewKey) {
1914
+ const Scalar4 = blsctModule.Scalar;
1915
+ const PublicKey5 = blsctModule.PublicKey;
1916
+ const scalar = Scalar4.deserialize(viewKey.serialize());
1917
+ return PublicKey5.fromScalar(scalar);
1918
+ }
1919
+ bytesToHex(bytes) {
1920
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1921
+ }
1922
+ hexToBytes(hex) {
1923
+ const bytes = new Uint8Array(hex.length / 2);
1924
+ for (let i = 0; i < hex.length; i += 2) {
1925
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
1926
+ }
1927
+ return bytes;
1928
+ }
1929
+ /**
1930
+ * Get wallet balance (sum of unspent output amounts)
1931
+ * @param tokenId - Optional token ID to filter by (null for NAV)
1932
+ * @returns Balance in satoshis
1933
+ */
1934
+ async getBalance(tokenId = null) {
1935
+ if (!this.isOpen) {
1936
+ throw new Error("Database not initialized");
1937
+ }
1938
+ let query = "SELECT SUM(amount) as total FROM wallet_outputs WHERE is_spent = 0";
1939
+ if (tokenId === null) {
1940
+ query += " AND (token_id IS NULL OR token_id = ?)";
1941
+ } else {
1942
+ query += " AND token_id = ?";
1943
+ }
1944
+ const result = this.db.exec(query);
1945
+ if (result.length === 0 || result[0].values.length === 0 || result[0].values[0][0] === null) {
1946
+ return 0n;
1947
+ }
1948
+ return BigInt(result[0].values[0][0]);
1949
+ }
1950
+ /**
1951
+ * Get unspent outputs (UTXOs)
1952
+ * @param tokenId - Optional token ID to filter by (null for NAV)
1953
+ * @returns Array of unspent outputs
1954
+ */
1955
+ async getUnspentOutputs(tokenId = null) {
1956
+ if (!this.isOpen) {
1957
+ throw new Error("Database not initialized");
1958
+ }
1959
+ let query = `
1960
+ SELECT output_hash, tx_hash, output_index, block_height, amount, memo, token_id,
1961
+ blinding_key, spending_key, is_spent, spent_tx_hash, spent_block_height
1962
+ FROM wallet_outputs
1963
+ WHERE is_spent = 0
1964
+ `;
1965
+ const params = [];
1966
+ if (tokenId === null) {
1967
+ query += " AND (token_id IS NULL OR token_id = ?)";
1968
+ params.push("0000000000000000000000000000000000000000000000000000000000000000");
1969
+ } else {
1970
+ query += " AND token_id = ?";
1971
+ params.push(tokenId);
1972
+ }
1973
+ query += " ORDER BY block_height ASC";
1974
+ const stmt = this.db.prepare(query);
1975
+ if (params.length > 0) {
1976
+ stmt.bind(params);
1977
+ }
1978
+ const outputs = [];
1979
+ while (stmt.step()) {
1980
+ const row = stmt.getAsObject();
1981
+ outputs.push({
1982
+ outputHash: row.output_hash,
1983
+ txHash: row.tx_hash,
1984
+ outputIndex: row.output_index,
1985
+ blockHeight: row.block_height,
1986
+ amount: BigInt(row.amount),
1987
+ memo: row.memo,
1988
+ tokenId: row.token_id,
1989
+ blindingKey: row.blinding_key,
1990
+ spendingKey: row.spending_key,
1991
+ isSpent: row.is_spent === 1,
1992
+ spentTxHash: row.spent_tx_hash,
1993
+ spentBlockHeight: row.spent_block_height
1994
+ });
1995
+ }
1996
+ stmt.free();
1997
+ return outputs;
1998
+ }
1999
+ /**
2000
+ * Get all outputs (spent and unspent)
2001
+ * @returns Array of all wallet outputs
2002
+ */
2003
+ async getAllOutputs() {
2004
+ if (!this.isOpen) {
2005
+ throw new Error("Database not initialized");
2006
+ }
2007
+ const stmt = this.db.prepare(`
2008
+ SELECT output_hash, tx_hash, output_index, block_height, amount, memo, token_id,
2009
+ blinding_key, spending_key, is_spent, spent_tx_hash, spent_block_height
2010
+ FROM wallet_outputs
2011
+ ORDER BY block_height ASC
2012
+ `);
2013
+ const outputs = [];
2014
+ while (stmt.step()) {
2015
+ const row = stmt.getAsObject();
2016
+ outputs.push({
2017
+ outputHash: row.output_hash,
2018
+ txHash: row.tx_hash,
2019
+ outputIndex: row.output_index,
2020
+ blockHeight: row.block_height,
2021
+ amount: BigInt(row.amount),
2022
+ memo: row.memo,
2023
+ tokenId: row.token_id,
2024
+ blindingKey: row.blinding_key,
2025
+ spendingKey: row.spending_key,
2026
+ isSpent: row.is_spent === 1,
2027
+ spentTxHash: row.spent_tx_hash,
2028
+ spentBlockHeight: row.spent_block_height
2029
+ });
2030
+ }
2031
+ stmt.free();
2032
+ return outputs;
2033
+ }
2034
+ };
2035
+ var WebSocketClass;
2036
+ var isBrowserWebSocket = false;
2037
+ if (typeof globalThis.window !== "undefined" && globalThis.window?.WebSocket) {
2038
+ WebSocketClass = globalThis.window.WebSocket;
2039
+ isBrowserWebSocket = true;
2040
+ } else {
2041
+ try {
2042
+ WebSocketClass = __require("ws");
2043
+ } catch (error) {
2044
+ throw new Error("WebSocket not available. In Node.js, install ws: npm install ws");
2045
+ }
2046
+ }
2047
+ function attachWebSocketHandler(ws, event, handler) {
2048
+ if (isBrowserWebSocket) {
2049
+ switch (event) {
2050
+ case "open":
2051
+ ws.onopen = handler;
2052
+ break;
2053
+ case "message":
2054
+ ws.onmessage = (evt) => handler(evt.data);
2055
+ break;
2056
+ case "error":
2057
+ ws.onerror = () => handler(new Error("WebSocket error"));
2058
+ break;
2059
+ case "close":
2060
+ ws.onclose = handler;
2061
+ break;
2062
+ }
2063
+ } else {
2064
+ ws.on(event, handler);
2065
+ }
2066
+ }
2067
+ var ElectrumError = class extends Error {
2068
+ constructor(message, code, data) {
2069
+ super(message);
2070
+ this.code = code;
2071
+ this.data = data;
2072
+ this.name = "ElectrumError";
2073
+ }
2074
+ };
2075
+ var ElectrumClient = class {
2076
+ constructor(options = {}) {
2077
+ this.ws = null;
2078
+ this.requestId = 0;
2079
+ this.pendingRequests = /* @__PURE__ */ new Map();
2080
+ this.connected = false;
2081
+ this.reconnectAttempts = 0;
2082
+ this.options = {
2083
+ host: options.host || "localhost",
2084
+ port: options.port || 50001,
2085
+ ssl: options.ssl || false,
2086
+ timeout: options.timeout || 3e4,
2087
+ clientName: options.clientName || "navio-sdk",
2088
+ clientVersion: options.clientVersion || "1.4"
2089
+ };
2090
+ }
2091
+ /**
2092
+ * Connect to the Electrum server
2093
+ */
2094
+ async connect() {
2095
+ if (this.connected && this.ws) {
2096
+ return;
2097
+ }
2098
+ return new Promise((resolve, reject) => {
2099
+ const protocol = this.options.ssl ? "wss" : "ws";
2100
+ const url = `${protocol}://${this.options.host}:${this.options.port}`;
2101
+ try {
2102
+ this.ws = new WebSocketClass(url);
2103
+ attachWebSocketHandler(this.ws, "open", async () => {
2104
+ this.connected = true;
2105
+ this.reconnectAttempts = 0;
2106
+ try {
2107
+ await this.call("server.version", this.options.clientName, this.options.clientVersion);
2108
+ resolve();
2109
+ } catch (error) {
2110
+ reject(error);
2111
+ }
2112
+ });
2113
+ attachWebSocketHandler(this.ws, "message", (data) => {
2114
+ try {
2115
+ const dataStr = typeof data === "string" ? data : data.toString();
2116
+ const response = JSON.parse(dataStr);
2117
+ this.handleResponse(response);
2118
+ } catch (error) {
2119
+ console.error("Error parsing response:", error);
2120
+ }
2121
+ });
2122
+ attachWebSocketHandler(this.ws, "error", (error) => {
2123
+ this.connected = false;
2124
+ if (this.reconnectAttempts === 0) {
2125
+ reject(error);
2126
+ }
2127
+ });
2128
+ attachWebSocketHandler(this.ws, "close", () => {
2129
+ this.connected = false;
2130
+ this.ws = null;
2131
+ for (const [, { reject: reject2, timeout }] of Array.from(this.pendingRequests)) {
2132
+ clearTimeout(timeout);
2133
+ reject2(new Error("Connection closed"));
2134
+ }
2135
+ this.pendingRequests.clear();
2136
+ });
2137
+ } catch (error) {
2138
+ reject(error);
2139
+ }
2140
+ });
2141
+ }
2142
+ /**
2143
+ * Handle incoming response from server
2144
+ */
2145
+ handleResponse(response) {
2146
+ if (response.id !== void 0 && this.pendingRequests.has(response.id)) {
2147
+ const { resolve, reject, timeout } = this.pendingRequests.get(response.id);
2148
+ clearTimeout(timeout);
2149
+ this.pendingRequests.delete(response.id);
2150
+ if (response.error) {
2151
+ const error = new ElectrumError(
2152
+ response.error.message || JSON.stringify(response.error),
2153
+ response.error.code,
2154
+ response.error.data
2155
+ );
2156
+ reject(error);
2157
+ } else {
2158
+ resolve(response.result);
2159
+ }
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Make an RPC call to the Electrum server
2164
+ * @param method - RPC method name
2165
+ * @param params - Method parameters
2166
+ * @returns Promise resolving to the result
2167
+ */
2168
+ async call(method, ...params) {
2169
+ if (!this.connected || !this.ws) {
2170
+ throw new Error("Not connected to Electrum server. Call connect() first.");
2171
+ }
2172
+ return new Promise((resolve, reject) => {
2173
+ const id = ++this.requestId;
2174
+ const request = {
2175
+ id,
2176
+ method,
2177
+ params
2178
+ };
2179
+ const timeout = setTimeout(() => {
2180
+ if (this.pendingRequests.has(id)) {
2181
+ this.pendingRequests.delete(id);
2182
+ reject(new Error(`Request timeout for method: ${method}`));
2183
+ }
2184
+ }, this.options.timeout);
2185
+ this.pendingRequests.set(id, { resolve, reject, timeout });
2186
+ try {
2187
+ this.ws.send(JSON.stringify(request));
2188
+ } catch (error) {
2189
+ clearTimeout(timeout);
2190
+ this.pendingRequests.delete(id);
2191
+ reject(error);
2192
+ }
2193
+ });
2194
+ }
2195
+ /**
2196
+ * Disconnect from the server
2197
+ */
2198
+ disconnect() {
2199
+ if (this.ws) {
2200
+ this.ws.close();
2201
+ this.ws = null;
2202
+ }
2203
+ this.connected = false;
2204
+ }
2205
+ /**
2206
+ * Check if connected to server
2207
+ */
2208
+ isConnected() {
2209
+ return this.connected;
2210
+ }
2211
+ // ============================================================================
2212
+ // Blockchain Methods
2213
+ // ============================================================================
2214
+ /**
2215
+ * Get server version
2216
+ * @returns Server version information
2217
+ */
2218
+ async getServerVersion() {
2219
+ return this.call("server.version", this.options.clientName, this.options.clientVersion);
2220
+ }
2221
+ /**
2222
+ * Get block header for a given height
2223
+ * @param height - Block height
2224
+ * @returns Block header
2225
+ */
2226
+ async getBlockHeader(height) {
2227
+ return this.call("blockchain.block.header", height);
2228
+ }
2229
+ /**
2230
+ * Get block headers for a range of heights
2231
+ * @param startHeight - Starting block height
2232
+ * @param count - Number of headers to fetch
2233
+ * @returns Block headers
2234
+ */
2235
+ async getBlockHeaders(startHeight, count) {
2236
+ return this.call("blockchain.block.headers", startHeight, count);
2237
+ }
2238
+ /**
2239
+ * Subscribe to block headers
2240
+ * @param callback - Callback function for new headers
2241
+ */
2242
+ async subscribeBlockHeaders(callback) {
2243
+ const header = await this.call("blockchain.headers.subscribe");
2244
+ callback(header);
2245
+ }
2246
+ /**
2247
+ * Get chain tip height
2248
+ * @returns Current chain tip height
2249
+ */
2250
+ async getChainTipHeight() {
2251
+ const response = await this.call("blockchain.headers.subscribe");
2252
+ if (typeof response === "number") {
2253
+ return response;
2254
+ }
2255
+ if (response && typeof response.height === "number") {
2256
+ return response.height;
2257
+ }
2258
+ throw new Error(`Unexpected blockchain.headers.subscribe response: ${JSON.stringify(response)}`);
2259
+ }
2260
+ // ============================================================================
2261
+ // Transaction Key Methods (Navio-specific)
2262
+ // ============================================================================
2263
+ /**
2264
+ * Get transaction keys for a specific transaction
2265
+ * @param txHash - Transaction hash (hex string)
2266
+ * @returns Transaction keys
2267
+ */
2268
+ async getTransactionKeys(txHash) {
2269
+ return this.call("blockchain.transaction.get_keys", txHash);
2270
+ }
2271
+ /**
2272
+ * Get all transaction keys for a block
2273
+ * @param height - Block height
2274
+ * @returns Array of transaction keys for the block
2275
+ */
2276
+ async getBlockTransactionKeys(height) {
2277
+ return this.call("blockchain.block.get_txs_keys", height);
2278
+ }
2279
+ /**
2280
+ * Get transaction keys for a range of blocks (paginated)
2281
+ * @param startHeight - Starting block height
2282
+ * @returns Range result with blocks and next height
2283
+ */
2284
+ async getBlockTransactionKeysRange(startHeight) {
2285
+ const result = await this.call("blockchain.block.get_range_txs_keys", startHeight);
2286
+ const blocks = [];
2287
+ if (result.blocks && Array.isArray(result.blocks)) {
2288
+ for (let i = 0; i < result.blocks.length; i++) {
2289
+ const blockData = result.blocks[i];
2290
+ const blockHeight = startHeight + i;
2291
+ const txKeys = [];
2292
+ if (Array.isArray(blockData)) {
2293
+ for (const txKeyData of blockData) {
2294
+ if (typeof txKeyData === "object" && txKeyData !== null) {
2295
+ if ("txHash" in txKeyData && "keys" in txKeyData) {
2296
+ txKeys.push({
2297
+ txHash: txKeyData.txHash || "",
2298
+ keys: txKeyData.keys
2299
+ });
2300
+ } else {
2301
+ const txHash = txKeyData.txHash || txKeyData.hash || "";
2302
+ txKeys.push({
2303
+ txHash,
2304
+ keys: txKeyData
2305
+ });
2306
+ }
2307
+ }
2308
+ }
2309
+ }
2310
+ blocks.push({
2311
+ height: blockHeight,
2312
+ txKeys
2313
+ });
2314
+ }
2315
+ }
2316
+ return {
2317
+ blocks,
2318
+ nextHeight: result.next_height || startHeight + blocks.length
2319
+ };
2320
+ }
2321
+ /**
2322
+ * Fetch all transaction keys from genesis to chain tip
2323
+ * @param progressCallback - Optional callback for progress updates
2324
+ * @returns Array of all block transaction keys
2325
+ */
2326
+ async fetchAllTransactionKeys(progressCallback) {
2327
+ const tipHeight = await this.getChainTipHeight();
2328
+ const allBlocks = [];
2329
+ let currentHeight = 0;
2330
+ let totalBlocksProcessed = 0;
2331
+ while (currentHeight <= tipHeight) {
2332
+ const rangeResult = await this.getBlockTransactionKeysRange(currentHeight);
2333
+ allBlocks.push(...rangeResult.blocks);
2334
+ totalBlocksProcessed += rangeResult.blocks.length;
2335
+ if (progressCallback) {
2336
+ progressCallback(currentHeight, tipHeight, totalBlocksProcessed);
2337
+ }
2338
+ currentHeight = rangeResult.nextHeight;
2339
+ if (currentHeight <= rangeResult.blocks[rangeResult.blocks.length - 1]?.height) {
2340
+ throw new Error("Server did not advance next_height properly");
2341
+ }
2342
+ }
2343
+ return allBlocks;
2344
+ }
2345
+ /**
2346
+ * Get serialized transaction output by output hash (Navio-specific)
2347
+ * @param outputHash - Output hash (hex string)
2348
+ * @returns Serialized output (hex string)
2349
+ */
2350
+ async getTransactionOutput(outputHash) {
2351
+ return this.call("blockchain.transaction.get_output", outputHash);
2352
+ }
2353
+ // ============================================================================
2354
+ // Transaction Methods
2355
+ // ============================================================================
2356
+ /**
2357
+ * Get raw transaction
2358
+ * @param txHash - Transaction hash (hex string)
2359
+ * @param verbose - Return verbose transaction data
2360
+ * @param blockHash - Optional block hash for context
2361
+ * @returns Raw transaction or verbose transaction data
2362
+ */
2363
+ async getRawTransaction(txHash, verbose = false, blockHash) {
2364
+ return this.call("blockchain.transaction.get", txHash, verbose, blockHash);
2365
+ }
2366
+ /**
2367
+ * Broadcast a transaction
2368
+ * @param rawTx - Raw transaction (hex string)
2369
+ * @returns Transaction hash if successful
2370
+ */
2371
+ async broadcastTransaction(rawTx) {
2372
+ return this.call("blockchain.transaction.broadcast", rawTx);
2373
+ }
2374
+ // ============================================================================
2375
+ // Address/Script Hash Methods
2376
+ // ============================================================================
2377
+ /**
2378
+ * Get transaction history for a script hash
2379
+ * @param scriptHash - Script hash (hex string, reversed)
2380
+ * @returns Transaction history
2381
+ */
2382
+ async getHistory(scriptHash) {
2383
+ return this.call("blockchain.scripthash.get_history", scriptHash);
2384
+ }
2385
+ /**
2386
+ * Get unspent transaction outputs for a script hash
2387
+ * @param scriptHash - Script hash (hex string, reversed)
2388
+ * @returns Unspent outputs
2389
+ */
2390
+ async getUnspent(scriptHash) {
2391
+ return this.call("blockchain.scripthash.listunspent", scriptHash);
2392
+ }
2393
+ /**
2394
+ * Subscribe to script hash updates
2395
+ * @param scriptHash - Script hash (hex string, reversed)
2396
+ * @param callback - Callback for status updates
2397
+ */
2398
+ async subscribeScriptHash(scriptHash, callback) {
2399
+ const status = await this.call("blockchain.scripthash.subscribe", scriptHash);
2400
+ callback(status);
2401
+ }
2402
+ // ============================================================================
2403
+ // Utility Methods
2404
+ // ============================================================================
2405
+ /**
2406
+ * Calculate script hash from address (for Electrum protocol)
2407
+ * @param address - Address string
2408
+ * @returns Script hash (hex string, reversed for Electrum)
2409
+ */
2410
+ static calculateScriptHash(address) {
2411
+ const hash = ripemd160(sha256(Buffer.from(address, "utf-8")));
2412
+ return Buffer.from(hash).reverse().toString("hex");
2413
+ }
2414
+ /**
2415
+ * Reverse a hex string (for Electrum protocol hash format)
2416
+ * @param hex - Hex string
2417
+ * @returns Reversed hex string
2418
+ */
2419
+ static reverseHex(hex) {
2420
+ const bytes = Buffer.from(hex, "hex");
2421
+ return Buffer.from(bytes.reverse()).toString("hex");
2422
+ }
2423
+ };
2424
+ var TransactionKeysSync = class {
2425
+ // Keep last 1k block hashes by default
2426
+ /**
2427
+ * Create a new TransactionKeysSync instance
2428
+ * @param walletDB - The wallet database
2429
+ * @param provider - A SyncProvider or ElectrumClient instance
2430
+ */
2431
+ constructor(walletDB, provider) {
2432
+ this.keyManager = null;
2433
+ this.syncState = null;
2434
+ this.blockHashRetention = 1e3;
2435
+ this.walletDB = walletDB;
2436
+ if ("type" in provider && (provider.type === "electrum" || provider.type === "p2p" || provider.type === "custom")) {
2437
+ this.syncProvider = provider;
2438
+ } else {
2439
+ this.syncProvider = this.wrapElectrumClient(provider);
2440
+ }
2441
+ }
2442
+ /**
2443
+ * Wrap an ElectrumClient as a SyncProvider for backwards compatibility
2444
+ */
2445
+ wrapElectrumClient(client) {
2446
+ return {
2447
+ type: "electrum",
2448
+ connect: () => client.connect(),
2449
+ disconnect: () => client.disconnect(),
2450
+ isConnected: () => client.isConnected(),
2451
+ getChainTipHeight: () => client.getChainTipHeight(),
2452
+ getChainTip: async () => {
2453
+ const height = await client.getChainTipHeight();
2454
+ const header = await client.getBlockHeader(height);
2455
+ const hash = Buffer.from(sha256(sha256(Buffer.from(header, "hex")))).reverse().toString("hex");
2456
+ return { height, hash };
2457
+ },
2458
+ getBlockHeader: (height) => client.getBlockHeader(height),
2459
+ getBlockHeaders: (startHeight, count) => client.getBlockHeaders(startHeight, count),
2460
+ getBlockTransactionKeysRange: (startHeight) => client.getBlockTransactionKeysRange(startHeight),
2461
+ getBlockTransactionKeys: async (height) => {
2462
+ const result = await client.getBlockTransactionKeys(height);
2463
+ return Array.isArray(result) ? result : [];
2464
+ },
2465
+ getTransactionOutput: (outputHash) => client.getTransactionOutput(outputHash),
2466
+ broadcastTransaction: (rawTx) => client.broadcastTransaction(rawTx),
2467
+ getRawTransaction: (txHash, verbose) => client.getRawTransaction(txHash, verbose)
2468
+ };
2469
+ }
2470
+ /**
2471
+ * Get the sync provider being used
2472
+ */
2473
+ getSyncProvider() {
2474
+ return this.syncProvider;
2475
+ }
2476
+ /**
2477
+ * Get the provider type (electrum, p2p, or custom)
2478
+ */
2479
+ getProviderType() {
2480
+ return this.syncProvider.type;
2481
+ }
2482
+ /**
2483
+ * Set the KeyManager instance for output detection
2484
+ * @param keyManager - The KeyManager instance
2485
+ */
2486
+ setKeyManager(keyManager) {
2487
+ this.keyManager = keyManager;
2488
+ }
2489
+ /**
2490
+ * Initialize sync manager
2491
+ * Loads sync state from database
2492
+ */
2493
+ async initialize() {
2494
+ if (!this.keyManager) {
2495
+ try {
2496
+ this.keyManager = await this.walletDB.loadWallet();
2497
+ } catch {
2498
+ this.keyManager = await this.walletDB.createWallet();
2499
+ }
2500
+ }
2501
+ this.syncState = await this.loadSyncState();
2502
+ }
2503
+ /**
2504
+ * Get current sync state
2505
+ */
2506
+ getSyncState() {
2507
+ return this.syncState;
2508
+ }
2509
+ /**
2510
+ * Get last synced height
2511
+ */
2512
+ getLastSyncedHeight() {
2513
+ return this.syncState?.lastSyncedHeight ?? -1;
2514
+ }
2515
+ /**
2516
+ * Check if sync is needed
2517
+ */
2518
+ async isSyncNeeded() {
2519
+ if (!this.syncState) {
2520
+ return true;
2521
+ }
2522
+ const chainTip = await this.syncProvider.getChainTipHeight();
2523
+ return chainTip > this.syncState.lastSyncedHeight;
2524
+ }
2525
+ /**
2526
+ * Synchronize transaction keys from Electrum server
2527
+ * @param options - Sync options
2528
+ * @returns Number of transaction keys synced
2529
+ */
2530
+ async sync(options = {}) {
2531
+ if (!this.keyManager) {
2532
+ await this.initialize();
2533
+ }
2534
+ const {
2535
+ startHeight,
2536
+ endHeight,
2537
+ onProgress,
2538
+ stopOnReorg = true,
2539
+ verifyHashes = true,
2540
+ saveInterval = 100,
2541
+ keepTxKeys = false,
2542
+ blockHashRetention = 1e3
2543
+ } = options;
2544
+ this.blockHashRetention = blockHashRetention;
2545
+ const lastSynced = this.syncState?.lastSyncedHeight ?? -1;
2546
+ let defaultStartHeight = lastSynced + 1;
2547
+ if (lastSynced === -1) {
2548
+ const creationHeight = await this.walletDB.getCreationHeight();
2549
+ if (creationHeight > 0) {
2550
+ defaultStartHeight = creationHeight;
2551
+ }
2552
+ }
2553
+ const syncStartHeight = startHeight ?? defaultStartHeight;
2554
+ const chainTip = await this.syncProvider.getChainTipHeight();
2555
+ const syncEndHeight = endHeight ?? chainTip;
2556
+ if (syncStartHeight > syncEndHeight) {
2557
+ return 0;
2558
+ }
2559
+ if (this.syncState && verifyHashes) {
2560
+ const reorgInfo = await this.checkReorganization(this.syncState.lastSyncedHeight);
2561
+ if (reorgInfo) {
2562
+ if (stopOnReorg) {
2563
+ throw new Error(
2564
+ `Chain reorganization detected at height ${reorgInfo.height}. Old hash: ${reorgInfo.oldHash}, New hash: ${reorgInfo.newHash}. Need to revert ${reorgInfo.blocksToRevert} blocks.`
2565
+ );
2566
+ } else {
2567
+ await this.handleReorganization(reorgInfo);
2568
+ }
2569
+ }
2570
+ }
2571
+ let totalTxKeysSynced = 0;
2572
+ let currentHeight = syncStartHeight;
2573
+ let blocksProcessed = 0;
2574
+ let lastSaveHeight = syncStartHeight - 1;
2575
+ while (currentHeight <= syncEndHeight) {
2576
+ const rangeResult = await this.syncProvider.getBlockTransactionKeysRange(currentHeight);
2577
+ const blockHeights = rangeResult.blocks.filter((block) => block.height <= syncEndHeight).map((block) => block.height);
2578
+ let blockHeadersMap = /* @__PURE__ */ new Map();
2579
+ if (blockHeights.length > 0 && verifyHashes) {
2580
+ const firstHeight = blockHeights[0];
2581
+ const count = blockHeights.length;
2582
+ const headersResult = await this.syncProvider.getBlockHeaders(firstHeight, count);
2583
+ const headerSize = 160;
2584
+ const hex = headersResult.hex;
2585
+ for (let i = 0; i < count && i * headerSize < hex.length; i++) {
2586
+ const height = firstHeight + i;
2587
+ const headerStart = i * headerSize;
2588
+ const headerEnd = headerStart + headerSize;
2589
+ const headerHex = hex.substring(headerStart, headerEnd);
2590
+ blockHeadersMap.set(height, headerHex);
2591
+ }
2592
+ }
2593
+ for (const block of rangeResult.blocks) {
2594
+ if (block.height > syncEndHeight) {
2595
+ break;
2596
+ }
2597
+ let blockHash;
2598
+ if (verifyHashes && blockHeadersMap.has(block.height)) {
2599
+ const blockHeader = blockHeadersMap.get(block.height);
2600
+ blockHash = this.extractBlockHash(blockHeader);
2601
+ } else if (verifyHashes) {
2602
+ const blockHeader = await this.syncProvider.getBlockHeader(block.height);
2603
+ blockHash = this.extractBlockHash(blockHeader);
2604
+ } else {
2605
+ const blockHeader = await this.syncProvider.getBlockHeader(block.height);
2606
+ blockHash = this.extractBlockHash(blockHeader);
2607
+ }
2608
+ if (verifyHashes) {
2609
+ await this.storeBlockHash(block.height, blockHash, chainTip);
2610
+ if (this.syncState && block.height <= this.syncState.lastSyncedHeight) {
2611
+ const storedHash = await this.getStoredBlockHash(block.height);
2612
+ if (storedHash && storedHash !== blockHash) {
2613
+ const reorgInfo = {
2614
+ height: block.height,
2615
+ oldHash: storedHash,
2616
+ newHash: blockHash,
2617
+ blocksToRevert: this.syncState.lastSyncedHeight - block.height + 1
2618
+ };
2619
+ if (stopOnReorg) {
2620
+ throw new Error(
2621
+ `Chain reorganization detected at height ${block.height}. Old hash: ${storedHash}, New hash: ${blockHash}.`
2622
+ );
2623
+ } else {
2624
+ await this.handleReorganization(reorgInfo);
2625
+ }
2626
+ }
2627
+ }
2628
+ } else {
2629
+ await this.storeBlockHash(block.height, blockHash, chainTip);
2630
+ }
2631
+ const txKeysCount = await this.storeBlockTransactionKeys(block, blockHash, keepTxKeys);
2632
+ if (this.keyManager) {
2633
+ await this.processBlockForSpentOutputs(block, blockHash);
2634
+ }
2635
+ totalTxKeysSynced += txKeysCount;
2636
+ blocksProcessed++;
2637
+ if (onProgress) {
2638
+ onProgress(block.height, syncEndHeight, blocksProcessed, totalTxKeysSynced, false);
2639
+ }
2640
+ }
2641
+ if (rangeResult.blocks.length > 0) {
2642
+ const lastBlock = rangeResult.blocks[rangeResult.blocks.length - 1];
2643
+ let lastBlockHash;
2644
+ if (blockHeadersMap.has(lastBlock.height)) {
2645
+ const lastBlockHeader = blockHeadersMap.get(lastBlock.height);
2646
+ lastBlockHash = this.extractBlockHash(lastBlockHeader);
2647
+ } else {
2648
+ const lastBlockHeader = await this.syncProvider.getBlockHeader(lastBlock.height);
2649
+ lastBlockHash = this.extractBlockHash(lastBlockHeader);
2650
+ }
2651
+ await this.updateSyncState({
2652
+ lastSyncedHeight: lastBlock.height,
2653
+ lastSyncedHash: lastBlockHash,
2654
+ totalTxKeysSynced: (this.syncState?.totalTxKeysSynced ?? 0) + totalTxKeysSynced,
2655
+ lastSyncTime: Date.now(),
2656
+ chainTipAtLastSync: chainTip
2657
+ });
2658
+ if (saveInterval > 0 && lastBlock.height - lastSaveHeight >= saveInterval) {
2659
+ await this.walletDB.saveDatabase();
2660
+ lastSaveHeight = lastBlock.height;
2661
+ }
2662
+ }
2663
+ currentHeight = rangeResult.nextHeight;
2664
+ if (currentHeight <= rangeResult.blocks[rangeResult.blocks.length - 1]?.height) {
2665
+ throw new Error(
2666
+ `Server did not advance next_height properly. Current: ${currentHeight}, Last block: ${rangeResult.blocks[rangeResult.blocks.length - 1]?.height}`
2667
+ );
2668
+ }
2669
+ }
2670
+ await this.walletDB.saveDatabase();
2671
+ return totalTxKeysSynced;
2672
+ }
2673
+ /**
2674
+ * Check for chain reorganization
2675
+ * @param height - Height to check
2676
+ * @returns Reorganization info if detected, null otherwise
2677
+ */
2678
+ async checkReorganization(height) {
2679
+ if (!this.syncState || height < 0) {
2680
+ return null;
2681
+ }
2682
+ const currentHeader = await this.syncProvider.getBlockHeader(height);
2683
+ const currentHash = this.extractBlockHash(currentHeader);
2684
+ const storedHash = await this.getStoredBlockHash(height);
2685
+ if (storedHash && storedHash !== currentHash) {
2686
+ let commonHeight = height - 1;
2687
+ while (commonHeight >= 0) {
2688
+ const commonHeader = await this.syncProvider.getBlockHeader(commonHeight);
2689
+ const commonHash = this.extractBlockHash(commonHeader);
2690
+ const storedCommonHash = await this.getStoredBlockHash(commonHeight);
2691
+ if (storedCommonHash === commonHash) {
2692
+ break;
2693
+ }
2694
+ commonHeight--;
2695
+ }
2696
+ return {
2697
+ height: commonHeight + 1,
2698
+ oldHash: storedHash,
2699
+ newHash: currentHash,
2700
+ blocksToRevert: height - commonHeight
2701
+ };
2702
+ }
2703
+ return null;
2704
+ }
2705
+ /**
2706
+ * Handle chain reorganization
2707
+ * @param reorgInfo - Reorganization information
2708
+ */
2709
+ async handleReorganization(reorgInfo) {
2710
+ console.log(
2711
+ `Handling reorganization: reverting ${reorgInfo.blocksToRevert} blocks from height ${reorgInfo.height}`
2712
+ );
2713
+ const revertHeight = reorgInfo.height + reorgInfo.blocksToRevert - 1;
2714
+ await this.revertBlocks(reorgInfo.height, revertHeight);
2715
+ await this.updateSyncState({
2716
+ lastSyncedHeight: reorgInfo.height - 1,
2717
+ lastSyncedHash: await this.getStoredBlockHash(reorgInfo.height - 1) || "",
2718
+ totalTxKeysSynced: this.syncState?.totalTxKeysSynced ?? 0,
2719
+ lastSyncTime: Date.now(),
2720
+ chainTipAtLastSync: await this.syncProvider.getChainTipHeight()
2721
+ });
2722
+ }
2723
+ /**
2724
+ * Process block transactions to detect spent outputs
2725
+ * @param block - Block transaction keys
2726
+ * @param _blockHash - Block hash (for reference)
2727
+ */
2728
+ async processBlockForSpentOutputs(block, _blockHash) {
2729
+ for (const txKeys of block.txKeys) {
2730
+ const txHash = txKeys.txHash || "";
2731
+ if (!txHash) {
2732
+ continue;
2733
+ }
2734
+ const keys = txKeys.keys || {};
2735
+ const inputs = keys?.inputs || keys?.vin || [];
2736
+ if (Array.isArray(inputs)) {
2737
+ for (const input of inputs) {
2738
+ const outputHash = input?.outputHash || input?.output_hash || input?.prevout?.hash;
2739
+ if (outputHash) {
2740
+ const db = this.walletDB.getDatabase();
2741
+ const stmt = db.prepare(
2742
+ "SELECT output_hash FROM wallet_outputs WHERE output_hash = ? AND is_spent = 0"
2743
+ );
2744
+ stmt.bind([outputHash]);
2745
+ if (stmt.step()) {
2746
+ const updateStmt = db.prepare(
2747
+ `UPDATE wallet_outputs
2748
+ SET is_spent = 1, spent_tx_hash = ?, spent_block_height = ?
2749
+ WHERE output_hash = ?`
2750
+ );
2751
+ updateStmt.run([txHash, block.height, outputHash]);
2752
+ updateStmt.free();
2753
+ }
2754
+ stmt.free();
2755
+ }
2756
+ }
2757
+ }
2758
+ }
2759
+ }
2760
+ /**
2761
+ * Revert blocks from database
2762
+ * @param startHeight - Start height to revert from
2763
+ * @param endHeight - End height to revert to
2764
+ */
2765
+ async revertBlocks(startHeight, endHeight) {
2766
+ const db = this.walletDB.getDatabase();
2767
+ for (let height = startHeight; height <= endHeight; height++) {
2768
+ const stmt = db.prepare("DELETE FROM tx_keys WHERE block_height = ?");
2769
+ stmt.run([height]);
2770
+ stmt.free();
2771
+ const deleteOutputsStmt = db.prepare("DELETE FROM wallet_outputs WHERE block_height = ?");
2772
+ deleteOutputsStmt.run([height]);
2773
+ deleteOutputsStmt.free();
2774
+ const unspendStmt = db.prepare(
2775
+ `UPDATE wallet_outputs
2776
+ SET is_spent = 0, spent_tx_hash = NULL, spent_block_height = NULL
2777
+ WHERE spent_block_height = ?`
2778
+ );
2779
+ unspendStmt.run([height]);
2780
+ unspendStmt.free();
2781
+ const hashStmt = db.prepare("DELETE FROM block_hashes WHERE height = ?");
2782
+ hashStmt.run([height]);
2783
+ hashStmt.free();
2784
+ }
2785
+ }
2786
+ /**
2787
+ * Store transaction keys for a block
2788
+ * @param block - Block transaction keys
2789
+ * @param blockHash - Block hash
2790
+ * @param keepTxKeys - Whether to keep transaction keys in database after processing
2791
+ * @returns Number of transaction keys stored
2792
+ */
2793
+ async storeBlockTransactionKeys(block, blockHash, keepTxKeys = false) {
2794
+ const db = this.walletDB.getDatabase();
2795
+ let count = 0;
2796
+ for (const txKeys of block.txKeys) {
2797
+ let txHash = txKeys.txHash;
2798
+ if (!txHash && txKeys.keys && typeof txKeys.keys === "object") {
2799
+ txHash = txKeys.keys.txHash || txKeys.keys.hash || "";
2800
+ }
2801
+ if (!txHash) {
2802
+ txHash = `block_${block.height}_tx_${count}`;
2803
+ }
2804
+ if (this.keyManager) {
2805
+ await this.processTransactionKeys(txHash, txKeys.keys, block.height, blockHash);
2806
+ }
2807
+ if (keepTxKeys) {
2808
+ const stmt = db.prepare(
2809
+ "INSERT OR REPLACE INTO tx_keys (tx_hash, block_height, keys_data) VALUES (?, ?, ?)"
2810
+ );
2811
+ stmt.run([txHash, block.height, JSON.stringify(txKeys.keys)]);
2812
+ stmt.free();
2813
+ }
2814
+ count++;
2815
+ }
2816
+ return count;
2817
+ }
2818
+ /**
2819
+ * Process transaction keys to detect and store wallet outputs
2820
+ * @param txHash - Transaction hash
2821
+ * @param keys - Transaction keys data
2822
+ * @param blockHeight - Block height
2823
+ * @param _blockHash - Block hash (for reference, currently unused)
2824
+ */
2825
+ async processTransactionKeys(txHash, keys, blockHeight, _blockHash) {
2826
+ if (!this.keyManager) {
2827
+ return;
2828
+ }
2829
+ const outputs = keys[1]?.outputs || keys[1]?.vout || [];
2830
+ if (!Array.isArray(outputs)) {
2831
+ return;
2832
+ }
2833
+ for (let outputIndex = 0; outputIndex < outputs.length; outputIndex++) {
2834
+ const outputKeys = outputs[outputIndex];
2835
+ const blindingKey = outputKeys?.blindingKey || outputKeys?.blinding_key;
2836
+ const spendingKey = outputKeys?.spendingKey || outputKeys?.spending_key;
2837
+ const viewTag = outputKeys?.viewTag ?? outputKeys?.view_tag;
2838
+ const outputHash = outputKeys?.outputHash || outputKeys?.output_hash;
2839
+ if (!blindingKey || !spendingKey || viewTag === void 0 || !outputHash) {
2840
+ console.warn(
2841
+ `Invalid output keys: blindingKey: ${blindingKey}, spendingKey: ${spendingKey}, viewTag: ${viewTag}, outputHash: ${outputHash}, blockHeight: ${blockHeight}`
2842
+ );
2843
+ continue;
2844
+ }
2845
+ const PublicKey5 = blsctModule.PublicKey;
2846
+ let blindingKeyObj = PublicKey5.deserialize(blindingKey);
2847
+ let spendingKeyObj = PublicKey5.deserialize(spendingKey);
2848
+ const isMine = this.keyManager.isMineByKeys(blindingKeyObj, spendingKeyObj, viewTag);
2849
+ if (isMine) {
2850
+ try {
2851
+ const outputHex = await this.syncProvider.getTransactionOutput(outputHash);
2852
+ const RangeProof2 = blsctModule.RangeProof;
2853
+ const AmountRecoveryReq2 = blsctModule.AmountRecoveryReq;
2854
+ let recoveredAmount = 0;
2855
+ let recoveredMemo = null;
2856
+ let tokenIdHex = null;
2857
+ try {
2858
+ const nonce = this.keyManager.calculateNonce(blindingKeyObj);
2859
+ const rangeProofResult = this.extractRangeProofFromOutput(outputHex);
2860
+ if (rangeProofResult.rangeProofHex) {
2861
+ const rangeProof = RangeProof2.deserialize(rangeProofResult.rangeProofHex);
2862
+ const req = new AmountRecoveryReq2(rangeProof, nonce);
2863
+ const results = RangeProof2.recoverAmounts([req]);
2864
+ if (results.length > 0 && results[0].isSucc) {
2865
+ recoveredAmount = Number(results[0].amount);
2866
+ recoveredMemo = results[0].msg || null;
2867
+ }
2868
+ }
2869
+ tokenIdHex = rangeProofResult.tokenIdHex;
2870
+ } catch {
2871
+ }
2872
+ await this.storeWalletOutput(
2873
+ outputHash,
2874
+ txHash,
2875
+ outputIndex,
2876
+ blockHeight,
2877
+ outputHex,
2878
+ recoveredAmount,
2879
+ recoveredMemo,
2880
+ tokenIdHex,
2881
+ blindingKey,
2882
+ spendingKey,
2883
+ false,
2884
+ // not spent
2885
+ null,
2886
+ // spent_tx_hash
2887
+ null
2888
+ // spent_block_height
2889
+ );
2890
+ } catch (error) {
2891
+ console.warn(`Failed to fetch output ${outputHash} for tx ${txHash}:`, error);
2892
+ }
2893
+ }
2894
+ }
2895
+ }
2896
+ /**
2897
+ * Extract range proof from serialized CTxOut data
2898
+ * @param outputHex - Serialized output data (hex)
2899
+ * @returns Object containing rangeProofHex and tokenIdHex
2900
+ */
2901
+ extractRangeProofFromOutput(outputHex) {
2902
+ try {
2903
+ const data = Buffer.from(outputHex, "hex");
2904
+ let offset = 0;
2905
+ const MAX_AMOUNT = 0x7fffffffffffffffn;
2906
+ const HAS_BLSCT_KEYS = 1n;
2907
+ const HAS_TOKENID = 2n;
2908
+ const value = data.readBigInt64LE(offset);
2909
+ offset += 8;
2910
+ let flags = 0n;
2911
+ if (value === MAX_AMOUNT) {
2912
+ flags = data.readBigUInt64LE(offset);
2913
+ offset += 8;
2914
+ if ((flags & 8n) !== 0n) {
2915
+ offset += 8;
2916
+ }
2917
+ }
2918
+ const scriptLen = data[offset];
2919
+ offset += 1 + scriptLen;
2920
+ let rangeProofHex = null;
2921
+ let tokenIdHex = null;
2922
+ if ((flags & HAS_BLSCT_KEYS) !== 0n) {
2923
+ const rangeProofStart = offset;
2924
+ const vsCount = data[offset];
2925
+ offset += 1;
2926
+ offset += vsCount * 48;
2927
+ if (vsCount > 0) {
2928
+ const lsCount = data[offset];
2929
+ offset += 1;
2930
+ offset += lsCount * 48;
2931
+ const rsCount = data[offset];
2932
+ offset += 1;
2933
+ offset += rsCount * 48;
2934
+ offset += 3 * 48;
2935
+ offset += 5 * 32;
2936
+ }
2937
+ const rangeProofEnd = offset;
2938
+ rangeProofHex = data.subarray(rangeProofStart, rangeProofEnd).toString("hex");
2939
+ offset += 3 * 48 + 2;
2940
+ }
2941
+ if ((flags & HAS_TOKENID) !== 0n) {
2942
+ tokenIdHex = data.subarray(offset, offset + 64).toString("hex");
2943
+ }
2944
+ return { rangeProofHex, tokenIdHex };
2945
+ } catch (e) {
2946
+ return { rangeProofHex: null, tokenIdHex: null };
2947
+ }
2948
+ }
2949
+ /**
2950
+ * Store wallet output in database
2951
+ * @param outputHash - Output hash
2952
+ * @param txHash - Transaction hash
2953
+ * @param outputIndex - Output index
2954
+ * @param blockHeight - Block height
2955
+ * @param outputData - Serialized output data (hex)
2956
+ * @param amount - Recovered amount in satoshis
2957
+ * @param memo - Recovered memo/message
2958
+ * @param tokenId - Token ID (hex)
2959
+ * @param blindingKey - Blinding public key (hex)
2960
+ * @param spendingKey - Spending public key (hex)
2961
+ * @param isSpent - Whether output is spent
2962
+ * @param spentTxHash - Transaction hash that spent this output (if spent)
2963
+ * @param spentBlockHeight - Block height where output was spent (if spent)
2964
+ */
2965
+ async storeWalletOutput(outputHash, txHash, outputIndex, blockHeight, outputData, amount, memo, tokenId, blindingKey, spendingKey, isSpent, spentTxHash, spentBlockHeight) {
2966
+ const db = this.walletDB.getDatabase();
2967
+ const stmt = db.prepare(
2968
+ `INSERT OR REPLACE INTO wallet_outputs
2969
+ (output_hash, tx_hash, output_index, block_height, output_data, amount, memo, token_id, blinding_key, spending_key, is_spent, spent_tx_hash, spent_block_height, created_at)
2970
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2971
+ );
2972
+ stmt.run([
2973
+ outputHash,
2974
+ txHash,
2975
+ outputIndex,
2976
+ blockHeight,
2977
+ outputData,
2978
+ amount,
2979
+ memo,
2980
+ tokenId,
2981
+ blindingKey,
2982
+ spendingKey,
2983
+ isSpent ? 1 : 0,
2984
+ spentTxHash,
2985
+ spentBlockHeight,
2986
+ Date.now()
2987
+ ]);
2988
+ stmt.free();
2989
+ }
2990
+ /**
2991
+ * Extract block hash from block header
2992
+ * @param headerHex - Block header in hex
2993
+ * @returns Block hash (hex string)
2994
+ */
2995
+ extractBlockHash(headerHex) {
2996
+ const headerBytes = Buffer.from(headerHex, "hex");
2997
+ const hash = sha256(sha256(headerBytes));
2998
+ return Buffer.from(hash).reverse().toString("hex");
2999
+ }
3000
+ /**
3001
+ * Get stored block hash from database
3002
+ * @param height - Block height
3003
+ * @returns Block hash or null if not found
3004
+ */
3005
+ async getStoredBlockHash(height) {
3006
+ const db = this.walletDB.getDatabase();
3007
+ const stmt = db.prepare("SELECT hash FROM block_hashes WHERE height = ?");
3008
+ stmt.bind([height]);
3009
+ if (stmt.step()) {
3010
+ const row = stmt.getAsObject();
3011
+ stmt.free();
3012
+ return row.hash;
3013
+ }
3014
+ stmt.free();
3015
+ return null;
3016
+ }
3017
+ /**
3018
+ * Store block hash in database
3019
+ * Only stores if within retention window (if retention is enabled)
3020
+ * @param height - Block height
3021
+ * @param hash - Block hash
3022
+ * @param chainTip - Current chain tip (optional, to avoid repeated fetches)
3023
+ */
3024
+ async storeBlockHash(height, hash, chainTip) {
3025
+ const db = this.walletDB.getDatabase();
3026
+ if (this.blockHashRetention > 0) {
3027
+ const currentChainTip = chainTip ?? await this.syncProvider.getChainTipHeight();
3028
+ const retentionStart = Math.max(0, currentChainTip - this.blockHashRetention + 1);
3029
+ if (height < retentionStart) {
3030
+ return;
3031
+ }
3032
+ if (height % 100 === 0) {
3033
+ const cleanupStmt = db.prepare("DELETE FROM block_hashes WHERE height < ?");
3034
+ cleanupStmt.run([retentionStart]);
3035
+ cleanupStmt.free();
3036
+ }
3037
+ }
3038
+ const stmt = db.prepare("INSERT OR REPLACE INTO block_hashes (height, hash) VALUES (?, ?)");
3039
+ stmt.run([height, hash]);
3040
+ stmt.free();
3041
+ }
3042
+ /**
3043
+ * Load sync state from database
3044
+ * @returns Sync state or null if not found
3045
+ */
3046
+ async loadSyncState() {
3047
+ try {
3048
+ const db = this.walletDB.getDatabase();
3049
+ const result = db.exec("SELECT * FROM sync_state LIMIT 1");
3050
+ if (result.length > 0 && result[0].values.length > 0) {
3051
+ const row = result[0].values[0];
3052
+ return {
3053
+ lastSyncedHeight: row[1],
3054
+ lastSyncedHash: row[2],
3055
+ totalTxKeysSynced: row[3],
3056
+ lastSyncTime: row[4],
3057
+ chainTipAtLastSync: row[5]
3058
+ };
3059
+ }
3060
+ return null;
3061
+ } catch {
3062
+ return null;
3063
+ }
3064
+ }
3065
+ /**
3066
+ * Update sync state in database
3067
+ * @param state - Sync state to update
3068
+ */
3069
+ async updateSyncState(state) {
3070
+ const db = this.walletDB.getDatabase();
3071
+ const currentState = this.syncState || {
3072
+ lastSyncedHeight: -1,
3073
+ lastSyncedHash: "",
3074
+ totalTxKeysSynced: 0,
3075
+ lastSyncTime: 0,
3076
+ chainTipAtLastSync: 0
3077
+ };
3078
+ const newState = {
3079
+ ...currentState,
3080
+ ...state
3081
+ };
3082
+ const stmt = db.prepare(
3083
+ `INSERT OR REPLACE INTO sync_state
3084
+ (id, last_synced_height, last_synced_hash, total_tx_keys_synced, last_sync_time, chain_tip_at_last_sync)
3085
+ VALUES (0, ?, ?, ?, ?, ?)`
3086
+ );
3087
+ stmt.run([
3088
+ newState.lastSyncedHeight,
3089
+ newState.lastSyncedHash,
3090
+ newState.totalTxKeysSynced,
3091
+ newState.lastSyncTime,
3092
+ newState.chainTipAtLastSync
3093
+ ]);
3094
+ stmt.free();
3095
+ this.syncState = newState;
3096
+ }
3097
+ /**
3098
+ * Get transaction keys for a specific transaction
3099
+ * @param txHash - Transaction hash
3100
+ * @returns Transaction keys or null if not found
3101
+ */
3102
+ async getTransactionKeys(txHash) {
3103
+ try {
3104
+ const db = this.walletDB.getDatabase();
3105
+ const stmt = db.prepare("SELECT keys_data FROM tx_keys WHERE tx_hash = ?");
3106
+ stmt.bind([txHash]);
3107
+ if (stmt.step()) {
3108
+ const row = stmt.getAsObject();
3109
+ stmt.free();
3110
+ return JSON.parse(row.keys_data);
3111
+ }
3112
+ stmt.free();
3113
+ return null;
3114
+ } catch {
3115
+ return null;
3116
+ }
3117
+ }
3118
+ /**
3119
+ * Get transaction keys for a block
3120
+ * @param height - Block height
3121
+ * @returns Array of transaction keys
3122
+ */
3123
+ async getBlockTransactionKeys(height) {
3124
+ try {
3125
+ const db = this.walletDB.getDatabase();
3126
+ const result = db.exec("SELECT tx_hash, keys_data FROM tx_keys WHERE block_height = ?", [
3127
+ height
3128
+ ]);
3129
+ if (result.length > 0) {
3130
+ return result[0].values.map((row) => ({
3131
+ txHash: row[0],
3132
+ keys: JSON.parse(row[1])
3133
+ }));
3134
+ }
3135
+ return [];
3136
+ } catch {
3137
+ return [];
3138
+ }
3139
+ }
3140
+ /**
3141
+ * Reset sync state (for testing or full resync)
3142
+ */
3143
+ async resetSyncState() {
3144
+ const db = this.walletDB.getDatabase();
3145
+ db.run("DELETE FROM sync_state");
3146
+ db.run("DELETE FROM tx_keys");
3147
+ db.run("DELETE FROM block_hashes");
3148
+ this.syncState = null;
3149
+ }
3150
+ };
3151
+ var BaseSyncProvider = class {
3152
+ constructor(options = {}) {
3153
+ this.debug = options.debug ?? false;
3154
+ this.timeout = options.timeout ?? 3e4;
3155
+ }
3156
+ log(...args) {
3157
+ if (this.debug) {
3158
+ console.log(`[${this.type}]`, ...args);
3159
+ }
3160
+ }
3161
+ /**
3162
+ * Extract block hash from raw block header (80 bytes hex)
3163
+ * Block hash is double SHA256 of header, reversed for display
3164
+ */
3165
+ extractBlockHash(headerHex) {
3166
+ const headerBytes = Buffer.from(headerHex, "hex");
3167
+ const hash = sha256(sha256(headerBytes));
3168
+ return Buffer.from(hash).reverse().toString("hex");
3169
+ }
3170
+ /**
3171
+ * Parse a raw block header into structured format
3172
+ */
3173
+ parseBlockHeader(headerHex, height) {
3174
+ const header = Buffer.from(headerHex, "hex");
3175
+ if (header.length !== 80) {
3176
+ throw new Error(`Invalid header length: ${header.length}, expected 80`);
3177
+ }
3178
+ return {
3179
+ height,
3180
+ hash: this.extractBlockHash(headerHex),
3181
+ version: header.readInt32LE(0),
3182
+ prevHash: header.subarray(4, 36).reverse().toString("hex"),
3183
+ merkleRoot: header.subarray(36, 68).reverse().toString("hex"),
3184
+ timestamp: header.readUInt32LE(68),
3185
+ bits: header.readUInt32LE(72),
3186
+ nonce: header.readUInt32LE(76),
3187
+ rawHex: headerHex
3188
+ };
3189
+ }
3190
+ };
3191
+ var NetworkMagic = {
3192
+ MAINNET: Buffer.from([219, 210, 177, 172]),
3193
+ TESTNET: Buffer.from([28, 3, 187, 131]),
3194
+ REGTEST: Buffer.from([253, 191, 159, 251])
3195
+ };
3196
+ var DefaultPorts = {
3197
+ MAINNET: 44440,
3198
+ TESTNET: 33670,
3199
+ REGTEST: 18444
3200
+ };
3201
+ var MessageType = {
3202
+ VERSION: "version",
3203
+ VERACK: "verack",
3204
+ PING: "ping",
3205
+ PONG: "pong",
3206
+ GETADDR: "getaddr",
3207
+ ADDR: "addr",
3208
+ INV: "inv",
3209
+ GETDATA: "getdata",
3210
+ NOTFOUND: "notfound",
3211
+ GETBLOCKS: "getblocks",
3212
+ GETHEADERS: "getheaders",
3213
+ HEADERS: "headers",
3214
+ BLOCK: "block",
3215
+ TX: "tx",
3216
+ GETOUTPUTDATA: "getoutputdata",
3217
+ MEMPOOL: "mempool",
3218
+ REJECT: "reject",
3219
+ SENDHEADERS: "sendheaders",
3220
+ SENDCMPCT: "sendcmpct",
3221
+ CMPCTBLOCK: "cmpctblock",
3222
+ GETBLOCKTXN: "getblocktxn",
3223
+ BLOCKTXN: "blocktxn"
3224
+ };
3225
+ var ServiceFlags = {
3226
+ NODE_NONE: 0n,
3227
+ NODE_NETWORK: 1n << 0n,
3228
+ NODE_BLOOM: 1n << 2n,
3229
+ NODE_WITNESS: 1n << 3n,
3230
+ NODE_COMPACT_FILTERS: 1n << 6n,
3231
+ NODE_NETWORK_LIMITED: 1n << 10n,
3232
+ NODE_P2P_V2: 1n << 11n
3233
+ };
3234
+ var InvType = {
3235
+ ERROR: 0,
3236
+ MSG_TX: 1,
3237
+ MSG_BLOCK: 2,
3238
+ MSG_FILTERED_BLOCK: 3,
3239
+ MSG_CMPCT_BLOCK: 4,
3240
+ MSG_WTX: 5,
3241
+ MSG_DTX: 6,
3242
+ MSG_DWTX: 7,
3243
+ MSG_OUTPUT_HASH: 8,
3244
+ MSG_WITNESS_FLAG: 1 << 30,
3245
+ MSG_WITNESS_BLOCK: 2 | 1 << 30,
3246
+ MSG_WITNESS_TX: 1 | 1 << 30
3247
+ };
3248
+ var PROTOCOL_VERSION = 70016;
3249
+ var P2PClient = class {
3250
+ constructor(options) {
3251
+ this.socket = null;
3252
+ this.connected = false;
3253
+ this.handshakeComplete = false;
3254
+ this.receiveBuffer = Buffer.alloc(0);
3255
+ this.pendingMessages = /* @__PURE__ */ new Map();
3256
+ this.messageHandlers = /* @__PURE__ */ new Map();
3257
+ this.messageQueue = /* @__PURE__ */ new Map();
3258
+ this.peerVersion = 0;
3259
+ this._peerServices = 0n;
3260
+ this.peerStartHeight = 0;
3261
+ const network = options.network ?? "testnet";
3262
+ this.options = {
3263
+ host: options.host,
3264
+ port: options.port ?? DefaultPorts[network.toUpperCase()],
3265
+ network,
3266
+ timeout: options.timeout ?? 3e4,
3267
+ userAgent: options.userAgent ?? "/navio-sdk:0.1.0/",
3268
+ debug: options.debug ?? false,
3269
+ services: options.services ?? ServiceFlags.NODE_NETWORK | ServiceFlags.NODE_WITNESS
3270
+ };
3271
+ this.magic = NetworkMagic[network.toUpperCase()];
3272
+ this.nonce = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
3273
+ }
3274
+ log(...args) {
3275
+ if (this.options.debug) {
3276
+ console.log("[P2P]", ...args);
3277
+ }
3278
+ }
3279
+ /**
3280
+ * Connect to the peer and complete handshake
3281
+ */
3282
+ async connect() {
3283
+ if (this.connected) {
3284
+ return;
3285
+ }
3286
+ return new Promise((resolve, reject) => {
3287
+ this.socket = new net.Socket();
3288
+ const connectionTimeout = setTimeout(() => {
3289
+ this.socket?.destroy();
3290
+ reject(new Error("Connection timeout"));
3291
+ }, this.options.timeout);
3292
+ this.socket.on("connect", async () => {
3293
+ this.log(`Connected to ${this.options.host}:${this.options.port}`);
3294
+ this.connected = true;
3295
+ try {
3296
+ await this.sendVersion();
3297
+ await this.waitForHandshake();
3298
+ clearTimeout(connectionTimeout);
3299
+ this.handshakeComplete = true;
3300
+ this.log("Handshake complete");
3301
+ resolve();
3302
+ } catch (error) {
3303
+ clearTimeout(connectionTimeout);
3304
+ this.disconnect();
3305
+ reject(error);
3306
+ }
3307
+ });
3308
+ this.socket.on("data", (data) => {
3309
+ this.handleData(data);
3310
+ });
3311
+ this.socket.on("error", (error) => {
3312
+ this.log("Socket error:", error.message);
3313
+ clearTimeout(connectionTimeout);
3314
+ this.connected = false;
3315
+ reject(error);
3316
+ });
3317
+ this.socket.on("close", (hadError) => {
3318
+ this.log(`Connection closed (hadError=${hadError}, receiveBuffer=${this.receiveBuffer.length} bytes)`);
3319
+ this.connected = false;
3320
+ this.handshakeComplete = false;
3321
+ for (const [, pending] of this.pendingMessages) {
3322
+ clearTimeout(pending.timer);
3323
+ pending.reject(new Error("Connection closed"));
3324
+ }
3325
+ this.pendingMessages.clear();
3326
+ });
3327
+ this.socket.connect(this.options.port, this.options.host);
3328
+ });
3329
+ }
3330
+ /**
3331
+ * Disconnect from the peer
3332
+ */
3333
+ disconnect() {
3334
+ if (this.socket) {
3335
+ this.socket.destroy();
3336
+ this.socket = null;
3337
+ }
3338
+ this.connected = false;
3339
+ this.handshakeComplete = false;
3340
+ }
3341
+ /**
3342
+ * Check if connected and handshake complete
3343
+ */
3344
+ isConnected() {
3345
+ return this.connected && this.handshakeComplete;
3346
+ }
3347
+ /**
3348
+ * Get peer's advertised start height
3349
+ */
3350
+ getPeerStartHeight() {
3351
+ return this.peerStartHeight;
3352
+ }
3353
+ /**
3354
+ * Get peer's protocol version
3355
+ */
3356
+ getPeerVersion() {
3357
+ return this.peerVersion;
3358
+ }
3359
+ /**
3360
+ * Get peer's advertised services
3361
+ */
3362
+ getPeerServices() {
3363
+ return this._peerServices;
3364
+ }
3365
+ /**
3366
+ * Handle incoming data
3367
+ */
3368
+ handleData(data) {
3369
+ this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
3370
+ while (this.receiveBuffer.length >= 24) {
3371
+ if (!this.receiveBuffer.subarray(0, 4).equals(this.magic)) {
3372
+ const magicIndex = this.receiveBuffer.indexOf(this.magic, 1);
3373
+ if (magicIndex === -1) {
3374
+ this.receiveBuffer = Buffer.alloc(0);
3375
+ return;
3376
+ }
3377
+ this.receiveBuffer = this.receiveBuffer.subarray(magicIndex);
3378
+ continue;
3379
+ }
3380
+ const header = this.parseHeader(this.receiveBuffer);
3381
+ const totalLength = 24 + header.length;
3382
+ if (this.receiveBuffer.length < totalLength) {
3383
+ return;
3384
+ }
3385
+ const payload = this.receiveBuffer.subarray(24, totalLength);
3386
+ const expectedChecksum = this.calculateChecksum(payload);
3387
+ if (!header.checksum.equals(expectedChecksum)) {
3388
+ this.log("Checksum mismatch for", header.command);
3389
+ this.receiveBuffer = this.receiveBuffer.subarray(totalLength);
3390
+ continue;
3391
+ }
3392
+ this.receiveBuffer = this.receiveBuffer.subarray(totalLength);
3393
+ const message = { command: header.command, payload };
3394
+ this.log("Received:", header.command, `(${header.length} bytes)`);
3395
+ this.dispatchMessage(message);
3396
+ }
3397
+ }
3398
+ /**
3399
+ * Parse message header
3400
+ */
3401
+ parseHeader(buffer) {
3402
+ return {
3403
+ magic: buffer.subarray(0, 4),
3404
+ command: buffer.subarray(4, 16).toString("ascii").replace(/\0+$/, ""),
3405
+ length: buffer.readUInt32LE(16),
3406
+ checksum: buffer.subarray(20, 24)
3407
+ };
3408
+ }
3409
+ /**
3410
+ * Calculate message checksum (first 4 bytes of double SHA256)
3411
+ */
3412
+ calculateChecksum(payload) {
3413
+ const hash = sha256(sha256(payload));
3414
+ return Buffer.from(hash.subarray(0, 4));
3415
+ }
3416
+ /**
3417
+ * Dispatch message to handlers
3418
+ */
3419
+ dispatchMessage(message) {
3420
+ const pending = this.pendingMessages.get(message.command);
3421
+ if (pending) {
3422
+ clearTimeout(pending.timer);
3423
+ this.pendingMessages.delete(message.command);
3424
+ pending.resolve(message);
3425
+ return;
3426
+ }
3427
+ const handlers = this.messageHandlers.get(message.command);
3428
+ if (handlers) {
3429
+ for (const handler of handlers) {
3430
+ handler(message);
3431
+ }
3432
+ }
3433
+ switch (message.command) {
3434
+ case MessageType.PING:
3435
+ this.handlePing(message.payload);
3436
+ break;
3437
+ default:
3438
+ const queue = this.messageQueue.get(message.command) ?? [];
3439
+ queue.push(message);
3440
+ this.messageQueue.set(message.command, queue);
3441
+ break;
3442
+ }
3443
+ }
3444
+ /**
3445
+ * Register a message handler
3446
+ */
3447
+ onMessage(command, handler) {
3448
+ const handlers = this.messageHandlers.get(command) ?? [];
3449
+ handlers.push(handler);
3450
+ this.messageHandlers.set(command, handlers);
3451
+ }
3452
+ /**
3453
+ * Send a raw message
3454
+ */
3455
+ sendMessage(command, payload = Buffer.alloc(0)) {
3456
+ if (!this.socket) {
3457
+ throw new Error("Not connected");
3458
+ }
3459
+ const header = Buffer.alloc(24);
3460
+ this.magic.copy(header, 0);
3461
+ const cmdBuffer = Buffer.alloc(12);
3462
+ Buffer.from(command, "ascii").copy(cmdBuffer);
3463
+ cmdBuffer.copy(header, 4);
3464
+ header.writeUInt32LE(payload.length, 16);
3465
+ const checksum = this.calculateChecksum(payload);
3466
+ checksum.copy(header, 20);
3467
+ const message = Buffer.concat([header, payload]);
3468
+ this.socket.write(message);
3469
+ this.log("Sent:", command, `(${payload.length} bytes)`);
3470
+ }
3471
+ /**
3472
+ * Send a message and wait for a specific response
3473
+ */
3474
+ async sendAndWait(command, payload, responseCommand, timeout) {
3475
+ return new Promise((resolve, reject) => {
3476
+ const timer = setTimeout(() => {
3477
+ this.pendingMessages.delete(responseCommand);
3478
+ reject(new Error(`Timeout waiting for ${responseCommand}`));
3479
+ }, timeout ?? this.options.timeout);
3480
+ this.pendingMessages.set(responseCommand, { resolve, reject, timer });
3481
+ this.sendMessage(command, payload);
3482
+ });
3483
+ }
3484
+ /**
3485
+ * Wait for a specific message
3486
+ */
3487
+ async waitForMessage(command, timeout) {
3488
+ const queue = this.messageQueue.get(command);
3489
+ if (queue && queue.length > 0) {
3490
+ const message = queue.shift();
3491
+ if (queue.length === 0) {
3492
+ this.messageQueue.delete(command);
3493
+ }
3494
+ return message;
3495
+ }
3496
+ return new Promise((resolve, reject) => {
3497
+ const timer = setTimeout(() => {
3498
+ this.pendingMessages.delete(command);
3499
+ reject(new Error(`Timeout waiting for ${command}`));
3500
+ }, timeout ?? this.options.timeout);
3501
+ this.pendingMessages.set(command, { resolve, reject, timer });
3502
+ });
3503
+ }
3504
+ // ============================================================================
3505
+ // Protocol Messages
3506
+ // ============================================================================
3507
+ /**
3508
+ * Send version message
3509
+ */
3510
+ async sendVersion() {
3511
+ const payload = this.buildVersionPayload();
3512
+ this.sendMessage(MessageType.VERSION, payload);
3513
+ }
3514
+ /**
3515
+ * Build version message payload
3516
+ */
3517
+ buildVersionPayload() {
3518
+ const now = BigInt(Math.floor(Date.now() / 1e3));
3519
+ const userAgentBytes = Buffer.from(this.options.userAgent, "utf8");
3520
+ const userAgentVarInt = this.encodeVarInt(userAgentBytes.length);
3521
+ const payloadSize = 4 + // version
3522
+ 8 + // services
3523
+ 8 + // timestamp
3524
+ 26 + // addr_recv
3525
+ 26 + // addr_from
3526
+ 8 + // nonce
3527
+ userAgentVarInt.length + userAgentBytes.length + 4 + // start_height
3528
+ 1;
3529
+ const payload = Buffer.alloc(payloadSize);
3530
+ let offset = 0;
3531
+ payload.writeInt32LE(PROTOCOL_VERSION, offset);
3532
+ offset += 4;
3533
+ payload.writeBigUInt64LE(this.options.services, offset);
3534
+ offset += 8;
3535
+ payload.writeBigInt64LE(now, offset);
3536
+ offset += 8;
3537
+ payload.writeBigUInt64LE(ServiceFlags.NODE_NETWORK, offset);
3538
+ offset += 8;
3539
+ Buffer.from("00000000000000000000ffff7f000001", "hex").copy(payload, offset);
3540
+ offset += 16;
3541
+ payload.writeUInt16BE(this.options.port, offset);
3542
+ offset += 2;
3543
+ payload.writeBigUInt64LE(this.options.services, offset);
3544
+ offset += 8;
3545
+ Buffer.from("00000000000000000000ffff7f000001", "hex").copy(payload, offset);
3546
+ offset += 16;
3547
+ payload.writeUInt16BE(0, offset);
3548
+ offset += 2;
3549
+ payload.writeBigUInt64LE(this.nonce, offset);
3550
+ offset += 8;
3551
+ userAgentVarInt.copy(payload, offset);
3552
+ offset += userAgentVarInt.length;
3553
+ userAgentBytes.copy(payload, offset);
3554
+ offset += userAgentBytes.length;
3555
+ payload.writeInt32LE(0, offset);
3556
+ offset += 4;
3557
+ payload.writeUInt8(1, offset);
3558
+ return payload;
3559
+ }
3560
+ /**
3561
+ * Wait for handshake completion
3562
+ */
3563
+ async waitForHandshake() {
3564
+ const versionPromise = this.waitForMessage(MessageType.VERSION, 1e4);
3565
+ const verackPromise = this.waitForMessage(MessageType.VERACK, 1e4);
3566
+ const versionMsg = await versionPromise;
3567
+ this.parseVersionMessage(versionMsg.payload);
3568
+ this.sendMessage(MessageType.VERACK);
3569
+ await verackPromise;
3570
+ }
3571
+ /**
3572
+ * Parse version message
3573
+ */
3574
+ parseVersionMessage(payload) {
3575
+ let offset = 0;
3576
+ this.peerVersion = payload.readInt32LE(offset);
3577
+ offset += 4;
3578
+ this._peerServices = payload.readBigUInt64LE(offset);
3579
+ offset += 8;
3580
+ offset += 8 + 26 + 26 + 8;
3581
+ const { value: userAgentLen, bytesRead } = this.decodeVarInt(payload.subarray(offset));
3582
+ offset += bytesRead;
3583
+ const userAgent = payload.subarray(offset, offset + Number(userAgentLen)).toString("utf8");
3584
+ offset += Number(userAgentLen);
3585
+ this.peerStartHeight = payload.readInt32LE(offset);
3586
+ this.log(`Peer version: ${this.peerVersion}, user agent: ${userAgent}, height: ${this.peerStartHeight}`);
3587
+ }
3588
+ /**
3589
+ * Handle ping message
3590
+ */
3591
+ handlePing(payload) {
3592
+ this.sendMessage(MessageType.PONG, payload);
3593
+ }
3594
+ /**
3595
+ * Send getheaders message
3596
+ */
3597
+ async getHeaders(locatorHashes, hashStop) {
3598
+ const payload = this.buildBlockLocatorPayload(locatorHashes, hashStop);
3599
+ const response = await this.sendAndWait(MessageType.GETHEADERS, payload, MessageType.HEADERS);
3600
+ return this.parseHeadersMessage(response.payload);
3601
+ }
3602
+ /**
3603
+ * Build block locator payload for getheaders/getblocks
3604
+ */
3605
+ buildBlockLocatorPayload(hashes, hashStop) {
3606
+ const hashCount = this.encodeVarInt(hashes.length);
3607
+ const payloadSize = 4 + hashCount.length + hashes.length * 32 + 32;
3608
+ const payload = Buffer.alloc(payloadSize);
3609
+ let offset = 0;
3610
+ payload.writeUInt32LE(PROTOCOL_VERSION, offset);
3611
+ offset += 4;
3612
+ hashCount.copy(payload, offset);
3613
+ offset += hashCount.length;
3614
+ for (const hash of hashes) {
3615
+ hash.copy(payload, offset);
3616
+ offset += 32;
3617
+ }
3618
+ if (hashStop) {
3619
+ hashStop.copy(payload, offset);
3620
+ }
3621
+ return payload;
3622
+ }
3623
+ /**
3624
+ * Parse headers message
3625
+ *
3626
+ * Note: Navio's headers message format differs from Bitcoin's.
3627
+ * Bitcoin includes a varint tx_count (always 0) after each 80-byte header.
3628
+ * Navio sends just the raw 80-byte headers with no tx_count.
3629
+ */
3630
+ parseHeadersMessage(payload) {
3631
+ let offset = 0;
3632
+ const { value: count, bytesRead } = this.decodeVarInt(payload);
3633
+ offset += bytesRead;
3634
+ this.log(`Parsing ${count} headers from ${payload.length} bytes`);
3635
+ const headers = [];
3636
+ for (let i = 0; i < Number(count); i++) {
3637
+ if (offset + 80 > payload.length) {
3638
+ this.log(`Headers parse: reached end of payload at header ${i}`);
3639
+ break;
3640
+ }
3641
+ const header = payload.subarray(offset, offset + 80);
3642
+ headers.push(Buffer.from(header));
3643
+ offset += 80;
3644
+ }
3645
+ this.log(`Parsed ${headers.length} headers`);
3646
+ return headers;
3647
+ }
3648
+ /**
3649
+ * Send getdata message
3650
+ */
3651
+ async getData(inventory) {
3652
+ const payload = this.buildInvPayload(inventory);
3653
+ this.sendMessage(MessageType.GETDATA, payload);
3654
+ }
3655
+ /**
3656
+ * Build inventory payload for inv/getdata
3657
+ */
3658
+ buildInvPayload(inventory) {
3659
+ const countVarInt = this.encodeVarInt(inventory.length);
3660
+ const payloadSize = countVarInt.length + inventory.length * 36;
3661
+ const payload = Buffer.alloc(payloadSize);
3662
+ let offset = 0;
3663
+ countVarInt.copy(payload, offset);
3664
+ offset += countVarInt.length;
3665
+ for (const inv of inventory) {
3666
+ payload.writeUInt32LE(inv.type, offset);
3667
+ offset += 4;
3668
+ inv.hash.copy(payload, offset);
3669
+ offset += 32;
3670
+ }
3671
+ return payload;
3672
+ }
3673
+ /**
3674
+ * Request a block by hash
3675
+ */
3676
+ async getBlock(blockHash) {
3677
+ const inventory = [{ type: InvType.MSG_WITNESS_BLOCK, hash: blockHash }];
3678
+ const getDataPayload = this.buildInvPayload(inventory);
3679
+ return this.sendAndWait(MessageType.GETDATA, getDataPayload, MessageType.BLOCK);
3680
+ }
3681
+ /**
3682
+ * Request transaction outputs by output hash (Navio-specific)
3683
+ */
3684
+ async getOutputData(outputHashes) {
3685
+ const countVarInt = this.encodeVarInt(outputHashes.length);
3686
+ const payloadSize = countVarInt.length + outputHashes.length * 32;
3687
+ const payload = Buffer.alloc(payloadSize);
3688
+ let offset = 0;
3689
+ countVarInt.copy(payload, offset);
3690
+ offset += countVarInt.length;
3691
+ for (const hash of outputHashes) {
3692
+ hash.copy(payload, offset);
3693
+ offset += 32;
3694
+ }
3695
+ return this.sendAndWait(MessageType.GETOUTPUTDATA, payload, MessageType.TX);
3696
+ }
3697
+ /**
3698
+ * Send sendheaders message to prefer headers announcements
3699
+ */
3700
+ sendSendHeaders() {
3701
+ this.sendMessage(MessageType.SENDHEADERS);
3702
+ }
3703
+ // ============================================================================
3704
+ // Utility Methods
3705
+ // ============================================================================
3706
+ /**
3707
+ * Encode a variable-length integer
3708
+ */
3709
+ encodeVarInt(value) {
3710
+ const n = typeof value === "bigint" ? value : BigInt(value);
3711
+ if (n < 253) {
3712
+ const buf = Buffer.alloc(1);
3713
+ buf.writeUInt8(Number(n));
3714
+ return buf;
3715
+ } else if (n <= 65535) {
3716
+ const buf = Buffer.alloc(3);
3717
+ buf.writeUInt8(253);
3718
+ buf.writeUInt16LE(Number(n), 1);
3719
+ return buf;
3720
+ } else if (n <= 4294967295) {
3721
+ const buf = Buffer.alloc(5);
3722
+ buf.writeUInt8(254);
3723
+ buf.writeUInt32LE(Number(n), 1);
3724
+ return buf;
3725
+ } else {
3726
+ const buf = Buffer.alloc(9);
3727
+ buf.writeUInt8(255);
3728
+ buf.writeBigUInt64LE(n, 1);
3729
+ return buf;
3730
+ }
3731
+ }
3732
+ /**
3733
+ * Decode a variable-length integer
3734
+ */
3735
+ decodeVarInt(buffer) {
3736
+ const first = buffer.readUInt8(0);
3737
+ if (first < 253) {
3738
+ return { value: BigInt(first), bytesRead: 1 };
3739
+ } else if (first === 253) {
3740
+ return { value: BigInt(buffer.readUInt16LE(1)), bytesRead: 3 };
3741
+ } else if (first === 254) {
3742
+ return { value: BigInt(buffer.readUInt32LE(1)), bytesRead: 5 };
3743
+ } else {
3744
+ return { value: buffer.readBigUInt64LE(1), bytesRead: 9 };
3745
+ }
3746
+ }
3747
+ /**
3748
+ * Reverse a hash for display (Bitcoin uses little-endian internally, big-endian for display)
3749
+ */
3750
+ static reverseHash(hash) {
3751
+ return Buffer.from(hash).reverse();
3752
+ }
3753
+ /**
3754
+ * Convert display hash to internal format
3755
+ */
3756
+ static hashFromDisplay(hexHash) {
3757
+ return Buffer.from(hexHash, "hex").reverse();
3758
+ }
3759
+ /**
3760
+ * Convert internal hash to display format
3761
+ */
3762
+ static hashToDisplay(hash) {
3763
+ return Buffer.from(hash).reverse().toString("hex");
3764
+ }
3765
+ };
3766
+
3767
+ // src/p2p-sync.ts
3768
+ var _P2PSyncProvider = class _P2PSyncProvider extends BaseSyncProvider {
3769
+ constructor(options) {
3770
+ super(options);
3771
+ this.type = "p2p";
3772
+ // Header chain state
3773
+ this.headersByHash = /* @__PURE__ */ new Map();
3774
+ this.headersByHeight = /* @__PURE__ */ new Map();
3775
+ this.chainTipHeight = -1;
3776
+ this.chainTipHash = "";
3777
+ this.genesisHash = "";
3778
+ // Block cache (limited size)
3779
+ this.blockCache = /* @__PURE__ */ new Map();
3780
+ this.maxBlockCacheSize = 10;
3781
+ // Pending block requests
3782
+ this.pendingBlocks = /* @__PURE__ */ new Map();
3783
+ this.options = {
3784
+ host: options.host,
3785
+ port: options.port ?? 33570,
3786
+ network: options.network ?? "testnet",
3787
+ timeout: options.timeout ?? 3e4,
3788
+ debug: options.debug ?? false,
3789
+ userAgent: options.userAgent ?? "/navio-sdk:0.1.0/",
3790
+ maxBlocksPerRequest: options.maxBlocksPerRequest ?? 16,
3791
+ maxHeadersPerRequest: options.maxHeadersPerRequest ?? 2e3
3792
+ };
3793
+ this.client = new P2PClient({
3794
+ host: this.options.host,
3795
+ port: this.options.port,
3796
+ network: this.options.network,
3797
+ timeout: this.options.timeout,
3798
+ debug: this.options.debug,
3799
+ userAgent: this.options.userAgent
3800
+ });
3801
+ }
3802
+ /**
3803
+ * Connect to the P2P node
3804
+ */
3805
+ async connect() {
3806
+ await this.client.connect();
3807
+ this.log("Connected to P2P node");
3808
+ this.client.sendSendHeaders();
3809
+ this.chainTipHeight = this.client.getPeerStartHeight();
3810
+ this.log(`Peer reports height: ${this.chainTipHeight}`);
3811
+ this.client.onMessage(MessageType.BLOCK, (msg) => {
3812
+ this.handleBlockMessage(msg);
3813
+ });
3814
+ if (this.chainTipHeight > 0) {
3815
+ await this.syncHeaders(0, Math.min(100, this.chainTipHeight));
3816
+ }
3817
+ }
3818
+ /**
3819
+ * Disconnect from the P2P node
3820
+ */
3821
+ disconnect() {
3822
+ this.client.disconnect();
3823
+ this.headersByHash.clear();
3824
+ this.headersByHeight.clear();
3825
+ this.blockCache.clear();
3826
+ this.chainTipHeight = -1;
3827
+ }
3828
+ /**
3829
+ * Check if connected
3830
+ */
3831
+ isConnected() {
3832
+ return this.client.isConnected();
3833
+ }
3834
+ /**
3835
+ * Get current chain tip height
3836
+ */
3837
+ async getChainTipHeight() {
3838
+ if (this.chainTipHeight >= 0) {
3839
+ return this.chainTipHeight;
3840
+ }
3841
+ return this.client.getPeerStartHeight();
3842
+ }
3843
+ /**
3844
+ * Get current chain tip
3845
+ */
3846
+ async getChainTip() {
3847
+ return {
3848
+ height: this.chainTipHeight,
3849
+ hash: this.chainTipHash
3850
+ };
3851
+ }
3852
+ /**
3853
+ * Get a single block header
3854
+ */
3855
+ async getBlockHeader(height) {
3856
+ const cached = this.headersByHeight.get(height);
3857
+ if (cached) {
3858
+ return cached.rawHex;
3859
+ }
3860
+ await this.syncHeaders(height, 1);
3861
+ const header = this.headersByHeight.get(height);
3862
+ if (!header) {
3863
+ throw new Error(`Failed to fetch header at height ${height}`);
3864
+ }
3865
+ return header.rawHex;
3866
+ }
3867
+ /**
3868
+ * Get multiple block headers
3869
+ */
3870
+ async getBlockHeaders(startHeight, count) {
3871
+ await this.syncHeaders(startHeight, count);
3872
+ const headers = [];
3873
+ for (let h = startHeight; h < startHeight + count; h++) {
3874
+ const cached = this.headersByHeight.get(h);
3875
+ if (cached) {
3876
+ headers.push(cached.rawHex);
3877
+ } else {
3878
+ break;
3879
+ }
3880
+ }
3881
+ return {
3882
+ count: headers.length,
3883
+ hex: headers.join(""),
3884
+ max: this.options.maxHeadersPerRequest
3885
+ };
3886
+ }
3887
+ /**
3888
+ * Sync headers from the network
3889
+ */
3890
+ async syncHeaders(fromHeight, count) {
3891
+ const locatorHashes = [];
3892
+ let step = 1;
3893
+ let height = fromHeight > 0 ? fromHeight - 1 : 0;
3894
+ while (height >= 0) {
3895
+ const header = this.headersByHeight.get(height);
3896
+ if (header) {
3897
+ locatorHashes.push(P2PClient.hashFromDisplay(header.hash));
3898
+ }
3899
+ if (height === 0) break;
3900
+ height -= step;
3901
+ if (locatorHashes.length > 10) step *= 2;
3902
+ }
3903
+ if (this.genesisHash && locatorHashes.length === 0) {
3904
+ locatorHashes.push(P2PClient.hashFromDisplay(this.genesisHash));
3905
+ }
3906
+ if (locatorHashes.length === 0) {
3907
+ locatorHashes.push(Buffer.alloc(32));
3908
+ }
3909
+ const rawHeaders = await this.client.getHeaders(locatorHashes);
3910
+ this.log(`Received ${rawHeaders.length} headers`);
3911
+ const pendingHeaders = [];
3912
+ for (const rawHeader of rawHeaders) {
3913
+ const headerHex = rawHeader.toString("hex");
3914
+ const hash = this.extractBlockHash(headerHex);
3915
+ const prevHashBytes = rawHeader.subarray(4, 36);
3916
+ const prevHash = Buffer.from(prevHashBytes).reverse().toString("hex");
3917
+ pendingHeaders.push({ rawHeader, hash, prevHash, headerHex });
3918
+ }
3919
+ for (let i = 0; i < pendingHeaders.length; i++) {
3920
+ const { hash, prevHash, headerHex } = pendingHeaders[i];
3921
+ let headerHeight;
3922
+ if (prevHash === "0".repeat(64)) {
3923
+ headerHeight = 0;
3924
+ this.genesisHash = hash;
3925
+ } else if (this.headersByHash.has(prevHash)) {
3926
+ const prevHeader = this.headersByHash.get(prevHash);
3927
+ headerHeight = prevHeader.height + 1;
3928
+ } else if (i === 0 && this.headersByHeight.size === 0) {
3929
+ this.log(`Initial sync: first header is genesis (height 0)`);
3930
+ headerHeight = 0;
3931
+ this.genesisHash = hash;
3932
+ } else if (i > 0) {
3933
+ const prevInBatch = pendingHeaders[i - 1];
3934
+ if (prevInBatch.hash === prevHash) {
3935
+ const prevCached = this.headersByHash.get(prevInBatch.hash);
3936
+ if (prevCached) {
3937
+ headerHeight = prevCached.height + 1;
3938
+ } else {
3939
+ this.log(`Previous header in batch not yet cached: ${prevInBatch.hash.substring(0, 16)}...`);
3940
+ continue;
3941
+ }
3942
+ } else {
3943
+ if (i < 5) {
3944
+ this.log(`Header ${hash.substring(0, 16)}... doesn't chain from previous in batch`);
3945
+ }
3946
+ continue;
3947
+ }
3948
+ } else {
3949
+ this.log(`Cannot determine height for header ${hash.substring(0, 16)}...`);
3950
+ continue;
3951
+ }
3952
+ const cached = {
3953
+ height: headerHeight,
3954
+ hash,
3955
+ rawHex: headerHex,
3956
+ prevHash
3957
+ };
3958
+ this.headersByHash.set(hash, cached);
3959
+ this.headersByHeight.set(headerHeight, cached);
3960
+ if (headerHeight > this.chainTipHeight) {
3961
+ this.chainTipHeight = headerHeight;
3962
+ this.chainTipHash = hash;
3963
+ }
3964
+ }
3965
+ this.log(`Processed headers. Chain tip: height=${this.chainTipHeight}, cached=${this.headersByHeight.size} headers`);
3966
+ if (rawHeaders.length >= this.options.maxHeadersPerRequest - 1 && this.chainTipHeight < this.client.getPeerStartHeight()) {
3967
+ await this.syncHeaders(this.chainTipHeight, count);
3968
+ }
3969
+ }
3970
+ /**
3971
+ * Get transaction keys for a range of blocks
3972
+ */
3973
+ async getBlockTransactionKeysRange(startHeight) {
3974
+ const blocks = [];
3975
+ const maxBlocks = this.options.maxBlocksPerRequest;
3976
+ const batchEndHeight = Math.min(startHeight + maxBlocks - 1, this.chainTipHeight);
3977
+ try {
3978
+ await this.ensureHeadersSyncedTo(batchEndHeight);
3979
+ } catch (e) {
3980
+ this.log(`Could not sync headers to ${batchEndHeight}: ${e}`);
3981
+ }
3982
+ let highestCached = 0;
3983
+ for (const h of this.headersByHeight.keys()) {
3984
+ if (h > highestCached) highestCached = h;
3985
+ }
3986
+ const effectiveTip = Math.min(this.chainTipHeight, highestCached);
3987
+ if (startHeight > effectiveTip) {
3988
+ return {
3989
+ blocks: [],
3990
+ nextHeight: startHeight
3991
+ };
3992
+ }
3993
+ for (let height = startHeight; height < startHeight + maxBlocks && height <= effectiveTip; height++) {
3994
+ try {
3995
+ const txKeys = await this.getBlockTransactionKeys(height);
3996
+ blocks.push({
3997
+ height,
3998
+ txKeys
3999
+ });
4000
+ } catch (e) {
4001
+ this.log(`Error getting tx keys for block ${height}: ${e}`);
4002
+ break;
4003
+ }
4004
+ }
4005
+ const lastHeight = blocks.length > 0 ? blocks[blocks.length - 1].height : startHeight;
4006
+ return {
4007
+ blocks,
4008
+ nextHeight: lastHeight + 1
4009
+ };
4010
+ }
4011
+ /**
4012
+ * Get transaction keys for a single block
4013
+ */
4014
+ async getBlockTransactionKeys(height) {
4015
+ await this.ensureHeadersSyncedTo(height);
4016
+ const header = this.headersByHeight.get(height);
4017
+ if (!header) {
4018
+ throw new Error(`Cannot get header for height ${height}`);
4019
+ }
4020
+ const blockData = await this.fetchBlock(header.hash);
4021
+ return this.parseBlockTransactionKeys(blockData);
4022
+ }
4023
+ /**
4024
+ * Ensure headers are synced up to the specified height
4025
+ */
4026
+ async ensureHeadersSyncedTo(targetHeight) {
4027
+ if (this.headersByHeight.has(targetHeight)) {
4028
+ return;
4029
+ }
4030
+ let highestKnown = -1;
4031
+ for (const h of this.headersByHeight.keys()) {
4032
+ if (h > highestKnown) highestKnown = h;
4033
+ }
4034
+ if (highestKnown >= targetHeight) {
4035
+ return;
4036
+ }
4037
+ let attempts = 0;
4038
+ const maxAttempts = 100;
4039
+ while (!this.headersByHeight.has(targetHeight) && attempts < maxAttempts) {
4040
+ attempts++;
4041
+ this.log(`Need header ${targetHeight}, highest known: ${highestKnown}`);
4042
+ const syncFrom = highestKnown + 1;
4043
+ await this.syncHeaders(syncFrom, this.options.maxHeadersPerRequest);
4044
+ let newHighest = -1;
4045
+ for (const h of this.headersByHeight.keys()) {
4046
+ if (h > newHighest) newHighest = h;
4047
+ }
4048
+ if (newHighest <= highestKnown) {
4049
+ this.log(`No new headers available, highest: ${highestKnown}`);
4050
+ return;
4051
+ }
4052
+ highestKnown = newHighest;
4053
+ }
4054
+ }
4055
+ /**
4056
+ * Fetch a block by hash
4057
+ */
4058
+ async fetchBlock(hashHex) {
4059
+ const cached = this.blockCache.get(hashHex);
4060
+ if (cached) {
4061
+ return cached;
4062
+ }
4063
+ const pending = this.pendingBlocks.get(hashHex);
4064
+ if (pending) {
4065
+ return pending;
4066
+ }
4067
+ const blockHashBuffer = P2PClient.hashFromDisplay(hashHex);
4068
+ const promise = this.client.getBlock(blockHashBuffer).then((msg) => {
4069
+ this.pendingBlocks.delete(hashHex);
4070
+ if (this.blockCache.size >= this.maxBlockCacheSize) {
4071
+ const oldest = this.blockCache.keys().next().value;
4072
+ if (oldest) this.blockCache.delete(oldest);
4073
+ }
4074
+ this.blockCache.set(hashHex, msg.payload);
4075
+ return msg.payload;
4076
+ });
4077
+ this.pendingBlocks.set(hashHex, promise);
4078
+ return promise;
4079
+ }
4080
+ /**
4081
+ * Handle incoming block message
4082
+ */
4083
+ handleBlockMessage(msg) {
4084
+ const headerHex = msg.payload.subarray(0, 80).toString("hex");
4085
+ const hash = this.extractBlockHash(headerHex);
4086
+ if (this.blockCache.size >= this.maxBlockCacheSize) {
4087
+ const oldest = this.blockCache.keys().next().value;
4088
+ if (oldest) this.blockCache.delete(oldest);
4089
+ }
4090
+ this.blockCache.set(hash, msg.payload);
4091
+ this.log(`Received block: ${hash}`);
4092
+ }
4093
+ /**
4094
+ * Parse block data and extract transaction keys
4095
+ *
4096
+ * Parses all transactions in the block and extracts BLSCT output keys
4097
+ * for wallet output detection.
4098
+ */
4099
+ parseBlockTransactionKeys(blockData) {
4100
+ const txKeys = [];
4101
+ let offset = 80;
4102
+ const version = blockData.readInt32LE(0);
4103
+ const isPoS = (version & 16777216) !== 0;
4104
+ this.log(`Block version: 0x${version.toString(16)}, isPoS: ${isPoS}, size: ${blockData.length}`);
4105
+ if (isPoS) {
4106
+ const proofSkipResult = this.skipPoSProof(blockData, offset);
4107
+ if (proofSkipResult.error) {
4108
+ this.log(`Error skipping PoS proof: ${proofSkipResult.error}`);
4109
+ return txKeys;
4110
+ }
4111
+ offset = proofSkipResult.newOffset;
4112
+ this.log(`Skipped PoS proof, offset now: ${offset}`);
4113
+ }
4114
+ const { value: txCount, bytesRead } = this.decodeVarInt(blockData, offset);
4115
+ offset += bytesRead;
4116
+ this.log(`Parsing ${txCount} transactions from block (offset: ${offset})`);
4117
+ for (let txIndex = 0; txIndex < Number(txCount); txIndex++) {
4118
+ const result = this.parseBlsctTransaction(blockData, offset);
4119
+ if (result.error) {
4120
+ this.log(`Error parsing tx ${txIndex}/${txCount}: ${result.error}`);
4121
+ break;
4122
+ }
4123
+ offset = result.newOffset;
4124
+ if (result.outputKeys.length > 0) {
4125
+ txKeys.push({
4126
+ txHash: result.txHash,
4127
+ keys: this.formatOutputKeys(result.outputKeys)
4128
+ });
4129
+ }
4130
+ }
4131
+ return txKeys;
4132
+ }
4133
+ /**
4134
+ * Skip PoS proof in block data
4135
+ */
4136
+ skipPoSProof(data, offset) {
4137
+ try {
4138
+ const G1_SIZE = 48;
4139
+ const SCALAR_SIZE = 32;
4140
+ offset += 8 * G1_SIZE;
4141
+ offset += 6 * SCALAR_SIZE;
4142
+ const { value: lsCount, bytesRead: lsCountBytes } = this.decodeVarInt(data, offset);
4143
+ offset += lsCountBytes;
4144
+ offset += Number(lsCount) * G1_SIZE;
4145
+ const { value: rsCount, bytesRead: rsCountBytes } = this.decodeVarInt(data, offset);
4146
+ offset += rsCountBytes;
4147
+ offset += Number(rsCount) * G1_SIZE;
4148
+ offset += 3 * SCALAR_SIZE;
4149
+ const { value: rpLsCount, bytesRead: rpLsCountBytes } = this.decodeVarInt(data, offset);
4150
+ offset += rpLsCountBytes;
4151
+ offset += Number(rpLsCount) * G1_SIZE;
4152
+ const { value: rpRsCount, bytesRead: rpRsCountBytes } = this.decodeVarInt(data, offset);
4153
+ offset += rpRsCountBytes;
4154
+ offset += Number(rpRsCount) * G1_SIZE;
4155
+ offset += 3 * G1_SIZE;
4156
+ offset += 5 * SCALAR_SIZE;
4157
+ return { newOffset: offset };
4158
+ } catch (e) {
4159
+ return { newOffset: offset, error: String(e) };
4160
+ }
4161
+ }
4162
+ /**
4163
+ * Parse a BLSCT transaction and extract output keys
4164
+ */
4165
+ parseBlsctTransaction(data, offset) {
4166
+ const startOffset = offset;
4167
+ const outputKeys = [];
4168
+ try {
4169
+ const version = data.readInt32LE(offset);
4170
+ offset += 4;
4171
+ const TX_BLSCT_MARKER = 32;
4172
+ const isBLSCT = (version & TX_BLSCT_MARKER) !== 0;
4173
+ let hasWitness = false;
4174
+ let witnessFlags = 0;
4175
+ if (data[offset] === 0) {
4176
+ if (offset + 1 < data.length && data[offset + 1] !== 0) {
4177
+ witnessFlags = data[offset + 1];
4178
+ hasWitness = (witnessFlags & 1) !== 0;
4179
+ offset += 2;
4180
+ }
4181
+ }
4182
+ const { value: inputCount, bytesRead: inputCountBytes } = this.decodeVarInt(data, offset);
4183
+ offset += inputCountBytes;
4184
+ for (let i = 0; i < Number(inputCount); i++) {
4185
+ offset += 32;
4186
+ const { value: scriptSigLen, bytesRead: sigLenBytes } = this.decodeVarInt(data, offset);
4187
+ offset += sigLenBytes;
4188
+ if (Number(scriptSigLen) > 1e4) {
4189
+ return { txHash: "", outputKeys: [], newOffset: offset, error: `Invalid scriptSig length: ${scriptSigLen}` };
4190
+ }
4191
+ offset += Number(scriptSigLen);
4192
+ offset += 4;
4193
+ }
4194
+ const { value: outputCount, bytesRead: outputCountBytes } = this.decodeVarInt(data, offset);
4195
+ offset += outputCountBytes;
4196
+ for (let i = 0; i < Number(outputCount); i++) {
4197
+ const outputResult = this.parseBlsctOutput(data, offset, i);
4198
+ if (outputResult.error) {
4199
+ return { txHash: "", outputKeys: [], newOffset: offset, error: outputResult.error };
4200
+ }
4201
+ offset = outputResult.newOffset;
4202
+ if (outputResult.keys) {
4203
+ outputKeys.push(outputResult.keys);
4204
+ }
4205
+ }
4206
+ if (hasWitness) {
4207
+ for (let i = 0; i < Number(inputCount); i++) {
4208
+ const { value: witnessCount, bytesRead: wcBytes } = this.decodeVarInt(data, offset);
4209
+ offset += wcBytes;
4210
+ for (let j = 0; j < Number(witnessCount); j++) {
4211
+ const { value: itemLen, bytesRead: ilBytes } = this.decodeVarInt(data, offset);
4212
+ offset += ilBytes;
4213
+ offset += Number(itemLen);
4214
+ }
4215
+ }
4216
+ }
4217
+ offset += 4;
4218
+ if (isBLSCT) {
4219
+ offset += 96;
4220
+ }
4221
+ const txData = data.subarray(startOffset, offset);
4222
+ const txHash = this.calculateBlsctTxHash(txData, hasWitness, startOffset, version);
4223
+ return {
4224
+ txHash,
4225
+ outputKeys,
4226
+ newOffset: offset
4227
+ };
4228
+ } catch (e) {
4229
+ return {
4230
+ txHash: "",
4231
+ outputKeys: [],
4232
+ newOffset: offset,
4233
+ error: `Parse error: ${e}`
4234
+ };
4235
+ }
4236
+ }
4237
+ /**
4238
+ * Parse a BLSCT output and extract keys
4239
+ */
4240
+ parseBlsctOutput(data, offset, outputIndex) {
4241
+ try {
4242
+ const rawValue = data.readBigInt64LE(offset);
4243
+ offset += 8;
4244
+ let flags = 0n;
4245
+ if (rawValue === _P2PSyncProvider.MAX_AMOUNT) {
4246
+ flags = data.readBigUInt64LE(offset);
4247
+ offset += 8;
4248
+ if (flags & BigInt(_P2PSyncProvider.TRANSPARENT_VALUE_MARKER)) {
4249
+ offset += 8;
4250
+ }
4251
+ }
4252
+ const { value: scriptLen, bytesRead: scriptLenBytes } = this.decodeVarInt(data, offset);
4253
+ offset += scriptLenBytes;
4254
+ offset += Number(scriptLen);
4255
+ const hasBlsctData = (flags & BigInt(_P2PSyncProvider.BLSCT_MARKER)) !== 0n;
4256
+ const hasTokenId = (flags & BigInt(_P2PSyncProvider.TOKEN_MARKER)) !== 0n;
4257
+ const hasPredicate = (flags & BigInt(_P2PSyncProvider.PREDICATE_MARKER)) !== 0n;
4258
+ let keys = null;
4259
+ if (hasBlsctData) {
4260
+ const blsctResult = this.parseBlsctData(data, offset, outputIndex);
4261
+ if (blsctResult.error) {
4262
+ return { keys: null, newOffset: offset, error: blsctResult.error };
4263
+ }
4264
+ offset = blsctResult.newOffset;
4265
+ keys = blsctResult.keys;
4266
+ }
4267
+ if (hasTokenId) {
4268
+ offset += 64;
4269
+ }
4270
+ if (hasPredicate) {
4271
+ const { value: predicateLen, bytesRead: predLenBytes } = this.decodeVarInt(data, offset);
4272
+ offset += predLenBytes;
4273
+ offset += Number(predicateLen);
4274
+ }
4275
+ return { keys, newOffset: offset };
4276
+ } catch (e) {
4277
+ return { keys: null, newOffset: offset, error: `Output parse error: ${e}` };
4278
+ }
4279
+ }
4280
+ /**
4281
+ * Parse BLSCT data from output
4282
+ */
4283
+ parseBlsctData(data, offset, outputIndex) {
4284
+ try {
4285
+ const proofResult = this.parseRangeProof(data, offset);
4286
+ if (proofResult.error) {
4287
+ return { keys: null, newOffset: offset, error: proofResult.error };
4288
+ }
4289
+ offset = proofResult.newOffset;
4290
+ if (proofResult.hasData) {
4291
+ const spendingKey = data.subarray(offset, offset + _P2PSyncProvider.G1_POINT_SIZE).toString("hex");
4292
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4293
+ const blindingKey = data.subarray(offset, offset + _P2PSyncProvider.G1_POINT_SIZE).toString("hex");
4294
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4295
+ const ephemeralKey = data.subarray(offset, offset + _P2PSyncProvider.G1_POINT_SIZE).toString("hex");
4296
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4297
+ const viewTag = data.readUInt16LE(offset);
4298
+ offset += 2;
4299
+ const outputHash = this.hashBuffer(Buffer.from(`output:${outputIndex}`));
4300
+ return {
4301
+ keys: {
4302
+ blindingKey,
4303
+ spendingKey,
4304
+ ephemeralKey,
4305
+ viewTag,
4306
+ outputHash,
4307
+ hasRangeProof: true
4308
+ },
4309
+ newOffset: offset
4310
+ };
4311
+ }
4312
+ return { keys: null, newOffset: offset };
4313
+ } catch (e) {
4314
+ return { keys: null, newOffset: offset, error: `BLSCT data parse error: ${e}` };
4315
+ }
4316
+ }
4317
+ /**
4318
+ * Parse bulletproofs_plus range proof
4319
+ */
4320
+ parseRangeProof(data, offset) {
4321
+ try {
4322
+ const { value: vsCount, bytesRead: vsCountBytes } = this.decodeVarInt(data, offset);
4323
+ offset += vsCountBytes;
4324
+ const numVs = Number(vsCount);
4325
+ offset += numVs * _P2PSyncProvider.G1_POINT_SIZE;
4326
+ if (numVs > 0) {
4327
+ const { value: lsCount, bytesRead: lsCountBytes } = this.decodeVarInt(data, offset);
4328
+ offset += lsCountBytes;
4329
+ offset += Number(lsCount) * _P2PSyncProvider.G1_POINT_SIZE;
4330
+ const { value: rsCount, bytesRead: rsCountBytes } = this.decodeVarInt(data, offset);
4331
+ offset += rsCountBytes;
4332
+ offset += Number(rsCount) * _P2PSyncProvider.G1_POINT_SIZE;
4333
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4334
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4335
+ offset += _P2PSyncProvider.G1_POINT_SIZE;
4336
+ offset += _P2PSyncProvider.SCALAR_SIZE;
4337
+ offset += _P2PSyncProvider.SCALAR_SIZE;
4338
+ offset += _P2PSyncProvider.SCALAR_SIZE;
4339
+ offset += _P2PSyncProvider.SCALAR_SIZE;
4340
+ offset += _P2PSyncProvider.SCALAR_SIZE;
4341
+ }
4342
+ return { hasData: numVs > 0, newOffset: offset };
4343
+ } catch (e) {
4344
+ return { hasData: false, newOffset: offset, error: `RangeProof parse error: ${e}` };
4345
+ }
4346
+ }
4347
+ /**
4348
+ * Calculate BLSCT transaction hash
4349
+ */
4350
+ calculateBlsctTxHash(txData, _hasWitness, _startOffset, _version) {
4351
+ const hash = sha256(sha256(txData));
4352
+ return Buffer.from(hash).reverse().toString("hex");
4353
+ }
4354
+ /**
4355
+ * Hash a buffer using SHA256
4356
+ */
4357
+ hashBuffer(data) {
4358
+ return Buffer.from(sha256(data)).toString("hex");
4359
+ }
4360
+ /**
4361
+ * Format output keys for the sync interface
4362
+ */
4363
+ formatOutputKeys(outputKeys) {
4364
+ return {
4365
+ outputs: outputKeys
4366
+ };
4367
+ }
4368
+ /**
4369
+ * Get serialized transaction output by output hash
4370
+ */
4371
+ async getTransactionOutput(outputHash) {
4372
+ const outputHashBuffer = Buffer.from(outputHash, "hex");
4373
+ const response = await this.client.getOutputData([outputHashBuffer]);
4374
+ return response.payload.toString("hex");
4375
+ }
4376
+ /**
4377
+ * Broadcast a transaction
4378
+ */
4379
+ async broadcastTransaction(rawTx) {
4380
+ const txData = Buffer.from(rawTx, "hex");
4381
+ const txHash = this.calculateBlsctTxHash(txData, false, 0, 0);
4382
+ this.log(`Broadcasting transaction: ${txHash}`);
4383
+ return txHash;
4384
+ }
4385
+ /**
4386
+ * Get raw transaction
4387
+ */
4388
+ async getRawTransaction(txHash, _verbose) {
4389
+ const inv = [{ type: InvType.MSG_WITNESS_TX, hash: P2PClient.hashFromDisplay(txHash) }];
4390
+ await this.client.getData(inv);
4391
+ const response = await this.client.waitForMessage(MessageType.TX, this.options.timeout);
4392
+ return response.payload.toString("hex");
4393
+ }
4394
+ /**
4395
+ * Decode variable-length integer from buffer
4396
+ */
4397
+ decodeVarInt(buffer, offset) {
4398
+ const first = buffer.readUInt8(offset);
4399
+ if (first < 253) {
4400
+ return { value: BigInt(first), bytesRead: 1 };
4401
+ } else if (first === 253) {
4402
+ return { value: BigInt(buffer.readUInt16LE(offset + 1)), bytesRead: 3 };
4403
+ } else if (first === 254) {
4404
+ return { value: BigInt(buffer.readUInt32LE(offset + 1)), bytesRead: 5 };
4405
+ } else {
4406
+ return { value: buffer.readBigUInt64LE(offset + 1), bytesRead: 9 };
4407
+ }
4408
+ }
4409
+ };
4410
+ // ============================================================================
4411
+ // BLSCT Constants
4412
+ // ============================================================================
4413
+ _P2PSyncProvider.G1_POINT_SIZE = 48;
4414
+ // Compressed G1 point
4415
+ _P2PSyncProvider.SCALAR_SIZE = 32;
4416
+ // MCL Scalar
4417
+ _P2PSyncProvider.MAX_AMOUNT = BigInt("0x7FFFFFFFFFFFFFFF");
4418
+ _P2PSyncProvider.BLSCT_MARKER = 1;
4419
+ _P2PSyncProvider.TOKEN_MARKER = 2;
4420
+ _P2PSyncProvider.PREDICATE_MARKER = 4;
4421
+ _P2PSyncProvider.TRANSPARENT_VALUE_MARKER = 8;
4422
+ var P2PSyncProvider = _P2PSyncProvider;
4423
+
4424
+ // src/electrum-sync.ts
4425
+ var ElectrumSyncProvider = class extends BaseSyncProvider {
4426
+ constructor(options = {}) {
4427
+ super(options);
4428
+ this.type = "electrum";
4429
+ this.chainTipHeight = -1;
4430
+ this.chainTipHash = "";
4431
+ this.client = new ElectrumClient(options);
4432
+ }
4433
+ /**
4434
+ * Get the underlying ElectrumClient for direct access to additional methods
4435
+ */
4436
+ getClient() {
4437
+ return this.client;
4438
+ }
4439
+ /**
4440
+ * Connect to the Electrum server
4441
+ */
4442
+ async connect() {
4443
+ await this.client.connect();
4444
+ this.log("Connected to Electrum server");
4445
+ this.chainTipHeight = await this.client.getChainTipHeight();
4446
+ const header = await this.client.getBlockHeader(this.chainTipHeight);
4447
+ this.chainTipHash = this.extractBlockHash(header);
4448
+ }
4449
+ /**
4450
+ * Disconnect from the Electrum server
4451
+ */
4452
+ disconnect() {
4453
+ this.client.disconnect();
4454
+ }
4455
+ /**
4456
+ * Check if connected
4457
+ */
4458
+ isConnected() {
4459
+ return this.client.isConnected();
4460
+ }
4461
+ /**
4462
+ * Get current chain tip height
4463
+ */
4464
+ async getChainTipHeight() {
4465
+ this.chainTipHeight = await this.client.getChainTipHeight();
4466
+ return this.chainTipHeight;
4467
+ }
4468
+ /**
4469
+ * Get current chain tip
4470
+ */
4471
+ async getChainTip() {
4472
+ const height = await this.getChainTipHeight();
4473
+ const header = await this.client.getBlockHeader(height);
4474
+ this.chainTipHash = this.extractBlockHash(header);
4475
+ return {
4476
+ height,
4477
+ hash: this.chainTipHash
4478
+ };
4479
+ }
4480
+ /**
4481
+ * Get a single block header
4482
+ */
4483
+ async getBlockHeader(height) {
4484
+ return this.client.getBlockHeader(height);
4485
+ }
4486
+ /**
4487
+ * Get multiple block headers
4488
+ */
4489
+ async getBlockHeaders(startHeight, count) {
4490
+ return this.client.getBlockHeaders(startHeight, count);
4491
+ }
4492
+ /**
4493
+ * Get transaction keys for a range of blocks
4494
+ */
4495
+ async getBlockTransactionKeysRange(startHeight) {
4496
+ return this.client.getBlockTransactionKeysRange(startHeight);
4497
+ }
4498
+ /**
4499
+ * Get transaction keys for a single block
4500
+ */
4501
+ async getBlockTransactionKeys(height) {
4502
+ const result = await this.client.getBlockTransactionKeys(height);
4503
+ if (Array.isArray(result)) {
4504
+ return result.map((txKeyData, index) => {
4505
+ if (typeof txKeyData === "object" && txKeyData !== null) {
4506
+ if ("txHash" in txKeyData && "keys" in txKeyData) {
4507
+ return txKeyData;
4508
+ } else {
4509
+ return {
4510
+ txHash: txKeyData.txHash || txKeyData.hash || `tx_${index}`,
4511
+ keys: txKeyData
4512
+ };
4513
+ }
4514
+ }
4515
+ return {
4516
+ txHash: `tx_${index}`,
4517
+ keys: txKeyData
4518
+ };
4519
+ });
4520
+ }
4521
+ return [];
4522
+ }
4523
+ /**
4524
+ * Get serialized transaction output by output hash
4525
+ */
4526
+ async getTransactionOutput(outputHash) {
4527
+ return this.client.getTransactionOutput(outputHash);
4528
+ }
4529
+ /**
4530
+ * Broadcast a transaction
4531
+ */
4532
+ async broadcastTransaction(rawTx) {
4533
+ return this.client.broadcastTransaction(rawTx);
4534
+ }
4535
+ /**
4536
+ * Get raw transaction
4537
+ */
4538
+ async getRawTransaction(txHash, verbose) {
4539
+ return this.client.getRawTransaction(txHash, verbose);
4540
+ }
4541
+ // ============================================================================
4542
+ // Additional Electrum-specific Methods (passthrough)
4543
+ // ============================================================================
4544
+ /**
4545
+ * Get server version
4546
+ */
4547
+ async getServerVersion() {
4548
+ return this.client.getServerVersion();
4549
+ }
4550
+ /**
4551
+ * Subscribe to block headers
4552
+ */
4553
+ async subscribeBlockHeaders(callback) {
4554
+ return this.client.subscribeBlockHeaders(callback);
4555
+ }
4556
+ /**
4557
+ * Get transaction keys for a specific transaction
4558
+ */
4559
+ async getTransactionKeys(txHash) {
4560
+ return this.client.getTransactionKeys(txHash);
4561
+ }
4562
+ /**
4563
+ * Fetch all transaction keys from genesis to chain tip
4564
+ */
4565
+ async fetchAllTransactionKeys(progressCallback) {
4566
+ return this.client.fetchAllTransactionKeys(progressCallback);
4567
+ }
4568
+ /**
4569
+ * Get transaction history for a script hash
4570
+ */
4571
+ async getHistory(scriptHash) {
4572
+ return this.client.getHistory(scriptHash);
4573
+ }
4574
+ /**
4575
+ * Get unspent transaction outputs for a script hash
4576
+ */
4577
+ async getUnspent(scriptHash) {
4578
+ return this.client.getUnspent(scriptHash);
4579
+ }
4580
+ /**
4581
+ * Subscribe to script hash updates
4582
+ */
4583
+ async subscribeScriptHash(scriptHash, callback) {
4584
+ return this.client.subscribeScriptHash(scriptHash, callback);
4585
+ }
4586
+ };
4587
+ var _NavioClient = class _NavioClient {
4588
+ /**
4589
+ * Create a new NavioClient instance
4590
+ * @param config - Client configuration
4591
+ */
4592
+ constructor(config) {
4593
+ this.keyManager = null;
4594
+ this.initialized = false;
4595
+ // Legacy: keep electrumClient reference for backwards compatibility
4596
+ this.electrumClient = null;
4597
+ // Background sync state
4598
+ this.backgroundSyncTimer = null;
4599
+ this.backgroundSyncOptions = null;
4600
+ this.isBackgroundSyncing = false;
4601
+ this.isSyncInProgress = false;
4602
+ this.lastKnownBalance = 0n;
4603
+ if ("electrum" in config && !("backend" in config)) {
4604
+ this.config = {
4605
+ ...config,
4606
+ backend: "electrum"
4607
+ };
4608
+ } else {
4609
+ this.config = config;
4610
+ }
4611
+ this.config.backend = this.config.backend || "electrum";
4612
+ this.config.network = this.config.network || "mainnet";
4613
+ _NavioClient.configureNetwork(this.config.network);
4614
+ if (this.config.backend === "electrum" && !this.config.electrum) {
4615
+ throw new Error('Electrum options required when backend is "electrum"');
4616
+ }
4617
+ if (this.config.backend === "p2p" && !this.config.p2p) {
4618
+ throw new Error('P2P options required when backend is "p2p"');
4619
+ }
4620
+ this.walletDB = new WalletDB(this.config.walletDbPath);
4621
+ if (this.config.backend === "p2p") {
4622
+ this.syncProvider = new P2PSyncProvider(this.config.p2p);
4623
+ } else {
4624
+ this.electrumClient = new ElectrumClient(this.config.electrum);
4625
+ this.syncProvider = new ElectrumSyncProvider(this.config.electrum);
4626
+ }
4627
+ this.syncManager = new TransactionKeysSync(this.walletDB, this.syncProvider);
4628
+ }
4629
+ /**
4630
+ * Configure the navio-blsct library for the specified network
4631
+ */
4632
+ static configureNetwork(network) {
4633
+ const chainMap = {
4634
+ mainnet: BlsctChain.Mainnet,
4635
+ testnet: BlsctChain.Testnet,
4636
+ signet: BlsctChain.Signet,
4637
+ regtest: BlsctChain.Regtest
4638
+ };
4639
+ setChain(chainMap[network]);
4640
+ }
4641
+ /**
4642
+ * Get the current network configuration
4643
+ */
4644
+ getNetwork() {
4645
+ return this.config.network || "mainnet";
4646
+ }
4647
+ /**
4648
+ * Initialize the client
4649
+ * Loads or creates wallet, connects to backend, and initializes sync manager
4650
+ */
4651
+ async initialize() {
4652
+ if (this.initialized) {
4653
+ return;
4654
+ }
4655
+ if (this.config.restoreFromSeed) {
4656
+ this.keyManager = await this.walletDB.restoreWallet(
4657
+ this.config.restoreFromSeed,
4658
+ this.config.restoreFromHeight
4659
+ );
4660
+ this.syncManager.setKeyManager(this.keyManager);
4661
+ await this.syncProvider.connect();
4662
+ } else {
4663
+ try {
4664
+ this.keyManager = await this.walletDB.loadWallet();
4665
+ this.syncManager.setKeyManager(this.keyManager);
4666
+ await this.syncProvider.connect();
4667
+ } catch (error) {
4668
+ if (this.config.createWalletIfNotExists) {
4669
+ let creationHeight;
4670
+ if (this.config.creationHeight !== void 0) {
4671
+ creationHeight = this.config.creationHeight;
4672
+ } else {
4673
+ await this.syncProvider.connect();
4674
+ const chainTip = await this.syncProvider.getChainTipHeight();
4675
+ creationHeight = Math.max(0, chainTip - _NavioClient.CREATION_HEIGHT_MARGIN);
4676
+ }
4677
+ this.keyManager = await this.walletDB.createWallet(creationHeight);
4678
+ this.syncManager.setKeyManager(this.keyManager);
4679
+ } else {
4680
+ throw new Error(
4681
+ `Wallet not found at ${this.config.walletDbPath}. Set createWalletIfNotExists: true to create a new wallet.`
4682
+ );
4683
+ }
4684
+ }
4685
+ }
4686
+ await this.syncManager.initialize();
4687
+ this.initialized = true;
4688
+ }
4689
+ /**
4690
+ * Get the backend type being used
4691
+ */
4692
+ getBackendType() {
4693
+ return this.config.backend || "electrum";
4694
+ }
4695
+ /**
4696
+ * Get the sync provider
4697
+ */
4698
+ getSyncProvider() {
4699
+ return this.syncProvider;
4700
+ }
4701
+ /**
4702
+ * Get the KeyManager instance
4703
+ * @returns KeyManager instance
4704
+ */
4705
+ getKeyManager() {
4706
+ if (!this.keyManager) {
4707
+ throw new Error("Client not initialized. Call initialize() first.");
4708
+ }
4709
+ return this.keyManager;
4710
+ }
4711
+ /**
4712
+ * Get the WalletDB instance
4713
+ * @returns WalletDB instance
4714
+ */
4715
+ getWalletDB() {
4716
+ return this.walletDB;
4717
+ }
4718
+ /**
4719
+ * Get the ElectrumClient instance (only available when using electrum backend)
4720
+ * @returns ElectrumClient instance or null if using P2P backend
4721
+ * @deprecated Use getSyncProvider() instead for backend-agnostic code
4722
+ */
4723
+ getElectrumClient() {
4724
+ return this.electrumClient;
4725
+ }
4726
+ /**
4727
+ * Get the TransactionKeysSync instance
4728
+ * @returns TransactionKeysSync instance
4729
+ */
4730
+ getSyncManager() {
4731
+ return this.syncManager;
4732
+ }
4733
+ /**
4734
+ * Synchronize transaction keys from the backend.
4735
+ *
4736
+ * By default, syncs once to the current chain tip and returns.
4737
+ * Use `startBackgroundSync()` to enable continuous synchronization.
4738
+ *
4739
+ * @param options - Sync options
4740
+ * @returns Number of transaction keys synced
4741
+ */
4742
+ async sync(options) {
4743
+ if (!this.initialized) {
4744
+ await this.initialize();
4745
+ }
4746
+ return this.syncManager.sync(options || {});
4747
+ }
4748
+ // ============================================================
4749
+ // Background Sync Methods
4750
+ // ============================================================
4751
+ /**
4752
+ * Start continuous background synchronization.
4753
+ *
4754
+ * The client will poll for new blocks and automatically sync new transactions.
4755
+ * Callbacks are invoked for new blocks, transactions, and balance changes.
4756
+ *
4757
+ * @param options - Background sync options
4758
+ *
4759
+ * @example
4760
+ * ```typescript
4761
+ * await client.startBackgroundSync({
4762
+ * pollInterval: 10000, // Check every 10 seconds
4763
+ * onNewBlock: (height) => console.log(`New block: ${height}`),
4764
+ * onBalanceChange: (newBal, oldBal) => {
4765
+ * console.log(`Balance changed: ${Number(oldBal)/1e8} -> ${Number(newBal)/1e8} NAV`);
4766
+ * },
4767
+ * onError: (err) => console.error('Sync error:', err),
4768
+ * });
4769
+ * ```
4770
+ */
4771
+ async startBackgroundSync(options = {}) {
4772
+ if (this.isBackgroundSyncing) {
4773
+ return;
4774
+ }
4775
+ if (!this.initialized) {
4776
+ await this.initialize();
4777
+ }
4778
+ this.backgroundSyncOptions = {
4779
+ pollInterval: options.pollInterval ?? 1e4,
4780
+ ...options
4781
+ };
4782
+ this.isBackgroundSyncing = true;
4783
+ this.lastKnownBalance = await this.getBalance();
4784
+ await this.performBackgroundSync();
4785
+ this.backgroundSyncTimer = setInterval(async () => {
4786
+ await this.performBackgroundSync();
4787
+ }, this.backgroundSyncOptions.pollInterval);
4788
+ }
4789
+ /**
4790
+ * Stop background synchronization.
4791
+ */
4792
+ stopBackgroundSync() {
4793
+ if (this.backgroundSyncTimer) {
4794
+ clearInterval(this.backgroundSyncTimer);
4795
+ this.backgroundSyncTimer = null;
4796
+ }
4797
+ this.isBackgroundSyncing = false;
4798
+ this.backgroundSyncOptions = null;
4799
+ }
4800
+ /**
4801
+ * Check if background sync is running.
4802
+ * @returns True if background sync is active
4803
+ */
4804
+ isBackgroundSyncActive() {
4805
+ return this.isBackgroundSyncing;
4806
+ }
4807
+ /**
4808
+ * Perform a single background sync cycle.
4809
+ * Called by the polling timer.
4810
+ */
4811
+ async performBackgroundSync() {
4812
+ if (this.isSyncInProgress) {
4813
+ return;
4814
+ }
4815
+ this.isSyncInProgress = true;
4816
+ const opts = this.backgroundSyncOptions;
4817
+ try {
4818
+ const needsSync = await this.isSyncNeeded();
4819
+ if (!needsSync) {
4820
+ this.isSyncInProgress = false;
4821
+ return;
4822
+ }
4823
+ const chainTip = await this.syncProvider.getChainTipHeight();
4824
+ const lastSynced = this.getLastSyncedHeight();
4825
+ await this.syncManager.sync({
4826
+ onProgress: opts?.onProgress,
4827
+ saveInterval: opts?.saveInterval,
4828
+ verifyHashes: opts?.verifyHashes,
4829
+ keepTxKeys: opts?.keepTxKeys,
4830
+ blockHashRetention: opts?.blockHashRetention
4831
+ });
4832
+ if (opts?.onNewBlock && chainTip > lastSynced) {
4833
+ try {
4834
+ const headerHex = await this.syncProvider.getBlockHeader(chainTip);
4835
+ const hash = this.hashBlockHeader(headerHex);
4836
+ opts.onNewBlock(chainTip, hash);
4837
+ } catch {
4838
+ opts.onNewBlock(chainTip, "");
4839
+ }
4840
+ }
4841
+ if (opts?.onBalanceChange) {
4842
+ const newBalance = await this.getBalance();
4843
+ if (newBalance !== this.lastKnownBalance) {
4844
+ opts.onBalanceChange(newBalance, this.lastKnownBalance);
4845
+ this.lastKnownBalance = newBalance;
4846
+ }
4847
+ }
4848
+ } catch (error) {
4849
+ if (opts?.onError) {
4850
+ opts.onError(error);
4851
+ }
4852
+ } finally {
4853
+ this.isSyncInProgress = false;
4854
+ }
4855
+ }
4856
+ /**
4857
+ * Check if synchronization is needed
4858
+ * @returns True if sync is needed
4859
+ */
4860
+ async isSyncNeeded() {
4861
+ if (!this.initialized) {
4862
+ await this.initialize();
4863
+ }
4864
+ return this.syncManager.isSyncNeeded();
4865
+ }
4866
+ /**
4867
+ * Get last synced block height
4868
+ * @returns Last synced height, or -1 if never synced
4869
+ */
4870
+ getLastSyncedHeight() {
4871
+ return this.syncManager.getLastSyncedHeight();
4872
+ }
4873
+ /**
4874
+ * Get sync state
4875
+ * @returns Current sync state
4876
+ */
4877
+ getSyncState() {
4878
+ return this.syncManager.getSyncState();
4879
+ }
4880
+ /**
4881
+ * Get the wallet creation height (block height to start scanning from)
4882
+ * @returns Creation height or 0 if not set
4883
+ */
4884
+ async getCreationHeight() {
4885
+ return this.walletDB.getCreationHeight();
4886
+ }
4887
+ /**
4888
+ * Set the wallet creation height
4889
+ * @param height - Block height when wallet was created
4890
+ */
4891
+ async setCreationHeight(height) {
4892
+ await this.walletDB.setCreationHeight(height);
4893
+ }
4894
+ /**
4895
+ * Get wallet metadata
4896
+ * @returns Wallet metadata or null if not available
4897
+ */
4898
+ async getWalletMetadata() {
4899
+ return this.walletDB.getWalletMetadata();
4900
+ }
4901
+ /**
4902
+ * Get chain tip from the backend
4903
+ * @returns Current chain tip height and hash
4904
+ */
4905
+ async getChainTip() {
4906
+ if (!this.initialized) {
4907
+ await this.initialize();
4908
+ }
4909
+ const height = await this.syncProvider.getChainTipHeight();
4910
+ if (this.config.backend === "p2p") {
4911
+ const p2pProvider = this.syncProvider;
4912
+ if (typeof p2pProvider.getChainTip === "function") {
4913
+ return p2pProvider.getChainTip();
4914
+ }
4915
+ }
4916
+ try {
4917
+ const headerHex = await this.syncProvider.getBlockHeader(height);
4918
+ const hash = this.hashBlockHeader(headerHex);
4919
+ return { height, hash };
4920
+ } catch {
4921
+ return { height, hash: "" };
4922
+ }
4923
+ }
4924
+ /**
4925
+ * Disconnect from backend and close database
4926
+ */
4927
+ async disconnect() {
4928
+ this.stopBackgroundSync();
4929
+ if (this.syncProvider) {
4930
+ this.syncProvider.disconnect();
4931
+ }
4932
+ if (this.walletDB) {
4933
+ await this.walletDB.close();
4934
+ }
4935
+ this.initialized = false;
4936
+ }
4937
+ /**
4938
+ * Get the current configuration
4939
+ */
4940
+ getConfig() {
4941
+ return { ...this.config };
4942
+ }
4943
+ /**
4944
+ * Check if client is connected to the backend
4945
+ */
4946
+ isConnected() {
4947
+ return this.syncProvider?.isConnected() ?? false;
4948
+ }
4949
+ /**
4950
+ * Hash a block header to get block hash
4951
+ */
4952
+ hashBlockHeader(headerHex) {
4953
+ const headerBytes = Buffer.from(headerHex, "hex");
4954
+ const hash = sha256(sha256(headerBytes));
4955
+ return Buffer.from(hash).reverse().toString("hex");
4956
+ }
4957
+ // ============================================================
4958
+ // Wallet Balance & Output Methods
4959
+ // ============================================================
4960
+ /**
4961
+ * Get wallet balance in satoshis
4962
+ * @param tokenId - Optional token ID to filter by (null for NAV)
4963
+ * @returns Balance in satoshis as bigint
4964
+ */
4965
+ async getBalance(tokenId = null) {
4966
+ return this.walletDB.getBalance(tokenId);
4967
+ }
4968
+ /**
4969
+ * Get wallet balance in NAV (with decimals)
4970
+ * @param tokenId - Optional token ID to filter by (null for NAV)
4971
+ * @returns Balance as a number with 8 decimal places
4972
+ */
4973
+ async getBalanceNav(tokenId = null) {
4974
+ const balanceSatoshis = await this.getBalance(tokenId);
4975
+ return Number(balanceSatoshis) / 1e8;
4976
+ }
4977
+ /**
4978
+ * Get unspent outputs (UTXOs)
4979
+ * @param tokenId - Optional token ID to filter by (null for NAV)
4980
+ * @returns Array of unspent wallet outputs
4981
+ */
4982
+ async getUnspentOutputs(tokenId = null) {
4983
+ return this.walletDB.getUnspentOutputs(tokenId);
4984
+ }
4985
+ /**
4986
+ * Get all wallet outputs (spent and unspent)
4987
+ * @returns Array of all wallet outputs
4988
+ */
4989
+ async getAllOutputs() {
4990
+ return this.walletDB.getAllOutputs();
4991
+ }
4992
+ };
4993
+ /**
4994
+ * Safety margin (in blocks) when setting creation height for new wallets.
4995
+ * This ensures we don't miss any transactions that might be in recent blocks.
4996
+ */
4997
+ _NavioClient.CREATION_HEIGHT_MARGIN = 100;
4998
+ var NavioClient = _NavioClient;
4999
+
5000
+ export { BaseSyncProvider, DefaultPorts, ElectrumClient, ElectrumError, ElectrumSyncProvider, InvType, KeyManager, MessageType, NavioClient, NetworkMagic, P2PClient, P2PSyncProvider, PROTOCOL_VERSION, ServiceFlags, TransactionKeysSync, WalletDB };
5001
+ //# sourceMappingURL=index.mjs.map
5002
+ //# sourceMappingURL=index.mjs.map