@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,596 @@
1
+ import {Buffer} from "buffer";
2
+ import {MempoolApiError} from "../errors/MempoolApiError";
3
+ import {BitcoinNetwork, tryWithRetries} from "@atomiqlabs/base";
4
+
5
+ /**
6
+ * Bitcoin transaction confirmation status
7
+ */
8
+ export type BitcoinTransactionStatus = {
9
+ confirmed: boolean,
10
+ block_height: number,
11
+ block_hash: string,
12
+ block_time: number
13
+ };
14
+
15
+ /**
16
+ * Bitcoin transaction output
17
+ */
18
+ export type TxVout = {
19
+ scriptpubkey: string,
20
+ scriptpubkey_asm: string,
21
+ scriptpubkey_type: string,
22
+ scriptpubkey_address: string,
23
+ value: number
24
+ };
25
+
26
+ /**
27
+ * Bitcoin transaction input
28
+ */
29
+ export type TxVin = {
30
+ txid: string,
31
+ vout: number,
32
+ prevout: TxVout,
33
+ scriptsig: string,
34
+ scriptsig_asm: string,
35
+ witness: string[],
36
+ is_coinbase: boolean,
37
+ sequence: number,
38
+ inner_witnessscript_asm: string
39
+ };
40
+
41
+ /**
42
+ * Full Bitcoin transaction data
43
+ */
44
+ export type BitcoinTransaction = {
45
+ txid: string,
46
+ version: number,
47
+ locktime: number,
48
+ vin: TxVin[],
49
+ vout: TxVout[],
50
+ size: number,
51
+ weight: number,
52
+ fee: number,
53
+ status: BitcoinTransactionStatus
54
+ };
55
+
56
+ /**
57
+ * Bitcoin block data
58
+ */
59
+ export type BlockData = {
60
+ bits: number,
61
+ difficulty: number,
62
+ extras: any,
63
+ height: number,
64
+ id: string,
65
+ mediantime: number,
66
+ merkle_root: string,
67
+ nonce: number,
68
+ previousblockhash: string,
69
+ size: number,
70
+ timestamp: number,
71
+ tx_count: number,
72
+ version: number,
73
+ weight: number
74
+ }
75
+
76
+ /**
77
+ * Bitcoin block header data
78
+ */
79
+ export type BitcoinBlockHeader = {
80
+ id: string,
81
+ height: number,
82
+ version: number,
83
+ timestamp: number,
84
+ tx_count: number,
85
+ size: number,
86
+ weight: number,
87
+ merkle_root: string,
88
+ previousblockhash: string,
89
+ mediantime: number,
90
+ nonce: number,
91
+ bits: number,
92
+ difficulty: number
93
+ };
94
+
95
+ /**
96
+ * Lightning network node info
97
+ */
98
+ export type LNNodeInfo = {
99
+ public_key: string,
100
+ alias: string,
101
+ first_seen: number,
102
+ updated_at: number,
103
+ color: string,
104
+ sockets: string,
105
+ as_number: number,
106
+ city_id: number,
107
+ country_id: number,
108
+ subdivision_id: number,
109
+ longtitude: number,
110
+ latitude: number,
111
+ iso_code: string,
112
+ as_organization: string,
113
+ city: {[lang: string]: string},
114
+ country: {[lang: string]: string},
115
+ subdivision: {[lang: string]: string},
116
+ active_channel_count: number,
117
+ capacity: string,
118
+ opened_channel_count: number,
119
+ closed_channel_count: number
120
+ };
121
+
122
+ /**
123
+ * Address information as returned from the mempool api
124
+ */
125
+ export type AddressInfo = {
126
+ address: string;
127
+ chain_stats: {
128
+ funded_txo_count: number;
129
+ funded_txo_sum: number;
130
+ spent_txo_count: number;
131
+ spent_txo_sum: number;
132
+ tx_count: number;
133
+ };
134
+ mempool_stats: {
135
+ funded_txo_count: number;
136
+ funded_txo_sum: number;
137
+ spent_txo_count: number;
138
+ spent_txo_sum: number;
139
+ tx_count: number;
140
+ };
141
+ };
142
+
143
+ /**
144
+ * Transaction CPFP data response as returned from the mempool.space API
145
+ */
146
+ export type TransactionCPFPData = {
147
+ ancestors: {
148
+ txid: string,
149
+ fee: number,
150
+ weight: number
151
+ }[],
152
+ descendants: {
153
+ txid: string,
154
+ fee: number,
155
+ weight: number
156
+ }[],
157
+ effectiveFeePerVsize: number,
158
+ sigops: number,
159
+ adjustedVsize: number
160
+ };
161
+
162
+ /**
163
+ * Bitcoin fees data response as returned from the mempool.space API
164
+ */
165
+ export type BitcoinFees = {
166
+ fastestFee: number,
167
+ halfHourFee: number,
168
+ hourFee: number,
169
+ economyFee: number,
170
+ minimumFee: number
171
+ };
172
+
173
+ /**
174
+ * Pending (predicted next block based on the current mempool) block data response as returned from the mempool.space API
175
+ */
176
+ export type BitcoinPendingBlock = {
177
+ blockSize: number,
178
+ blockVSize: number,
179
+ nTx: number,
180
+ totalFees: number,
181
+ medianFee: number,
182
+ feeRange: number[]
183
+ };
184
+
185
+ /**
186
+ * Block status as returned from the mempool.space API
187
+ */
188
+ export type BlockStatus = {
189
+ in_best_chain: boolean,
190
+ height: number,
191
+ next_best: string
192
+ };
193
+
194
+ /**
195
+ * Transaction merkle proof as returned by the mempool.space API
196
+ */
197
+ export type TransactionProof = {
198
+ block_height: number,
199
+ merkle: string[],
200
+ pos: number
201
+ };
202
+
203
+ /**
204
+ * Transaction output spend as returned by the mempool.space API
205
+ */
206
+ export type TransactionOutspend = {
207
+ spent: boolean,
208
+ txid: string,
209
+ vin: number,
210
+ status: BitcoinTransactionStatus
211
+ };
212
+
213
+ const MempoolApiEndpoints: {[network in BitcoinNetwork]?: string[]} = {
214
+ [BitcoinNetwork.MAINNET]: [
215
+ "https://mempool.space/api/",
216
+ "https://mempool.fra.mempool.space/api/",
217
+ "https://mempool.va1.mempool.space/api/",
218
+ "https://mempool.tk7.mempool.space/api/"
219
+ ],
220
+ [BitcoinNetwork.TESTNET]: [
221
+ "https://mempool.space/testnet/api/",
222
+ "https://mempool.fra.mempool.space/testnet/api/",
223
+ "https://mempool.va1.mempool.space/testnet/api/",
224
+ "https://mempool.tk7.mempool.space/testnet/api/"
225
+ ],
226
+ [BitcoinNetwork.TESTNET4]: [
227
+ "https://mempool.space/testnet4/api/",
228
+ "https://mempool.fra.mempool.space/testnet4/api/",
229
+ "https://mempool.va1.mempool.space/testnet4/api/",
230
+ "https://mempool.tk7.mempool.space/testnet4/api/"
231
+ ]
232
+ }
233
+
234
+ /**
235
+ * Mempool.space REST API client for Bitcoin blockchain data
236
+ *
237
+ * @category Bitcoin
238
+ */
239
+ export class MempoolApi {
240
+
241
+ backends: {
242
+ url: string,
243
+ operational: boolean | null
244
+ }[];
245
+ timeout: number;
246
+
247
+ /**
248
+ * Returns api url that should be operational
249
+ *
250
+ * @private
251
+ */
252
+ private getOperationalApi(): {url: string, operational: boolean | null} | undefined {
253
+ return this.backends.find(e => e.operational===true);
254
+ }
255
+
256
+ /**
257
+ * Returns api urls that are maybe operational, in case none is considered operational returns all of the price
258
+ * apis such that they can be tested again whether they are operational
259
+ *
260
+ * @private
261
+ */
262
+ private getMaybeOperationalApis(): {url: string, operational: boolean | null}[] {
263
+ let operational = this.backends.filter(e => e.operational===true || e.operational===null);
264
+ if(operational.length===0) {
265
+ this.backends.forEach(e => e.operational=null);
266
+ operational = this.backends;
267
+ }
268
+ return operational;
269
+ }
270
+
271
+ /**
272
+ * Sends a GET or POST request to the mempool api, handling the non-200 responses as errors & throwing
273
+ *
274
+ * @param url
275
+ * @param path
276
+ * @param responseType
277
+ * @param type
278
+ * @param body
279
+ */
280
+ private async _request<T>(
281
+ url: string,
282
+ path: string,
283
+ responseType: T extends string ? "str" : "obj",
284
+ type: "GET" | "POST" = "GET",
285
+ body?: string | any
286
+ ) : Promise<T> {
287
+ const response: Response = await fetch(url+path, {
288
+ method: type,
289
+ signal: AbortSignal.timeout(this.timeout),
290
+ body: typeof(body)==="string" ? body : JSON.stringify(body)
291
+ });
292
+
293
+ if(response.status!==200) {
294
+ let resp: string;
295
+ try {
296
+ resp = await response.text();
297
+ } catch (e) {
298
+ throw new MempoolApiError(response.statusText, response.status);
299
+ }
300
+ throw new MempoolApiError(resp, response.status);
301
+ }
302
+
303
+ if(responseType==="str") return await response.text() as any;
304
+ return await response.json();
305
+ }
306
+
307
+ /**
308
+ * Sends request in parallel to multiple maybe operational api urls
309
+ *
310
+ * @param path
311
+ * @param responseType
312
+ * @param type
313
+ * @param body
314
+ * @private
315
+ */
316
+ private async requestFromMaybeOperationalUrls<T>(
317
+ path: string,
318
+ responseType: T extends string ? "str" : "obj",
319
+ type: "GET" | "POST" = "GET",
320
+ body?: string | any
321
+ ) : Promise<T> {
322
+ try {
323
+ return await Promise.any<T>(this.getMaybeOperationalApis().map(
324
+ obj => (async () => {
325
+ try {
326
+ const result = await this._request<T>(obj.url, path, responseType, type, body);
327
+ obj.operational = true;
328
+ return result;
329
+ } catch (e) {
330
+ //Only mark as non operational on 5xx server errors!
331
+ if(e instanceof MempoolApiError && Math.floor(e.httpCode/100)!==5) {
332
+ obj.operational = true;
333
+ throw e;
334
+ } else {
335
+ obj.operational = false;
336
+ throw e;
337
+ }
338
+ }
339
+ })()
340
+ ))
341
+ } catch (_e: any) {
342
+ const e = _e as AggregateError;
343
+ throw e.errors.find(err => err instanceof MempoolApiError && Math.floor(err.httpCode/100)!==5) || e.errors[0];
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Sends a request to mempool API, first tries to use the operational API (if any) and if that fails it falls back
349
+ * to using maybe operational price APIs
350
+ *
351
+ * @param path
352
+ * @param responseType
353
+ * @param type
354
+ * @param body
355
+ * @private
356
+ */
357
+ private async request<T>(
358
+ path: string,
359
+ responseType: T extends string ? "str" : "obj",
360
+ type: "GET" | "POST" = "GET",
361
+ body?: string | any
362
+ ) : Promise<T> {
363
+ return tryWithRetries<T>(() => {
364
+ const operationalPriceApi = this.getOperationalApi();
365
+ if(operationalPriceApi!=null) {
366
+ return this._request(operationalPriceApi.url, path, responseType, type, body).catch(err => {
367
+ //Only retry on 5xx server errors!
368
+ if(err instanceof MempoolApiError && Math.floor(err.httpCode/100)!==5) throw err;
369
+ operationalPriceApi.operational = false;
370
+ return this.requestFromMaybeOperationalUrls(path, responseType, type, body);
371
+ });
372
+ }
373
+ return this.requestFromMaybeOperationalUrls(path, responseType, type, body);
374
+ }, undefined, (err: any) => err instanceof MempoolApiError && Math.floor(err.httpCode/100)!==5);
375
+ }
376
+
377
+ constructor(network: BitcoinNetwork, timeout?: number);
378
+ constructor(url: string | string[], timeout?: number);
379
+ constructor(urlOrNetwork: BitcoinNetwork | string | string[], timeout?: number) {
380
+ if(typeof(urlOrNetwork)==="number") {
381
+ const endpoints = MempoolApiEndpoints[urlOrNetwork];
382
+ if(endpoints==null)
383
+ throw new Error(`No default endpoints found for ${BitcoinNetwork[urlOrNetwork]} network, please pass the manually as string or string[]`);
384
+ this.backends = endpoints.map(val => ({url: val, operational: null}))
385
+ } else {
386
+ if(Array.isArray(urlOrNetwork)) {
387
+ this.backends = urlOrNetwork.map(val => ({url: val, operational: null}));
388
+ } else {
389
+ this.backends = [{url: urlOrNetwork, operational: null}];
390
+ }
391
+ }
392
+ this.timeout = timeout ?? 15*1000;
393
+ }
394
+
395
+ /**
396
+ * Returns information about a specific lightning network node as identified by the public key (in hex encoding)
397
+ *
398
+ * @param pubkey
399
+ */
400
+ getLNNodeInfo(pubkey: string): Promise<LNNodeInfo | null> {
401
+ //500, 200
402
+ return this.request<LNNodeInfo>("v1/lightning/nodes/"+pubkey, "obj").catch((e: Error) => {
403
+ if(e.message==="This node does not exist, or our node is not seeing it yet") return null;
404
+ throw e;
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Returns on-chain transaction as identified by its txId
410
+ *
411
+ * @param txId
412
+ */
413
+ getTransaction(txId: string): Promise<BitcoinTransaction | null> {
414
+ //404 ("Transaction not found"), 200
415
+ return this.request<BitcoinTransaction>("tx/"+txId, "obj").catch((e: Error) => {
416
+ if(e.message==="Transaction not found") return null;
417
+ throw e;
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Returns raw binary encoded bitcoin transaction, also strips the witness data from the transaction
423
+ *
424
+ * @param txId
425
+ */
426
+ async getRawTransaction(txId: string): Promise<Buffer | null> {
427
+ //404 ("Transaction not found"), 200
428
+ const rawTransaction: string | null = await this.request<string>("tx/"+txId+"/hex", "str").catch((e: Error) => {
429
+ if(e.message==="Transaction not found") return null;
430
+ throw e;
431
+ });
432
+ return rawTransaction==null ? null : Buffer.from(rawTransaction, "hex")
433
+ }
434
+
435
+ /**
436
+ * Returns confirmed & unconfirmed balance of the specific bitcoin address
437
+ *
438
+ * @param address
439
+ */
440
+ async getAddressBalances(address: string): Promise<{
441
+ confirmedBalance: bigint,
442
+ unconfirmedBalance: bigint
443
+ }> {
444
+ //400 ("Invalid Bitcoin address"), 200
445
+ const jsonBody = await this.request<AddressInfo>("address/"+address, "obj");
446
+
447
+ const confirmedInput = BigInt(jsonBody.chain_stats.funded_txo_sum);
448
+ const confirmedOutput = BigInt(jsonBody.chain_stats.spent_txo_sum);
449
+ const unconfirmedInput = BigInt(jsonBody.mempool_stats.funded_txo_sum);
450
+ const unconfirmedOutput = BigInt(jsonBody.mempool_stats.spent_txo_sum);
451
+
452
+ return {
453
+ confirmedBalance: confirmedInput - confirmedOutput,
454
+ unconfirmedBalance: unconfirmedInput - unconfirmedOutput
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Returns CPFP (children pays for parent) data for a given transaction
460
+ *
461
+ * @param txId
462
+ */
463
+ getCPFPData(txId: string): Promise<TransactionCPFPData> {
464
+ //200
465
+ return this.request<TransactionCPFPData>("v1/cpfp/"+txId, "obj");
466
+ }
467
+
468
+ /**
469
+ * Returns UTXOs (unspent transaction outputs) for a given address
470
+ *
471
+ * @param address
472
+ */
473
+ async getAddressUTXOs(address: string): Promise<{
474
+ txid: string,
475
+ vout: number,
476
+ status: {
477
+ confirmed: boolean,
478
+ block_height: number,
479
+ block_hash: string,
480
+ block_time: number
481
+ },
482
+ value: bigint
483
+ }[]> {
484
+ //400 ("Invalid Bitcoin address"), 200
485
+ let jsonBody = await this.request<any[]>("address/"+address+"/utxo", "obj");
486
+ jsonBody.forEach(e => e.value = BigInt(e.value));
487
+
488
+ return jsonBody;
489
+ }
490
+
491
+ /**
492
+ * Returns current on-chain bitcoin fees
493
+ */
494
+ getFees(): Promise<BitcoinFees> {
495
+ //200
496
+ return this.request<BitcoinFees>("v1/fees/recommended", "obj");
497
+ }
498
+
499
+ /**
500
+ * Returns all transactions for a given address
501
+ *
502
+ * @param address
503
+ */
504
+ getAddressTransactions(address: string): Promise<BitcoinTransaction[]> {
505
+ //400 ("Invalid Bitcoin address"), 200
506
+ return this.request<BitcoinTransaction[]>("address/"+address+"/txs", "obj");
507
+ }
508
+
509
+ /**
510
+ * Returns expected pending (mempool) blocks
511
+ */
512
+ getPendingBlocks(): Promise<BitcoinPendingBlock[]> {
513
+ //200
514
+ return this.request<BitcoinPendingBlock[]>("v1/fees/mempool-blocks", "obj");
515
+ }
516
+
517
+ /**
518
+ * Returns the blockheight of the current bitcoin blockchain's tip
519
+ */
520
+ async getTipBlockHeight() : Promise<number> {
521
+ //200
522
+ const response: string = await this.request<string>("blocks/tip/height", "str");
523
+ return parseInt(response);
524
+ }
525
+
526
+ /**
527
+ * Returns the bitcoin blockheader as identified by its blockhash
528
+ *
529
+ * @param blockhash
530
+ */
531
+ getBlockHeader(blockhash: string): Promise<BitcoinBlockHeader> {
532
+ //404 ("Block not found"), 200
533
+ return this.request<BitcoinBlockHeader>("block/"+blockhash, "obj");
534
+ }
535
+
536
+ /**
537
+ * Returns the block status
538
+ *
539
+ * @param blockhash
540
+ */
541
+ getBlockStatus(blockhash: string): Promise<BlockStatus> {
542
+ //200
543
+ return this.request<BlockStatus>("block/"+blockhash+"/status", "obj");
544
+ }
545
+
546
+ /**
547
+ * Returns the transaction's proof (merkle proof)
548
+ *
549
+ * @param txId
550
+ */
551
+ getTransactionProof(txId: string) : Promise<TransactionProof> {
552
+ //404 ("Transaction not found or is unconfirmed"), 200
553
+ return this.request<TransactionProof>("tx/"+txId+"/merkle-proof", "obj");
554
+ }
555
+
556
+ /**
557
+ * Returns the transaction's proof (merkle proof)
558
+ *
559
+ * @param txId
560
+ */
561
+ getOutspends(txId: string) : Promise<TransactionOutspend[]> {
562
+ //404 ("Transaction not found"), 200
563
+ return this.request<TransactionOutspend[]>("tx/"+txId+"/outspends", "obj");
564
+ }
565
+
566
+ /**
567
+ * Returns blockhash of a block at a specific blockheight
568
+ *
569
+ * @param height
570
+ */
571
+ getBlockHash(height: number): Promise<string> {
572
+ //404 ("Block not found"), 200
573
+ return this.request<string>("block-height/"+height, "str");
574
+ }
575
+
576
+ /**
577
+ * Returns past 15 blockheaders before (and including) the specified height
578
+ *
579
+ * @param endHeight
580
+ */
581
+ getPast15BlockHeaders(endHeight: number) : Promise<BlockData[]> {
582
+ //200
583
+ return this.request<BlockData[]>("v1/blocks/"+endHeight, "obj");
584
+ }
585
+
586
+ /**
587
+ * Sends raw hex encoded bitcoin transaction
588
+ *
589
+ * @param transactionHex
590
+ */
591
+ sendTransaction(transactionHex: string): Promise<string> {
592
+ //400??, 200
593
+ return this.request<string>("tx", "str", "POST", transactionHex);
594
+ }
595
+
596
+ }
@@ -0,0 +1,88 @@
1
+ import {BtcBlock} from "@atomiqlabs/base";
2
+ import {Buffer} from "buffer";
3
+
4
+ export type MempoolBitcoinBlockType = {
5
+ id: string,
6
+ height: number,
7
+ version: number,
8
+ timestamp: number,
9
+ tx_count: number,
10
+ size: number,
11
+ weight: number,
12
+ merkle_root: string,
13
+ previousblockhash: string,
14
+ mediantime: number,
15
+ nonce: number,
16
+ bits: number,
17
+ difficulty: number
18
+ }
19
+
20
+ export class MempoolBitcoinBlock implements BtcBlock {
21
+
22
+ id: string;
23
+ height: number;
24
+ version: number;
25
+ timestamp: number;
26
+ tx_count: number;
27
+ size: number;
28
+ weight: number;
29
+ merkle_root: string;
30
+ previousblockhash: string;
31
+ mediantime: number;
32
+ nonce: number;
33
+ bits: number;
34
+ difficulty: number;
35
+
36
+ constructor(obj: MempoolBitcoinBlockType) {
37
+ this.id = obj.id;
38
+ this.height = obj.height;
39
+ this.version = obj.version;
40
+ this.timestamp = obj.timestamp;
41
+ this.tx_count = obj.tx_count;
42
+ this.size = obj.size;
43
+ this.weight = obj.weight;
44
+ this.merkle_root = obj.merkle_root;
45
+ this.previousblockhash = obj.previousblockhash;
46
+ this.mediantime = obj.mediantime;
47
+ this.nonce = obj.nonce;
48
+ this.bits = obj.bits;
49
+ this.difficulty = obj.difficulty;
50
+ }
51
+
52
+ getHeight(): number {
53
+ return this.height;
54
+ }
55
+
56
+ getHash(): string {
57
+ return this.id;
58
+ }
59
+
60
+ getMerkleRoot(): string {
61
+ return this.merkle_root;
62
+ }
63
+
64
+ getNbits(): number {
65
+ return this.bits;
66
+ }
67
+
68
+ getNonce(): number {
69
+ return this.nonce;
70
+ }
71
+
72
+ getPrevBlockhash(): string {
73
+ return this.previousblockhash;
74
+ }
75
+
76
+ getTimestamp(): number {
77
+ return this.timestamp;
78
+ }
79
+
80
+ getVersion(): number {
81
+ return this.version;
82
+ }
83
+
84
+ getChainWork(): Buffer {
85
+ throw new Error("Unsupported");
86
+ }
87
+
88
+ }