Files
xo-stack/packages/stack/src/blockchain/base-blockchain.ts
T

233 lines
9.0 KiB
TypeScript

import { EventEmitter } from '../utils/index.js';
import {
type BlockchainCallback,
type BlockchainUTXOs,
type BlockchainTransactions,
type BlockchainUnsubscribeCallback,
} from './types.js';
import { Transaction } from '@xocash/primitives';
export type BlockchainEvents = {
isConnectedUpdated: boolean;
blockHeightUpdated: number | undefined;
};
/**
* Abstract base class representing a blockchain interface implementation.
* Extends EventEmitter to provide blockchain-specific event handling capabilities.
*
* This class serves as a template for concrete blockchain implementations,
* providing a standardized interface for common blockchain operations such as
* fetching transactions, managing UTXOs, and handling blockchain subscriptions.
*
* @abstract
* @extends {EventEmitter<BlockchainEvents>}
*/
export abstract class BaseBlockchain<
AddressNotificationPayload = undefined,
TransactionNotificationPayload = undefined,
> extends EventEmitter<BlockchainEvents> {
// TODO: Lockscript might be better as the Subscribe Handler.
// From this, we can convert it to address.
// Fulcrum has Scripthash too - but I'm hesitant about this.
// Scripthash means SHA256'ing the Lockscript.
// This is fine - but, at scale, will become expensive.
// For example, imagine a HDWallet with 2000+ addresses.
// And we want to subscribe to all of them.
// Alternatively, we could just pass in an Address Primitive?
// And then have the Adapter decide what it wants to use?
/**
* Initializes the blockchain connection and required resources.
* Should be called before performing any blockchain operations.
*
* @returns {Promise<void>} A promise that resolves when the blockchain connection is established
* @throws {Error} If the connection cannot be established
*/
abstract start(): Promise<void>;
/**
* Gracefully terminates the blockchain connection and cleans up resources.
* Should be called when blockchain operations are no longer needed.
*
* @returns {Promise<void>} A promise that resolves when the shutdown is complete
*/
abstract stop(): Promise<void>;
//-----------------------------------------------------------------------
// Traits (refer to traits.ts for docs)
//-----------------------------------------------------------------------
/**
* Retrieves all unspent transaction outputs (UTXOs) for a given address.
*
* @param {string} address - The blockchain address to query
* @returns {Promise<BlockchainUTXOs>} A promise that resolves with the unspent outputs
* @throws {Error} If the address is invalid or the query fails
*/
abstract fetchUnspents(address: string): Promise<BlockchainUTXOs>;
/**
* Retrieves all transactions associated with a specific address.
*
* @param {string} address - The blockchain address to query
* @returns {Promise<BlockchainTransactions>} A promise that resolves with the list of transactions
* @throws {Error} If the address is invalid or the query fails
*/
abstract fetchAddressHistory(
address: string,
): Promise<BlockchainTransactions>;
/**
* Retrieves detailed information about a specific transaction.
*
* @param {string} txHash - The transaction hash/ID to look up
* @returns {Promise<Transaction>} A promise that resolves with the transaction details
* @throws {Error} If the transaction cannot be found or the query fails
*/
abstract fetchTransaction(txHash: string): Promise<Transaction>;
/**
* Broadcasts a raw transaction to the blockchain network.
*
* @param {Uint8Array} transaction - The serialized transaction data to broadcast
* @returns {Promise<void>} A promise that resolves when the transaction is successfully broadcast
* @throws {Error} If the transaction is invalid or broadcasting fails
*/
abstract broadcastTransaction(transaction: Uint8Array): Promise<void>;
/**
* Subscribes to updates for a specific lockscript.
* The provided callback will be invoked when relevant events occur for the address.
*
* @param lockscript - The blockchain lockscript to monitor
* @param callback - The callback function to handle address-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established
* @throws {Error} If the subscription cannot be created
*/
abstract subscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<BlockchainUnsubscribeCallback>;
/**
* Removes a subscription for a specific blockchain address.
*
* @param lockscript - The blockchain lockscript to unsubscribe from
* @param callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed
* @throws {Error} If the subscription cannot be removed
*/
abstract unsubscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<boolean>;
/**
* Subscribes to updates for a specific blockchain address.
* The provided callback will be invoked when relevant events occur for the address.
*
* @param {string} address - The blockchain address to monitor
* @param {AddressCallback} callback - The callback function to handle address-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established
* @throws {Error} If the subscription cannot be created
*/
abstract subscribeAddress(
address: string,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<BlockchainUnsubscribeCallback>;
/**
* Removes a subscription for a specific blockchain address.
*
* @param {string} address - The blockchain address to unsubscribe from
* @param {AddressCallback} callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed
* @throws {Error} If the subscription cannot be removed
*/
abstract unsubscribeAddress(
address: string,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<boolean>;
/**
* Subscribes to updates for a specific transaction.
* The provided callback will be invoked when relevant events occur for the transaction.
*
* @param {string} transactionHash - The transaction hash to monitor
* @param {TransactionCallback} callback - The callback function to handle transaction-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established
* @throws {Error} If the subscription cannot be created
*/
abstract subscribeTransaction(
transactionHash: string,
callback: BlockchainCallback<TransactionNotificationPayload>,
): Promise<BlockchainUnsubscribeCallback>;
/**
* Removes a subscription for a specific transaction.
*
* @param {string} transactionHash - The transaction hash to unsubscribe from
* @param {TransactionCallback} callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed
* @throws {Error} If the subscription cannot be removed
*/
abstract unsubscribeTransaction(
transactionHash: string,
callback: BlockchainCallback<TransactionNotificationPayload>,
): Promise<boolean>;
/**
* Waits for a transaction to be seen by the node.
*
* @param tx - The transaction to wait for. Can be a Uint8Array, Transaction instance, or a string.
* @param timeoutMs - The maximum time to wait in milliseconds before timing out. Defaults to 10,000ms (10 seconds).
* @returns A Promise that resolves when the transaction is seen by the node or rejects if the timeout is reached.
* @throws Error if the transaction is not seen by the node within the specified timeout.
*/
async waitForTransaction(
tx: Uint8Array | Transaction | string,
timeoutMs = 10_000,
) {
// Cast the transaction.
if (!(tx instanceof Transaction)) {
tx = Transaction.from(tx);
}
const txHash = tx.toHashHex();
// First, subscribe to the transaction
let unsubscribeCallback: () => void;
const subscriptionPromise = this.subscribeTransaction(txHash, () => {
if (unsubscribeCallback) {
unsubscribeCallback();
}
});
// Then create the wait promise without an async executor
const waitForTxPromise = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
if (unsubscribeCallback) {
unsubscribeCallback();
}
reject(new Error(`Failed to broadcast ${txHash}`));
}, timeoutMs);
// Create a subscribe function so that we get notified when transaction is seen by network
unsubscribeCallback = () => {
clearTimeout(timeout);
this.unsubscribeTransaction(txHash, unsubscribeCallback);
resolve();
};
});
// Wait for subscription to be set up first
await subscriptionPromise;
// Then wait for either resolution or timeout
return waitForTxPromise;
}
}