@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.
- package/LICENSE +201 -0
- package/dist/errors/MempoolApiError.d.ts +9 -0
- package/dist/errors/MempoolApiError.js +17 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/mempool/MempoolApi.d.ts +371 -0
- package/dist/mempool/MempoolApi.js +333 -0
- package/dist/mempool/MempoolBitcoinBlock.d.ts +44 -0
- package/dist/mempool/MempoolBitcoinBlock.js +48 -0
- package/dist/mempool/MempoolBitcoinRpc.d.ts +167 -0
- package/dist/mempool/MempoolBitcoinRpc.js +418 -0
- package/dist/synchronizer/MempoolBtcRelaySynchronizer.d.ts +30 -0
- package/dist/synchronizer/MempoolBtcRelaySynchronizer.js +109 -0
- package/package.json +34 -0
- package/src/errors/MempoolApiError.ts +18 -0
- package/src/index.ts +8 -0
- package/src/mempool/MempoolApi.ts +596 -0
- package/src/mempool/MempoolBitcoinBlock.ts +88 -0
- package/src/mempool/MempoolBitcoinRpc.ts +498 -0
- package/src/synchronizer/MempoolBtcRelaySynchronizer.ts +133 -0
|
@@ -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
|
+
}
|