UNPKG

@bitcoinerlab/discovery

Version:

A TypeScript library for retrieving Bitcoin funds from ranged descriptors, leveraging @bitcoinerlab/explorer for standardized access to multiple blockchain explorers.

720 lines (719 loc) 38.5 kB
/// <reference types="node" /> import { deriveDataFactory } from './deriveData'; import { Network, Transaction } from 'bitcoinjs-lib'; import type { BIP32Interface } from 'bip32'; import type { Explorer } from '@bitcoinerlab/explorer'; import { OutputCriteria, NetworkId, TxId, TxData, Descriptor, Account, DescriptorIndex, DiscoveryData, Utxo, TxStatus, Stxo, TxHex, TxAttribution, TxWithOrder, TxoMap, Txo } from './types'; type Conflict = { descriptor: Descriptor; txo: Txo; index?: number; }; /** * Creates and returns a Discovery class for discovering funds in a Bitcoin network * using descriptors. The class provides methods for descriptor expression discovery, * balance checking, transaction status checking, and so on. * * @returns A Discovery class, constructed with the given explorer instance. */ export declare function DiscoveryFactory( /** * The explorer instance that communicates with the * Bitcoin network. It is responsible for fetching blockchain data like UTXOs, * transaction details, etc. */ explorer: Explorer, /** * The Bitcoin network to use. * One of bitcoinjs-lib [`networks`](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/src/networks.js) (or another one following the same interface). */ network: Network): { Discovery: { new ({ descriptorsCacheSize, outputsPerDescriptorCacheSize, imported }?: { /** * Cache size limit for descriptor expressions. * The cache is used to speed up data queries by avoiding unnecessary * recomputations. However, it is essential to manage the memory * usage of the application. If the cache size is unbounded, it could lead * to excessive memory usage and degrade the application's performance, * especially when dealing with a large number of descriptor expressions. * On the other hand, a very small cache size may lead to more frequent cache * evictions, causing the library to return a different reference for the same data * when the same method is called multiple times, even if the underlying data has not * changed. This is contrary to the immutability principles that the library is built upon. * Ultimately, this limit acts as a trade-off between memory usage, computational efficiency, * and immutability. Set to 0 for unbounded caches. * @defaultValue 1000 */ descriptorsCacheSize?: number; /** * Cache size limit for outputs per descriptor, related to the number of outputs * in ranged descriptor expressions. Similar to the `descriptorsCacheSize`, * this cache is used to speed up data queries and avoid recomputations. * As each descriptor can have multiple indices (if ranged), the number of outputs can grow rapidly, * leading to increased memory usage. Setting a limit helps keep memory usage in check, * while also maintaining the benefits of immutability and computational efficiency. * Set to 0 for unbounded caches. * @defaultValue 10000 */ outputsPerDescriptorCacheSize?: number; /** * Optional parameter used to initialize the Discovery instance with * previously exported data with * {@link _Internal_.Discovery.export | `export()`}. This allows for the * continuation of a previous discovery process. The `imported` object * should contain `discoveryData` and a `version` string. The * `discoveryData` is deeply cloned upon import to ensure that the * internal state of the Discovery instance is isolated from * external changes. The `version` is used to verify that the imported * data model is compatible with the current version of the Discovery * class. */ imported?: { discoveryData: DiscoveryData; version: string; }; }): { "__#3@#derivers": ReturnType<typeof deriveDataFactory>; "__#3@#discoveryData": DiscoveryData; /** * Finds the descriptor (and index) that corresponds to the scriptPubKey * passed as argument. * @private * @param options */ "__#3@#getDescriptorByScriptPubKey"({ networkId, scriptPubKey, gapLimit }: { /** * Network to check. */ networkId: NetworkId; /** * The scriptPubKey to check for uniqueness. */ scriptPubKey: Buffer; /** * When the descriptor is ranged, it will keep searching for the scriptPubKey * to non-set indices above the last one set until reaching the gapLimit. * If you only need to get one of the existing already-fetched descriptors, * leave gapLimit to zero. */ gapLimit?: number; }): { descriptor: Descriptor; index: DescriptorIndex; } | undefined; /** * Ensures that a scriptPubKey is unique and has not already been set by * a different descriptor. This prevents accounting for duplicate unspent * transaction outputs (utxos) and balances when different descriptors could * represent the same scriptPubKey (e.g., xpub vs wif). * * @throws If the scriptPubKey is not unique. * @private * @param options */ "__#3@#ensureScriptPubKeyUniqueness"({ networkId, scriptPubKey }: { /** * Network to check. */ networkId: NetworkId; /** * The scriptPubKey to check for uniqueness. */ scriptPubKey: Buffer; }): void; /** * Asynchronously discovers an output, given a descriptor expression and * index. It first retrieves the output, * computes its scriptHash, and fetches the transaction history associated * with this scriptHash from the explorer. It then updates the internal * discoveryData accordingly. * * This function has side-effects as it modifies the internal discoveryData * state of the Discovery class instance. This state keeps track of * transaction info and descriptors relevant to the discovery process. * * This method is useful for updating the state based on new * transactions and output. * * This method does not retrieve the txHex associated with the Output. * An additional #fetchTxs must be performed. * * @param options * @returns A promise that resolves to a boolean indicating whether any transactions were found for the provided scriptPubKey. */ "__#3@#fetchOutput"({ descriptor, index }: { /** * The descriptor expression associated with the scriptPubKey to discover. */ descriptor: Descriptor; /** * The descriptor index associated with the scriptPubKey to discover (if ranged). */ index?: number; }): Promise<boolean>; /** * Asynchronously fetches all raw transaction data from all transactions * associated with all the outputs fetched. * * @param options * @returns Resolves when all the transactions have been fetched and stored in discoveryData. */ "__#3@#fetchTxs"(): Promise<void>; /** * Asynchronously fetches one or more descriptor expressions, retrieving * all the historical txs associated with the outputs represented by the * expressions. * * @param options * @returns Resolves when the fetch operation completes. If used expressions * are found, waits for the discovery of associated transactions. */ fetch({ descriptor, index, descriptors, gapLimit, onUsed, onChecking, onProgress, next }: { /** * Descriptor expression representing one or potentially multiple outputs * if ranged. * Use either `descriptor` or `descriptors`, but not both simultaneously. */ descriptor?: Descriptor; /** * An optional index associated with a ranged `descriptor`. Not applicable * when using the `descriptors` array, even if its elements are ranged. */ index?: number; /** * Array of descriptor expressions. Use either `descriptors` or `descriptor`, * but not both simultaneously. */ descriptors?: Array<Descriptor>; /** * The gap limit for the fetch operation when retrieving ranged descriptors. * @defaultValue 20 */ gapLimit?: number; /** * Optional callback function triggered once a descriptor's output has been * identified as previously used in a transaction. It provides a way to react * or perform side effects based on this finding. * @param descriptorOrDescriptors - The original descriptor or array of descriptors * that have been determined to have a used output. */ onUsed?: (descriptorOrDescriptors: Descriptor | Array<Descriptor>) => Promise<void>; /** * Optional callback function invoked at the beginning of checking a descriptor * to determine its usage status. This can be used to signal the start of a * descriptor's check, potentially for logging or UI updates. * @param descriptorOrDescriptors - The descriptor or array of descriptors being checked. */ onChecking?: (descriptorOrDescriptors: Descriptor | Array<Descriptor>) => Promise<void>; /** * Optional callback function invoked for each output (address index) being * checked for a descriptor. This is called before fetching data for the output. * @param descriptor - The descriptor being processed. * @param index - The index of the output within the descriptor. */ onProgress?: (descriptor: Descriptor, index: DescriptorIndex) => Promise<boolean | void>; /** * Optional function triggered immediately after detecting that a descriptor's output * has been used previously. By invoking this function, it's possible to initiate * parallel discovery processes. The primary `discover` method will only resolve * once both its main discovery process and any supplementary processes initiated * by `next` have completed. Essentially, it ensures that all discovery, * both primary and secondary, finishes before moving on. */ next?: () => Promise<void>; }): Promise<void>; /** * Retrieves the fetching status and the timestamp of the last fetch for a descriptor. * * Use this function to check if the data for a specific descriptor, or an index within * a ranged descriptor, is currently being fetched or has been fetched. * * This function also helps to avoid errors when attempting to derive data from descriptors with incomplete data, * ensuring that subsequent calls to data derivation methods such as `getUtxos` or * `getBalance` only occur once the necessary data has been successfully retrieved (and does not return `undefined`). * * @returns An object with the fetching status (`fetching`) and the last * fetch time (`timeFetched`), or undefined if never fetched. */ whenFetched({ descriptor, index }: { /** * Descriptor expression representing one or potentially multiple outputs * if ranged. */ descriptor: Descriptor; /** * An optional index associated with a ranged `descriptor`. */ index?: number; }): { fetching: boolean; timeFetched: number; } | undefined; /** * Makes sure that data was retrieved before trying to derive from it. */ "__#3@#ensureFetched"({ descriptor, index, descriptors }: { /** * Descriptor expression representing one or potentially multiple outputs * if ranged. * Use either `descriptor` or `descriptors`, but not both simultaneously. */ descriptor?: Descriptor; /** * An optional index associated with a ranged `descriptor`. Not applicable * when using the `descriptors` array, even if its elements are ranged. */ index?: number; /** * Array of descriptor expressions. Use either `descriptors` or `descriptor`, * but not both simultaneously. */ descriptors?: Array<Descriptor>; }): void; /** * Asynchronously discovers standard accounts (pkh, sh(wpkh), wpkh) associated * with a master node. It uses a given gap limit for * discovery. * * @param options * @returns Resolves when all the standrd accounts from the master node have * been discovered. */ fetchStandardAccounts({ masterNode, gapLimit, onAccountUsed, onAccountChecking, onAccountProgress }: { /** * The master node to discover accounts from. */ masterNode: BIP32Interface; /** * The gap limit for address discovery * @defaultValue 20 */ gapLimit?: number; /** * Optional callback function triggered when an {@link Account account} * (associated with the master node) has been identified as having past * transactions. It's called with the external descriptor * of the account (`keyPath = /0/*`) that is active. * * @param account - The external descriptor of the account that has been determined to have prior transaction activity. */ onAccountUsed?: (account: Account) => Promise<void>; /** * Optional callback function invoked just as the system starts to evaluate the transaction * activity of an {@link Account account} (associated with the master node). * Useful for signaling the initiation of the discovery process for a * particular account, often for UI updates or logging purposes. * * @param account - The external descriptor of the account that is currently being evaluated for transaction activity. */ onAccountChecking?: (account: Account) => Promise<void>; /** * Optional callback function invoked for each output (address index) being * checked for an account's descriptors (external and internal). * @param data - Object containing account, descriptor, and index. */ onAccountProgress?: (data: { account: Account; descriptor: Descriptor; index: number; }) => Promise<boolean | void>; }): Promise<void>; /** * Retrieves the array of descriptors with used outputs. * The result is cached based on the size specified in the constructor. * As long as this cache size is not exceeded, this function will maintain * the same object reference if the returned array hasn't changed. * This characteristic can be particularly beneficial in * React and similar projects, where re-rendering occurs based on reference changes. * * @param options * @returns Returns an array of descriptor expressions. * These are derived from the discovery information. * */ getUsedDescriptors(): Array<Descriptor>; /** * Retrieves all the {@link Account accounts} with used outputs: * those descriptors with keyPaths ending in `{/0/*, /1/*}`. An account is identified * by its external descriptor `keyPath = /0/*`. The result is cached based on * the size specified in the constructor. As long as this cache size is not * exceeded, this function will maintain the same object reference if the returned array remains unchanged. * This characteristic can be especially beneficial in * React or similar projects, where re-rendering occurs based on reference changes. * * @param options * @returns An array of accounts, each represented * as its external descriptor expression. */ getUsedAccounts(): Array<Account>; /** * Retrieves descriptor expressions associated with a specific account. * The result is cached based on the size specified in the constructor. * As long as this cache size is not exceeded, this function will maintain * the same object reference. This characteristic can be especially * beneficial in React or similar projects, where re-rendering occurs based * on reference changes. * * @param options * @returns An array of descriptor expressions * associated with the specified account. */ getAccountDescriptors({ account }: { /** * The {@link Account account} associated with the descriptors. */ account: Account; }): [Descriptor, Descriptor]; /** * Retrieves unspent transaction outputs (UTXOs) and balance associated with * one or more descriptor expressions and transaction status. * In addition it also retrieves spent transaction outputs (STXOS) which correspond * to previous UTXOs that have been spent. * * This method is useful for accessing the available funds for specific * descriptor expressions in the wallet, considering the transaction status * (confirmed, unconfirmed, or both). * * The return value is computed based on the current state of discoveryData. * The method uses memoization to maintain the same object reference for the * returned result, given the same input parameters, as long as the * corresponding UTXOs in discoveryData haven't changed. * This can be useful in environments such as React where * preserving object identity can prevent unnecessary re-renders. * * @param outputCriteria * @returns An object containing the UTXOs associated with the * scriptPubKeys and the total balance of these UTXOs. * It also returns previous UTXOs that had been * eventually spent as stxos: Array<Stxo> * Finally, it returns `txoMap`. `txoMap` maps all the txos (unspent or spent * outputs) with their corresponding `indexedDescriptor: IndexedDescriptor` * (see {@link IndexedDescriptor IndexedDescriptor}) * */ getUtxosAndBalance({ descriptor, index, descriptors, txStatus }: OutputCriteria): { utxos: Array<Utxo>; stxos: Array<Stxo>; txoMap: TxoMap; balance: number; }; /** * Convenience function which internally invokes the * `getUtxosAndBalance(options).balance` method. */ getBalance(outputCriteria: OutputCriteria): number; /** * Convenience function which internally invokes the * `getUtxosAndBalance(options).utxos` method. */ getUtxos(outputCriteria: OutputCriteria): Array<Utxo>; /** * Retrieves the next available index for a given descriptor. * * The method retrieves the currently highest index used, and returns the * next available index by incrementing it by 1. * * @param options * @returns The next available index. */ getNextIndex({ descriptor, txStatus }: { /** * The ranged descriptor expression for which to retrieve the next * available index. */ descriptor: Descriptor; /** * A scriptPubKey will be considered as used when * its transaction status is txStatus * extracting UTXOs and balance. * @defaultValue TxStatus.ALL */ txStatus?: TxStatus; }): number; /** * Retrieves the transaction history for one or more descriptor expressions. * * This method accesses transaction records associated with descriptor expressions * and transaction status. * * When `withAttributions` is `false`, it returns an array of historical transactions * (`Array<TxData>`). See {@link TxData TxData}. * * To determine if each transaction corresponds to a sent/received transaction, set * `withAttributions` to `true`. * * When `withAttributions` is `true`, this function returns an array of * {@link TxAttribution TxAttribution} elements. * * `TxAttribution` identifies the owner of the previous output for each input and * the owner of the output for each transaction. * * This is useful in wallet applications to specify whether inputs are from owned * outputs (e.g., change from a previous transaction) or from third parties. It * also specifies if outputs are destined to third parties or are internal change. * This helps wallet apps show transaction history with "Sent" or "Received" labels, * considering only transactions with third parties. * * See {@link TxAttribution TxAttribution} for a complete list of items returned per * transaction. * * The return value is computed based on the current state of `discoveryData`. The * method uses memoization to maintain the same object reference for the returned * result, given the same input parameters, as long as the corresponding transaction * records in `discoveryData` haven't changed. * * This can be useful in environments such as React, where preserving object identity * can prevent unnecessary re-renders. * * @param outputCriteria - Criteria for selecting transaction outputs, including descriptor * expressions, transaction status, and whether to include attributions. * @param withAttributions - Whether to include attributions in the returned data. * @returns An array containing transaction information associated with the descriptor * expressions. */ getHistory({ descriptor, index, descriptors, txStatus }: OutputCriteria, withAttributions?: boolean): Array<TxData> | Array<TxAttribution>; /** * Retrieves the hexadecimal representation of a transaction (TxHex) from the * discoveryData given the transaction ID (TxId) or a Unspent Transaction Output (Utxo) * * @param options * @returns The hexadecimal representation of the transaction. * @throws Will throw an error if the transaction ID is invalid or if the TxHex is not found. */ getTxHex({ txId, utxo }: { /** * The transaction ID. */ txId?: TxId; /** * The UTXO. */ utxo?: Utxo; }): TxHex; /** * Retrieves the transaction data as a bitcoinjs-lib * {@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/transaction.ts Transaction} * object given the transaction * ID (TxId) or a Unspent Transaction Output (Utxo) or the hexadecimal * representation of the transaction (it will then use memoization). * The transaction data is obtained by first getting * the transaction hexadecimal representation using getTxHex() method * (unless the txHex was passed). * * Use this method for quick access to the Transaction object, which avoids the * need to parse the transaction hexadecimal representation (txHex). * The data will have already been computed and cached for efficiency within * the Discovery class. * * @param options * @returns The transaction data as a Transaction object. */ getTransaction({ txId, txHex, utxo }: { /** * The transaction ID. */ txId?: TxId; /** * The transaction txHex. */ txHex?: TxId; /** * The UTXO. */ utxo?: Utxo; }): Transaction; /** * Compares two transactions based on their blockHeight and input dependencies. * Can be used as callback in Array.sort function to sort from old to new. * * @param txWithOrderA - The first transaction data to compare. * @param txWithOrderB - The second transaction data to compare. * * txWithOrderA and txWithOrderB should contain the `blockHeight` (use 0 if * in the mempool) and either `tx` (`Transaction` type) or `txHex` (the * hexadecimal representation of the transaction) * * @returns < 0 if txWithOrderA is older than txWithOrderB, > 0 if * txWithOrderA is newer than txWithOrderB, and 0 if undecided. */ compareTxOrder<TA extends TxWithOrder, TB extends TxWithOrder>(txWithOrderA: TA, txWithOrderB: TB): number; /** * Given an unspent tx output, this function retrieves its descriptor (if still unspent). * Alternatively, pass a txo (any transaction output, which may have been * spent already or not) and this function will also retrieve its descriptor. * txo can be in any of these formats: `${txId}:${vout}` or * using its extended form: `${txId}:${vout}:${recipientTxId}:${recipientVin}` * * Returns the descriptor (and index if ranged) or undefined if not found. */ getDescriptor({ utxo, txo }: { /** * The UTXO. */ utxo?: Utxo; txo?: Utxo; }): { descriptor: Descriptor; index?: number; } | undefined; /** * Pushes a transaction to the network and updates the internal state. * * This function first broadcasts the transaction using the configured explorer. * It then attempts to update the internal `discoveryData` by calling * `addTransaction`. * * If `addTransaction` reports that one or more inputs of the pushed transaction * were already considered spent by other transactions in the library's records * (e.g., in an RBF scenario or a double-spend attempt already known to the * library), `push` will automatically attempt to synchronize the state. It * does this by calling `fetch` on all unique descriptors associated with the * conflicting input(s). This helps ensure the library's state reflects the * most current information from the blockchain/mempool regarding these conflicts. * * The `gapLimit` parameter is used both when `addTransaction` is called and * during any subsequent automatic `fetch` operation triggered by conflicts. * It helps discover new outputs (e.g., change addresses) that might become * active due to the transaction. * * Note: The success of broadcasting the transaction via `explorer.push(txHex)` * depends on the network and node policies. Even if broadcast is successful, * the transaction might not be immediately visible in the mempool or might be * replaced. A warning is logged if the transaction is not found in the * mempool shortly after being pushed. The final state in the library will * reflect the outcome of the internal `addTransaction` call and any * automatic synchronization that occurred. */ push({ txHex, gapLimit }: { /** * The hexadecimal representation of the transaction to push. */ txHex: TxHex; /** * The gap limit for descriptor discovery. Defaults to 20. */ gapLimit?: number; }): Promise<void>; /** * Given a transaction it updates the internal `discoveryData` state to * include it. * * This function is useful when a transaction affecting one of the * descriptors has been pushed to the blockchain by a third party. It allows * updating the internal representation without performing a more expensive * `fetch` operation. * * If the transaction was recently pushed to the blockchain, set * `txData.irreversible = false` and `txData.blockHeight = 0`. * * The transaction is represented by `txData`, where * `txData = { blockHeight: number; irreversible: boolean; txHex: TxHex; }`. * * It includes its hexadecimal representation `txHex`, its `blockHeight` * (zero if it's in the mempool), and whether it is `irreversible` or * not. `irreversible` is set by the Explorer, using the configuration parameter * `irrevConfThresh` (defaults to `IRREV_CONF_THRESH = 3`). It can be obtained * by calling explorer.fetchTxHistory(), for example. Set it to * `false` when it's been just pushed (which will be the typical use of this * function). * * The `gapLimit` parameter is essential for managing descriptor discovery. * When addint a transaction, there is a possibility the transaction is * adding new funds as change (for example). If the range for that index * does not exist yet, the `gapLimit` helps to update the descriptor * corresponding to a new UTXO for new indices within the gap limit. * * This function updates the internal `discoveryData` state to include the * provided transaction, but only if it doesn't attempt to spend outputs * already considered spent by the library. * * It first checks all inputs of the transaction. If any input corresponds to * a `txo` (a previously known output) that is not a current `utxo` * (i.e., it's already considered spent by another transaction in the * library's records), this function will not modify the library state. * Instead, it will return an object detailing all such conflicting inputs. * This allows the caller (e.g., the `push` method) to handle these * conflicts, for instance, by re-fetching the state of all affected descriptors. * * If no such input conflicts are found, the transaction is processed: * its details are added to the `txMap`, and relevant `txId`s are associated * with the `OutputData` of both its inputs (if owned) and outputs (if owned). * * For other types of errors (e.g., invalid input data), it may still throw. * * Refer to the `push` method's documentation for how it utilizes this * return status for automatic state synchronization. * * @returns An object indicating the outcome: * - `{ success: true }` if the transaction was added without conflicts. * - `{ success: false; reason: 'INPUTS_ALREADY_SPENT'; conflicts: Array<{ descriptor: Descriptor; txo: Utxo; index?: number }> }` * if one or more inputs of the transaction were already considered spent. * `conflicts` contains an array of all such detected conflicts. */ addTransaction({ txData, gapLimit }: { /** * The hexadecimal representation of the tx and its associated data. * `txData = { blockHeight: number; irreversible: boolean; txHex: TxHex; }`. */ txData: TxData; /** * The gap limit for descriptor discovery. Defaults to 20. */ gapLimit?: number; }): { success: true; } | { success: false; reason: 'INPUTS_ALREADY_SPENT'; conflicts: Array<Conflict>; }; /** * Retrieves the Explorer instance. * * @returns The Explorer instance. */ getExplorer(): Explorer; /** * Exports the current state of the Discovery instance. * This method is used to serialize the state of the Discovery instance so * that it can be saved and potentially re-imported later using the * `imported` parameter in the constructor. * * The exported data includes a version string and a deep clone of the * internal discovery data. The deep cloning process ensures that the * exported data is a snapshot of the internal state, isolated from future * changes to the Discovery instance. This isolation maintains the integrity * and immutability of the exported data. * * The inclusion of a version string in the exported data allows for * compatibility checks when re-importing the data. This check ensures that * the data model of the imported data is compatible with the current * version of the Discovery class. * * The exported data is guaranteed to be serializable, allowing it to be * safely stored or transmitted. It can be serialized using JSON.stringify * or other serialization methods, such as structured serialization * (https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal). * This feature ensures that the data can be serialized and deserialized * without loss of integrity, facilitating data persistence * and transfer across different sessions or environments. * * @returns An object containing the version string and the serialized discovery data. */ export(): { version: string; discoveryData: DiscoveryData; }; }; }; }; /** * The {@link DiscoveryFactory | `DiscoveryFactory`} function internally creates and returns the {@link _Internal_.Discovery | `Discovery`} class. * This class is specialized for the provided `Explorer`, which is responsible for fetching blockchain data like transaction details. * Use `DiscoveryInstance` to declare instances for this class: `const: DiscoveryInstance = new Discovery();` * * See the {@link _Internal_.Discovery | documentation for the internal Discovery class} for a complete list of available methods. */ type DiscoveryInstance = InstanceType<ReturnType<typeof DiscoveryFactory>['Discovery']>; export { DiscoveryInstance };