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/README.md +758 -0
- package/dist/index.d.mts +2500 -0
- package/dist/index.d.ts +2500 -0
- package/dist/index.js +5050 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5002 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
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
|