Skip to content

Commit 3ed970e

Browse files
authored
Merge pull request #6 from bitcoinerlab/feature/fetchBlockStatus
feat: Implement fetchBlockStatus and fix block height comparison bug
2 parents 676f06f + fd71b57 commit 3ed970e

File tree

7 files changed

+190
-49
lines changed

7 files changed

+190
-49
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@bitcoinerlab/explorer",
33
"description": "Bitcoin Blockchain Explorer: Client Interface featuring Esplora and Electrum Implementations.",
44
"homepage": "https://github.com/bitcoinerlab/explorer",
5-
"version": "0.1.3",
5+
"version": "0.2.0",
66
"author": "Jose-Luis Landabaso",
77
"license": "MIT",
88
"prettier": "@bitcoinerlab/configs/prettierConfig.json",
@@ -53,7 +53,7 @@
5353
},
5454
"dependencies": {
5555
"bitcoinjs-lib": "^6.1.3",
56-
"electrum-client": "github:BlueWallet/rn-electrum-client#76c0ea35e1a50c47f3a7f818d529ebd100161496",
56+
"electrum-client": "github:BlueWallet/rn-electrum-client#47acb51149e97fab249c3f8a314f708dbee4fb6e",
5757
"net": "^1.0.2",
5858
"tls": "^0.0.1"
5959
}

src/electrum-client.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,7 @@ declare module 'electrum-client' {
3939
blockchainEstimatefee(target: number): Promise<number>;
4040

4141
blockchainTransaction_broadcast(txHex: string): Promise<string>;
42+
43+
blockchainBlock_header(height: number): Promise<string>;
4244
}
4345
}

src/electrum.ts

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ElectrumClient from 'electrum-client';
44
import { checkFeeEstimates } from './checkFeeEstimates';
55
//API: https://electrumx.readthedocs.io/en/latest/protocol-methods.html
66

