@bitcoinerlab/discovery
Advanced tools
Comparing version 0.1.0 to 1.0.0
/// <reference types="node" /> | ||
import memoizee from 'memoizee'; | ||
import { NetworkId, Expression, Account, DescriptorIndex, DiscoveryInfo, TxStatus, DescriptorInfo, TxId, TxInfo } from './types'; | ||
import { NetworkId, Descriptor, Account, DescriptorIndex, DiscoveryData, TxStatus, DescriptorData, TxId, TxData } from './types'; | ||
import { Transaction, Network } from 'bitcoinjs-lib'; | ||
export declare const canonicalize: (expressions: Array<Expression> | Expression, network: Network) => string | string[]; | ||
export declare function deriveDataFactory({ expressionsCacheSize, indicesPerExpressionCacheSize }: { | ||
expressionsCacheSize: number; | ||
indicesPerExpressionCacheSize: number; | ||
export declare function canonicalize(descriptorOrDescriptors: Array<Descriptor> | Descriptor, network: Network): string | string[]; | ||
export declare function deriveDataFactory({ descriptorsCacheSize, outputsPerDescriptorCacheSize }: { | ||
descriptorsCacheSize: number; | ||
outputsPerDescriptorCacheSize: number; | ||
}): { | ||
deriveScriptPubKey: (networkId: NetworkId, expression: Expression, index: DescriptorIndex) => Buffer; | ||
deriveUtxosAndBalanceByScriptPubKey: (networkId: NetworkId, txInfoRecords: Record<TxId, TxInfo>, descriptors: Record<Expression, DescriptorInfo>, expression: Expression, index: DescriptorIndex, txStatus: TxStatus) => { | ||
deriveScriptPubKey: (networkId: NetworkId, descriptor: Descriptor, index: DescriptorIndex) => Buffer; | ||
deriveUtxosAndBalanceByOutput: (networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptor: Descriptor, index: DescriptorIndex, txStatus: TxStatus) => { | ||
utxos: string[]; | ||
balance: number; | ||
}; | ||
deriveUtxosAndBalanceByExpressions: (networkId: NetworkId, txInfoRecords: Record<TxId, TxInfo>, descriptors: Record<Expression, DescriptorInfo>, expressions: Array<Expression> | Expression, txStatus: TxStatus) => { | ||
deriveUtxosAndBalance: (networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptorOrDescriptors: Array<Descriptor> | Descriptor, txStatus: TxStatus) => { | ||
utxos: string[]; | ||
balance: number; | ||
}; | ||
deriveExpressions: (discoveryInfo: DiscoveryInfo, networkId: NetworkId) => string[]; | ||
deriveAccounts: (discoveryInfo: DiscoveryInfo, networkId: NetworkId) => string[]; | ||
deriveAccountExpressions: ((account: Account) => [Expression, Expression]) & memoizee.Memoized<(account: Account) => [Expression, Expression]>; | ||
deriveHistoryByScriptPubKey: (txInfoRecords: Record<TxId, TxInfo>, descriptors: Record<Expression, DescriptorInfo>, expression: Expression, index: DescriptorIndex, txStatus: TxStatus) => TxInfo[]; | ||
deriveHistory: (txInfoRecords: Record<TxId, TxInfo>, descriptors: Record<Expression, DescriptorInfo>, expressions: Array<Expression> | Expression, txStatus: TxStatus) => TxInfo[]; | ||
deriveUsedDescriptors: (discoveryData: DiscoveryData, networkId: NetworkId) => string[]; | ||
deriveUsedAccounts: (discoveryData: DiscoveryData, networkId: NetworkId) => string[]; | ||
deriveAccountDescriptors: ((account: Account) => [Descriptor, Descriptor]) & memoizee.Memoized<(account: Account) => [Descriptor, Descriptor]>; | ||
deriveHistoryByOutput: (txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptor: Descriptor, index: DescriptorIndex, txStatus: TxStatus) => TxData[]; | ||
deriveHistory: (txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptorOrDescriptors: Array<Descriptor> | Descriptor, txStatus: TxStatus) => TxData[]; | ||
transactionFromHex: typeof Transaction.fromHex & memoizee.Memoized<typeof Transaction.fromHex>; | ||
}; |
@@ -39,27 +39,27 @@ "use strict"; | ||
const secp256k1 = __importStar(require("@bitcoinerlab/secp256k1")); | ||
const { Descriptor, expand } = (0, descriptors_1.DescriptorsFactory)(secp256k1); | ||
const { Output, expand } = (0, descriptors_1.DescriptorsFactory)(secp256k1); | ||
const networks_1 = require("./networks"); | ||
const canonicalize = (expressions, network) => { | ||
function canonicalize(descriptorOrDescriptors, network) { | ||
let isDifferent = false; | ||
const expressionArray = Array.isArray(expressions) | ||
? expressions | ||
: [expressions]; | ||
const canonicalExpressions = []; | ||
expressionArray.forEach(expression => { | ||
const canonicalExpression = expand({ | ||
expression, | ||
const descriptorArray = Array.isArray(descriptorOrDescriptors) | ||
? descriptorOrDescriptors | ||
: [descriptorOrDescriptors]; | ||
const canonicalDescriptors = []; | ||
descriptorArray.forEach(descriptor => { | ||
const canonicalDescriptor = expand({ | ||
descriptor, | ||
network | ||
}).canonicalExpression; | ||
if (expression !== canonicalExpression) | ||
if (descriptor !== canonicalDescriptor) | ||
isDifferent = true; | ||
canonicalExpressions.push(canonicalExpression); | ||
canonicalDescriptors.push(canonicalDescriptor); | ||
}); | ||
if (isDifferent) | ||
return canonicalExpressions; | ||
return canonicalDescriptors; | ||
else | ||
return expressions; | ||
}; | ||
return descriptorOrDescriptors; | ||
} | ||
exports.canonicalize = canonicalize; | ||
function deriveDataFactory({ expressionsCacheSize = 0, indicesPerExpressionCacheSize = 0 }) { | ||
const deriveScriptPubKeyFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((expression) => (0, memoizee_1.default)((index) => { | ||
function deriveDataFactory({ descriptorsCacheSize = 0, outputsPerDescriptorCacheSize = 0 }) { | ||
const deriveScriptPubKeyFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((descriptor) => (0, memoizee_1.default)((index) => { | ||
const network = (0, networks_1.getNetwork)(networkId); | ||
@@ -69,23 +69,24 @@ //Note there is no need to pass a network (bitcoin will be default) but | ||
//the network | ||
const descriptor = index === 'non-ranged' | ||
? new Descriptor({ expression, network }) | ||
: new Descriptor({ expression, index, network }); | ||
const scriptPubKey = descriptor.getScriptPubKey(); | ||
const output = index === 'non-ranged' | ||
? new Output({ descriptor, network }) | ||
: new Output({ descriptor, index, network }); | ||
const scriptPubKey = output.getScriptPubKey(); | ||
return scriptPubKey; | ||
}, { primitive: true, max: indicesPerExpressionCacheSize }), { primitive: true, max: expressionsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
}, { primitive: true, max: outputsPerDescriptorCacheSize }), { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveScriptPubKey = (networkId, expression, index) => deriveScriptPubKeyFactory(networkId)(expression)(index); | ||
const coreDeriveUtxosByScriptPubKey = (networkId, expression, index, txInfoArray, txStatus) => { | ||
const scriptPubKey = deriveScriptPubKey(networkId, expression, index); | ||
const deriveScriptPubKey = (networkId, descriptor, index) => deriveScriptPubKeyFactory(networkId)(descriptor)(index); | ||
const coreDeriveUtxosByOutput = (networkId, descriptor, index, txDataArray, txStatus) => { | ||
const scriptPubKey = deriveScriptPubKey(networkId, descriptor, index); | ||
const allOutputs = []; | ||
const spentOutputs = []; | ||
//Note that txInfoArray cannot be assumed to be in correct order. See: | ||
//Note that txDataArray cannot be assumed to be in correct order. See: | ||
//https://github.com/Blockstream/esplora/issues/165#issuecomment-1584471718 | ||
for (const txInfo of txInfoArray) { | ||
//TODO: but we should guarantee same order always so use txId as second order criteria? | ||
for (const txData of txDataArray) { | ||
if (txStatus === types_1.TxStatus.ALL || | ||
(txStatus === types_1.TxStatus.IRREVERSIBLE && txInfo.irreversible) || | ||
(txStatus === types_1.TxStatus.CONFIRMED && txInfo.blockHeight !== 0)) { | ||
const txHex = txInfo.txHex; | ||
(txStatus === types_1.TxStatus.IRREVERSIBLE && txData.irreversible) || | ||
(txStatus === types_1.TxStatus.CONFIRMED && txData.blockHeight !== 0)) { | ||
const txHex = txData.txHex; | ||
if (!txHex) | ||
throw new Error(`txHex not yet retrieved for an element of ${expression}, ${index}`); | ||
throw new Error(`txHex not yet retrieved for an element of ${descriptor}, ${index}`); | ||
const tx = transactionFromHex(txHex); | ||
@@ -117,69 +118,67 @@ const txId = tx.getId(); | ||
}; | ||
const deriveUtxosAndBalanceByScriptPubKeyFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((expression) => (0, memoizee_1.default)((index) => { | ||
const deriveUtxosAndBalanceByOutputFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptor) => (0, memoizee_1.default)((index) => { | ||
// Create one function per each expression x index x txStatus | ||
// coreDeriveUtxosByScriptPubKey shares all params wrt the parent | ||
// function except for additional param txInfoArray. | ||
// As soon as txInfoArray in coreDeriveUtxosByScriptPubKey changes, | ||
// coreDeriveUtxosByOutput shares all params wrt the parent | ||
// function except for additional param txDataArray. | ||
// As soon as txDataArray in coreDeriveUtxosByOutput changes, | ||
// it will resets its memory. However, it always returns the same | ||
// reference if the resulting array is shallowy-equal: | ||
const deriveUtxosByScriptPubKey = (0, memoizers_1.memoizeOneWithShallowArraysCheck)(coreDeriveUtxosByScriptPubKey); | ||
const deriveUtxosByOutput = (0, memoizers_1.memoizeOneWithShallowArraysCheck)(coreDeriveUtxosByOutput); | ||
let lastUtxos = null; | ||
let lastBalance; | ||
return (0, memoizee_1.default)((txInfoRecords, descriptors) => { | ||
const txInfoArray = deriveTxInfoArray(txInfoRecords, descriptors, expression, index); | ||
const utxos = deriveUtxosByScriptPubKey(networkId, expression, index, txInfoArray, txStatus); | ||
return (0, memoizee_1.default)((txMap, descriptorMap) => { | ||
const txDataArray = deriveTxDataArray(txMap, descriptorMap, descriptor, index); | ||
const utxos = deriveUtxosByOutput(networkId, descriptor, index, txDataArray, txStatus); | ||
if (lastUtxos && (0, shallow_equal_1.shallowEqualArrays)(lastUtxos, utxos)) | ||
return { utxos: lastUtxos, balance: lastBalance }; | ||
lastUtxos = utxos; | ||
lastBalance = coreDeriveUtxosBalance(txInfoRecords, utxos); | ||
lastBalance = coreDeriveUtxosBalance(txMap, utxos); | ||
return { utxos, balance: lastBalance }; | ||
}, { max: 1 }); | ||
}, { primitive: true, max: indicesPerExpressionCacheSize }), { primitive: true, max: expressionsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
}, { primitive: true, max: outputsPerDescriptorCacheSize }), { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveUtxosAndBalanceByScriptPubKey = (networkId, txInfoRecords, descriptors, expression, index, txStatus) => deriveUtxosAndBalanceByScriptPubKeyFactory(networkId)(txStatus)(expression)(index)(txInfoRecords, descriptors); | ||
const coreDeriveTxInfoArray = (txIds, txInfoRecords) => txIds.map(txId => { | ||
const txInfo = txInfoRecords[txId]; | ||
if (!txInfo) | ||
throw new Error(`txInfo not saved for ${txId}`); | ||
return txInfo; | ||
const deriveUtxosAndBalanceByOutput = (networkId, txMap, descriptorMap, descriptor, index, txStatus) => deriveUtxosAndBalanceByOutputFactory(networkId)(txStatus)(descriptor)(index)(txMap, descriptorMap); | ||
const coreDeriveTxDataArray = (txIds, txMap) => txIds.map(txId => { | ||
const txData = txMap[txId]; | ||
if (!txData) | ||
throw new Error(`txData not saved for ${txId}`); | ||
return txData; | ||
}); | ||
const deriveTxInfoArrayFactory = (0, memoizee_1.default)((expression) => (0, memoizee_1.default)((index) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txInfoRecords, descriptors) => { | ||
const scriptPubKeyInfoRecords = descriptors[expression]?.scriptPubKeyInfoRecords || | ||
{}; | ||
const txIds = scriptPubKeyInfoRecords[index]?.txIds || []; | ||
const txInfoArray = coreDeriveTxInfoArray(txIds, txInfoRecords); | ||
return txInfoArray; | ||
const deriveTxDataArrayFactory = (0, memoizee_1.default)((descriptor) => (0, memoizee_1.default)((index) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txMap, descriptorMap) => { | ||
const range = deriveUsedRange(descriptorMap[descriptor]); | ||
const txIds = range[index]?.txIds || []; | ||
const txDataArray = coreDeriveTxDataArray(txIds, txMap); | ||
return txDataArray; | ||
}); | ||
}, { primitive: true, max: indicesPerExpressionCacheSize }), { primitive: true, max: expressionsCacheSize }); | ||
const deriveTxInfoArray = (txInfoRecords, descriptors, expression, index) => deriveTxInfoArrayFactory(expression)(index)(txInfoRecords, descriptors); | ||
const deriveHistoryByScriptPubKeyFactory = (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((expression) => (0, memoizee_1.default)((index) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txInfoRecords, descriptors) => { | ||
const txInfoArray = deriveTxInfoArray(txInfoRecords, descriptors, expression, index); | ||
return txInfoArray.filter(txInfo => txStatus === types_1.TxStatus.ALL || | ||
}, { primitive: true, max: outputsPerDescriptorCacheSize }), { primitive: true, max: descriptorsCacheSize }); | ||
const deriveTxDataArray = (txMap, descriptorMap, descriptor, index) => deriveTxDataArrayFactory(descriptor)(index)(txMap, descriptorMap); | ||
const deriveHistoryByOutputFactory = (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptor) => (0, memoizee_1.default)((index) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txMap, descriptorMap) => { | ||
const txDataArray = deriveTxDataArray(txMap, descriptorMap, descriptor, index); | ||
return txDataArray.filter(txData => txStatus === types_1.TxStatus.ALL || | ||
(txStatus === types_1.TxStatus.IRREVERSIBLE && | ||
txInfo.irreversible) || | ||
txData.irreversible) || | ||
(txStatus === types_1.TxStatus.CONFIRMED && | ||
txInfo.blockHeight !== 0)); | ||
txData.blockHeight !== 0)); | ||
}); | ||
}, { | ||
primitive: true, | ||
max: indicesPerExpressionCacheSize | ||
}), { primitive: true, max: expressionsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
max: outputsPerDescriptorCacheSize | ||
}), { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveHistoryByScriptPubKey = (txInfoRecords, descriptors, expression, index, txStatus) => deriveHistoryByScriptPubKeyFactory(txStatus)(expression)(index)(txInfoRecords, descriptors); | ||
const coreDeriveHistory = (descriptors, txInfoRecords, expressions, txStatus) => { | ||
const deriveHistoryByOutput = (txMap, descriptorMap, descriptor, index, txStatus) => deriveHistoryByOutputFactory(txStatus)(descriptor)(index)(txMap, descriptorMap); | ||
const coreDeriveHistory = (descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const history = []; | ||
const expressionArray = Array.isArray(expressions) | ||
? expressions | ||
: [expressions]; | ||
for (const expression of expressionArray) { | ||
const scriptPubKeyInfoRecords = descriptors[expression]?.scriptPubKeyInfoRecords || | ||
{}; | ||
Object.keys(scriptPubKeyInfoRecords) | ||
const descriptorArray = Array.isArray(descriptorOrDescriptors) | ||
? descriptorOrDescriptors | ||
: [descriptorOrDescriptors]; | ||
for (const descriptor of descriptorArray) { | ||
const range = deriveUsedRange(descriptorMap[descriptor]); | ||
Object.keys(range) | ||
.sort() //Sort it to be deterministic | ||
.forEach(indexStr => { | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
history.push(...deriveHistoryByScriptPubKey(txInfoRecords, descriptors, expression, index, txStatus)); | ||
history.push(...deriveHistoryByOutput(txMap, descriptorMap, descriptor, index, txStatus)); | ||
}); | ||
@@ -193,26 +192,26 @@ } | ||
//that belong to the same blockHeight | ||
return dedupedHistory.sort((txInfoA, txInfoB) => txInfoA.blockHeight - txInfoB.blockHeight); | ||
//TODO: but we should guarantee same order always so use txId as second order criteria? | ||
return dedupedHistory.sort((txDataA, txDataB) => txDataA.blockHeight - txDataB.blockHeight); | ||
}; | ||
const deriveHistoryFactory = (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((expressions) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txInfoRecords, descriptors) => coreDeriveHistory(descriptors, txInfoRecords, expressions, txStatus)); | ||
}, { primitive: true, max: expressionsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
const deriveHistoryFactory = (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptorOrDescriptors) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txMap, descriptorMap) => coreDeriveHistory(descriptorMap, txMap, descriptorOrDescriptors, txStatus)); | ||
}, { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveHistory = (txInfoRecords, descriptors, expressions, txStatus) => deriveHistoryFactory(txStatus)(expressions)(txInfoRecords, descriptors); | ||
const coreDeriveUtxosByExpressions = (networkId, descriptors, txInfoRecords, expressions, txStatus) => { | ||
const deriveHistory = (txMap, descriptorMap, descriptorOrDescriptors, txStatus) => deriveHistoryFactory(txStatus)(descriptorOrDescriptors)(txMap, descriptorMap); | ||
const coreDeriveUtxos = (networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const utxos = []; | ||
const expressionArray = Array.isArray(expressions) | ||
? expressions | ||
: [expressions]; | ||
for (const expression of expressionArray) { | ||
const scriptPubKeyInfoRecords = descriptors[expression]?.scriptPubKeyInfoRecords || | ||
{}; | ||
Object.keys(scriptPubKeyInfoRecords) | ||
const descriptorArray = Array.isArray(descriptorOrDescriptors) | ||
? descriptorOrDescriptors | ||
: [descriptorOrDescriptors]; | ||
for (const descriptor of descriptorArray) { | ||
const range = deriveUsedRange(descriptorMap[descriptor]); | ||
Object.keys(range) | ||
.sort() //Sort it to be deterministic | ||
.forEach(indexStr => { | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
utxos.push(...deriveUtxosAndBalanceByScriptPubKey(networkId, txInfoRecords, descriptors, expression, index, txStatus).utxos); | ||
utxos.push(...deriveUtxosAndBalanceByOutput(networkId, txMap, descriptorMap, descriptor, index, txStatus).utxos); | ||
}); | ||
} | ||
//Deduplicate in case of expression: Array<Expression> with duplicated | ||
//expressions | ||
//Deduplicate in case of expression: Array<Descriptor> with duplicated | ||
//descriptorOrDescriptors | ||
const dedupedUtxos = [...new Set(utxos)]; | ||
@@ -225,18 +224,18 @@ return dedupedUtxos; | ||
//each tuple of txStatus x expressions | ||
const deriveUtxosAndBalanceByExpressionsFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((expressions) => { | ||
const deriveUtxosAndBalanceFactory = (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptorOrDescriptors) => { | ||
let lastUtxos = null; | ||
let lastBalance; | ||
return (0, memoizee_1.default)((txInfoRecords, descriptors) => { | ||
const utxos = coreDeriveUtxosByExpressions(networkId, descriptors, txInfoRecords, expressions, txStatus); | ||
return (0, memoizee_1.default)((txMap, descriptorMap) => { | ||
const utxos = coreDeriveUtxos(networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus); | ||
if (lastUtxos && (0, shallow_equal_1.shallowEqualArrays)(lastUtxos, utxos)) | ||
return { utxos: lastUtxos, balance: lastBalance }; | ||
lastUtxos = utxos; | ||
lastBalance = coreDeriveUtxosBalance(txInfoRecords, utxos); | ||
lastBalance = coreDeriveUtxosBalance(txMap, utxos); | ||
return { utxos, balance: lastBalance }; | ||
}, { max: 1 }); | ||
}, { primitive: true, max: expressionsCacheSize } //potentially ininite search space. limit to 100 expressions per txStatus combination | ||
}, { primitive: true, max: descriptorsCacheSize } //potentially ininite search space. limit to 100 descriptorOrDescriptors per txStatus combination | ||
), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveUtxosAndBalanceByExpressions = (networkId, txInfoRecords, descriptors, expressions, txStatus) => deriveUtxosAndBalanceByExpressionsFactory(networkId)(txStatus)(expressions)(txInfoRecords, descriptors); | ||
const deriveUtxosAndBalance = (networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus) => deriveUtxosAndBalanceFactory(networkId)(txStatus)(descriptorOrDescriptors)(txMap, descriptorMap); | ||
const transactionFromHex = (0, memoizee_1.default)(bitcoinjs_lib_1.Transaction.fromHex, { | ||
@@ -246,3 +245,3 @@ primitive: true, | ||
}); | ||
const coreDeriveUtxosBalance = (txInfoRecords, utxos) => { | ||
const coreDeriveUtxosBalance = (txMap, utxos) => { | ||
let balance = 0; | ||
@@ -259,6 +258,6 @@ const firstDuplicate = utxos.find((element, index, arr) => { | ||
const vout = parseInt(voutStr); | ||
const txInfo = txInfoRecords[txId]; | ||
if (!txInfo) | ||
throw new Error(`txInfo not saved for ${txId}, vout:${vout} - ${utxo}`); | ||
const txHex = txInfo.txHex; | ||
const txData = txMap[txId]; | ||
if (!txData) | ||
throw new Error(`txData not saved for ${txId}, vout:${vout} - ${utxo}`); | ||
const txHex = txData.txHex; | ||
if (!txHex) | ||
@@ -275,18 +274,31 @@ throw new Error(`txHex not yet retrieved for ${txId}`); | ||
}; | ||
function scriptPubKeyHasRecords(scriptPubKeyInfoRecords) { | ||
if (scriptPubKeyInfoRecords === undefined) | ||
return false; | ||
for (const prop in scriptPubKeyInfoRecords) | ||
if (Object.prototype.hasOwnProperty.call(scriptPubKeyInfoRecords, prop)) | ||
return true; | ||
return false; | ||
/** | ||
* Filters the provided descriptor object's range with | ||
* records that have non-empty transaction ID arrays. | ||
* | ||
* @returns An object containing only the records from the input range with | ||
* non-empty txIds arrays. | ||
*/ | ||
function deriveUsedRange(descriptorData) { | ||
const usedRange = {}; | ||
if (!descriptorData) | ||
return usedRange; | ||
for (const [indexStr, outputData] of Object.entries(descriptorData.range)) { | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
if (outputData.txIds.length > 0) | ||
usedRange[index] = outputData; | ||
} | ||
return usedRange; | ||
} | ||
const coreDeriveExpressions = (discoveryInfo, networkId) => { | ||
const descriptors = discoveryInfo[networkId].descriptors; | ||
return Object.keys(descriptors) | ||
.filter(expression => scriptPubKeyHasRecords(descriptors[expression]?.scriptPubKeyInfoRecords)) | ||
function isDescriptorUsed(descriptorData) { | ||
return Object.keys(deriveUsedRange(descriptorData)).length !== 0; | ||
} | ||
const coreDeriveUsedDescriptors = (discoveryData, networkId) => { | ||
const descriptorMap = discoveryData[networkId].descriptorMap; | ||
return Object.keys(descriptorMap) | ||
.filter(descriptor => isDescriptorUsed(descriptorMap[descriptor])) | ||
.sort(); | ||
}; | ||
const deriveExpressionsFactory = (0, memoizee_1.default)((networkId) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((discoveryInfo) => coreDeriveExpressions(discoveryInfo, networkId)); | ||
const deriveUsedDescriptorsFactory = (0, memoizee_1.default)((networkId) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((discoveryData) => coreDeriveUsedDescriptors(discoveryData, networkId)); | ||
}, { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
@@ -301,9 +313,11 @@ ); | ||
* | ||
* @param {DiscoveryInfo} discoveryInfo - Information regarding discovery. | ||
* @param {NetworkId} networkId - The network identifier. | ||
* @returns {Array<Expression>} - Descriptor expressions. | ||
* @returns Descriptor expressions. | ||
*/ | ||
const deriveExpressions = (discoveryInfo, networkId) => deriveExpressionsFactory(networkId)(discoveryInfo); | ||
const deriveUsedDescriptors = ( | ||
/** The network identifier. */ | ||
discoveryData, | ||
/** Descriptor expressions. */ | ||
networkId) => deriveUsedDescriptorsFactory(networkId)(discoveryData); | ||
/** | ||
* Derives the accounts from the discoveryInfo. | ||
* Derives the accounts from the discoveryData. | ||
* Descriptor expressions of an account share the same pattern, except for | ||
@@ -316,41 +330,38 @@ * their keyInfo, which can end with either /0/* or /1/*. | ||
*/ | ||
const coreDeriveAccounts = (discoveryInfo, networkId) => { | ||
const expressions = coreDeriveExpressions(discoveryInfo, networkId); | ||
const accounts = []; | ||
const coreDeriveUsedAccounts = (discoveryData, networkId) => { | ||
const descriptors = coreDeriveUsedDescriptors(discoveryData, networkId); | ||
const network = (0, networks_1.getNetwork)(networkId); | ||
const expandedDescriptors = expressions.map(expression => ({ | ||
expression, | ||
...expand({ expression, network }) | ||
})); | ||
for (const { expression, expansionMap } of expandedDescriptors) { | ||
for (const key in expansionMap) { | ||
const keyInfo = expansionMap[key]; | ||
if (!keyInfo) | ||
throw new Error(`keyInfo not defined for key ${key} in ${expression}`); | ||
if (keyInfo.keyPath === '/0/*' || keyInfo.keyPath === '/1/*') { | ||
const account = expression.replace(/\/1\/\*/g, '/0/*'); | ||
if (!accounts.includes(account)) | ||
accounts.push(account); | ||
const accountSet = new Set(); //Use Set to avoid duplicates | ||
for (const descriptor of descriptors) { | ||
const { expansionMap } = expand({ descriptor, network }); | ||
if (expansionMap) | ||
for (const keyInfo of Object.values(expansionMap)) { | ||
if (!keyInfo) { | ||
throw new Error(`Missing keyInfo in expansionMap for descriptor ${descriptor}`); | ||
} | ||
if (keyInfo.keyPath === '/0/*' || keyInfo.keyPath === '/1/*') { | ||
const account = descriptor.replace(/\/1\/\*/g, '/0/*'); | ||
accountSet.add(account); | ||
} | ||
} | ||
} | ||
} | ||
return accounts.sort(); //So it's deterministic | ||
return Array.from(accountSet).sort(); //sort the Array so it's deterministic | ||
}; | ||
const deriveAccountsFactory = (0, memoizee_1.default)((networkId) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((discoveryInfo) => coreDeriveAccounts(discoveryInfo, networkId)); | ||
const deriveUsedAccountsFactory = (0, memoizee_1.default)((networkId) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((discoveryData) => coreDeriveUsedAccounts(discoveryData, networkId)); | ||
}, { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
); | ||
const deriveAccounts = (discoveryInfo, networkId) => deriveAccountsFactory(networkId)(discoveryInfo); | ||
const deriveAccountExpressions = (0, memoizee_1.default)((account) => [ | ||
const deriveUsedAccounts = (discoveryData, networkId) => deriveUsedAccountsFactory(networkId)(discoveryData); | ||
const deriveAccountDescriptors = (0, memoizee_1.default)((account) => [ | ||
account, | ||
account.replace(/\/0\/\*/g, '/1/*') | ||
], { primitive: true, max: expressionsCacheSize }); | ||
], { primitive: true, max: descriptorsCacheSize }); | ||
return { | ||
deriveScriptPubKey, | ||
deriveUtxosAndBalanceByScriptPubKey, | ||
deriveUtxosAndBalanceByExpressions, | ||
deriveExpressions, | ||
deriveAccounts, | ||
deriveAccountExpressions, | ||
deriveHistoryByScriptPubKey, | ||
deriveUtxosAndBalanceByOutput, | ||
deriveUtxosAndBalance, | ||
deriveUsedDescriptors, | ||
deriveUsedAccounts, | ||
deriveAccountDescriptors, | ||
deriveHistoryByOutput, | ||
deriveHistory, | ||
@@ -357,0 +368,0 @@ transactionFromHex |
@@ -6,5 +6,5 @@ /// <reference types="node" /> | ||
import type { Explorer } from '@bitcoinerlab/explorer'; | ||
import { NetworkId, TxId, TxHex, TxInfo, Expression, Account, DescriptorIndex, DiscoveryInfo, Utxo, TxStatus } from './types'; | ||
import { OutputCriteria, NetworkId, TxId, TxHex, TxData, Descriptor, Account, DiscoveryData, Utxo, TxStatus } from './types'; | ||
/** | ||
* Creates and returns a Discovery class for discovering funds in a Bitcoin wallet | ||
* Creates and returns a Discovery class for discovering funds in a Bitcoin network | ||
* using descriptors. The class provides methods for descriptor expression discovery, | ||
@@ -21,7 +21,12 @@ * balance checking, transaction status checking, and so on. | ||
*/ | ||
explorer: Explorer): { | ||
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 ({ expressionsCacheSize, indicesPerExpressionCacheSize }?: { | ||
new ({ descriptorsCacheSize, outputsPerDescriptorCacheSize }?: { | ||
/** | ||
* Cache size limit for descriptor expressions per network. | ||
* Cache size limit for descriptor expressions. | ||
* The cache is used to speed up data queries by avoiding unnecessary | ||
@@ -40,8 +45,8 @@ * recomputations. However, it is essential to manage the memory | ||
*/ | ||
expressionsCacheSize: number; | ||
descriptorsCacheSize: number; | ||
/** | ||
* Cache size limit for indices per expression, related to the number of addresses | ||
* in ranged descriptor expressions. Similar to the `expressionsCacheSize`, | ||
* 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 expression can have multiple indices, the number of indices can grow rapidly, | ||
* 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, | ||
@@ -52,6 +57,6 @@ * while also maintaining the benefits of immutability and computational efficiency. | ||
*/ | ||
indicesPerExpressionCacheSize: number; | ||
outputsPerDescriptorCacheSize: number; | ||
}): { | ||
"__#3@#derivers": ReturnType<typeof deriveDataFactory>; | ||
discoveryInfo: DiscoveryInfo; | ||
"__#3@#discoveryData": DiscoveryData; | ||
/** | ||
@@ -78,59 +83,67 @@ * Ensures that a scriptPubKey is unique and has not already been set by | ||
/** | ||
* Asynchronously discovers a scriptPubKey, given a descriptor expression, | ||
* descriptor index, and network. It first retrieves the scriptPubKey, | ||
* 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 | ||
* discoveryInfo accordingly. | ||
* discoveryData accordingly. | ||
* | ||
* This function has side-effects as it modifies the internal discoveryInfo | ||
* 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 of the wallet based on new | ||
* transactions and scriptPubKeys. | ||
* 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. | ||
*/ | ||
discoverScriptPubKey({ expression, index, network }: { | ||
"__#3@#fetchOutput"({ descriptor, index }: { | ||
/** | ||
* The descriptor expression associated with the scriptPubKey to discover. | ||
*/ | ||
expression: Expression; | ||
descriptor: Descriptor; | ||
/** | ||
* The descriptor index associated with the scriptPubKey to discover. | ||
* The descriptor index associated with the scriptPubKey to discover (if ranged). | ||
*/ | ||
index: DescriptorIndex; | ||
/** | ||
* The network associated with the scriptPubKey to discover. | ||
*/ | ||
network: Network; | ||
index?: number; | ||
}): Promise<boolean>; | ||
/** | ||
* Asynchronously fetches all transactions associated with a specific network | ||
* for all used scriptPubKeys. | ||
* Asynchronously fetches all raw transaction data from all transactions | ||
* associated with all the outputs fetched. | ||
* | ||
* @param options | ||
* @returns Resolves when all the transactions for the provided network have been fetched and stored in discoveryInfo. | ||
* @returns Resolves when all the transactions have been fetched and stored in discoveryData. | ||
*/ | ||
discoverTxs({ network }: { | ||
/** | ||
* The network whose transactions are to be fetched. | ||
*/ | ||
network: Network; | ||
}): Promise<void>; | ||
"__#3@#fetchTxs"(): Promise<void>; | ||
/** | ||
* Asynchronously fetches one or more descriptor expressions, including | ||
* their associated transaction data. | ||
* 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. | ||
* @returns Resolves when the fetch operation completes. If used expressions | ||
* are found, waits for the discovery of associated transactions. | ||
*/ | ||
discover({ expressions, gapLimit, network, onUsed, onChecking, next }: { | ||
fetch({ descriptor, index, descriptors, gapLimit, onUsed, onChecking, next }: { | ||
/** | ||
* The descriptor expression(s) to be fetched. Can be a single expression or an array. | ||
* Descriptor expression representing one or potentially multiple outputs | ||
* if ranged. | ||
* Use either `descriptor` or `descriptors`, but not both simultaneously. | ||
*/ | ||
expressions: Expression | Array<Expression>; | ||
descriptor?: Descriptor; | ||
/** | ||
* The gap limit for the fetch operation. | ||
* 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 | ||
@@ -140,27 +153,84 @@ */ | ||
/** | ||
* The network associated with the expressions. | ||
* 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. | ||
*/ | ||
network: Network; | ||
onUsed?: (descriptorOrDescriptors: Descriptor | Array<Descriptor>) => void; | ||
/** | ||
* Optional callback function. Invoked when a used expression is found. Provided with the same input descriptor expressions. | ||
* 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. | ||
*/ | ||
onUsed?: (expression: Expression | Array<Expression>) => void; | ||
onChecking?: (descriptorOrDescriptors: Descriptor | Array<Descriptor>) => void; | ||
/** | ||
* Optional callback function. Invoked when a used expression is started to being checked. Provided with the same input descriptor expressions. | ||
* 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. | ||
*/ | ||
onChecking?: (expression: Expression | Array<Expression>) => void; | ||
/** | ||
* Optional function that returns a Promise. Invoked once a used expression is found and the Promise it returns is awaited. | ||
*/ | ||
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 in a specific network. It uses a given gap limit for | ||
* wallet discovery. | ||
* with a master node. It uses a given gap limit for | ||
* discovery. | ||
* | ||
* @param options | ||
* @returns Resolves when all the accounts from the master node have been discovered. | ||
* @returns Resolves when all the standrd accounts from the master node have | ||
* been discovered. | ||
*/ | ||
discoverStandardAccounts({ masterNode, gapLimit, network, onAccountUsed, onAccountChecking }: { | ||
fetchStandardAccounts({ masterNode, gapLimit, onAccountUsed, onAccountChecking }: { | ||
/** | ||
@@ -176,13 +246,25 @@ * The master node to discover accounts from. | ||
/** | ||
* The network in which to discover the accounts. | ||
* 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. | ||
*/ | ||
network: Network; | ||
onAccountUsed?: (account: Account) => 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) => void; | ||
}): Promise<void>; | ||
/** | ||
* Retrieves an array of descriptor expressions associated with a specific | ||
* network. The result is cached based on the size specified in the constructor. | ||
* 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 per networkId if the returned array hasn't changed. | ||
* the same object reference if the returned array hasn't changed. | ||
* This characteristic can be particularly beneficial in | ||
@@ -193,19 +275,12 @@ * React and similar projects, where re-rendering occurs based on reference changes. | ||
* @returns Returns an array of descriptor expressions. | ||
* These are derived from the discovery information of the wallet and the | ||
* provided network. | ||
* These are derived from the discovery information. | ||
* | ||
*/ | ||
getExpressions({ network }: { | ||
/** | ||
* The network associated with the descriptors. | ||
*/ | ||
network: Network; | ||
}): Array<Expression>; | ||
getUsedDescriptors(): Array<Descriptor>; | ||
/** | ||
* Retrieves all the accounts in the wallet: those descriptors with keyPaths | ||
* ending in `{/0/*, /1/*}`. An account is identified | ||
* 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 per | ||
* networkId if the returned array remains unchanged. | ||
* exceeded, this function will maintain the same object reference if the returned array remains unchanged. | ||
* This characteristic can be especially beneficial in | ||
@@ -218,8 +293,3 @@ * React or similar projects, where re-rendering occurs based on reference changes. | ||
*/ | ||
getAccounts({ network }: { | ||
/** | ||
* The network associated with the descriptors. | ||
*/ | ||
network: Network; | ||
}): Array<Account>; | ||
getUsedAccounts(): Array<Account>; | ||
/** | ||
@@ -237,55 +307,12 @@ * Retrieves descriptor expressions associated with a specific account. | ||
*/ | ||
getAccountExpressions({ account }: { | ||
getAccountDescriptors({ account }: { | ||
/** | ||
* The account associated with the descriptors. | ||
* The {@link Account account} associated with the descriptors. | ||
*/ | ||
account: Account; | ||
}): [Expression, Expression]; | ||
}): [Descriptor, Descriptor]; | ||
/** | ||
* Retrieves unspent transaction outputs (UTXOs) and balance associated with | ||
* a specific scriptPubKey, described by an expression and index within a | ||
* specified network and transaction status. | ||
* one or more descriptor expressions and transaction status. | ||
* | ||
* This method is useful for accessing the available funds for a specific | ||
* scriptPubKey in the wallet, considering the transaction status | ||
* (confirmed, unconfirmed, or both). | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. | ||
* 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 discoveryInfo haven't changed. | ||
* This can be useful in environments such as React where | ||
* preserving object identity can prevent unnecessary re-renders. | ||
* | ||
* @param options | ||
* @returns An object containing the UTXOs associated with the | ||
* scriptPubKey and the total balance of these UTXOs. | ||
*/ | ||
getUtxosByScriptPubKey({ expression, index, network, txStatus }: { | ||
/** | ||
* The descriptor expression associated with the scriptPubKey. | ||
*/ | ||
expression: Expression; | ||
/** | ||
* The descriptor index associated with the scriptPubKey. | ||
*/ | ||
index: DescriptorIndex; | ||
/** | ||
* The network associated with the scriptPubKey. | ||
*/ | ||
network: Network; | ||
/** | ||
* The transaction status to consider when extracting UTXOs and balance. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}): { | ||
utxos: Array<Utxo>; | ||
balance: number; | ||
}; | ||
/** | ||
* Retrieves unspent transaction outputs (UTXOs) and balance associated with | ||
* one or more descriptor expressions within a specified network and | ||
* transaction status. | ||
* | ||
* This method is useful for accessing the available funds for specific | ||
@@ -295,29 +322,14 @@ * descriptor expressions in the wallet, considering the transaction status | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. | ||
* 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 discoveryInfo haven't changed. | ||
* 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 options | ||
* @param outputCriteria | ||
* @returns An object containing the UTXOs associated with the | ||
* scriptPubKeys and the total balance of these UTXOs. | ||
*/ | ||
getUtxos({ expressions, network, txStatus }: { | ||
/** | ||
* The descriptor expression(s) associated with the scriptPubKeys. | ||
* Can be a single expression or an array of expressions. | ||
*/ | ||
expressions: Expression | Array<Expression>; | ||
/** | ||
* The network associated with the scriptPubKeys. | ||
*/ | ||
network: Network; | ||
/** | ||
* The transaction status to consider when extracting UTXOs and balance. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}): { | ||
getUtxosAndBalance({ descriptor, index, descriptors, txStatus }: OutputCriteria): { | ||
utxos: Array<Utxo>; | ||
@@ -328,23 +340,12 @@ balance: number; | ||
* Convenience function which internally invokes the | ||
* `getUtxos(options).balance` method. | ||
* `getUtxosAndBalance(options).balance` method. | ||
*/ | ||
getBalance(options: { | ||
/** | ||
* The descriptor expression(s) associated with the scriptPubKeys. | ||
* Can be a single expression or an array of expressions. | ||
*/ | ||
expressions: Expression | Array<Expression>; | ||
/** | ||
* The network associated with the scriptPubKeys. | ||
*/ | ||
network: Network; | ||
/** | ||
* The transaction status to consider when extracting UTXOs and balance. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}): number; | ||
getBalance(outputCriteria: OutputCriteria): number; | ||
/** | ||
* Retrieves the next available index for a given expression within a | ||
* specified network. | ||
* Convenience function which internally invokes the | ||
* `getUtxosAndBalance(options).utxos` method. | ||
*/ | ||
getUtxos(outputCriteria: OutputCriteria): Array<Utxo>; | ||
/** | ||
* Retrieves the next available index for a given descriptor. | ||
* | ||
@@ -357,12 +358,8 @@ * The method retrieves the currently highest index used, and returns the | ||
*/ | ||
getNextIndex({ network, expression, txStatus }: { | ||
getNextIndex({ descriptor, txStatus }: { | ||
/** | ||
* The network associated with the account. | ||
*/ | ||
network: Network; | ||
/** | ||
* The ranged descriptor expression for which to retrieve the next | ||
* available index. | ||
*/ | ||
expression: Expression; | ||
descriptor: Descriptor; | ||
/** | ||
@@ -377,47 +374,11 @@ * A scriptPubKey will be considered as used when | ||
/** | ||
* Retrieves the transaction history for a specific script public key. | ||
* | ||
* This method is useful for fetching transaction records associated with a specific | ||
* script public key within a specified network and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. 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 | ||
* discoveryInfo haven't changed. | ||
* | ||
* This can be useful in environments such as React where preserving object identity can | ||
* prevent unnecessary re-renders. | ||
* | ||
* @param options | ||
* @returns An array containing transaction info associated with the script public key. | ||
*/ | ||
getHistoryByScriptPubKey({ expression, index, network, txStatus }: { | ||
/** | ||
* The descriptor expression. | ||
*/ | ||
expression: Expression; | ||
/** | ||
* The index in the descriptor. | ||
*/ | ||
index: DescriptorIndex; | ||
/** | ||
* The network associated with the scriptPubKey. | ||
*/ | ||
network: Network; | ||
/** | ||
* The transaction status to consider when fetching transaction history. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}): Array<TxInfo>; | ||
/** | ||
* Retrieves the transaction history for one or more descriptor expressions. | ||
* | ||
* This method is useful for accessing transaction records associated with one or more | ||
* descriptor expressions within a specified network and transaction status. | ||
* descriptor expressions and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. The method | ||
* 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 | ||
* discoveryInfo haven't changed. | ||
* discoveryData haven't changed. | ||
* | ||
@@ -427,24 +388,9 @@ * This can be useful in environments such as React where preserving object identity can | ||
* | ||
* @param options | ||
* @param outputCriteria | ||
* @returns An array containing transaction info associated with the descriptor expressions. | ||
*/ | ||
getHistory({ expressions, network, txStatus }: { | ||
/** | ||
* One or more descriptor expressions. | ||
*/ | ||
expressions: Expression | Array<Expression>; | ||
/** | ||
* The network associated with the descriptor expressions. | ||
*/ | ||
network: Network; | ||
/** | ||
* The transaction status to consider when fetching transaction history. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}): Array<TxInfo>; | ||
getHistory({ descriptor, index, descriptors, txStatus }: OutputCriteria): Array<TxData>; | ||
/** | ||
* Retrieves the hexadecimal representation of a transaction (TxHex) from the | ||
* discoveryInfo given the transaction ID (TxId) or a Unspent Transaction Output (Utxo) | ||
* as well as the network in which the transaction occurred. | ||
* discoveryData given the transaction ID (TxId) or a Unspent Transaction Output (Utxo) | ||
* | ||
@@ -455,16 +401,17 @@ * @param options | ||
*/ | ||
getTxHex({ network, tx }: { | ||
getTxHex({ txId, utxo }: { | ||
/** | ||
* The network where the transaction took place. | ||
* The transaction ID. | ||
*/ | ||
network: Network; | ||
txId?: TxId; | ||
/** | ||
* The transaction ID or a UTXO. | ||
* The UTXO. | ||
*/ | ||
tx: TxId | Utxo; | ||
utxo?: Utxo; | ||
}): TxHex; | ||
/** | ||
* Retrieves the transaction data as a Transaction object given the transaction | ||
* ID (TxId) or a Unspent Transaction Output (Utxo) and the network in which | ||
* the transaction occurred. The transaction data is obtained by first getting | ||
* 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). The transaction data is obtained by first getting | ||
* the transaction hexadecimal representation using getTxHex() method. | ||
@@ -480,32 +427,17 @@ * | ||
*/ | ||
getTransaction({ network, tx }: { | ||
getTransaction({ txId, utxo }: { | ||
/** | ||
* The network where the transaction took place. | ||
* The transaction ID. | ||
*/ | ||
network: Network; | ||
txId?: TxId; | ||
/** | ||
* The transaction ID or a UTXO. | ||
* The UTXO. | ||
*/ | ||
tx: TxId | Utxo; | ||
utxo?: Utxo; | ||
}): Transaction; | ||
/** | ||
* Given a UTXO, this function retrieves the scriptPubKeys that can spend | ||
* the UTXO. | ||
* | ||
* It is important to note that a single UTXO might have multiple | ||
* scriptPubKeys. This can occur in scenarios where the UTXO has multiple | ||
* spending conditions that can be satisfied differently by the discovered | ||
* expressions. | ||
* | ||
* To ensure high flexibility, the returned scriptPubKeys are represented by | ||
* their descriptor expression and index (in cases of BIP32 ranged | ||
* descriptors). Additionally, this function provides the txHex of the | ||
* preceding transaction and its output index (vout) for convenience. | ||
* Given an unspent tx output, this function retrieves its descriptor. | ||
*/ | ||
getScriptPubKeysByUtxo({ network, utxo }: { | ||
getDescriptor({ utxo }: { | ||
/** | ||
* The network where the transaction took place. | ||
*/ | ||
network: Network; | ||
/** | ||
* The UTXO. | ||
@@ -515,16 +447,6 @@ */ | ||
}): { | ||
expression: Expression; | ||
index: DescriptorIndex; | ||
vout: number; | ||
txHex: TxHex; | ||
}[]; | ||
descriptor: Descriptor; | ||
index?: number; | ||
} | undefined; | ||
/** | ||
* Retrieves the current state of discovery information. This information | ||
* includes details about transactions, descriptors, and network-specific | ||
* details that are stored during the wallet discovery process. | ||
* | ||
* @returns The current state of the discovery information. | ||
*/ | ||
getDiscoveryInfo(): DiscoveryInfo; | ||
/** | ||
* Retrieves the Explorer instance. | ||
@@ -531,0 +453,0 @@ * |
"use strict"; | ||
// TODO | ||
// | ||
// | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
//fix getTxHex | ||
// | ||
// | ||
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com | ||
@@ -17,2 +32,4 @@ // Distributed under the MIT software license | ||
exports.DiscoveryFactory = void 0; | ||
//TODO: Important to emphasize that we don't allow different descritptors for | ||
//the same output | ||
const immer_1 = require("immer"); | ||
@@ -27,3 +44,3 @@ const shallow_equal_1 = require("shallow-equal"); | ||
/** | ||
* Creates and returns a Discovery class for discovering funds in a Bitcoin wallet | ||
* Creates and returns a Discovery class for discovering funds in a Bitcoin network | ||
* using descriptors. The class provides methods for descriptor expression discovery, | ||
@@ -40,6 +57,11 @@ * balance checking, transaction status checking, and so on. | ||
*/ | ||
explorer) { | ||
var _Discovery_instances, _Discovery_derivers, _Discovery_ensureScriptPubKeyUniqueness; | ||
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) { | ||
var _Discovery_instances, _Discovery_derivers, _Discovery_discoveryData, _Discovery_ensureScriptPubKeyUniqueness, _Discovery_fetchOutput, _Discovery_fetchTxs, _Discovery_ensureFetched; | ||
/** | ||
* A class to discover funds in a Bitcoin wallet using descriptors. | ||
* A class to discover funds in a Bitcoin network using descriptors. | ||
* The {@link DiscoveryFactory | `DiscoveryFactory`} function internally creates and returns an instance of this class. | ||
@@ -51,159 +73,62 @@ * The returned class is specialized for the provided `Explorer`, which is responsible for fetching blockchain data like transaction details. | ||
* Constructs a Discovery instance. Discovery is used to discover funds | ||
* in a Bitcoin wallet using descriptors. | ||
* in a Bitcoin network using descriptors. | ||
* | ||
* @param options | ||
*/ | ||
constructor({ expressionsCacheSize = 1000, indicesPerExpressionCacheSize = 10000 } = { | ||
expressionsCacheSize: 1000, | ||
indicesPerExpressionCacheSize: 10000 | ||
constructor({ descriptorsCacheSize = 1000, outputsPerDescriptorCacheSize = 10000 } = { | ||
descriptorsCacheSize: 1000, | ||
outputsPerDescriptorCacheSize: 10000 | ||
}) { | ||
_Discovery_instances.add(this); | ||
_Discovery_derivers.set(this, void 0); | ||
this.discoveryInfo = {}; | ||
_Discovery_discoveryData.set(this, void 0); | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, {}, "f"); | ||
for (const networkId of Object.values(types_1.NetworkId)) { | ||
const txInfoRecords = {}; | ||
const descriptors = {}; | ||
const networkInfo = { | ||
descriptors, | ||
txInfoRecords | ||
const txMap = {}; | ||
const descriptorMap = {}; | ||
const networkData = { | ||
descriptorMap, | ||
txMap | ||
}; | ||
this.discoveryInfo[networkId] = networkInfo; | ||
__classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId] = networkData; | ||
} | ||
__classPrivateFieldSet(this, _Discovery_derivers, (0, deriveData_1.deriveDataFactory)({ | ||
expressionsCacheSize, | ||
indicesPerExpressionCacheSize | ||
descriptorsCacheSize, | ||
outputsPerDescriptorCacheSize | ||
}), "f"); | ||
} | ||
/** | ||
* Asynchronously discovers a scriptPubKey, given a descriptor expression, | ||
* descriptor index, and network. It first retrieves the scriptPubKey, | ||
* computes its scriptHash, and fetches the transaction history associated | ||
* with this scriptHash from the explorer. It then updates the internal | ||
* discoveryInfo accordingly. | ||
* Asynchronously fetches one or more descriptor expressions, retrieving | ||
* all the historical txs associated with the outputs represented by the | ||
* expressions. | ||
* | ||
* This function has side-effects as it modifies the internal discoveryInfo | ||
* 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 of the wallet based on new | ||
* transactions and scriptPubKeys. | ||
* | ||
* @param options | ||
* @returns A promise that resolves to a boolean indicating whether any transactions were found for the provided scriptPubKey. | ||
* @returns Resolves when the fetch operation completes. If used expressions | ||
* are found, waits for the discovery of associated transactions. | ||
*/ | ||
async discoverScriptPubKey({ expression, index, network }) { | ||
expression = (0, deriveData_1.canonicalize)(expression, network); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const scriptPubKey = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveScriptPubKey(networkId, expression, index); | ||
//https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes | ||
const scriptHash = Buffer.from(bitcoinjs_lib_1.crypto.sha256(scriptPubKey)) | ||
.reverse() | ||
.toString('hex'); | ||
const txHistoryArray = await explorer.fetchTxHistory({ | ||
scriptHash | ||
}); | ||
//console.log('TRACE', { scriptHash, txHistoryArray }); | ||
this.discoveryInfo = (0, immer_1.produce)(this.discoveryInfo, discoveryInfo => { | ||
// Update txInfoRecords | ||
const txInfoRecords = discoveryInfo[networkId].txInfoRecords; | ||
txHistoryArray.forEach(({ txId, irreversible, blockHeight }) => { | ||
const txInfo = txInfoRecords[txId]; | ||
if (!txInfo) { | ||
txInfoRecords[txId] = { irreversible, blockHeight }; | ||
} | ||
else { | ||
txInfo.irreversible = irreversible; | ||
txInfo.blockHeight = blockHeight; | ||
} | ||
}); | ||
//Update descriptors | ||
const scriptPubKeyInfoRecords = discoveryInfo[networkId].descriptors[expression] | ||
?.scriptPubKeyInfoRecords; | ||
if (!scriptPubKeyInfoRecords) | ||
throw new Error(`scriptPubKeyInfoRecords does not exist for ${networkId} and ${expression}`); | ||
const scriptPubKeyInfo = scriptPubKeyInfoRecords[index]; | ||
const txIds = txHistoryArray.map(txHistory => txHistory.txId); | ||
if (txIds.length) { | ||
if (!scriptPubKeyInfo) { | ||
__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_ensureScriptPubKeyUniqueness).call(this, { networkId, scriptPubKey }); | ||
scriptPubKeyInfoRecords[index] = { txIds, timeFetched: now() }; | ||
} | ||
else { | ||
if (!(0, shallow_equal_1.shallowEqualArrays)(txIds, scriptPubKeyInfo.txIds)) { | ||
scriptPubKeyInfo.txIds = txIds; | ||
} | ||
scriptPubKeyInfo.timeFetched = now(); | ||
} | ||
} | ||
else { | ||
if (scriptPubKeyInfo) { | ||
delete scriptPubKeyInfoRecords[index]; | ||
} | ||
} | ||
}); | ||
return !!txHistoryArray.length; | ||
} | ||
/** | ||
* Asynchronously fetches all transactions associated with a specific network | ||
* for all used scriptPubKeys. | ||
* | ||
* @param options | ||
* @returns Resolves when all the transactions for the provided network have been fetched and stored in discoveryInfo. | ||
*/ | ||
async discoverTxs({ network }) { | ||
const txHexRecords = {}; | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const networkInfo = this.discoveryInfo[networkId]; | ||
for (const expression in networkInfo.descriptors) { | ||
const scriptPubKeyInfoRecords = networkInfo.descriptors[expression]?.scriptPubKeyInfoRecords || []; | ||
for (const index in scriptPubKeyInfoRecords) { | ||
const txIds = scriptPubKeyInfoRecords[index]?.txIds; | ||
if (!txIds) | ||
throw new Error(`Error: cannot retrieve txs for nonexising scriptPubKey: ${networkId}, ${expression}, ${index}`); | ||
for (const txId of txIds) | ||
if (!networkInfo.txInfoRecords[txId]?.txHex) | ||
txHexRecords[txId] = await explorer.fetchTx(txId); | ||
} | ||
} | ||
if (Object.keys(txHexRecords).length) { | ||
this.discoveryInfo = (0, immer_1.produce)(this.discoveryInfo, discoveryInfo => { | ||
for (const txId in txHexRecords) { | ||
const txHex = txHexRecords[txId]; | ||
if (!txHex) | ||
throw new Error(`txHex not retrieved for ${txId}`); | ||
const txInfo = discoveryInfo[networkId].txInfoRecords[txId]; | ||
if (!txInfo) | ||
throw new Error(`txInfo does not exist for ${txId}`); | ||
txInfo.txHex = txHex; | ||
} | ||
}); | ||
} | ||
} | ||
/** | ||
* Asynchronously fetches one or more descriptor expressions, including | ||
* their associated transaction data. | ||
* | ||
* @param options | ||
* @returns Resolves when the fetch operation completes. If used expressions are found, waits for the discovery of associated transactions. | ||
*/ | ||
async discover({ expressions, gapLimit = 20, network, onUsed, onChecking, next }) { | ||
const inputExpressions = expressions; | ||
async fetch({ descriptor, index, descriptors, gapLimit = 20, onUsed, onChecking, next }) { | ||
const descriptorOrDescriptors = descriptor || descriptors; | ||
if ((descriptor && descriptors) || !descriptorOrDescriptors) | ||
throw new Error(`Pass descriptor or descriptors`); | ||
if (typeof index !== 'undefined' && | ||
(descriptors || !descriptor?.includes('*'))) | ||
throw new Error(`Don't pass index`); | ||
if (onChecking) | ||
onChecking(inputExpressions); | ||
expressions = (0, deriveData_1.canonicalize)(expressions, network); | ||
onChecking(descriptorOrDescriptors); | ||
const canonicalInput = (0, deriveData_1.canonicalize)(descriptorOrDescriptors, network); | ||
let nextPromise; | ||
let usedExpressions = false; | ||
let usedExpressionsNotified = false; | ||
const expressionArray = Array.isArray(expressions) | ||
? expressions | ||
: [expressions]; | ||
let usedOutput = false; | ||
let usedOutputNotified = false; | ||
const descriptorArray = Array.isArray(canonicalInput) | ||
? canonicalInput | ||
: [canonicalInput]; | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
for (const expression of expressionArray) { | ||
this.discoveryInfo = (0, immer_1.produce)(this.discoveryInfo, discoveryInfo => { | ||
const descriptorInfo = discoveryInfo[networkId].descriptors[expression]; | ||
for (const descriptor of descriptorArray) { | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
const descriptorInfo = discoveryData[networkId].descriptorMap[descriptor]; | ||
if (!descriptorInfo) { | ||
discoveryInfo[networkId].descriptors[expression] = { | ||
discoveryData[networkId].descriptorMap[descriptor] = { | ||
timeFetched: 0, | ||
fetching: true, | ||
scriptPubKeyInfoRecords: {} | ||
range: {} | ||
}; | ||
@@ -214,14 +139,13 @@ } | ||
} | ||
}); | ||
}), "f"); | ||
let gap = 0; | ||
let index = 0; | ||
const isRanged = expression.indexOf('*') !== -1; | ||
while (isRanged ? gap < gapLimit : index < 1) { | ||
const used = await this.discoverScriptPubKey({ | ||
expression, | ||
index: isRanged ? index : 'non-ranged', | ||
network | ||
index = index || 0; //If it was a passed argument use it; othewise start at zero | ||
const isRanged = descriptor.indexOf('*') !== -1; | ||
while (isRanged ? gap < gapLimit : index < 1 /*once if unranged*/) { | ||
const used = await __classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_fetchOutput).call(this, { | ||
descriptor, | ||
...(isRanged ? { index } : {}) | ||
}); | ||
if (used) { | ||
usedExpressions = true; | ||
usedOutput = true; | ||
gap = 0; | ||
@@ -234,18 +158,18 @@ } | ||
index++; | ||
if (used && onUsed && usedExpressionsNotified === false) { | ||
onUsed(inputExpressions); | ||
usedExpressionsNotified = true; | ||
if (used && onUsed && usedOutputNotified === false) { | ||
onUsed(descriptorOrDescriptors); | ||
usedOutputNotified = true; | ||
} | ||
} | ||
this.discoveryInfo = (0, immer_1.produce)(this.discoveryInfo, discoveryInfo => { | ||
const descriptorInfo = discoveryInfo[networkId].descriptors[expression]; | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
const descriptorInfo = discoveryData[networkId].descriptorMap[descriptor]; | ||
if (!descriptorInfo) | ||
throw new Error(`Descriptor for ${networkId} and ${expression} does not exist`); | ||
throw new Error(`Descriptor for ${networkId} and ${descriptor} does not exist`); | ||
descriptorInfo.fetching = false; | ||
descriptorInfo.timeFetched = now(); | ||
}); | ||
}), "f"); | ||
} | ||
const promises = []; | ||
if (usedExpressions) | ||
promises.push(this.discoverTxs({ network })); | ||
if (usedOutput) | ||
promises.push(__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_fetchTxs).call(this)); | ||
if (nextPromise) | ||
@@ -256,14 +180,51 @@ promises.push(nextPromise); | ||
/** | ||
* 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 }) { | ||
if (typeof index !== 'undefined' && descriptor.indexOf('*') === -1) | ||
throw new Error(`Pass index (optionally) only for ranged descriptors`); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptorData = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap[descriptor]; | ||
if (!descriptorData) | ||
return undefined; | ||
if (typeof index !== 'number') { | ||
return { | ||
fetching: descriptorData.fetching, | ||
timeFetched: descriptorData.timeFetched | ||
}; | ||
} | ||
else { | ||
const internalIndex = typeof index === 'number' ? index : 'non-ranged'; | ||
const outputData = descriptorData.range[internalIndex]; | ||
if (!outputData) | ||
return undefined; | ||
else | ||
return { | ||
fetching: outputData.fetching, | ||
timeFetched: outputData.timeFetched | ||
}; | ||
} | ||
} | ||
/** | ||
* Asynchronously discovers standard accounts (pkh, sh(wpkh), wpkh) associated | ||
* with a master node in a specific network. It uses a given gap limit for | ||
* wallet discovery. | ||
* with a master node. It uses a given gap limit for | ||
* discovery. | ||
* | ||
* @param options | ||
* @returns Resolves when all the accounts from the master node have been discovered. | ||
* @returns Resolves when all the standrd accounts from the master node have | ||
* been discovered. | ||
*/ | ||
async discoverStandardAccounts({ masterNode, gapLimit = 20, network, onAccountUsed, onAccountChecking }) { | ||
async fetchStandardAccounts({ masterNode, gapLimit = 20, onAccountUsed, onAccountChecking }) { | ||
const discoveryTasks = []; | ||
const { pkhBIP32, shWpkhBIP32, wpkhBIP32 } = descriptors_1.scriptExpressions; | ||
if (!network) | ||
throw new Error(`Error: provide a network`); | ||
if (!masterNode) | ||
@@ -274,3 +235,3 @@ throw new Error(`Error: provide a masterNode`); | ||
const next = async () => { | ||
const expressions = [0, 1].map(change => expressionFn({ | ||
const descriptors = [0, 1].map(change => expressionFn({ | ||
masterNode, | ||
@@ -282,11 +243,10 @@ network, | ||
})); | ||
const account = expressions[0]; | ||
//console.log('STANDARD', { expressions, gapLimit, account }); | ||
const account = descriptors[0]; | ||
//console.log('STANDARD', { descriptors, gapLimit, account }); | ||
accountNumber++; | ||
const onUsed = onAccountUsed && (() => onAccountUsed(account)); | ||
const onChecking = onAccountChecking && (() => onAccountChecking(account)); | ||
await this.discover({ | ||
expressions, | ||
await this.fetch({ | ||
descriptors, | ||
gapLimit, | ||
network, | ||
next, | ||
@@ -302,6 +262,6 @@ ...(onUsed ? { onUsed } : {}), | ||
/** | ||
* Retrieves an array of descriptor expressions associated with a specific | ||
* network. The result is cached based on the size specified in the constructor. | ||
* 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 per networkId if the returned array hasn't changed. | ||
* the same object reference if the returned array hasn't changed. | ||
* This characteristic can be particularly beneficial in | ||
@@ -312,17 +272,15 @@ * React and similar projects, where re-rendering occurs based on reference changes. | ||
* @returns Returns an array of descriptor expressions. | ||
* These are derived from the discovery information of the wallet and the | ||
* provided network. | ||
* These are derived from the discovery information. | ||
* | ||
*/ | ||
getExpressions({ network }) { | ||
getUsedDescriptors() { | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveExpressions(this.discoveryInfo, networkId); | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUsedDescriptors(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), networkId); | ||
} | ||
/** | ||
* Retrieves all the accounts in the wallet: those descriptors with keyPaths | ||
* ending in `{/0/*, /1/*}`. An account is identified | ||
* 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 per | ||
* networkId if the returned array remains unchanged. | ||
* exceeded, this function will maintain the same object reference if the returned array remains unchanged. | ||
* This characteristic can be especially beneficial in | ||
@@ -335,5 +293,5 @@ * React or similar projects, where re-rendering occurs based on reference changes. | ||
*/ | ||
getAccounts({ network }) { | ||
getUsedAccounts() { | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveAccounts(this.discoveryInfo, networkId); | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUsedAccounts(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), networkId); | ||
} | ||
@@ -352,37 +310,9 @@ /** | ||
*/ | ||
getAccountExpressions({ account }) { | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveAccountExpressions(account); | ||
getAccountDescriptors({ account }) { | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveAccountDescriptors(account); | ||
} | ||
/** | ||
* Retrieves unspent transaction outputs (UTXOs) and balance associated with | ||
* a specific scriptPubKey, described by an expression and index within a | ||
* specified network and transaction status. | ||
* one or more descriptor expressions and transaction status. | ||
* | ||
* This method is useful for accessing the available funds for a specific | ||
* scriptPubKey in the wallet, considering the transaction status | ||
* (confirmed, unconfirmed, or both). | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. | ||
* 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 discoveryInfo haven't changed. | ||
* This can be useful in environments such as React where | ||
* preserving object identity can prevent unnecessary re-renders. | ||
* | ||
* @param options | ||
* @returns An object containing the UTXOs associated with the | ||
* scriptPubKey and the total balance of these UTXOs. | ||
*/ | ||
getUtxosByScriptPubKey({ expression, index, network, txStatus = types_1.TxStatus.ALL }) { | ||
expression = (0, deriveData_1.canonicalize)(expression, network); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const txInfoRecords = this.discoveryInfo[networkId].txInfoRecords; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUtxosAndBalanceByScriptPubKey(networkId, txInfoRecords, descriptors, expression, index, txStatus); | ||
} | ||
/** | ||
* Retrieves unspent transaction outputs (UTXOs) and balance associated with | ||
* one or more descriptor expressions within a specified network and | ||
* transaction status. | ||
* | ||
* This method is useful for accessing the available funds for specific | ||
@@ -392,30 +322,53 @@ * descriptor expressions in the wallet, considering the transaction status | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. | ||
* 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 discoveryInfo haven't changed. | ||
* 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 options | ||
* @param outputCriteria | ||
* @returns An object containing the UTXOs associated with the | ||
* scriptPubKeys and the total balance of these UTXOs. | ||
*/ | ||
getUtxos({ expressions, network, txStatus = types_1.TxStatus.ALL }) { | ||
expressions = (0, deriveData_1.canonicalize)(expressions, network); | ||
getUtxosAndBalance({ descriptor, index, descriptors, txStatus = types_1.TxStatus.ALL }) { | ||
__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_ensureFetched).call(this, { | ||
...(descriptor ? { descriptor } : {}), | ||
...(descriptors ? { descriptors } : {}), | ||
...(index ? { index } : {}) | ||
}); | ||
if ((descriptor && descriptors) || !(descriptor || descriptors)) | ||
throw new Error(`Pass descriptor or descriptors`); | ||
if (typeof index !== 'undefined' && | ||
(descriptors || !descriptor?.includes('*'))) | ||
throw new Error(`Don't pass index`); | ||
const descriptorOrDescriptors = (0, deriveData_1.canonicalize)((descriptor || descriptors), network); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const txInfoRecords = this.discoveryInfo[networkId].txInfoRecords; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUtxosAndBalanceByExpressions(networkId, txInfoRecords, descriptors, expressions, txStatus); | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
const txMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap; | ||
if (descriptor && | ||
(typeof index !== 'undefined' || !descriptor.includes('*'))) { | ||
const internalIndex = typeof index === 'number' ? index : 'non-ranged'; | ||
const txMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUtxosAndBalanceByOutput(networkId, txMap, descriptorMap, descriptorOrDescriptors, internalIndex, txStatus); | ||
} | ||
else | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUtxosAndBalance(networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
} | ||
/** | ||
* Convenience function which internally invokes the | ||
* `getUtxos(options).balance` method. | ||
* `getUtxosAndBalance(options).balance` method. | ||
*/ | ||
getBalance(options) { | ||
return this.getUtxos(options).balance; | ||
getBalance(outputCriteria) { | ||
return this.getUtxosAndBalance(outputCriteria).balance; | ||
} | ||
/** | ||
* Retrieves the next available index for a given expression within a | ||
* specified network. | ||
* Convenience function which internally invokes the | ||
* `getUtxosAndBalance(options).utxos` method. | ||
*/ | ||
getUtxos(outputCriteria) { | ||
return this.getUtxosAndBalance(outputCriteria).utxos; | ||
} | ||
/** | ||
* Retrieves the next available index for a given descriptor. | ||
* | ||
@@ -428,10 +381,11 @@ * The method retrieves the currently highest index used, and returns the | ||
*/ | ||
getNextIndex({ network, expression, txStatus = types_1.TxStatus.ALL }) { | ||
if (!expression || expression.indexOf('*') === -1) | ||
throw new Error(`Error: invalid ranged expression: ${expression}`); | ||
getNextIndex({ descriptor, txStatus = types_1.TxStatus.ALL }) { | ||
if (!descriptor || descriptor.indexOf('*') === -1) | ||
throw new Error(`Error: invalid ranged descriptor: ${descriptor}`); | ||
__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_ensureFetched).call(this, { descriptor }); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const txInfoRecords = this.discoveryInfo[networkId].txInfoRecords; | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
const txMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap; | ||
let index = 0; | ||
while (__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByScriptPubKey(txInfoRecords, descriptors, expression, index, txStatus).length) | ||
while (__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByOutput(txMap, descriptorMap, descriptor, index, txStatus).length) | ||
index++; | ||
@@ -441,35 +395,11 @@ return index; | ||
/** | ||
* Retrieves the transaction history for a specific script public key. | ||
* | ||
* This method is useful for fetching transaction records associated with a specific | ||
* script public key within a specified network and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. 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 | ||
* discoveryInfo haven't changed. | ||
* | ||
* This can be useful in environments such as React where preserving object identity can | ||
* prevent unnecessary re-renders. | ||
* | ||
* @param options | ||
* @returns An array containing transaction info associated with the script public key. | ||
*/ | ||
getHistoryByScriptPubKey({ expression, index, network, txStatus = types_1.TxStatus.ALL }) { | ||
expression = (0, deriveData_1.canonicalize)(expression, network); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const txInfoRecords = this.discoveryInfo[networkId].txInfoRecords; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByScriptPubKey(txInfoRecords, descriptors, expression, index, txStatus); | ||
} | ||
/** | ||
* Retrieves the transaction history for one or more descriptor expressions. | ||
* | ||
* This method is useful for accessing transaction records associated with one or more | ||
* descriptor expressions within a specified network and transaction status. | ||
* descriptor expressions and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryInfo. The method | ||
* 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 | ||
* discoveryInfo haven't changed. | ||
* discoveryData haven't changed. | ||
* | ||
@@ -479,16 +409,31 @@ * This can be useful in environments such as React where preserving object identity can | ||
* | ||
* @param options | ||
* @param outputCriteria | ||
* @returns An array containing transaction info associated with the descriptor expressions. | ||
*/ | ||
getHistory({ expressions, network, txStatus = types_1.TxStatus.ALL }) { | ||
expressions = (0, deriveData_1.canonicalize)(expressions, network); | ||
getHistory({ descriptor, index, descriptors, txStatus = types_1.TxStatus.ALL }) { | ||
if ((descriptor && descriptors) || !(descriptor || descriptors)) | ||
throw new Error(`Pass descriptor or descriptors`); | ||
if (typeof index !== 'undefined' && | ||
(descriptors || !descriptor?.includes('*'))) | ||
throw new Error(`Don't pass index`); | ||
const descriptorOrDescriptors = (0, deriveData_1.canonicalize)((descriptor || descriptors), network); | ||
__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_ensureFetched).call(this, { | ||
...(descriptor ? { descriptor } : {}), | ||
...(descriptors ? { descriptors } : {}), | ||
...(index ? { index } : {}) | ||
}); | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const txInfoRecords = this.discoveryInfo[networkId].txInfoRecords; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistory(txInfoRecords, descriptors, expressions, txStatus); | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
const txMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap; | ||
if (descriptor && | ||
(typeof index !== 'undefined' || !descriptor.includes('*'))) { | ||
const internalIndex = typeof index === 'number' ? index : 'non-ranged'; | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByOutput(txMap, descriptorMap, descriptorOrDescriptors, internalIndex, txStatus); | ||
} | ||
else | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistory(txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
} | ||
/** | ||
* Retrieves the hexadecimal representation of a transaction (TxHex) from the | ||
* discoveryInfo given the transaction ID (TxId) or a Unspent Transaction Output (Utxo) | ||
* as well as the network in which the transaction occurred. | ||
* discoveryData given the transaction ID (TxId) or a Unspent Transaction Output (Utxo) | ||
* | ||
@@ -499,8 +444,11 @@ * @param options | ||
*/ | ||
getTxHex({ network, tx }) { | ||
getTxHex({ txId, utxo }) { | ||
if ((txId && utxo) || (!txId && !utxo)) { | ||
throw new Error(`Error: Please provide either a txId or a utxo, not both or neither.`); | ||
} | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const txId = tx.indexOf(':') === -1 ? tx : tx.split(':')[0]; | ||
txId = utxo ? utxo.split(':')[0] : txId; | ||
if (!txId) | ||
throw new Error(`Error: invalid tx`); | ||
const txHex = this.discoveryInfo[networkId].txInfoRecords[txId]?.txHex; | ||
throw new Error(`Error: invalid input`); | ||
const txHex = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap[txId]?.txHex; | ||
if (!txHex) | ||
@@ -511,5 +459,6 @@ throw new Error(`Error: txHex not found`); | ||
/** | ||
* Retrieves the transaction data as a Transaction object given the transaction | ||
* ID (TxId) or a Unspent Transaction Output (Utxo) and the network in which | ||
* the transaction occurred. The transaction data is obtained by first getting | ||
* 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). The transaction data is obtained by first getting | ||
* the transaction hexadecimal representation using getTxHex() method. | ||
@@ -525,21 +474,13 @@ * | ||
*/ | ||
getTransaction({ network, tx }) { | ||
const txHex = this.getTxHex({ network, tx }); | ||
getTransaction({ txId, utxo }) { | ||
const txHex = this.getTxHex({ | ||
...(utxo ? { utxo } : {}), | ||
...(txId ? { txId } : {}) | ||
}); | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").transactionFromHex(txHex); | ||
} | ||
/** | ||
* Given a UTXO, this function retrieves the scriptPubKeys that can spend | ||
* the UTXO. | ||
* | ||
* It is important to note that a single UTXO might have multiple | ||
* scriptPubKeys. This can occur in scenarios where the UTXO has multiple | ||
* spending conditions that can be satisfied differently by the discovered | ||
* expressions. | ||
* | ||
* To ensure high flexibility, the returned scriptPubKeys are represented by | ||
* their descriptor expression and index (in cases of BIP32 ranged | ||
* descriptors). Additionally, this function provides the txHex of the | ||
* preceding transaction and its output index (vout) for convenience. | ||
* Given an unspent tx output, this function retrieves its descriptor. | ||
*/ | ||
getScriptPubKeysByUtxo({ network, utxo }) { | ||
getDescriptor({ utxo }) { | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
@@ -558,35 +499,30 @@ const split = utxo.split(':'); | ||
throw new Error(`Error: invalid utxo: ${utxo}`); | ||
const txHex = this.discoveryInfo[networkId].txInfoRecords[txId]?.txHex; | ||
const txHex = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap[txId]?.txHex; | ||
if (!txHex) | ||
throw new Error(`Error: txHex not found for ${utxo}`); | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const expressions = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveExpressions(this.discoveryInfo, networkId); | ||
const scriptPubKeys = []; | ||
expressions.forEach(expression => { | ||
const scriptPubKeyInfoRecords = descriptors[expression]?.scriptPubKeyInfoRecords || | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
const descriptors = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUsedDescriptors(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), networkId); | ||
let output; | ||
descriptors.forEach(descriptor => { | ||
const range = descriptorMap[descriptor]?.range || | ||
{}; | ||
Object.keys(scriptPubKeyInfoRecords).forEach(indexStr => { | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
if (this.getUtxosByScriptPubKey({ | ||
expression, | ||
index, | ||
network | ||
Object.keys(range).forEach(indexStr => { | ||
const isRanged = indexStr !== 'non-ranged'; | ||
const index = isRanged && Number(indexStr); | ||
if (this.getUtxosAndBalance({ | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}).utxos.includes(utxo)) { | ||
scriptPubKeys.push({ expression, index, vout, txHex }); | ||
if (output) | ||
throw new Error(`output {${descriptor}, ${index}} is already represented by {${output.descriptor}, ${output.index}} .`); | ||
output = { | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}; | ||
} | ||
}); | ||
}); | ||
return scriptPubKeys; | ||
return output; | ||
} | ||
/** | ||
* Retrieves the current state of discovery information. This information | ||
* includes details about transactions, descriptors, and network-specific | ||
* details that are stored during the wallet discovery process. | ||
* | ||
* @returns The current state of the discovery information. | ||
*/ | ||
getDiscoveryInfo() { | ||
return this.discoveryInfo; | ||
} | ||
/** | ||
* Retrieves the Explorer instance. | ||
@@ -600,16 +536,141 @@ * | ||
} | ||
_Discovery_derivers = new WeakMap(), _Discovery_instances = new WeakSet(), _Discovery_ensureScriptPubKeyUniqueness = function _Discovery_ensureScriptPubKeyUniqueness({ networkId, scriptPubKey }) { | ||
const descriptors = this.discoveryInfo[networkId].descriptors; | ||
const expressions = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveExpressions(this.discoveryInfo, networkId); | ||
expressions.forEach(expression => { | ||
const scriptPubKeyInfoRecords = descriptors[expression]?.scriptPubKeyInfoRecords || | ||
_Discovery_derivers = new WeakMap(), _Discovery_discoveryData = new WeakMap(), _Discovery_instances = new WeakSet(), _Discovery_ensureScriptPubKeyUniqueness = function _Discovery_ensureScriptPubKeyUniqueness({ networkId, scriptPubKey }) { | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
const descriptors = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUsedDescriptors(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), networkId); | ||
descriptors.forEach(descriptor => { | ||
const range = descriptorMap[descriptor]?.range || | ||
{}; | ||
Object.keys(scriptPubKeyInfoRecords).forEach(indexStr => { | ||
Object.keys(range).forEach(indexStr => { | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
if (scriptPubKey.equals(__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveScriptPubKey(networkId, expression, index) //This will be very fast (uses memoization) | ||
if (scriptPubKey.equals(__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveScriptPubKey(networkId, descriptor, index) //This will be very fast (uses memoization) | ||
)) { | ||
throw new Error(`The provided scriptPubKey is already set: ${expression}, ${index}.`); | ||
throw new Error(`The provided scriptPubKey is already set: ${descriptor}, ${index}.`); | ||
} | ||
}); | ||
}); | ||
}, _Discovery_fetchOutput = | ||
/** | ||
* 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. | ||
*/ | ||
async function _Discovery_fetchOutput({ descriptor, index }) { | ||
if ((typeof index !== 'undefined' && descriptor.indexOf('*') === -1) || | ||
(typeof index === 'undefined' && descriptor.indexOf('*') !== -1)) | ||
throw new Error(`Pass index for ranged descriptors`); | ||
const internalIndex = typeof index === 'number' ? index : 'non-ranged'; | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const scriptPubKey = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveScriptPubKey(networkId, (0, deriveData_1.canonicalize)(descriptor, network), internalIndex); | ||
//https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes | ||
const scriptHash = Buffer.from(bitcoinjs_lib_1.crypto.sha256(scriptPubKey)) | ||
.reverse() | ||
.toString('hex'); | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
const range = discoveryData[networkId].descriptorMap[descriptor]?.range; | ||
if (!range) | ||
throw new Error(`unset range ${networkId}:${descriptor}`); | ||
const outputData = range[internalIndex]; | ||
if (!outputData) { | ||
__classPrivateFieldGet(this, _Discovery_instances, "m", _Discovery_ensureScriptPubKeyUniqueness).call(this, { networkId, scriptPubKey }); | ||
range[internalIndex] = { txIds: [], fetching: true, timeFetched: 0 }; | ||
} | ||
else | ||
outputData.fetching = true; | ||
}), "f"); | ||
const txHistoryArray = await explorer.fetchTxHistory({ | ||
scriptHash | ||
}); | ||
//console.log('TRACE', { scriptHash, txHistoryArray }); | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
// Update txMap | ||
const txMap = discoveryData[networkId].txMap; | ||
txHistoryArray.forEach(({ txId, irreversible, blockHeight }) => { | ||
const txData = txMap[txId]; | ||
if (!txData) { | ||
txMap[txId] = { irreversible, blockHeight }; | ||
} | ||
else { | ||
txData.irreversible = irreversible; | ||
txData.blockHeight = blockHeight; | ||
} | ||
}); | ||
//Update descriptorMap | ||
const range = discoveryData[networkId].descriptorMap[descriptor]?.range; | ||
if (!range) | ||
throw new Error(`unset range ${networkId}:${descriptor}`); | ||
const outputData = range[internalIndex]; | ||
if (!outputData) | ||
throw new Error(`outputData unset with fetching:true`); | ||
const txIds = txHistoryArray.map(txHistory => txHistory.txId); | ||
outputData.fetching = false; | ||
outputData.timeFetched = now(); | ||
if (!(0, shallow_equal_1.shallowEqualArrays)(txIds, outputData.txIds)) | ||
outputData.txIds = txIds; | ||
}), "f"); | ||
return !!txHistoryArray.length; | ||
}, _Discovery_fetchTxs = | ||
/** | ||
* 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. | ||
*/ | ||
async function _Discovery_fetchTxs() { | ||
const txHexRecords = {}; | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const networkData = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId]; | ||
for (const descriptor in networkData.descriptorMap) { | ||
const range = networkData.descriptorMap[descriptor]?.range || []; | ||
for (const index in range) { | ||
const txIds = range[index]?.txIds; | ||
if (!txIds) | ||
throw new Error(`Error: cannot retrieve txs for nonexising scriptPubKey: ${networkId}, ${descriptor}, ${index}`); | ||
for (const txId of txIds) | ||
if (!networkData.txMap[txId]?.txHex) | ||
txHexRecords[txId] = await explorer.fetchTx(txId); | ||
} | ||
} | ||
if (Object.keys(txHexRecords).length) { | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
for (const txId in txHexRecords) { | ||
const txHex = txHexRecords[txId]; | ||
if (!txHex) | ||
throw new Error(`txHex not retrieved for ${txId}`); | ||
const txData = discoveryData[networkId].txMap[txId]; | ||
if (!txData) | ||
throw new Error(`txData does not exist for ${txId}`); | ||
txData.txHex = txHex; | ||
} | ||
}), "f"); | ||
} | ||
}, _Discovery_ensureFetched = function _Discovery_ensureFetched({ descriptor, index, descriptors }) { | ||
if ((descriptor && descriptors) || !(descriptor || descriptors)) | ||
throw new Error(`Pass descriptor or descriptors`); | ||
if (typeof index !== 'undefined' && | ||
(descriptors || !descriptor?.includes('*'))) | ||
throw new Error(`Don't pass index`); | ||
if (descriptors) | ||
descriptors.forEach(descriptor => { | ||
if (!this.whenFetched({ descriptor })) | ||
throw new Error(`Cannot derive data from ${descriptor} since it has not been previously fetched`); | ||
}); | ||
else if (descriptor && | ||
!this.whenFetched({ descriptor, ...(index ? { index } : {}) })) | ||
throw new Error(`Cannot derive data from ${descriptor}/${index} since it has not been previously fetched`); | ||
}; | ||
@@ -616,0 +677,0 @@ return { Discovery }; |
import { DiscoveryFactory, DiscoveryInstance } from './discovery'; | ||
export { DiscoveryFactory, DiscoveryInstance }; | ||
export * from './types'; | ||
export { OutputCriteria, TxStatus, Account, Utxo } from './types'; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DiscoveryFactory = void 0; | ||
exports.TxStatus = exports.DiscoveryFactory = void 0; | ||
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com | ||
@@ -22,2 +8,3 @@ // Distributed under the MIT software license | ||
Object.defineProperty(exports, "DiscoveryFactory", { enumerable: true, get: function () { return discovery_1.DiscoveryFactory; } }); | ||
__exportStar(require("./types"), exports); | ||
var types_1 = require("./types"); | ||
Object.defineProperty(exports, "TxStatus", { enumerable: true, get: function () { return types_1.TxStatus; } }); |
@@ -0,1 +1,25 @@ | ||
export type OutputCriteria = { | ||
/** | ||
* 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>; | ||
/** | ||
* Specifies the filtering criteria based on transaction status: | ||
* `TxStatus.ALL`, `TxStatus.IRREVERSIBLE`, or `TxStatus.CONFIRMED`. | ||
* @defaultValue TxStatus.ALL | ||
*/ | ||
txStatus?: TxStatus; | ||
}; | ||
/** | ||
@@ -27,5 +51,9 @@ * Enumeration of network identifiers. | ||
/** | ||
* Type definition for Unspent Transaction Output. Format: `${txId}:${vout}`. | ||
*/ | ||
export type Utxo = string; | ||
/** | ||
* Type definition for Transaction Information. | ||
*/ | ||
export type TxInfo = { | ||
export type TxData = { | ||
/** | ||
@@ -47,7 +75,8 @@ * The block height. | ||
*/ | ||
export type ScriptPubKeyInfo = { | ||
export type OutputData = { | ||
/** | ||
* Array of transaction IDs. | ||
* Array of transaction IDs associated with an output. | ||
*/ | ||
txIds: Array<TxId>; | ||
fetching: boolean; | ||
/** | ||
@@ -62,9 +91,9 @@ * UNIX timestamp of the last time Explorer.fetchTxHistory was called for | ||
*/ | ||
export type Expression = string; | ||
export type Descriptor = string; | ||
/** | ||
* Represents an account. Accounts are descriptors pairs with keyPaths | ||
* ending in `{/0/*, /1/*}`. Per convention, in @bitcoinerlab an account is | ||
* ending in `{/0/*, /1/*}`. Per convention, in BitcoinerLab an account is | ||
* identified by its external descriptor `keyPath = /0/*`. | ||
*/ | ||
export type Account = Expression; | ||
export type Account = Descriptor; | ||
/** | ||
@@ -79,8 +108,8 @@ * Represents the descriptor index for a ranged descriptor (number) or marks | ||
* @property {number} timeFetched - UNIX timestamp of the last fetch, 0 if never fetched. | ||
* @property {Record<DescriptorIndex, ScriptPubKeyInfo>} scriptPubKeyInfoRecords - Records of ScriptPubKeyInfo. | ||
* @property {Record<DescriptorIndex, OutputData>} range - Records of OutputData. | ||
*/ | ||
export type DescriptorInfo = { | ||
export type DescriptorData = { | ||
fetching: boolean; | ||
timeFetched: number; | ||
scriptPubKeyInfoRecords: Record<DescriptorIndex, ScriptPubKeyInfo>; | ||
range: Record<DescriptorIndex, OutputData>; | ||
}; | ||
@@ -94,11 +123,11 @@ /** | ||
*/ | ||
export type NetworkInfo = { | ||
export type NetworkData = { | ||
/** | ||
*Records of DescriptorInfo. | ||
*Records of DescriptorData. | ||
*/ | ||
descriptors: Record<Expression, DescriptorInfo>; | ||
descriptorMap: Record<Descriptor, DescriptorData>; | ||
/** | ||
*Records of TxInfo. | ||
*Records of TxData. | ||
*/ | ||
txInfoRecords: Record<TxId, TxInfo>; | ||
txMap: Record<TxId, TxData>; | ||
}; | ||
@@ -108,6 +137,2 @@ /** | ||
*/ | ||
export type DiscoveryInfo = Record<NetworkId, NetworkInfo>; | ||
/** | ||
* Type definition for Unspent Transaction Output. Format: `${txId}:${vout}`. | ||
*/ | ||
export type Utxo = string; | ||
export type DiscoveryData = Record<NetworkId, NetworkData>; |
"use strict"; | ||
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com | ||
// Distributed under the MIT software license | ||
// Example of a discoveryInfo object: | ||
// const discoveryInfo = { | ||
// ['TESTNET']: /*NetworkInfo*/ { | ||
// descriptors: { | ||
// "pkh([73c5da0a/44'/0'/0']xpub6B.../0/*)": /*DescriptorInfo*/ { | ||
// | ||
// | ||
// const discoveryData: DiscoveryData = Record<NetworkId, NetworkData> { | ||
// ['TESTNET']: /*NetworkData: {descriptorMap, txMap}*/ { | ||
// descriptorMap: Record<string, DescriptorData> { | ||
// "pkh([73c5da0a/44'/0'/0']xpub6B.../0/*)": DescriptorData { | ||
// fetching: true, | ||
// timeFetched: UNIXTIME_IN_SECONDS, | ||
// scriptPubKeyInfoRecords: { | ||
// range: Record<DescriptorIndex, OutputData>{ //DescriptorIndex = number|'non-ranged' | ||
// //this is the index in ranged-descriptors. Use "non-ranged" if non-ranged | ||
// 12: /*ScriptPubKeyInfo*/ { | ||
// 12: OutputData { | ||
// txIds: /*Array<TxId>*/['8923a3830d9c2eac01043ec30e75b0b2b7264697660f8f...'], | ||
// fetching: true, | ||
// timeFetched: UNIXTIME_IN_SECONDS | ||
@@ -20,4 +22,4 @@ // } | ||
// }, | ||
// txInfoRecords: { | ||
// ['8923a3830d9c2eac01043ec30e75b0b2b7264697660f8f615c0483']: /*TxInfo*/ { | ||
// txMap: Record<TxId, TxData> { | ||
// ['8923a3830d9c2eac01043ec30e75b0b2b7264697660f8f615c0483']: TxData { | ||
// blockHeight: 0, | ||
@@ -24,0 +26,0 @@ // irreversible: false, |
@@ -5,3 +5,3 @@ { | ||
"homepage": "https://github.com/bitcoinerlab/discovery", | ||
"version": "0.1.0", | ||
"version": "1.0.0", | ||
"author": "Jose-Luis Landabaso", | ||
@@ -47,3 +47,3 @@ "license": "MIT", | ||
"dependencies": { | ||
"@bitcoinerlab/descriptors": "^1.0.2", | ||
"@bitcoinerlab/descriptors": "^2.0.1", | ||
"@bitcoinerlab/explorer": "^0.1.0", | ||
@@ -50,0 +50,0 @@ "@bitcoinerlab/secp256k1": "^1.0.5", |
113
README.md
@@ -7,3 +7,3 @@ # Bitcoin Descriptor Blockchain Retrieval Library | ||
- **Descriptor-Based Data Retrieval:** Retrieves UTXOs, transaction history, and balances for various sources: ranged descriptors, accounts (comprising internal & external descriptors), and addresses (a descriptor specialized for a specific index). | ||
- **Descriptor-Based Data Retrieval:** Retrieves transaction history for various sources, including: ranged descriptors, accounts (comprising internal & external descriptors), and addresses (a descriptor specialized for a specific index). | ||
@@ -46,4 +46,3 @@ - **Transaction Status Filter:** Offers the ability to filter results by `TxStatus`: `ALL` (including transactions in the mempool), `CONFIRMED` (assuming one confirmation) and `IRREVERSIBLE` (for transactions with more than a user-defined number of confirmations). | ||
protocol: 'ssl', // 'ssl' and 'tcp' allowed | ||
network: networks.testnet // Specify the server's network; defaults to 'mainnet' if not specified | ||
network: networks.testnet // Specify the server's network; defaults to networks.bitcoin (mainnet) | ||
}); | ||
@@ -57,58 +56,108 @@ ``` | ||
3. **Create the Discovery Class**: | ||
After creating the explorer client instance, you can create the `Discovery` class, which you will use to query the Blockchain. The `Discovery` class is created using the `DiscoveryFactory` function, passing the previously created explorer instance. | ||
After creating the explorer client instance, you can create the `Discovery` class, which you will use to query the Blockchain. The `Discovery` class is created using the [`DiscoveryFactory` function](https://bitcoinerlab.com/modules/discovery/api/functions/DiscoveryFactory.html), passing the previously created explorer instance. | ||
```typescript | ||
import { DiscoveryFactory } from '@bitcoinerlab/discovery'; | ||
const { Discovery } = DiscoveryFactory(explorer); // where 'explorer' corresponds to | ||
// 'esploraExplorer' or 'electrumExplorer' above | ||
const { Discovery } = DiscoveryFactory(explorer, network); | ||
// where 'explorer' corresponds to 'esploraExplorer' or 'electrumExplorer' above | ||
await explorer.connect(); | ||
const discovery = new Discovery(); | ||
// Perform discovery operations... | ||
await explorer.close(); | ||
``` | ||
The `Discovery` constructor accepts an optional object with two properties that are crucial for managing the application's memory usage: | ||
The [`Discovery` constructor](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#constructor), `new Discovery({ descriptorsCacheSize, outputsPerDescriptorCacheSize })`, accepts an optional object with two properties that are crucial for managing the application's memory usage: | ||
- `expressionsCacheSize`: This property represents the cache size limit for descriptor expressions. The cache, implemented using memoizers, serves a dual purpose: it speeds up data queries by avoiding unnecessary recomputations, and it helps maintain immutability. Reaching the limit of the cache size may lead to a loss of immutability and the returned reference may change. This is not a critical issue, as the data is still correct, but it may trigger extra renders in the UI. The default value is 1000, and you can set it to 0 for unbounded caches. | ||
- `indicesPerExpressionCacheSize`: This property represents the cache size limit for indices per expression, related to the number of addresses in ranged descriptor expressions. Similar to the `expressionsCacheSize`, reaching the limit of this cache size may lead to the same immutability challenges. The default value is 10000, and you can set it to 0 for unbounded caches. | ||
- `descriptorsCacheSize`: This property represents the cache size limit for descriptor expressions. The cache, implemented using memoizers, serves a dual purpose: it speeds up data derivation by avoiding unnecessary recomputations, and it helps maintain immutability. Reaching the limit of the cache size may lead to a loss of immutability and the returned reference may change. This is not a critical issue, as the returned data is still correct, but it may trigger extra renders in the UI. The default value is `1000`, and you can set it to `0` for unbounded caches. | ||
- `outputsPerDescriptorCacheSize`: This property represents the cache size limit for indices per expression, related to the number of addresses in ranged descriptor expressions. Similar to the `descriptorsCacheSize`, reaching the limit of this cache size may lead to the same immutability challenges. The default value is `10000`, and you can set it to `0` for unbounded caches. | ||
It is important to note that the default values for `expressionsCacheSize` and `indicesPerExpressionCacheSize` should be sufficient for most projects. However, if you expect to work with a large number of descriptor expressions or addresses, you may need to adjust these values accordingly. Conversely, for projects that require minimal resources, you may consider reducing these values to conserve memory. | ||
It's noteworthy that the default settings for `descriptorsCacheSize` and `outputsPerDescriptorCacheSize` are adequate for most use cases. Yet, for projects handling a large volume of descriptor expressions or addresses, increasing these limits may be necessary. On the flip side, if conserving memory is a priority, particularly for projects with minimal resource needs, consider lowering these values. | ||
**Note**: The `connect` method must be run before starting any data queries to the blockchain, and the `close` method should be run after you have completed all necessary queries and no longer need to query the blockchain. | ||
4. **Using the Discovery Methods** | ||
Once you've instantiated the `Discovery` class, you have access to [a variety of methods](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#fetch) to fetch and derive blockchain data from *Bitcoin Output Descriptors*. | ||
Once you've instantiated the `Discovery` class, you can leverage its methods to interact with blockchain data. | ||
Descriptor expressions are a simple language used to describe collections of Bitcoin output scripts. They enable the `Discovery` class to fetch detailed blockchain information about specific outputs. For more comprehensive insights into descriptor expressions, refer to the [BitcoinerLab descriptors module](https://bitcoinerlab.com/modules/descriptors). | ||
To initiate (or update) the data retrieval process for addresses associated with a descriptor, whether ranged or fixed, execute [`fetch`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#fetch): | ||
For instance, if you want to fetch all the addresses from a ranged descriptor expression, execute: | ||
```typescript | ||
await discovery.fetch({ descriptor }); | ||
``` | ||
This method retrieves all associated outputs for a given descriptor. If the descriptor is ranged, you can also specify an index to target a specific output within that range. When dealing with multiple descriptors, use the `descriptors` parameter with an array of strings. See the [`fetch` API documentation](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#fetch) for detailed usage. | ||
**Note**: To ensure accurate data computations, fetch descriptor data (using the query above) before employing methods like `getUtxos`, `getBalance`, or others described below. An error will alert you when attempting to derive data from descriptors that have not been previously fetched. This ensures you do not compute data based on incomplete information. If you are unsure whether a descriptor has been previously fetched or need to ensure that the data is up-to-date, use [`whenFetched`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#whenFetched): | ||
```typescript | ||
await discovery.discover({ expressions, network, gapLimit: 3 }); | ||
const { utxos, balance } = discovery.getUtxos({ expressions, network }); | ||
const fetchStatus = discovery.whenFetched({ descriptor }); | ||
if (fetchStatus === undefined) { | ||
// The descriptor has not been fetched. | ||
} else { | ||
const secondsSinceFetched = (Date.now() - fetchStatus.timeFetched * 1000) / 1000; | ||
if (secondsSinceFetched > SOME_TIME_THRESHOLD) { | ||
// The descriptor data is outdated and may need to be fetched again. | ||
} | ||
} | ||
``` | ||
In this context, the term `expressions` can be a single string or an array of strings. These expressions represent [descriptor expressions](https://bitcoinerlab.com/modules/descriptors). If an expression is ranged, it will retrieve all the related `scriptPubKeys`. Subsequently, you can obtain the UTXOs and balance for that particular expression using the subsequent line. | ||
If fetch status is verified or known, proceed directly to the data derivation methods: | ||
Other beneficial methods include: | ||
- **Getting the Next Index**: | ||
If you're dealing with ranged descriptor expressions and want to determine the next available (unused) index, use: | ||
- **Deriving UTXOs**: | ||
Use [`getUtxos`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#getUtxos) to derive all unspent transaction outputs (UTXOs) from the fetched data: | ||
```typescript | ||
const index = discovery.getNextIndex({ expression, network }); | ||
const { utxos } = discovery.getUtxos({ descriptor }); | ||
``` | ||
- **Fetching ScriptPubKeys by UTXO**: | ||
This method is essential post-discovery. For a given UTXO, it yields all possible `scriptPubKeys` and related data that can consume the specified UTXO. It's worth noting that this method returns an array since multiple valid descriptor expressions might refer to the same output. | ||
- **Calculating Balance**: | ||
Use [`getBalance`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#getBalance) to calculate the total balance from the fetched data: | ||
```typescript | ||
discovery.getScriptPubKeysByUtxo({ utxo, network }); | ||
// This yields: Array<{ expression, index, vout, txHex }> | ||
const { balance } = discovery.getBalance({ descriptor }); | ||
``` | ||
This function is particularly useful when crafting a transaction capable of expending the UTXO, especially when paired with the @bitcoinerlab/descriptors library. | ||
- **Reviewing Transaction History**: | ||
To inspect all transactions associated with a specific descriptor expression (or an array of them), use: | ||
Other methods to derive or calculate data include: | ||
- **Determining the Next Index**: | ||
For ranged descriptor expressions, determine the next unused index: | ||
```typescript | ||
const history = discovery.getHistory({ expressions, network }); | ||
const index = discovery.getNextIndex({ descriptor }); | ||
``` | ||
See the [`getNextIndex` API documentation](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#getNextIndex) for detailed usage. | ||
- **Identifying Descriptors by UTXO**: | ||
Find the descriptor that corresponds to a specific UTXO using [`getDescriptor`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#getDescriptor): | ||
```typescript | ||
const descriptorData = discovery.getDescriptor({ utxo }); | ||
// Returns: { descriptor, index? }, with 'index' provided for ranged descriptors. | ||
``` | ||
This is particularly useful for transaction preparation when you need to instantiate a `new Output({ descriptor })` using the descriptor associated with the UTXO, as facilitated by the [@bitcoinerlab/descriptors](https://bitcoinerlab.com/modules/descriptors) library. | ||
- **Accessing Transaction History**: | ||
Access all transactions associated with a specific descriptor expression (or an array of them): | ||
```typescript | ||
const history = discovery.getHistory({ descriptors }); | ||
``` | ||
Refer to the [`getHistory` API](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#getHistory) for the details. | ||
- **Fetching Standard Accounts**: | ||
The [`fetchStandardAccounts`](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html#fetchStandardAccounts) method is a helper that automates the common task of retrieving or updating standard accounts (pkh, sh(wpkh), wpkh) associated with a master node. This method saves developers time and eliminates repetitive coding tasks. | ||
Efficiently retrieve wallet accounts with: | ||
```typescript | ||
await discovery.fetchStandardAccounts({ | ||
masterNode, | ||
gapLimit: 20, // The default gap limit | ||
onAccountUsed: (account) => { | ||
// Optional: Trigger app updates when an account with transactions is found. | ||
}, | ||
onAccountChecking: (account) => { | ||
// Optional: Implement app-specific logic when the check for an account begins. | ||
} | ||
}); | ||
``` | ||
Implement the `onAccountUsed` and `onAccountChecking` callbacks as needed for your app's functionality, such as UI updates or logging. | ||
For a comprehensive rundown of all available methods and their descriptions, please consult [the API documentation](https://bitcoinerlab.com/modules/descriptors/api/classes/_Internal_.Discovery.html). | ||
The methods listed above are only a part of all the `Discovery` class's functionality. For a complete overview of all available methods and their usage, refer to [the API documentation](https://bitcoinerlab.com/modules/discovery/api/classes/_Internal_.Discovery.html). | ||
@@ -115,0 +164,0 @@ ## API Documentation |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
108133
1806
0
221
+ Added@bitcoinerlab/descriptors@2.2.1(transitive)
+ Addedlodash.memoize@4.1.2(transitive)
- Removed@bitcoinerlab/descriptors@1.1.1(transitive)