From 4ec83c36fbaa399830128f1b9a13fd0cdb022900 Mon Sep 17 00:00:00 2001 From: Harvey Zuccon Date: Mon, 7 Jul 2025 02:13:22 +0000 Subject: [PATCH] Resolve "feat(blockchain): Add method to get the current blockheight" --- .../stack/src/blockchain/base-blockchain.ts | 9 ++++ .../src/blockchain/blockchain-electrum.ts | 51 +++++++++++++++++-- .../src/blockchain/blockchain-inmemory.ts | 7 +++ packages/stack/src/blockchain/types.ts | 13 ++++- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/stack/src/blockchain/base-blockchain.ts b/packages/stack/src/blockchain/base-blockchain.ts index 5361102..7c78871 100644 --- a/packages/stack/src/blockchain/base-blockchain.ts +++ b/packages/stack/src/blockchain/base-blockchain.ts @@ -5,6 +5,7 @@ import { type BlockchainUTXOs, type BlockchainTransactions, type BlockchainUnsubscribeCallback, + type BlockchainChainTip, } from './types.js'; import { BlockHeader, Transaction, TransactionHash } from '@xocash/primitives'; @@ -89,6 +90,14 @@ export abstract class BaseBlockchain< */ abstract fetchBlockHeader(blockHeight: number): Promise; + /** + * Retrieves the current chain tip block. + * + * @returns {Promise} A promise that resolves with the current chain tip + * @throws {Error} If the chain tip cannot be retrieved + */ + abstract fetchChainTip(): Promise; + /** * Retrieves detailed information about a specific transaction. * diff --git a/packages/stack/src/blockchain/blockchain-electrum.ts b/packages/stack/src/blockchain/blockchain-electrum.ts index 6bbe619..2e89fdf 100644 --- a/packages/stack/src/blockchain/blockchain-electrum.ts +++ b/packages/stack/src/blockchain/blockchain-electrum.ts @@ -32,6 +32,7 @@ import { fetchHistory, fetchTransaction, subscribeToBlockheaderUpdates, + fetchCurrentChainTip, } from '@electrum-cash/protocol'; import { hexToBin } from '@bitauth/libauth'; @@ -181,8 +182,11 @@ export class BlockchainElectrum extends BaseBlockchain< this.handleTransactionStatusUpdates.bind(this), ); - // Subscribe to block header notifications. - await subscribeToBlockheaderUpdates(this.electrumClient); + // Subscribe to block header notifications and fetch the chain tip. + await Promise.all([ + subscribeToBlockheaderUpdates(this.electrumClient), + this.fetchChainTip(), + ]); // Return to prevent further execution. return; @@ -338,6 +342,37 @@ export class BlockchainElectrum extends BaseBlockchain< return blockHeader; } + // TODO: ElectrumClient provides a chainHeight property that is populated after a `blockchain.headers.subscribe` call. + // We may be able to leverage this side-effect to avoid fetching the chain tip. + async fetchChainTip() { + // Wait for connection to be established before attempting to subscribe. + // TODO: Consider doing this at a higher level. + if (!this.isConnected) { + await this.waitFor('isConnectedUpdated', (isConnected) => isConnected); + } + + // If we don't currently have a block height, fetch the current chain tip. + // NOTE: We don't use our cache here because the key will be the same and so on-boot, we will have a stale value. + if (!this.blockHeight) { + const chainTip = await fetchCurrentChainTip(this.electrumClient); + + this.blockHeight = chainTip.height; + } + + // Get the block header + // NOTE: If we didn't have the block height, we are performing double-fetch here + const blockHeader = await this.fetchBlockHeader(this.blockHeight); + + // Create a chain tip object. + const chainTip = { + height: this.blockHeight, + blockHeader: blockHeader, + }; + + // Return the chain tip. + return chainTip; + } + async broadcastTransaction( tx: Transaction | Uint8Array | string, opts: BroadcastOpts = { timeoutMs: 10_000, waitUntilSeen: true }, @@ -546,13 +581,19 @@ export class BlockchainElectrum extends BaseBlockchain< return response; } - protected handleNewBlockchainHeaders( + protected async handleNewBlockchainHeaders( notification: HeadersSubscribeNotification, ) { - // Extract height from the first param. + // Extract height and hex from the first param. // NOTE: This is an object. // https://electrum-cash-protocol.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe - const { height } = notification.params[0]; + const { height, hex } = notification.params[0]; + + // Decode the raw data into our Block Header primitive. + const blockHeader = BlockHeader.fromHex(hex); + + // Update the cache with the new block header. + await this.store.set(`blockHeader.${height}`, blockHeader.toRaw()); // Assign the block height. this.blockHeight = height; diff --git a/packages/stack/src/blockchain/blockchain-inmemory.ts b/packages/stack/src/blockchain/blockchain-inmemory.ts index 4d3ae36..95509e5 100644 --- a/packages/stack/src/blockchain/blockchain-inmemory.ts +++ b/packages/stack/src/blockchain/blockchain-inmemory.ts @@ -5,6 +5,7 @@ import { type BlockchainTransactions, type BlockchainUTXO, type BlockchainUTXOs, + type BlockchainChainTip, } from './types.js'; import { @@ -103,6 +104,12 @@ export class BlockchainInMemory extends BaseBlockchain { return blockHeader; } + // TODO: Implement a method for fetching the chain tip. + // This is a placeholder for now. + async fetchChainTip(): Promise { + throw new Error('Not implemented yet'); + } + async broadcastTransaction( tx: Transaction | Uint8Array | string, opts: { skipValidation: boolean } = { skipValidation: false }, diff --git a/packages/stack/src/blockchain/types.ts b/packages/stack/src/blockchain/types.ts index 01de1c8..1126280 100644 --- a/packages/stack/src/blockchain/types.ts +++ b/packages/stack/src/blockchain/types.ts @@ -1,4 +1,10 @@ -import { Transaction, TransactionHash, UTXO, ExtMap } from '@xocash/primitives'; +import { + Transaction, + TransactionHash, + UTXO, + ExtMap, + BlockHeader, +} from '@xocash/primitives'; export type BlockchainCallback = ( /** The transaction hash, address, lockscript or scriptHash of this subscription. */ @@ -43,3 +49,8 @@ export type BlockHeightPayload = { type: 'block-height'; height: number | null; }; + +export type BlockchainChainTip = { + height: number; + blockHeader: BlockHeader; +};