7-
import { networks, Network } from 'bitcoinjs-lib';
7+
import { networks, Network, Block } from 'bitcoinjs-lib';
88
import {
99
ELECTRUM_BLOCKSTREAM_HOST,
1010
ELECTRUM_BLOCKSTREAM_PORT,
@@ -18,6 +18,7 @@ import {
1818
} from './constants';
1919
import {
2020
Explorer,
21+
BlockStatus,
2122
IRREV_CONF_THRESH,
2223
MAX_TX_PER_SCRIPTPUBKEY
2324
} from './interface';
@@ -71,10 +72,11 @@ function defaultElectrumServer(network: Network = networks.bitcoin): {
7172

7273
export class ElectrumExplorer implements Explorer {
7374
#irrevConfThresh: number;
74-
#blockTipHeight!: number;
75+
#tipBlockHeight!: number;
7576
#maxTxPerScriptPubKey: number;
7677
#pingInterval!: ReturnType<typeof setTimeout> | undefined;
7778
#client!: ElectrumClient | undefined;
79+
#blockStatusMap: Map<number, BlockStatus> = new Map();
7880

7981
#host: string;
8082
#port: number;
@@ -155,7 +157,7 @@ export class ElectrumExplorer implements Explorer {
155157
});
156158
this.#client.subscribe.on(
157159
'blockchain.headers.subscribe',
158-
(headers: Array<{ height: number }>) => {
160+
(headers: Array<{ height: number; hex: string }>) => {
159161
if (Array.isArray(headers)) {
160162
for (const header of headers) {
161163
this.#updateBlockTipHeight(header);
@@ -190,6 +192,24 @@ export class ElectrumExplorer implements Explorer {
190192
}, 60 * 1000); // 60 * 1000 ms = 1 minute
191193
}
192194

195+
/**
196+
* Implements {@link Explorer#fetchBlockStatus}.
197+
*/
198+
async fetchBlockStatus(
199+
blockHeight: number
200+
): Promise<BlockStatus | undefined> {
201+
let blockStatus = this.#blockStatusMap.get(blockHeight);
202+
if (blockStatus && blockStatus.irreversible) return blockStatus;
203+
if (blockHeight > this.#tipBlockHeight) return;
204+
205+
const client = await this.#getClient();
206+
const headerHex = await client.blockchainBlock_header(blockHeight);
207+
//cache header info to skip queries in fetchBlockStatus
208+
blockStatus = this.#updateBlockStatusMap(blockHeight, headerHex);
209+
210+
return blockStatus;
211+
}
212+
193213
/**
194214
* Implements {@link Explorer#close}.
195215
*/
@@ -213,21 +233,42 @@ export class ElectrumExplorer implements Explorer {
213233
}
214234
}
215235

216-
#updateBlockTipHeight(header: { height: number }) {
236+
#updateBlockStatusMap(blockHeight: number, headerHex: string): BlockStatus {
237+
let blockStatus = this.#blockStatusMap.get(blockHeight);
238+
if (blockStatus && blockStatus.irreversible) return blockStatus;
239+
240+
const headerBuffer = Buffer.from(headerHex, 'hex');
241+
const header = Block.fromBuffer(headerBuffer);
242+
243+
const blockHash = header.getId();
244+
const blockTime = header.timestamp;
245+
const numConfirmations = this.#tipBlockHeight - blockHeight + 1;
246+
const irreversible = numConfirmations >= this.#irrevConfThresh;
247+
248+
blockStatus = { blockHeight, blockHash, blockTime, irreversible };
249+
this.#blockStatusMap.set(blockHeight, blockStatus);
250+
251+
return blockStatus;
252+
}
253+
254+
#updateBlockTipHeight(header: { height: number; hex: string }) {
217255
if (
218256
header &&
257+
header.hex &&
219258
header.height &&
220-
(typeof this.#blockTipHeight === 'undefined' ||
221-
header.height > this.#blockTipHeight)
259+
(typeof this.#tipBlockHeight === 'undefined' ||
260+
header.height > this.#tipBlockHeight)
222261
) {
223-
this.#blockTipHeight = header.height;
224-
//this.#blockTime = Math.floor(+new Date() / 1000);
262+
this.#tipBlockHeight = header.height;
263+
264+
//cache header info to skip queries in fetchBlockStatus
265+
this.#updateBlockStatusMap(header.height, header.hex);
225266
}
226267
}
227268

228-
async getBlockHeight() {
229-
return this.#blockTipHeight;
230-
}
269+
//async #getBlockHeight() {
270+
// return this.#tipBlockHeight;
271+
//}
231272

232273
/**
233274
* Implements {@link Explorer#fetchAddress}.
@@ -309,13 +350,13 @@ export class ElectrumExplorer implements Explorer {
309350
*/
310351
async fetchBlockHeight(): Promise<number> {
311352
//Get's the client even if we don't need to use it. We call this so that it
312-
//throws if it's not connected (and this.#blockTipHeight is erroneous)
353+
//throws if it's not connected (and this.#tipBlockHeight is erroneous)
313354
await this.#getClient();
314-
if (this.#blockTipHeight === undefined)
355+
if (this.#tipBlockHeight === undefined)
315356
throw new Error(
316357
`Error: block tip height has not been retrieved yet. Probably not connected`
317358
);
318-
return this.#blockTipHeight;
359+
return this.#tipBlockHeight;
319360
}
320361

321362
/**
@@ -354,14 +395,14 @@ export class ElectrumExplorer implements Explorer {
354395
const transactionHistory = history.map(({ tx_hash, height }) => {
355396
const txId = tx_hash;
356397
const blockHeight: number = height || 0;
357-
if (blockHeight > this.#blockTipHeight) {
398+
if (blockHeight > this.#tipBlockHeight) {
358399
console.warn(
359-
`tx ${tx_hash} block height ${blockHeight} larger than the tip ${this.#blockTipHeight}`
400+
`tx ${tx_hash} block height ${blockHeight} larger than the tip ${this.#tipBlockHeight}`
360401
);
361-
this.#blockTipHeight = blockHeight;
402+
this.#tipBlockHeight = blockHeight;
362403
}
363404
const numConfirmations = blockHeight
364-
? this.#blockTipHeight - blockHeight + 1
405+
? this.#tipBlockHeight - blockHeight + 1
365406
: 0;
366407
const irreversible = numConfirmations >= this.#irrevConfThresh;
367408
return { txId, blockHeight, irreversible };

src/esplora.ts

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ESPLORA_BLOCKSTREAM_URL } from './constants';
44
import { reverseScriptHash } from './address';
55

66
import {
7+
BlockStatus,
78
Explorer,
89
IRREV_CONF_THRESH,
910
MAX_TX_PER_SCRIPTPUBKEY
@@ -60,10 +61,11 @@ function isValidHttpUrl(string: string): boolean {
6061
export class EsploraExplorer implements Explorer {
6162
#irrevConfThresh: number;
6263
#BLOCK_HEIGHT_CACHE_TIME: number = 3; //cache for 3 seconds at most
63-
#cachedBlockTipHeight: number = 0;
64-
#blockTipHeightCacheTime: number = 0;
64+
#cachedTipBlockHeight: number = 0;
65+
#tipBlockHeightCacheTime: number = 0;
6566
#url: string;
6667
#maxTxPerScriptPubKey: number;
68+
#blockStatusMap: Map<number, BlockStatus> = new Map();
6769

6870
/**
6971
* @param {object} params
@@ -95,6 +97,37 @@ export class EsploraExplorer implements Explorer {
9597
return;
9698
}
9799

100+
/**
101+
* Implements {@link Explorer#fetchBlockStatus}.
102+
*/
103+
async fetchBlockStatus(
104+
blockHeight: number
105+
): Promise<BlockStatus | undefined> {
106+
let blockStatus = this.#blockStatusMap.get(blockHeight);
107+
if (blockStatus && blockStatus.irreversible) return blockStatus;
108+
if (blockHeight > (await this.#getTipBlockHeight())) return;
109+
110+
const blockHash = await esploraFetchText(
111+
`${this.#url}/block-height/${blockHeight}`
112+
);
113+
const fetchedBlock = (await esploraFetchJson(
114+
`${this.#url}/block/${blockHash}`
115+
)) as { timestamp: number };
116+
const tipBlockHeight = await this.#getTipBlockHeight(blockHeight);
117+
const numConfirmations = tipBlockHeight - blockHeight + 1;
118+
const irreversible = numConfirmations >= this.#irrevConfThresh;
119+
blockStatus = {
120+
blockHeight,
121+
blockHash,
122+
blockTime: fetchedBlock.timestamp,
123+
irreversible
124+
};
125+
//cache this info to skip queries in fetchBlockStatus
126+
this.#blockStatusMap.set(blockHeight, blockStatus);
127+
128+
return blockStatus;
129+
}
130+
98131
async fetchAddressOrScriptHash({
99132
address,
100133
scriptHash
@@ -193,28 +226,28 @@ export class EsploraExplorer implements Explorer {
193226
* @returns A number representing the current height.
194227
*/
195228
async fetchBlockHeight(): Promise<number> {
196-
const blockTipHeight = parseInt(
229+
const tipBlockHeight = parseInt(
197230
await esploraFetchText(`${this.#url}/blocks/tip/height`)
198231
);
199-
this.#cachedBlockTipHeight = blockTipHeight;
200-
this.#blockTipHeightCacheTime = Math.floor(+new Date() / 1000);
201-
return blockTipHeight;
232+
this.#cachedTipBlockHeight = tipBlockHeight;
233+
this.#tipBlockHeightCacheTime = Math.floor(+new Date() / 1000);
234+
return tipBlockHeight;
202235
}
203236

204237
/** Returns the height of the last block.
205238
* It does not fetch it, unless either:
206239
* fetched before more than #BLOCK_HEIGHT_CACHE_TIME ago
207-
* the #cachedBlockTipHeight is behind the blockHeight passed as a param
240+
* the #cachedTipBlockHeight is behind the blockHeight passed as a param
208241
*/
209242

210-
async #getBlockHeight(blockHeight?: number): Promise<number> {
243+
async #getTipBlockHeight(blockHeight?: number): Promise<number> {
211244
const now: number = +new Date() / 1000;
212245
if (
213-
now - this.#blockTipHeightCacheTime > this.#BLOCK_HEIGHT_CACHE_TIME ||
214-
(blockHeight && blockHeight > this.#blockTipHeightCacheTime)
246+
now - this.#tipBlockHeightCacheTime > this.#BLOCK_HEIGHT_CACHE_TIME ||
247+
(blockHeight && blockHeight > this.#cachedTipBlockHeight)
215248
)
216249
await this.fetchBlockHeight();
217-
return this.#cachedBlockTipHeight;
250+
return this.#cachedTipBlockHeight;
218251
}
219252

220253
/**
@@ -242,7 +275,14 @@ export class EsploraExplorer implements Explorer {
242275

243276
const txHistory = [];
244277

245-
type FetchedTxs = Array<{ txid: string; status: { block_height: number } }>;
278+
type FetchedTxs = Array<{
279+
txid: string;
280+
status: {
281+
block_height: number | null;
282+
block_hash: string | null;
283+
block_time: number | null;
284+
};
285+
}>;
246286

247287
let fetchedTxs: FetchedTxs;
248288
let lastTxid: string | undefined;
@@ -258,15 +298,23 @@ export class EsploraExplorer implements Explorer {
258298
if (lastTx) {
259299
if (lastTx.status.block_height !== 0) lastTxid = lastTx.txid;
260300
for (const fetchedTx of fetchedTxs) {
301+
let irreversible = false;
302+
let blockHeight = 0;
261303
const txId = fetchedTx.txid;
262304
const status = fetchedTx.status;
263-
const blockHeight = status.block_height || 0;
264-
const blockTipHeight = await this.#getBlockHeight(blockHeight);
265-
const numConfirmations =
266-
blockHeight === 0 ? 0 : blockTipHeight - blockHeight + 1;
267-
const irreversible =
268-
blockHeight !== 0 && numConfirmations >= this.#irrevConfThresh;
269-
305+
if (status.block_hash && status.block_time && status.block_height) {
306+
const tipBlockHeight = await this.#getTipBlockHeight(blockHeight);
307+
blockHeight = status.block_height;
308+
const numConfirmations = tipBlockHeight - blockHeight + 1;
309+
irreversible = numConfirmations >= this.#irrevConfThresh;
310+
//cache this info to skip queries in fetchBlockStatus
311+
this.#blockStatusMap.set(status.block_height, {
312+
blockHeight,
313+
blockTime: status.block_time,
314+
blockHash: status.block_hash,
315+
irreversible
316+
});
317+
}
270318
txHistory.push({ txId, blockHeight, irreversible });
271319
if (txHistory.length > this.#maxTxPerScriptPubKey)
272320
throw new Error(`Too many transactions per address`);

src/interface.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,17 @@ export interface Explorer {
9999
fetchFeeEstimates(): Promise<Record<string, number>>;
100100

101101
/**
102-
* Get's current block height.
102+
* Get's the `BlockStatus: { blockHeight: number; blockHash: string; blockTime: number; }`)
103+
* of a certain `blockHeight`.
104+
*
105+
* Returns `undefined` if this block height has not been mined yet.
106+
* @async
107+
* @returns `BlockStatus | undefined`;
108+
*/
109+
fetchBlockStatus(blockHeight: number): Promise<BlockStatus | undefined>;
110+
111+
/**
112+
* Get's current block height (blockchain tip).
103113
* @async
104114
* @returns A number representing the current height.
105115
*/
@@ -114,3 +124,10 @@ export interface Explorer {
114124
*/
115125
push(txHex: string): Promise<string>;
116126
}
127+
128+
export type BlockStatus = {
129+
blockHeight: number;
130+
blockHash: string;
131+
blockTime: number;
132+
irreversible: boolean;
133+
};

0 commit comments

Comments
 (0)