@atomiqlabs/btc-mempool 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,498 @@
1
+ import {
2
+ BigIntBufferUtils,
3
+ BitcoinRpcWithAddressIndex,
4
+ BtcBlockWithTxs,
5
+ BtcSyncInfo,
6
+ BtcTx,
7
+ BtcTxWithBlockheight,
8
+ LightningNetworkApi, LNNodeLiquidity, timeoutPromise
9
+ } from "@atomiqlabs/base";
10
+ import {MempoolBitcoinBlock} from "./MempoolBitcoinBlock";
11
+ import {BitcoinTransaction, MempoolApi, TxVout} from "./MempoolApi";
12
+ import {Buffer} from "buffer";
13
+ import {Address, OutScript, Script, Transaction} from "@scure/btc-signer";
14
+ import {sha256} from "@noble/hashes/sha2";
15
+
16
+ const BITCOIN_BLOCKTIME = 600 * 1000;
17
+ const BITCOIN_BLOCKSIZE = 1024*1024;
18
+
19
+ function bitcoinTxToBtcTx(btcTx: Transaction): BtcTx {
20
+ return {
21
+ locktime: btcTx.lockTime,
22
+ version: btcTx.version,
23
+ confirmations: 0,
24
+ txid: Buffer.from(sha256(sha256(btcTx.toBytes(true, false)))).reverse().toString("hex"),
25
+ hex: Buffer.from(btcTx.toBytes(true, false)).toString("hex"),
26
+ raw: Buffer.from(btcTx.toBytes(true, true)).toString("hex"),
27
+ vsize: btcTx.isFinal ? btcTx.vsize : NaN,
28
+
29
+ outs: Array.from({length: btcTx.outputsLength}, (_, i) => i).map((index) => {
30
+ const output = btcTx.getOutput(index);
31
+ return {
32
+ value: Number(output.amount),
33
+ n: index,
34
+ scriptPubKey: {
35
+ asm: Script.decode(output.script!).map(val => typeof(val)==="object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
36
+ hex: Buffer.from(output.script!).toString("hex")
37
+ }
38
+ }
39
+ }),
40
+ ins: Array.from({length: btcTx.inputsLength}, (_, i) => i).map(index => {
41
+ const input = btcTx.getInput(index);
42
+ return {
43
+ txid: Buffer.from(input.txid!).toString("hex"),
44
+ vout: input.index!,
45
+ scriptSig: {
46
+ asm: Script.decode(input.finalScriptSig!).map(val => typeof(val)==="object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
47
+ hex: Buffer.from(input.finalScriptSig!).toString("hex")
48
+ },
49
+ sequence: input.sequence!,
50
+ txinwitness: input.finalScriptWitness==null ? [] : input.finalScriptWitness.map(witness => Buffer.from(witness).toString("hex"))
51
+ }
52
+ })
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Bitcoin RPC implementation via Mempool.space API
58
+ *
59
+ * @category Bitcoin
60
+ */
61
+ export class MempoolBitcoinRpc implements BitcoinRpcWithAddressIndex<MempoolBitcoinBlock>, LightningNetworkApi {
62
+
63
+ api: MempoolApi;
64
+
65
+ constructor(urlOrMempoolApi: MempoolApi | string | string[]) {
66
+ this.api = urlOrMempoolApi instanceof MempoolApi ? urlOrMempoolApi : new MempoolApi(urlOrMempoolApi);
67
+ }
68
+
69
+ /**
70
+ * Returns a txo hash for a specific transaction vout
71
+ *
72
+ * @param vout
73
+ * @private
74
+ */
75
+ private static getTxoHash(vout: TxVout): Buffer {
76
+ return Buffer.from(sha256(Buffer.concat([
77
+ BigIntBufferUtils.toBuffer(BigInt(vout.value), "le", 8),
78
+ Buffer.from(vout.scriptpubkey, "hex")
79
+ ])));
80
+ }
81
+
82
+ /**
83
+ * Returns delay in milliseconds till an unconfirmed transaction is expected to confirm, returns -1
84
+ * if the transaction won't confirm any time soon
85
+ *
86
+ * @param feeRate
87
+ * @private
88
+ */
89
+ private async getTimeTillConfirmation(feeRate: number): Promise<number> {
90
+ const mempoolBlocks = await this.api.getPendingBlocks();
91
+ const mempoolBlockIndex = mempoolBlocks.findIndex(block => block.feeRange[0]<=feeRate);
92
+ if(mempoolBlockIndex===-1) return -1;
93
+ //Last returned block is usually an aggregate (or a stack) of multiple btc blocks, if tx falls in this block
94
+ // and the last returned block really is an aggregate one (size bigger than BITCOIN_BLOCKSIZE) we return -1
95
+ if(
96
+ mempoolBlockIndex+1===mempoolBlocks.length &&
97
+ mempoolBlocks[mempoolBlocks.length-1].blockVSize>BITCOIN_BLOCKSIZE
98
+ ) return -1;
99
+ return (mempoolBlockIndex+1) * BITCOIN_BLOCKTIME;
100
+ }
101
+
102
+ /**
103
+ * @inheritDoc
104
+ */
105
+ async getConfirmationDelay(tx: {txid: string, confirmations?: number}, requiredConfirmations: number): Promise<number | null> {
106
+ if(tx.confirmations==null || tx.confirmations===0) {
107
+ //Get CPFP data
108
+ const cpfpData = await this.api.getCPFPData(tx.txid);
109
+ if(cpfpData==null) {
110
+ //Transaction is either confirmed in the meantime, or replaced
111
+ return null;
112
+ }
113
+ let confirmationDelay = (await this.getTimeTillConfirmation(cpfpData.effectiveFeePerVsize));
114
+ if(confirmationDelay!==-1) confirmationDelay += (requiredConfirmations-1)*BITCOIN_BLOCKTIME;
115
+ return confirmationDelay;
116
+ }
117
+ if(tx.confirmations>requiredConfirmations) return 0;
118
+ return ((requiredConfirmations-tx.confirmations)*BITCOIN_BLOCKTIME);
119
+ }
120
+
121
+ /**
122
+ * Converts mempool API's transaction to BtcTx object while fetching the raw tx separately
123
+ * @param tx Transaction to convert
124
+ * @private
125
+ */
126
+ private async toBtcTx(tx: BitcoinTransaction): Promise<BtcTxWithBlockheight | null> {
127
+ const base = await this.toBtcTxWithoutRawData(tx);
128
+ if(base==null) return null;
129
+ const rawTx = await this.api.getRawTransaction(tx.txid);
130
+ if(rawTx==null) return null;
131
+ //Strip witness data
132
+ const btcTx = Transaction.fromRaw(rawTx, {
133
+ allowLegacyWitnessUtxo: true,
134
+ allowUnknownInputs: true,
135
+ allowUnknownOutputs: true,
136
+ disableScriptCheck: true
137
+ });
138
+ const strippedRawTx = Buffer.from(btcTx.toBytes(true, false)).toString("hex");
139
+
140
+ return {
141
+ ...base,
142
+ hex: strippedRawTx,
143
+ raw: rawTx.toString("hex")
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Converts mempool API's transaction to BtcTx object, doesn't populate raw and hex fields
149
+ * @param tx Transaction to convert
150
+ * @private
151
+ */
152
+ private async toBtcTxWithoutRawData(tx: BitcoinTransaction): Promise<Omit<BtcTxWithBlockheight, "raw" | "hex">> {
153
+ let confirmations: number = 0;
154
+ if(tx.status!=null && tx.status.confirmed) {
155
+ const blockheight = await this.api.getTipBlockHeight();
156
+ confirmations = blockheight-tx.status.block_height+1;
157
+ }
158
+
159
+ return {
160
+ locktime: tx.locktime,
161
+ version: tx.version,
162
+ blockheight: tx.status?.block_height,
163
+ blockhash: tx.status?.block_hash,
164
+ confirmations,
165
+ txid: tx.txid,
166
+ vsize: tx.weight/4,
167
+ outs: tx.vout.map((e, index) => {
168
+ return {
169
+ value: e.value,
170
+ n: index,
171
+ scriptPubKey: {
172
+ hex: e.scriptpubkey,
173
+ asm: e.scriptpubkey_asm
174
+ }
175
+ }
176
+ }),
177
+ ins: tx.vin.map(e => {
178
+ return {
179
+ txid: e.txid,
180
+ vout: e.vout,
181
+ scriptSig: {
182
+ hex: e.scriptsig,
183
+ asm: e.scriptsig_asm
184
+ },
185
+ sequence: e.sequence,
186
+ txinwitness: e.witness
187
+ }
188
+ }),
189
+ inputAddresses: tx.vin.map(e => e.prevout.scriptpubkey_address)
190
+ };
191
+ }
192
+
193
+ /**
194
+ * @inheritDoc
195
+ */
196
+ getTipHeight(): Promise<number> {
197
+ return this.api.getTipBlockHeight();
198
+ }
199
+
200
+ /**
201
+ * @inheritDoc
202
+ */
203
+ async getBlockHeader(blockhash: string): Promise<MempoolBitcoinBlock> {
204
+ return new MempoolBitcoinBlock(await this.api.getBlockHeader(blockhash));
205
+ }
206
+
207
+ /**
208
+ * @inheritDoc
209
+ */
210
+ async getMerkleProof(txId: string, blockhash: string): Promise<{
211
+ reversedTxId: Buffer;
212
+ pos: number;
213
+ merkle: Buffer[];
214
+ blockheight: number
215
+ }> {
216
+ const proof = await this.api.getTransactionProof(txId);
217
+ return {
218
+ reversedTxId: Buffer.from(txId, "hex").reverse(),
219
+ pos: proof.pos,
220
+ merkle: proof.merkle.map(e => Buffer.from(e, "hex").reverse()),
221
+ blockheight: proof.block_height
222
+ };
223
+ }
224
+
225
+ /**
226
+ * @inheritDoc
227
+ */
228
+ async getTransaction(txId: string): Promise<BtcTxWithBlockheight | null> {
229
+ const tx = await this.api.getTransaction(txId);
230
+ if(tx==null) return null;
231
+ return await this.toBtcTx(tx);
232
+ }
233
+
234
+ /**
235
+ * @inheritDoc
236
+ */
237
+ async isInMainChain(blockhash: string): Promise<boolean> {
238
+ const blockStatus = await this.api.getBlockStatus(blockhash);
239
+ return blockStatus.in_best_chain;
240
+ }
241
+
242
+ /**
243
+ * @inheritDoc
244
+ */
245
+ getBlockhash(height: number): Promise<string> {
246
+ return this.api.getBlockHash(height);
247
+ }
248
+
249
+ /**
250
+ * @inheritDoc
251
+ */
252
+ getBlockWithTransactions(blockhash: string): Promise<BtcBlockWithTxs> {
253
+ throw new Error("Unsupported.");
254
+ }
255
+
256
+ /**
257
+ * @inheritDoc
258
+ */
259
+ async getSyncInfo(): Promise<BtcSyncInfo> {
260
+ const tipHeight = await this.api.getTipBlockHeight();
261
+ return {
262
+ verificationProgress: 1,
263
+ blocks: tipHeight,
264
+ headers: tipHeight,
265
+ ibd: false
266
+ };
267
+ }
268
+
269
+ /**
270
+ * @private
271
+ */
272
+ async getPast15Blocks(height: number): Promise<MempoolBitcoinBlock[]> {
273
+ return (await this.api.getPast15BlockHeaders(height)).map(blockHeader => new MempoolBitcoinBlock(blockHeader));
274
+ }
275
+
276
+ /**
277
+ * @inheritDoc
278
+ */
279
+ async checkAddressTxos(address: string, txoHash: Buffer): Promise<{
280
+ tx: Omit<BtcTxWithBlockheight, "hex" | "raw">,
281
+ vout: number
282
+ } | null> {
283
+ const allTxs = await this.api.getAddressTransactions(address);
284
+
285
+ const relevantTxs = allTxs
286
+ .map(tx => {
287
+ return {
288
+ tx,
289
+ vout: tx.vout.findIndex(vout => MempoolBitcoinRpc.getTxoHash(vout).equals(txoHash))
290
+ }
291
+ })
292
+ .filter(obj => obj.vout>=0)
293
+ .sort((a, b) => {
294
+ if(a.tx.status.confirmed && !b.tx.status.confirmed) return -1;
295
+ if(!a.tx.status.confirmed && b.tx.status.confirmed) return 1;
296
+ if(a.tx.status.confirmed && b.tx.status.confirmed) return a.tx.status.block_height-b.tx.status.block_height;
297
+ return 0;
298
+ });
299
+
300
+ if(relevantTxs.length===0) return null;
301
+
302
+ return {
303
+ tx: await this.toBtcTxWithoutRawData(relevantTxs[0].tx),
304
+ vout: relevantTxs[0].vout
305
+ };
306
+ }
307
+
308
+ /**
309
+ * @inheritDoc
310
+ */
311
+ async waitForAddressTxo(
312
+ address: string,
313
+ txoHash: Buffer,
314
+ requiredConfirmations: number,
315
+ stateUpdateCbk: (btcTx?: Omit<BtcTxWithBlockheight, "hex" | "raw">, vout?: number, txEtaMS?: number) => void,
316
+ abortSignal?: AbortSignal,
317
+ intervalSeconds?: number
318
+ ): Promise<{
319
+ tx: Omit<BtcTxWithBlockheight, "hex" | "raw">,
320
+ vout: number
321
+ }> {
322
+ if(abortSignal!=null) abortSignal.throwIfAborted();
323
+
324
+ while(abortSignal==null || !abortSignal.aborted) {
325
+ await timeoutPromise((intervalSeconds || 5)*1000, abortSignal);
326
+
327
+ const result = await this.checkAddressTxos(address, txoHash);
328
+ if(result==null) {
329
+ stateUpdateCbk();
330
+ continue;
331
+ }
332
+
333
+ const confirmationDelay = await this.getConfirmationDelay(result.tx, requiredConfirmations);
334
+ if(confirmationDelay==null) continue;
335
+
336
+ if(stateUpdateCbk!=null) stateUpdateCbk(
337
+ result.tx,
338
+ result.vout,
339
+ confirmationDelay
340
+ );
341
+
342
+ if(confirmationDelay===0) return result;
343
+ }
344
+
345
+ throw abortSignal.reason;
346
+ }
347
+
348
+ /**
349
+ * @inheritDoc
350
+ */
351
+ async waitForTransaction(
352
+ txId: string, requiredConfirmations: number,
353
+ stateUpdateCbk: (btcTx?: BtcTxWithBlockheight, txEtaMS?: number) => void,
354
+ abortSignal?: AbortSignal, intervalSeconds?: number
355
+ ): Promise<BtcTxWithBlockheight> {
356
+ if(abortSignal!=null) abortSignal.throwIfAborted();
357
+
358
+ while(abortSignal==null || !abortSignal.aborted) {
359
+ await timeoutPromise((intervalSeconds || 5)*1000, abortSignal);
360
+
361
+ const result = await this.getTransaction(txId);
362
+ if(result==null) {
363
+ stateUpdateCbk();
364
+ continue;
365
+ }
366
+
367
+ const confirmationDelay = await this.getConfirmationDelay(result, requiredConfirmations);
368
+ if(confirmationDelay==null) continue;
369
+
370
+ if(stateUpdateCbk!=null) stateUpdateCbk(
371
+ result,
372
+ confirmationDelay
373
+ );
374
+
375
+ if(confirmationDelay===0) return result;
376
+ }
377
+
378
+ throw abortSignal.reason;
379
+ }
380
+
381
+ /**
382
+ * @inheritDoc
383
+ */
384
+ async getLNNodeLiquidity(pubkey: string): Promise<LNNodeLiquidity | null> {
385
+ const nodeInfo = await this.api.getLNNodeInfo(pubkey);
386
+ if(nodeInfo==null) return null;
387
+ return {
388
+ publicKey: nodeInfo.public_key,
389
+ capacity: BigInt(nodeInfo.capacity),
390
+ numChannels: nodeInfo.active_channel_count
391
+ }
392
+ }
393
+
394
+ /**
395
+ * @inheritDoc
396
+ */
397
+ sendRawTransaction(rawTx: string): Promise<string> {
398
+ return this.api.sendTransaction(rawTx);
399
+ }
400
+
401
+ /**
402
+ * @inheritDoc
403
+ */
404
+ sendRawPackage(rawTx: string[]): Promise<string[]> {
405
+ throw new Error("Unsupported");
406
+ }
407
+
408
+ /**
409
+ * @inheritDoc
410
+ */
411
+ async isSpent(utxo: string, confirmed?: boolean): Promise<boolean> {
412
+ const [txId, voutStr] = utxo.split(":");
413
+ const vout = parseInt(voutStr);
414
+ const outspends = await this.api.getOutspends(txId);
415
+ if(outspends[vout]==null) return true;
416
+ if(confirmed) {
417
+ return outspends[vout].spent && outspends[vout].status.confirmed;
418
+ }
419
+ return outspends[vout].spent;
420
+ }
421
+
422
+ /**
423
+ * @inheritDoc
424
+ */
425
+ parseTransaction(rawTx: string): Promise<BtcTx> {
426
+ const btcTx = Transaction.fromRaw(Buffer.from(rawTx, "hex"), {
427
+ allowLegacyWitnessUtxo: true,
428
+ allowUnknownInputs: true,
429
+ allowUnknownOutputs: true,
430
+ disableScriptCheck: true
431
+ });
432
+ return Promise.resolve(bitcoinTxToBtcTx(btcTx));
433
+ }
434
+
435
+ /**
436
+ * @inheritDoc
437
+ */
438
+ getEffectiveFeeRate(btcTx: BtcTx): Promise<{ vsize: number; fee: number; feeRate: number }> {
439
+ throw new Error("Unsupported.");
440
+ }
441
+
442
+ /**
443
+ * @inheritDoc
444
+ */
445
+ async getFeeRate(): Promise<number> {
446
+ return (await this.api.getFees()).fastestFee;
447
+ }
448
+
449
+ /**
450
+ * @inheritDoc
451
+ */
452
+ getAddressBalances(address: string): Promise<{
453
+ confirmedBalance: bigint,
454
+ unconfirmedBalance: bigint
455
+ }> {
456
+ return this.api.getAddressBalances(address);
457
+ }
458
+
459
+ /**
460
+ * @inheritDoc
461
+ */
462
+ async getAddressUTXOs(address:string): Promise<{
463
+ txid: string,
464
+ vout: number,
465
+ confirmed: boolean,
466
+ block_height: number,
467
+ block_hash: string,
468
+ block_time: number
469
+ value: bigint
470
+ }[]> {
471
+ return (await this.api.getAddressUTXOs(address)).map(val => ({
472
+ txid: val.txid,
473
+ vout: val.vout,
474
+ confirmed: val.status.confirmed,
475
+ block_height: val.status.block_height,
476
+ block_hash: val.status.block_hash,
477
+ block_time: val.status.block_time,
478
+ value: val.value
479
+ }));
480
+ }
481
+
482
+ /**
483
+ * @inheritDoc
484
+ */
485
+ async getCPFPData(txId: string): Promise<{
486
+ effectiveFeePerVsize: number,
487
+ adjustedVsize: number
488
+ } | null> {
489
+ const cpfpData = await this.api.getCPFPData(txId);
490
+ if(cpfpData==null || cpfpData.effectiveFeePerVsize==null) return null;
491
+ return cpfpData;
492
+ }
493
+
494
+ outputScriptToAddress(outputScriptHex: string): Promise<string> {
495
+ return Promise.resolve(Address().encode(OutScript.decode(Buffer.from(outputScriptHex, "hex"))));
496
+ }
497
+
498
+ }
@@ -0,0 +1,133 @@
1
+ import {BtcRelay, BtcStoredHeader, RelaySynchronizer, timeoutPromise} from "@atomiqlabs/base";
2
+ import {MempoolBitcoinBlock} from "../mempool/MempoolBitcoinBlock";
3
+ import {MempoolBitcoinRpc} from "../mempool/MempoolBitcoinRpc";
4
+
5
+ /**
6
+ * Mempool.space API based bitcoin relay synchronizer
7
+ *
8
+ * @category Bitcoin
9
+ */
10
+ export class MempoolBtcRelaySynchronizer<B extends BtcStoredHeader<any>, TX> implements RelaySynchronizer<B, TX, MempoolBitcoinBlock > {
11
+
12
+ bitcoinRpc: MempoolBitcoinRpc;
13
+ btcRelay: BtcRelay<B, TX, MempoolBitcoinBlock>;
14
+
15
+ constructor(btcRelay: BtcRelay<B, TX, MempoolBitcoinBlock>, bitcoinRpc: MempoolBitcoinRpc) {
16
+ this.btcRelay = btcRelay;
17
+ this.bitcoinRpc = bitcoinRpc;
18
+ }
19
+
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ async syncToLatestTxs(signer: string, feeRate?: string): Promise<{
24
+ txs: TX[]
25
+ targetCommitedHeader: B,
26
+ computedHeaderMap: {[blockheight: number]: B},
27
+ blockHeaderMap: {[blockheight: number]: MempoolBitcoinBlock},
28
+ btcRelayTipCommitedHeader: B,
29
+ btcRelayTipBlockHeader: MempoolBitcoinBlock,
30
+ latestBlockHeader: MempoolBitcoinBlock,
31
+ startForkId?: number
32
+ }> {
33
+ const tipData = await this.btcRelay.getTipData();
34
+ if(tipData==null) throw new Error("BtcRelay tip data not found - probably not initialized?");
35
+
36
+ const latestKnownBlockLogData = await this.btcRelay.retrieveLatestKnownBlockLog();
37
+ if(latestKnownBlockLogData==null) throw new Error("Failed to get latest known block log");
38
+ const {resultStoredHeader, resultBitcoinHeader} = latestKnownBlockLogData;
39
+
40
+ let cacheData: {
41
+ forkId: number,
42
+ lastStoredHeader: B,
43
+ tx?: TX,
44
+ computedCommitedHeaders: B[]
45
+ } = {
46
+ forkId: resultStoredHeader.getBlockheight()<tipData.blockheight ? -1 : 0, //Indicate that we will be submitting blocks to fork
47
+ lastStoredHeader: resultStoredHeader,
48
+ computedCommitedHeaders: []
49
+ };
50
+
51
+ let spvTipBlockHeader = latestKnownBlockLogData.resultBitcoinHeader;
52
+ let spvTipBlockHeight = spvTipBlockHeader.height;
53
+
54
+ const txsList: TX[] = [];
55
+ const blockHeaderMap: {[blockheight: number]: MempoolBitcoinBlock} = {
56
+ [resultBitcoinHeader.getHeight()]: resultBitcoinHeader
57
+ };
58
+ const computedHeaderMap: {[blockheight: number]: B} = {
59
+ [resultStoredHeader.getBlockheight()]: resultStoredHeader
60
+ };
61
+ let startForkId: number | undefined = undefined;
62
+
63
+ let forkFee: string | undefined = feeRate;
64
+ let mainFee: string | undefined = feeRate;
65
+ const saveHeaders = async (headerCache: MempoolBitcoinBlock[]) => {
66
+ if(cacheData.forkId===-1) {
67
+ if(mainFee==null) mainFee = await this.btcRelay.getMainFeeRate(signer);
68
+ cacheData = await this.btcRelay.saveNewForkHeaders(signer, headerCache, cacheData.lastStoredHeader, tipData.chainWork, mainFee);
69
+ } else if(cacheData.forkId===0) {
70
+ if(mainFee==null) mainFee = await this.btcRelay.getMainFeeRate(signer);
71
+ cacheData = await this.btcRelay.saveMainHeaders(signer, headerCache, cacheData.lastStoredHeader, mainFee);
72
+ } else {
73
+ if(forkFee==null) forkFee = await this.btcRelay.getForkFeeRate(signer, cacheData.forkId);
74
+ cacheData = await this.btcRelay.saveForkHeaders(signer, headerCache, cacheData.lastStoredHeader, cacheData.forkId, tipData.chainWork, forkFee)
75
+ }
76
+ if(cacheData.forkId!==-1 && cacheData.forkId!==0) startForkId = cacheData.forkId;
77
+ txsList.push(cacheData.tx!);
78
+ for(let storedHeader of cacheData.computedCommitedHeaders) {
79
+ computedHeaderMap[storedHeader.getBlockheight()] = storedHeader;
80
+ }
81
+ };
82
+
83
+ let headerCache: MempoolBitcoinBlock[] = [];
84
+
85
+ while(true) {
86
+ const retrievedHeaders = await this.bitcoinRpc.getPast15Blocks(spvTipBlockHeight+15);
87
+ let startIndex = retrievedHeaders.findIndex(val => val.height === spvTipBlockHeight);
88
+ if(startIndex === -1) startIndex = retrievedHeaders.length; //Start from the last block
89
+
90
+ for(let i=startIndex-1;i>=0;i--) {
91
+ const header = retrievedHeaders[i];
92
+
93
+ blockHeaderMap[header.height] = header;
94
+ headerCache.push(header);
95
+
96
+ if(cacheData.forkId===0 ?
97
+ headerCache.length>=this.btcRelay.maxHeadersPerTx :
98
+ headerCache.length>=this.btcRelay.maxForkHeadersPerTx) {
99
+
100
+ await saveHeaders(headerCache);
101
+ headerCache = [];
102
+ }
103
+ }
104
+
105
+ if(retrievedHeaders.length>0) {
106
+ if(spvTipBlockHeight === retrievedHeaders[0].height) break; //Already at the tip
107
+ spvTipBlockHeight = retrievedHeaders[0].height;
108
+ await timeoutPromise(1000);
109
+ } else break;
110
+ }
111
+
112
+ if(headerCache.length>0) await saveHeaders(headerCache);
113
+
114
+ if(cacheData.forkId!==0) {
115
+ throw new Error("Unable to synchronize on-chain bitcoin light client! Not enough chainwork at connected RPC.");
116
+ }
117
+
118
+ return {
119
+ txs: txsList,
120
+ targetCommitedHeader: cacheData.lastStoredHeader,
121
+
122
+ blockHeaderMap,
123
+ computedHeaderMap,
124
+
125
+ btcRelayTipCommitedHeader: resultStoredHeader,
126
+ btcRelayTipBlockHeader: resultBitcoinHeader,
127
+
128
+ latestBlockHeader: spvTipBlockHeader,
129
+ startForkId
130
+ };
131
+ }
132
+
133
+ }