@bitcoinerlab/discovery
Advanced tools
Comparing version 1.0.4 to 1.1.0
/// <reference types="node" /> | ||
import memoizee from 'memoizee'; | ||
import { NetworkId, Descriptor, Account, DescriptorIndex, DiscoveryData, TxStatus, DescriptorData, TxId, TxData } from './types'; | ||
import { NetworkId, Descriptor, Account, DescriptorIndex, DiscoveryData, TxStatus, DescriptorData, TxAttribution, TxId, TxData } from './types'; | ||
import { Transaction, Network } from 'bitcoinjs-lib'; | ||
@@ -12,2 +12,3 @@ export declare function canonicalize(descriptorOrDescriptors: Array<Descriptor> | Descriptor, network: Network): string | string[]; | ||
deriveUtxosAndBalanceByOutput: (networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptor: Descriptor, index: DescriptorIndex, txStatus: TxStatus) => { | ||
stxos: string[]; | ||
utxos: string[]; | ||
@@ -17,2 +18,3 @@ balance: number; | ||
deriveUtxosAndBalance: (networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptorOrDescriptors: Array<Descriptor> | Descriptor, txStatus: TxStatus) => { | ||
stxos: string[]; | ||
utxos: string[]; | ||
@@ -24,5 +26,22 @@ balance: number; | ||
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[]; | ||
deriveHistoryByOutput: (withAttributions: boolean, networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptor: Descriptor, index: DescriptorIndex, txStatus: TxStatus) => TxData[] | { | ||
ins: ({ | ||
ownedPrevTxo: string; | ||
value: number; | ||
} | { | ||
ownedPrevTxo: string | false; | ||
value?: never; | ||
})[]; | ||
outs: { | ||
ownedTxo: string | false; | ||
value: number; | ||
}[]; | ||
netReceived: number; | ||
type: "CONSOLIDATED" | "RECEIVED" | "SENT" | "RECEIVED_AND_SENT"; | ||
txId: string; | ||
irreversible: boolean; | ||
blockHeight: number; | ||
}[]; | ||
deriveHistory: (withAttributions: boolean, networkId: NetworkId, txMap: Record<TxId, TxData>, descriptorMap: Record<Descriptor, DescriptorData>, descriptorOrDescriptors: Array<Descriptor> | Descriptor, txStatus: TxStatus) => TxData[] | TxAttribution[]; | ||
transactionFromHex: typeof Transaction.fromHex & memoizee.Memoized<typeof Transaction.fromHex>; | ||
}; |
@@ -84,9 +84,12 @@ "use strict"; | ||
const deriveScriptPubKey = (networkId, descriptor, index) => deriveScriptPubKeyFactory(networkId)(descriptor)(index); | ||
const coreDeriveUtxosByOutput = (networkId, descriptor, index, txDataArray, txStatus) => { | ||
const coreDeriveTxosByOutput = (networkId, descriptor, index, txDataArray, txStatus) => { | ||
const scriptPubKey = deriveScriptPubKey(networkId, descriptor, index); | ||
const allOutputs = []; | ||
const spentOutputs = []; | ||
//All prev outputs (spent or unspent) sent to this output descriptor: | ||
const allPrevOutputs = []; | ||
//all outputs in txDataArray which have been spent. | ||
//May be outputs NOT snt to thil output descriptor: | ||
const spendingTxIdByOutput = {}; //Means: Utxo was spent in txId | ||
//Note that txDataArray cannot be assumed to be in correct order. See: | ||
//https://github.com/Blockstream/esplora/issues/165#issuecomment-1584471718 | ||
//TODO: but we should guarantee same order always so use txId as second order criteria? | ||
//TODO: but we should guarantee same order always so use txId as second order criteria? - probably not needed? | ||
for (const txData of txDataArray) { | ||
@@ -106,5 +109,6 @@ if (txStatus === types_1.TxStatus.ALL || | ||
//Note we create a new Buffer since reverse() mutates the Buffer | ||
const inputId = Buffer.from(input.hash).reverse().toString('hex'); | ||
const spentOutputKey = `${inputId}:${input.index}`; | ||
spentOutputs.push(spentOutputKey); | ||
const prevTxId = Buffer.from(input.hash).reverse().toString('hex'); | ||
const prevVout = input.index; | ||
const prevUtxo = `${prevTxId}:${prevVout}`; | ||
spendingTxIdByOutput[prevUtxo] = `${txId}:${vin}`; //prevUtxo was spent by txId in input vin | ||
} | ||
@@ -117,3 +121,3 @@ for (let vout = 0; vout < tx.outs.length; vout++) { | ||
const outputKey = `${txId}:${vout}`; | ||
allOutputs.push(outputKey); | ||
allPrevOutputs.push(outputKey); | ||
} | ||
@@ -123,24 +127,37 @@ } | ||
} | ||
// UTXOs are those in allOutputs that are not in spentOutputs | ||
const utxos = allOutputs.filter(output => !spentOutputs.includes(output)); | ||
return utxos; | ||
// UTXOs are those in allPrevOutputs that have not been spent | ||
const utxos = allPrevOutputs.filter(output => !Object.keys(spendingTxIdByOutput).includes(output)); | ||
const stxos = allPrevOutputs | ||
.filter(output => Object.keys(spendingTxIdByOutput).includes(output)) | ||
.map(txo => `${txo}:${spendingTxIdByOutput[txo]}`); | ||
return { utxos, stxos }; | ||
}; | ||
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 | ||
// coreDeriveUtxosByOutput shares all params wrt the parent | ||
// coreDeriveTxosByOutput 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 deriveUtxosByOutput = (0, memoizers_1.memoizeOneWithShallowArraysCheck)(coreDeriveUtxosByOutput); | ||
// As soon as txDataArray in coreDeriveTxosByOutput changes, | ||
// it will resets its memory. | ||
const deriveTxosByOutput = (0, memoizee_1.default)(coreDeriveTxosByOutput, { | ||
max: 1 | ||
}); | ||
let lastUtxos = null; | ||
let lastStxos = null; | ||
let lastBalance; | ||
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 }; | ||
let { utxos, stxos } = deriveTxosByOutput(networkId, descriptor, index, txDataArray, txStatus); | ||
let balance; | ||
if (lastStxos && (0, shallow_equal_1.shallowEqualArrays)(lastStxos, stxos)) | ||
stxos = lastStxos; | ||
if (lastUtxos && (0, shallow_equal_1.shallowEqualArrays)(lastUtxos, utxos)) { | ||
utxos = lastUtxos; | ||
balance = lastBalance; | ||
} | ||
else | ||
balance = coreDeriveUtxosBalance(txMap, utxos); | ||
lastUtxos = utxos; | ||
lastBalance = coreDeriveUtxosBalance(txMap, utxos); | ||
return { utxos, balance: lastBalance }; | ||
lastStxos = stxos; | ||
lastBalance = balance; | ||
return { stxos, utxos, balance }; | ||
}, { max: 1 }); | ||
@@ -166,6 +183,96 @@ }, { primitive: true, max: outputsPerDescriptorCacheSize }), { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
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) => { | ||
const deriveAttributions = (txHistory, networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus) => { | ||
const { utxos, stxos } = deriveUtxosAndBalance(networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
//Suposedly Set.has is faster than Array.includes: | ||
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#performance | ||
const txoSet = new Set([ | ||
...utxos, | ||
...stxos.map(stxo => { | ||
const [txId, voutStr] = stxo.split(':'); | ||
if (txId === undefined || voutStr === undefined) { | ||
throw new Error(`Undefined txId or vout for STXO: ${stxo}`); | ||
} | ||
return `${txId}:${voutStr}`; | ||
}) | ||
]); | ||
return txHistory.map(txData => { | ||
const { txHex, irreversible, blockHeight } = txData; | ||
if (!txHex) | ||
throw new Error(`Error: txHex not found`); | ||
const tx = transactionFromHex(txHex); | ||
const txId = tx.getId(); | ||
const ins = tx.ins.map(input => { | ||
const prevTxId = Buffer.from(input.hash).reverse().toString('hex'); | ||
const prevVout = input.index; | ||
const prevTxo = `${prevTxId}:${prevVout}`; | ||
const ownedPrevTxo = txoSet.has(prevTxo) | ||
? prevTxo | ||
: false; | ||
if (ownedPrevTxo) { | ||
const prevTxHex = txMap[prevTxId]?.txHex; | ||
if (!prevTxHex) | ||
throw new Error(`txHex not set for ${prevTxId}`); | ||
const prevTx = transactionFromHex(prevTxHex); | ||
const value = prevTx.outs[prevVout]?.value; | ||
if (value === undefined) | ||
throw new Error(`value should exist for ${prevTxId}:${prevVout}`); | ||
return { ownedPrevTxo, value }; | ||
} | ||
else | ||
return { ownedPrevTxo }; | ||
}); | ||
const outs = tx.outs.map((output, vout) => { | ||
const txo = `${txId}:${vout}`; | ||
const value = output.value; | ||
const ownedTxo = txoSet.has(txo) ? txo : false; | ||
return { ownedTxo, value }; | ||
}); | ||
let netReceived = 0; | ||
//What I receive in my descriptors: | ||
for (const output of outs) | ||
netReceived += output.ownedTxo ? output.value : 0; | ||
//What i send from my descriptors: | ||
for (const input of ins) { | ||
if (input.ownedPrevTxo) { | ||
const value = input.value; | ||
if (value === undefined) | ||
throw new Error('input.value should be defined for ownedPrevTxo'); | ||
netReceived -= value; | ||
} | ||
} | ||
const allInputsOwned = ins.every(input => input.ownedPrevTxo); | ||
const someInputsOwned = ins.some(input => input.ownedPrevTxo); | ||
const allOutputsOwned = outs.every(output => output.ownedTxo); | ||
const someOutputsNotOwned = outs.some(output => !output.ownedTxo); | ||
const someOutputsOwned = outs.some(output => output.ownedTxo); | ||
const someInputsNotOwned = ins.some(input => !input.ownedPrevTxo); | ||
let type; | ||
if (allInputsOwned && allOutputsOwned) | ||
type = 'CONSOLIDATED'; | ||
else if (someInputsNotOwned && | ||
someInputsOwned && | ||
someOutputsNotOwned && | ||
someOutputsOwned) | ||
type = 'RECEIVED_AND_SENT'; | ||
else if (someInputsOwned && someOutputsNotOwned) | ||
type = 'SENT'; | ||
else if (someInputsNotOwned && someOutputsOwned) | ||
type = 'RECEIVED'; | ||
else | ||
throw new Error('Transaction type could not be determined.'); | ||
return { | ||
ins, | ||
outs, | ||
netReceived, | ||
type, | ||
txId, | ||
irreversible, | ||
blockHeight | ||
}; | ||
}); | ||
}; | ||
const deriveHistoryByOutputFactory = (0, memoizee_1.default)((withAttributions) => (0, memoizee_1.default)((networkId) => (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 || | ||
const txAllHistory = deriveTxDataArray(txMap, descriptorMap, descriptor, index); | ||
const txHistory = txAllHistory.filter(txData => txStatus === types_1.TxStatus.ALL || | ||
(txStatus === types_1.TxStatus.IRREVERSIBLE && | ||
@@ -175,2 +282,6 @@ txData.irreversible) || | ||
txData.blockHeight !== 0)); | ||
if (withAttributions) | ||
return deriveAttributions(txHistory, networkId, txMap, descriptorMap, descriptor, txStatus); | ||
else | ||
return txHistory; | ||
}); | ||
@@ -181,6 +292,8 @@ }, { | ||
}), { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
), { primitive: true } //unbounced cache for networkId | ||
), { primitive: true } //unbounded cache (no max setting) since withAttributions is space is 2 | ||
); | ||
const deriveHistoryByOutput = (txMap, descriptorMap, descriptor, index, txStatus) => deriveHistoryByOutputFactory(txStatus)(descriptor)(index)(txMap, descriptorMap); | ||
const coreDeriveHistory = (descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const history = []; | ||
const deriveHistoryByOutput = (withAttributions, networkId, txMap, descriptorMap, descriptor, index, txStatus) => deriveHistoryByOutputFactory(withAttributions)(networkId)(txStatus)(descriptor)(index)(txMap, descriptorMap); | ||
const coreDeriveHistory = (withAttributions, networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const txHistory = []; | ||
const descriptorArray = Array.isArray(descriptorOrDescriptors) | ||
@@ -195,3 +308,8 @@ ? descriptorOrDescriptors | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
history.push(...deriveHistoryByOutput(txMap, descriptorMap, descriptor, index, txStatus)); | ||
txHistory.push(...deriveHistoryByOutput( | ||
//Derive the normal txHistory without attributions (false). | ||
//This will be enhanced later if withAttributions is set. | ||
//Note that deriveAttributions uses txHistory (normal history) | ||
//as input | ||
false, networkId, txMap, descriptorMap, descriptor, index, txStatus)); | ||
}); | ||
@@ -201,16 +319,35 @@ } | ||
//and sort again by blockHeight | ||
const dedupedHistory = [...new Set(history)]; | ||
//since we have txs belonging to different expressions let's try to oder | ||
//them. Note that we cannot guarantee to keep correct order to txs | ||
const dedupedHistory = [...new Set(txHistory)]; | ||
//since we have txs belonging to different expressions let's try to order | ||
//them from old to new (blockHeight ascending order). | ||
//Note that we cannot guarantee to keep correct order to txs | ||
//that belong to the same 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); | ||
//TODO: but we should guarantee same order always so use txId as second order criteria? - probably not needed? | ||
const sortedHistory = dedupedHistory.sort((txDataA, txDataB) => { | ||
if (txDataA.blockHeight === 0 && txDataB.blockHeight === 0) { | ||
return 0; // Both are in mempool, keep their relative order unchanged | ||
} | ||
if (txDataA.blockHeight === 0) { | ||
return 1; // txDataA is in mempool, so it should come after txDataB | ||
} | ||
if (txDataB.blockHeight === 0) { | ||
return -1; // txDataB is in mempool, so it should come after txDataA | ||
} | ||
return txDataA.blockHeight - txDataB.blockHeight; // Regular ascending order sort | ||
}); | ||
if (withAttributions) | ||
return deriveAttributions(sortedHistory, networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
else | ||
return sortedHistory; | ||
}; | ||
const deriveHistoryFactory = (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptorOrDescriptors) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txMap, descriptorMap) => coreDeriveHistory(descriptorMap, txMap, descriptorOrDescriptors, txStatus)); | ||
const deriveHistoryFactory = (0, memoizee_1.default)((withAttributions) => (0, memoizee_1.default)((networkId) => (0, memoizee_1.default)((txStatus) => (0, memoizee_1.default)((descriptorOrDescriptors) => { | ||
return (0, memoizers_1.memoizeOneWithShallowArraysCheck)((txMap, descriptorMap) => coreDeriveHistory(withAttributions, networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus)); | ||
}, { primitive: true, max: descriptorsCacheSize }), { primitive: true } //unbounded cache (no max setting) since Search Space is small | ||
), { primitive: true } //unbounded cache for NetworkId | ||
), { primitive: true } //unbounded cache (no max setting) since withAttributions is space is 2 | ||
); | ||
const deriveHistory = (txMap, descriptorMap, descriptorOrDescriptors, txStatus) => deriveHistoryFactory(txStatus)(descriptorOrDescriptors)(txMap, descriptorMap); | ||
const coreDeriveUtxos = (networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const deriveHistory = (withAttributions, networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus) => deriveHistoryFactory(withAttributions)(networkId)(txStatus)(descriptorOrDescriptors)(txMap, descriptorMap); | ||
const coreDeriveTxos = (networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus) => { | ||
const utxos = []; | ||
const stxos = []; | ||
const descriptorArray = Array.isArray(descriptorOrDescriptors) | ||
@@ -225,3 +362,5 @@ ? descriptorOrDescriptors | ||
const index = indexStr === 'non-ranged' ? indexStr : Number(indexStr); | ||
utxos.push(...deriveUtxosAndBalanceByOutput(networkId, txMap, descriptorMap, descriptor, index, txStatus).utxos); | ||
const { utxos: utxosByO, stxos: stxosByO } = deriveUtxosAndBalanceByOutput(networkId, txMap, descriptorMap, descriptor, index, txStatus); | ||
utxos.push(...utxosByO); | ||
stxos.push(...stxosByO); | ||
}); | ||
@@ -232,3 +371,4 @@ } | ||
const dedupedUtxos = [...new Set(utxos)]; | ||
return dedupedUtxos; | ||
const dedupedStxos = [...new Set(stxos)]; | ||
return { utxos: dedupedUtxos, stxos: dedupedStxos }; | ||
}; | ||
@@ -241,10 +381,19 @@ //unbound memoizee wrt TxStatus is fine since it has a small Search Space | ||
let lastUtxos = null; | ||
let lastStxos = null; | ||
let lastBalance; | ||
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 }; | ||
let { utxos, stxos } = coreDeriveTxos(networkId, descriptorMap, txMap, descriptorOrDescriptors, txStatus); | ||
let balance; | ||
if (lastStxos && (0, shallow_equal_1.shallowEqualArrays)(lastStxos, stxos)) | ||
stxos = lastStxos; | ||
if (lastUtxos && (0, shallow_equal_1.shallowEqualArrays)(lastUtxos, utxos)) { | ||
utxos = lastUtxos; | ||
balance = lastBalance; | ||
} | ||
else | ||
balance = coreDeriveUtxosBalance(txMap, utxos); | ||
lastUtxos = utxos; | ||
lastBalance = coreDeriveUtxosBalance(txMap, utxos); | ||
return { utxos, balance: lastBalance }; | ||
lastStxos = stxos; | ||
lastBalance = balance; | ||
return { stxos, utxos, balance }; | ||
}, { max: 1 }); | ||
@@ -269,3 +418,3 @@ }, { primitive: true, max: descriptorsCacheSize } //potentially ininite search space. limit to 100 descriptorOrDescriptors per txStatus combination | ||
const [txId, voutStr] = utxo.split(':'); | ||
if (!txId || !voutStr) | ||
if (txId === undefined || voutStr === undefined) | ||
throw new Error(`Undefined txId or vout for UTXO: ${utxo}`); | ||
@@ -272,0 +421,0 @@ const vout = parseInt(voutStr); |
@@ -6,3 +6,3 @@ /// <reference types="node" /> | ||
import type { Explorer } from '@bitcoinerlab/explorer'; | ||
import { OutputCriteria, NetworkId, TxId, TxHex, TxData, Descriptor, Account, DiscoveryData, Utxo, TxStatus } from './types'; | ||
import { OutputCriteria, NetworkId, TxId, TxData, Descriptor, Account, DiscoveryData, Utxo, TxStatus, Stxo, TxHex, TxAttribution } from './types'; | ||
/** | ||
@@ -324,2 +324,4 @@ * Creates and returns a Discovery class for discovering funds in a Bitcoin network | ||
* one or more descriptor expressions and transaction status. | ||
* In addition it also retrieves spent transaction outputs (STXOS) which correspond | ||
* to previous UTXOs that have been spent. | ||
* | ||
@@ -340,5 +342,8 @@ * This method is useful for accessing the available funds for specific | ||
* scriptPubKeys and the total balance of these UTXOs. | ||
* It also returns previous UTXOs that had been | ||
* eventually spent as stxos: Array<Stxo> | ||
*/ | ||
getUtxosAndBalance({ descriptor, index, descriptors, txStatus }: OutputCriteria): { | ||
utxos: Array<Utxo>; | ||
stxos: Array<Stxo>; | ||
balance: number; | ||
@@ -382,17 +387,41 @@ }; | ||
* | ||
* This method is useful for accessing transaction records associated with one or more | ||
* descriptor expressions and transaction status. | ||
* This method accesses transaction records associated with descriptor expressions | ||
* and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryData. The method | ||
* uses memoization to maintain the same object reference for the returned result, given | ||
* the same input parameters, as long as the corresponding transaction records in | ||
* discoveryData haven't changed. | ||
* When `withAttributions` is `false`, it returns an array of historical transactions | ||
* (`Array<TxData>`). See {@link TxData TxData}. | ||
* | ||
* This can be useful in environments such as React where preserving object identity can | ||
* prevent unnecessary re-renders. | ||
* To determine if each transaction corresponds to a sent/received transaction, set | ||
* `withAttributions` to `true`. | ||
* | ||
* @param outputCriteria | ||
* @returns An array containing transaction info associated with the descriptor expressions. | ||
* When `withAttributions` is `true`, this function returns an array of | ||
* {@link TxAttribution TxAttribution} elements. | ||
* | ||
* `TxAttribution` identifies the owner of the previous output for each input and | ||
* the owner of the output for each transaction. | ||
* | ||
* This is useful in wallet applications to specify whether inputs are from owned | ||
* outputs (e.g., change from a previous transaction) or from third parties. It | ||
* also specifies if outputs are destined to third parties or are internal change. | ||
* This helps wallet apps show transaction history with "Sent" or "Received" labels, | ||
* considering only transactions with third parties. | ||
* | ||
* See {@link TxAttribution TxAttribution} for a complete list of items returned per | ||
* transaction. | ||
* | ||
* The return value is computed based on the current state of `discoveryData`. The | ||
* method uses memoization to maintain the same object reference for the returned | ||
* result, given the same input parameters, as long as the corresponding transaction | ||
* records in `discoveryData` haven't changed. | ||
* | ||
* This can be useful in environments such as React, where preserving object identity | ||
* can prevent unnecessary re-renders. | ||
* | ||
* @param outputCriteria - Criteria for selecting transaction outputs, including descriptor | ||
* expressions, transaction status, and whether to include attributions. | ||
* @param withAttributions - Whether to include attributions in the returned data. | ||
* @returns An array containing transaction information associated with the descriptor | ||
* expressions. | ||
*/ | ||
getHistory({ descriptor, index, descriptors, txStatus }: OutputCriteria): Array<TxData>; | ||
getHistory({ descriptor, index, descriptors, txStatus }: OutputCriteria, withAttributions?: boolean): Array<TxData> | Array<TxAttribution>; | ||
/** | ||
@@ -442,9 +471,18 @@ * Retrieves the hexadecimal representation of a transaction (TxHex) from the | ||
/** | ||
* Given an unspent tx output, this function retrieves its descriptor. | ||
* Given an unspent tx output, this function retrieves its descriptor (if still unspent). | ||
* Alternatively, pass a txo (any transaction output, which may have been | ||
* spent already or not) and this function will also retrieve its descriptor. | ||
* txo can be in any of these formats: `${txId}:${vout}` or | ||
* using its extended form: `${txId}:${vout}:${recipientTxId}:${recipientVin}` | ||
* | ||
* This query can be quite slow so use wisely. | ||
* | ||
* Returns the descriptor (and index if ranged) or undefined if not found. | ||
*/ | ||
getDescriptor({ utxo }: { | ||
getDescriptor({ utxo, txo }: { | ||
/** | ||
* The UTXO. | ||
*/ | ||
utxo: Utxo; | ||
utxo?: Utxo; | ||
txo?: Utxo; | ||
}): { | ||
@@ -451,0 +489,0 @@ descriptor: Descriptor; |
@@ -315,2 +315,4 @@ "use strict"; | ||
* one or more descriptor expressions and transaction status. | ||
* In addition it also retrieves spent transaction outputs (STXOS) which correspond | ||
* to previous UTXOs that have been spent. | ||
* | ||
@@ -331,2 +333,4 @@ * This method is useful for accessing the available funds for specific | ||
* scriptPubKeys and the total balance of these UTXOs. | ||
* It also returns previous UTXOs that had been | ||
* eventually spent as stxos: Array<Stxo> | ||
*/ | ||
@@ -337,3 +341,3 @@ getUtxosAndBalance({ descriptor, index, descriptors, txStatus = types_1.TxStatus.ALL }) { | ||
...(descriptors ? { descriptors } : {}), | ||
...(index ? { index } : {}) | ||
...(index !== undefined ? { index } : {}) | ||
}); | ||
@@ -389,3 +393,3 @@ if ((descriptor && descriptors) || !(descriptor || descriptors)) | ||
let index = 0; | ||
while (__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByOutput(txMap, descriptorMap, (0, deriveData_1.canonicalize)(descriptor, network), index, txStatus).length) | ||
while (__classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByOutput(false, networkId, txMap, descriptorMap, (0, deriveData_1.canonicalize)(descriptor, network), index, txStatus).length) | ||
index++; | ||
@@ -397,17 +401,41 @@ return index; | ||
* | ||
* This method is useful for accessing transaction records associated with one or more | ||
* descriptor expressions and transaction status. | ||
* This method accesses transaction records associated with descriptor expressions | ||
* and transaction status. | ||
* | ||
* The return value is computed based on the current state of discoveryData. The method | ||
* uses memoization to maintain the same object reference for the returned result, given | ||
* the same input parameters, as long as the corresponding transaction records in | ||
* discoveryData haven't changed. | ||
* When `withAttributions` is `false`, it returns an array of historical transactions | ||
* (`Array<TxData>`). See {@link TxData TxData}. | ||
* | ||
* This can be useful in environments such as React where preserving object identity can | ||
* prevent unnecessary re-renders. | ||
* To determine if each transaction corresponds to a sent/received transaction, set | ||
* `withAttributions` to `true`. | ||
* | ||
* @param outputCriteria | ||
* @returns An array containing transaction info associated with the descriptor expressions. | ||
* When `withAttributions` is `true`, this function returns an array of | ||
* {@link TxAttribution TxAttribution} elements. | ||
* | ||
* `TxAttribution` identifies the owner of the previous output for each input and | ||
* the owner of the output for each transaction. | ||
* | ||
* This is useful in wallet applications to specify whether inputs are from owned | ||
* outputs (e.g., change from a previous transaction) or from third parties. It | ||
* also specifies if outputs are destined to third parties or are internal change. | ||
* This helps wallet apps show transaction history with "Sent" or "Received" labels, | ||
* considering only transactions with third parties. | ||
* | ||
* See {@link TxAttribution TxAttribution} for a complete list of items returned per | ||
* transaction. | ||
* | ||
* The return value is computed based on the current state of `discoveryData`. The | ||
* method uses memoization to maintain the same object reference for the returned | ||
* result, given the same input parameters, as long as the corresponding transaction | ||
* records in `discoveryData` haven't changed. | ||
* | ||
* This can be useful in environments such as React, where preserving object identity | ||
* can prevent unnecessary re-renders. | ||
* | ||
* @param outputCriteria - Criteria for selecting transaction outputs, including descriptor | ||
* expressions, transaction status, and whether to include attributions. | ||
* @param withAttributions - Whether to include attributions in the returned data. | ||
* @returns An array containing transaction information associated with the descriptor | ||
* expressions. | ||
*/ | ||
getHistory({ descriptor, index, descriptors, txStatus = types_1.TxStatus.ALL }) { | ||
getHistory({ descriptor, index, descriptors, txStatus = types_1.TxStatus.ALL }, withAttributions = false) { | ||
if ((descriptor && descriptors) || !(descriptor || descriptors)) | ||
@@ -422,3 +450,3 @@ throw new Error(`Pass descriptor or descriptors`); | ||
...(descriptors ? { descriptors } : {}), | ||
...(index ? { index } : {}) | ||
...(index !== undefined ? { index } : {}) | ||
}); | ||
@@ -428,9 +456,11 @@ const networkId = (0, networks_1.getNetworkId)(network); | ||
const txMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap; | ||
let txDataArray = []; | ||
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); | ||
txDataArray = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistoryByOutput(withAttributions, networkId, txMap, descriptorMap, descriptorOrDescriptors, internalIndex, txStatus); | ||
} | ||
else | ||
return __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistory(txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
txDataArray = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveHistory(withAttributions, networkId, txMap, descriptorMap, descriptorOrDescriptors, txStatus); | ||
return txDataArray; | ||
} | ||
@@ -455,3 +485,3 @@ /** | ||
if (!txHex) | ||
throw new Error(`Error: txHex not found`); | ||
throw new Error(`Error: txHex not found for ${txId} while getting TxHex`); | ||
return txHex; | ||
@@ -482,21 +512,37 @@ } | ||
/** | ||
* Given an unspent tx output, this function retrieves its descriptor. | ||
* Given an unspent tx output, this function retrieves its descriptor (if still unspent). | ||
* Alternatively, pass a txo (any transaction output, which may have been | ||
* spent already or not) and this function will also retrieve its descriptor. | ||
* txo can be in any of these formats: `${txId}:${vout}` or | ||
* using its extended form: `${txId}:${vout}:${recipientTxId}:${recipientVin}` | ||
* | ||
* This query can be quite slow so use wisely. | ||
* | ||
* Returns the descriptor (and index if ranged) or undefined if not found. | ||
*/ | ||
getDescriptor({ utxo }) { | ||
getDescriptor({ utxo, txo }) { | ||
if (utxo && txo) | ||
throw new Error('Pass either txo or utxo, not both'); | ||
if (utxo) | ||
txo = utxo; | ||
const networkId = (0, networks_1.getNetworkId)(network); | ||
const split = utxo.split(':'); | ||
if (!txo) | ||
throw new Error('Pass either txo or utxo'); | ||
const split = txo.split(':'); | ||
if (split.length !== 2) | ||
throw new Error(`Error: invalid utxo: ${utxo}`); | ||
throw new Error(`Error: invalid txo: ${txo}`); | ||
const txId = split[0]; | ||
if (!txId) | ||
throw new Error(`Error: invalid utxo: ${utxo}`); | ||
throw new Error(`Error: invalid txo: ${txo}`); | ||
const strVout = split[1]; | ||
if (!strVout) | ||
throw new Error(`Error: invalid utxo: ${utxo}`); | ||
throw new Error(`Error: invalid txo: ${txo}`); | ||
const vout = parseInt(strVout); | ||
if (vout.toString() !== strVout) | ||
throw new Error(`Error: invalid utxo: ${utxo}`); | ||
const txHex = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].txMap[txId]?.txHex; | ||
if (!txHex) | ||
throw new Error(`Error: txHex not found for ${utxo}`); | ||
throw new Error(`Error: invalid txo: ${txo}`); | ||
//const txHex = this.#discoveryData[networkId].txMap[txId]?.txHex; | ||
//if (!txHex) | ||
// throw new Error( | ||
// `Error: txHex not found for ${txo} while looking for its descriptor.` | ||
// ); | ||
const descriptorMap = __classPrivateFieldGet(this, _Discovery_discoveryData, "f")[networkId].descriptorMap; | ||
@@ -511,13 +557,41 @@ const descriptors = __classPrivateFieldGet(this, _Discovery_derivers, "f").deriveUsedDescriptors(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), networkId); | ||
const index = isRanged && Number(indexStr); | ||
if (this.getUtxosAndBalance({ | ||
if (!txo) | ||
throw new Error('txo not defined'); | ||
const { utxos, stxos } = this.getUtxosAndBalance({ | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}).utxos.includes(utxo)) { | ||
if (output) | ||
throw new Error(`output {${descriptor}, ${index}} is already represented by {${output.descriptor}, ${output.index}} .`); | ||
output = { | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}; | ||
}); | ||
if (utxo) { | ||
if (utxos.includes(txo)) { | ||
if (output) | ||
throw new Error(`output {${descriptor}, ${index}} is already represented by {${output.descriptor}, ${output.index}} .`); | ||
output = { | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}; | ||
} | ||
} | ||
else { | ||
//Descriptor txos (Unspent txos and Spent txos). Note that | ||
//stxos have this format: `${txId}:${vout}:${recipientTxId}:${recipientVin}` | ||
//so normalize to Utxo format: | ||
const txoSet = new Set([ | ||
...utxos, | ||
...stxos.map(stxo => { | ||
const [txId, voutStr] = stxo.split(':'); | ||
if (txId === undefined || voutStr === undefined) { | ||
throw new Error(`Undefined txId or vout for STXO: ${stxo}`); | ||
} | ||
return `${txId}:${voutStr}`; | ||
}) | ||
]); | ||
if (txoSet.has(txo)) { | ||
if (output) | ||
throw new Error(`output {${descriptor}, ${index}} is already represented by {${output.descriptor}, ${output.index}} .`); | ||
output = { | ||
descriptor, | ||
...(isRanged ? { index: Number(indexStr) } : {}) | ||
}; | ||
} | ||
} | ||
}); | ||
@@ -631,3 +705,2 @@ }); | ||
}); | ||
//console.log('TRACE', { scriptHash, txHistoryArray }); | ||
__classPrivateFieldSet(this, _Discovery_discoveryData, (0, immer_1.produce)(__classPrivateFieldGet(this, _Discovery_discoveryData, "f"), discoveryData => { | ||
@@ -712,3 +785,6 @@ // Update txMap | ||
else if (descriptor && | ||
!this.whenFetched({ descriptor, ...(index ? { index } : {}) })) | ||
!this.whenFetched({ | ||
descriptor, | ||
...(index !== undefined ? { index } : {}) | ||
})) | ||
throw new Error(`Cannot derive data from ${descriptor}/${index} since it has not been previously fetched`); | ||
@@ -715,0 +791,0 @@ }; |
import { DiscoveryFactory, DiscoveryInstance } from './discovery'; | ||
export { DiscoveryFactory, DiscoveryInstance }; | ||
export { OutputCriteria, TxStatus, Account, Utxo } from './types'; | ||
export { OutputCriteria, TxStatus, Account, Utxo, Stxo, TxAttribution } from './types'; |
@@ -60,2 +60,9 @@ /** | ||
/** | ||
* Type definition for Spent Transaction Output. Format: | ||
* `${txId}:${vout}:${recipientTxId}:${recipientVin}`, | ||
* that is, a previous Utxo ${txId}:${vout} was eventually spent in this tx: | ||
* ${recipientTxId}:${recipientVin} | ||
*/ | ||
export type Stxo = string; | ||
/** | ||
* Type definition for Transaction Information. | ||
@@ -78,2 +85,60 @@ */ | ||
/** | ||
* Represents the attribution details of a transaction. | ||
* | ||
* `TxAttribution` is used to mark the owner of the inputs and outputs for each | ||
* transaction. | ||
* | ||
* This can be used in wallet apps to specify whether inputs are from owned | ||
* outputs (e.g., change in a previous transaction) or come from third parties. | ||
* Similarly, it specifies when outputs are either destined to third parties or | ||
* correspond to internal change. This is useful because wallet apps typically | ||
* show transaction history with "Sent" or "Received" labels, considering only | ||
* ins/outs from third parties. | ||
* | ||
* - `ownedPrevTxo/ownedTxo` indicates the ownership of the previous output/next | ||
* output: | ||
* - `false` if the previous/next output cannot be described by one of the | ||
* owned descriptors. | ||
* - An object containing the descriptor and optional index (for ranged | ||
* descriptors). | ||
* - `value` is the amount received/sent in this input/output. `value` will not | ||
* be set in inputs when inputs are not owned. | ||
* | ||
* - `netReceived` indicates the net amount received by the controlled | ||
* descriptors in this transaction. If > 0, it means funds were received; | ||
* otherwise, funds were sent. | ||
* | ||
* - `type`: | ||
* - `CONSOLIDATED`: ALL inputs and outputs are from/to owned descriptors. | ||
* - `RECEIVED_AND_SENT` if: | ||
* - SOME outputs are NOT owned and SOME inputs are owned, and | ||
* - SOME outputs are owned and SOME inputs are NOT owned. | ||
* This is an edge case that typically won't occur in wallets. | ||
* - `SENT`: | ||
* - if there are SOME outputs NOT owned and SOME inputs are owned. | ||
* - not `RECEIVED_AND_SENT`. | ||
* - `RECEIVED`: | ||
* - if there are SOME outputs owned and SOME inputs are NOT owned. | ||
* - not `RECEIVED_AND_SENT`. | ||
* | ||
* Tip: You can use `getDescriptor({txo: owned})` to see what descriptor | ||
* corresponds to `getDescriptor({txo: ins[x].ownedPrevTxo})` or | ||
* `getDescriptor({txo: outs[y].ownedTxo})`. | ||
*/ | ||
export type TxAttribution = { | ||
txId: TxId; | ||
blockHeight: number; | ||
irreversible: boolean; | ||
ins: Array<{ | ||
ownedPrevTxo: Utxo | false; | ||
value?: number; | ||
}>; | ||
outs: Array<{ | ||
ownedTxo: Utxo | false; | ||
value: number; | ||
}>; | ||
netReceived: number; | ||
type: 'CONSOLIDATED' | 'RECEIVED' | 'SENT' | 'RECEIVED_AND_SENT'; | ||
}; | ||
/** | ||
* Type definition for Script Public Key Information. | ||
@@ -80,0 +145,0 @@ */ |
@@ -5,3 +5,3 @@ { | ||
"homepage": "https://github.com/bitcoinerlab/discovery", | ||
"version": "1.0.4", | ||
"version": "1.1.0", | ||
"author": "Jose-Luis Landabaso", | ||
@@ -47,7 +47,7 @@ "license": "MIT", | ||
"dependencies": { | ||
"@bitcoinerlab/descriptors": "^2.0.1", | ||
"@bitcoinerlab/explorer": "^0.1.2", | ||
"@bitcoinerlab/secp256k1": "^1.0.5", | ||
"@bitcoinerlab/descriptors": "^2.1.0", | ||
"@bitcoinerlab/explorer": "^0.1.3", | ||
"@bitcoinerlab/secp256k1": "^1.1.1", | ||
"@types/memoizee": "^0.4.8", | ||
"bitcoinjs-lib": "^6.1.3", | ||
"bitcoinjs-lib": "^6.1.5", | ||
"immer": "^9.0.21", | ||
@@ -54,0 +54,0 @@ "lodash.clonedeep": "^4.5.0", |
131411
2238
Updatedbitcoinjs-lib@^6.1.5