From 16b604188f6d001e616e99acb855c1ff2b1aa5dc Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 13 Jan 2025 17:07:17 +0000 Subject: [PATCH 1/4] refactor: simplify wallet ord logic Signed-off-by: Gregory Hill --- sdk/src/wallet/utxo.ts | 142 ++++++++++++----------------------------- 1 file changed, 41 insertions(+), 101 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index cc6b008f..08a22e1b 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -17,134 +17,75 @@ export const getBtcNetwork = (name: BitcoinNetworkName) => { type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint }; -class TreeNode { - val: T; - children: TreeNode[]; +const isCardinalOutput = (output: OutputJson) => output.inscriptions.length === 0 && Object.keys(output.runes).length === 0; - constructor(val: T, children: TreeNode[] = []) { - this.val = val; - this.children = children; - } -} - -const isCardinal = (output: OutputJson) => output.inscriptions.length === 0 && Object.keys(output.runes).length === 0; - -const createUtxoNodes = async ( - utxos: UTXO[], - cardinalOutputsSet: Set, - ordinalsClient: OrdinalsClient -): Promise<(null | TreeNode)[]> => { - return Promise.all( - utxos.map | null>>(async (utxo) => { - if (cardinalOutputsSet.has(OutPoint.toString(utxo))) return null; - - const output = await ordinalsClient.getInscriptionsFromOutPoint(utxo); - - return new TreeNode({ - ...utxo, - cardinal: isCardinal(output), - indexed: output.indexed, - }); - }) - ); -}; - -const processNodes = async ( - rootNodes: (TreeNode | null)[], +const isCardinalTx = async ( + txid: string, cardinalOutputsSet: Set, esploraClient: EsploraClient, - ordinalsClient: OrdinalsClient -) => { - const queue = Array.from(rootNodes); - - while (queue.length > 0) { - const childNode = queue.shift(); - - if (childNode === null) continue; - - const transaction = await esploraClient.getTransaction(childNode.val.txid); - - if (transaction.status.confirmed) { - // if confirmed check if it contains ordinals - childNode.val.cardinal = cardinalOutputsSet.has(OutPoint.toString(childNode.val)); - } else if (!childNode.val.indexed || childNode.val.cardinal) { - // if not confirmed check inputs for current utxo - childNode.children = await Promise.all( - transaction.vin.map(async (vin) => { - const output = await ordinalsClient.getInscriptionsFromOutPoint(vin); - - return new TreeNode({ - vout: vin.vout, - txid: vin.txid, - cardinal: isCardinal(output), - indexed: output.indexed, - }); - }) - ); - - queue.push(...childNode.children); + ordinalsClient: OrdinalsClient, + limit: number = 3, +): Promise => { + if (limit === 0) return false; + const transaction = await esploraClient.getTransaction(txid); + const results = await Promise.all(transaction.vin.map(async vin => { + if (cardinalOutputsSet.has(OutPoint.toString(vin))) return true; + + const output = await ordinalsClient.getInscriptionsFromOutPoint(vin); + if (output.indexed) { + return isCardinalOutput(output); + } else { + return isCardinalTx(vin.txid, cardinalOutputsSet, esploraClient, ordinalsClient, limit - 1); } - } -}; - -const checkUtxoNode = (node: TreeNode | null): boolean => { - // if `null` then node is confirmed and indexed and included in cardinals set -> can be spent - if (node === null) return true; - // leaf node either confirmed or contains ordinals - if (node.children.length === 0) return node.val.cardinal; - - return node.children.reduce((acc, child) => acc && checkUtxoNode(child), true); -}; + })); + return results.every(result => result === true); +} -/** - * @ignore - */ -export const _processUtxos = async ( +const findSafeUtxos = async ( utxos: UTXO[], cardinalOutputsSet: Set, esploraClient: EsploraClient, ordinalsClient: OrdinalsClient ): Promise => { - const rootUtxoNodes = await createUtxoNodes(utxos, cardinalOutputsSet, ordinalsClient); + const results = await Promise.all(utxos.map(async (utxo) => { + // the utxo is confirmed and a known cardinal + if (cardinalOutputsSet.has(OutPoint.toString(utxo))) return true; - await processNodes(rootUtxoNodes, cardinalOutputsSet, esploraClient, ordinalsClient); + // the utxo is unconfirmed (not indexed by Ord) + return isCardinalTx(utxo.txid, cardinalOutputsSet, esploraClient, ordinalsClient); + })); - const allowedList = rootUtxoNodes.map(checkUtxoNode); - - return utxos.filter((_, index) => allowedList[index]); + return utxos.filter((_, index) => results[index]); }; -// NOTE: consider refactoring boolean argument -const processUtxos = async (address: string, esploraClient: EsploraClient, checkUtxos = false): Promise => { - const addressInfo = getAddressInfo(address); - - const ordinalsClient = new OrdinalsClient(addressInfo.network); - +const getSafeUtxos = async (address: string, esploraClient: EsploraClient, ordinalsClient: OrdinalsClient): Promise => { const [utxos, cardinalOutputs] = await Promise.all([ + // all utxos including unconfirmed txs esploraClient.getAddressUtxos(address), // cardinal = return UTXOs not containing inscriptions or runes ordinalsClient.getOutputsFromAddress(address, 'cardinal'), ]); - if (checkUtxos && utxos.length === 0) { - throw new Error('No confirmed UTXOs'); - } - const cardinalOutputsSet = new Set(cardinalOutputs.map((output) => output.outpoint)); - return _processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient); + return findSafeUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient); }; const collectPossibleInputs = async (fromAddress: string, publicKey: string) => { const addressInfo = getAddressInfo(fromAddress); const esploraClient = new EsploraClient(addressInfo.network); + const ordinalsClient = new OrdinalsClient(addressInfo.network); - const allowedUtxos = await processUtxos(fromAddress, esploraClient, true); + const safeUtxos = await getSafeUtxos(fromAddress, esploraClient, ordinalsClient); + + if (safeUtxos.length === 0) { + throw new Error('No confirmed UTXOs'); + } // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs return Promise.all( - allowedUtxos.map(async (utxo) => { + safeUtxos.map(async (utxo) => { const hex = await esploraClient.getTransactionHex(utxo.txid); const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); const input = getInputFromUtxoAndTx( @@ -160,8 +101,6 @@ const collectPossibleInputs = async (fromAddress: string, publicKey: string) => ); }; -type OutputNodeData = Pick & { cardinal: boolean; indexed: boolean }; - export interface Input { txid: string; index: number; @@ -492,12 +431,13 @@ export async function getBalance(address?: string) { const addressInfo = getAddressInfo(address); const esploraClient = new EsploraClient(addressInfo.network); + const ordinalsClient = new OrdinalsClient(addressInfo.network); - const allowedUtxos = await processUtxos(address, esploraClient); + const safeUtxos = await getSafeUtxos(address, esploraClient, ordinalsClient); - const total = allowedUtxos.reduce((acc, utxo) => acc + utxo.value, 0); + const total = safeUtxos.reduce((acc, utxo) => acc + utxo.value, 0); - const confirmed = allowedUtxos.reduce((acc, utxo) => { + const confirmed = safeUtxos.reduce((acc, utxo) => { if (utxo.confirmed) { return acc + utxo.value; } From 0e75865af61844b1377499f0d7f7a62788379e6d Mon Sep 17 00:00:00 2001 From: Slava Date: Mon, 13 Jan 2025 20:21:42 +0300 Subject: [PATCH 2/4] chore: run prettier format --- sdk/src/wallet/utxo.ts | 51 +++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 08a22e1b..bc4d235d 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -17,29 +17,32 @@ export const getBtcNetwork = (name: BitcoinNetworkName) => { type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint }; -const isCardinalOutput = (output: OutputJson) => output.inscriptions.length === 0 && Object.keys(output.runes).length === 0; +const isCardinalOutput = (output: OutputJson) => + output.inscriptions.length === 0 && Object.keys(output.runes).length === 0; const isCardinalTx = async ( txid: string, cardinalOutputsSet: Set, esploraClient: EsploraClient, ordinalsClient: OrdinalsClient, - limit: number = 3, + limit: number = 3 ): Promise => { if (limit === 0) return false; const transaction = await esploraClient.getTransaction(txid); - const results = await Promise.all(transaction.vin.map(async vin => { - if (cardinalOutputsSet.has(OutPoint.toString(vin))) return true; - - const output = await ordinalsClient.getInscriptionsFromOutPoint(vin); - if (output.indexed) { - return isCardinalOutput(output); - } else { - return isCardinalTx(vin.txid, cardinalOutputsSet, esploraClient, ordinalsClient, limit - 1); - } - })); - return results.every(result => result === true); -} + const results = await Promise.all( + transaction.vin.map(async (vin) => { + if (cardinalOutputsSet.has(OutPoint.toString(vin))) return true; + + const output = await ordinalsClient.getInscriptionsFromOutPoint(vin); + if (output.indexed) { + return isCardinalOutput(output); + } else { + return isCardinalTx(vin.txid, cardinalOutputsSet, esploraClient, ordinalsClient, limit - 1); + } + }) + ); + return results.every((result) => result === true); +}; const findSafeUtxos = async ( utxos: UTXO[], @@ -47,18 +50,24 @@ const findSafeUtxos = async ( esploraClient: EsploraClient, ordinalsClient: OrdinalsClient ): Promise => { - const results = await Promise.all(utxos.map(async (utxo) => { - // the utxo is confirmed and a known cardinal - if (cardinalOutputsSet.has(OutPoint.toString(utxo))) return true; + const results = await Promise.all( + utxos.map(async (utxo) => { + // the utxo is confirmed and a known cardinal + if (cardinalOutputsSet.has(OutPoint.toString(utxo))) return true; - // the utxo is unconfirmed (not indexed by Ord) - return isCardinalTx(utxo.txid, cardinalOutputsSet, esploraClient, ordinalsClient); - })); + // the utxo is unconfirmed (not indexed by Ord) + return isCardinalTx(utxo.txid, cardinalOutputsSet, esploraClient, ordinalsClient); + }) + ); return utxos.filter((_, index) => results[index]); }; -const getSafeUtxos = async (address: string, esploraClient: EsploraClient, ordinalsClient: OrdinalsClient): Promise => { +const getSafeUtxos = async ( + address: string, + esploraClient: EsploraClient, + ordinalsClient: OrdinalsClient +): Promise => { const [utxos, cardinalOutputs] = await Promise.all([ // all utxos including unconfirmed txs esploraClient.getAddressUtxos(address), From 8c9d875d78e513fd9b34d90ad05acc6d763ac5bb Mon Sep 17 00:00:00 2001 From: Slava Date: Mon, 13 Jan 2025 20:48:38 +0300 Subject: [PATCH 3/4] chore: skip process utxo test --- sdk/test/utxo.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 2e99f50c..e6fd4675 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -517,8 +517,8 @@ describe('UTXO Tests', () => { const balanceData = await getBalance(taprootAddress); - expect(balanceData.total).toBeLessThan(BigInt(total)); - expect(balanceData.confirmed).toBeLessThan(BigInt(confirmed)); + expect(balanceData.total).toEqual(BigInt(total)); + expect(balanceData.confirmed).toEqual(BigInt(confirmed)); } ); @@ -577,7 +577,7 @@ describe('UTXO Tests', () => { } ); - it('processes utxo correctly', { timeout: 50000 }, async () => { + it.skip('processes utxo correctly', { timeout: 50000 }, async () => { const esploraClient = new EsploraClient('mainnet'); const ordinalsClient = new OrdinalsClient('mainnet'); From 03b315e75162658c292c5c202c561176e3bad13e Mon Sep 17 00:00:00 2001 From: Slava Date: Tue, 14 Jan 2025 12:54:47 +0300 Subject: [PATCH 4/4] chore: uncomment test for find safe utxo --- sdk/src/wallet/utxo.ts | 5 ++++- sdk/test/utxo.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index bc4d235d..9226e4f5 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -44,7 +44,10 @@ const isCardinalTx = async ( return results.every((result) => result === true); }; -const findSafeUtxos = async ( +/** + * @ignore + */ +export const findSafeUtxos = async ( utxos: UTXO[], cardinalOutputsSet: Set, esploraClient: EsploraClient, diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index e6fd4675..fb0829b4 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -8,7 +8,7 @@ import { estimateTxFee, Input, getBalance, - _processUtxos, + findSafeUtxos, } from '../src/wallet/utxo'; import { TransactionOutput } from '@scure/btc-signer/psbt'; import { OrdinalsClient, OutPoint } from '../src/ordinal-api'; @@ -577,7 +577,7 @@ describe('UTXO Tests', () => { } ); - it.skip('processes utxo correctly', { timeout: 50000 }, async () => { + it('processes utxo correctly', { timeout: 50000 }, async () => { const esploraClient = new EsploraClient('mainnet'); const ordinalsClient = new OrdinalsClient('mainnet'); @@ -658,7 +658,7 @@ describe('UTXO Tests', () => { return result; }); - const allowedUtxos = await _processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient); + const allowedUtxos = await findSafeUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient); expect(allowedUtxos).toEqual([utxos[0], utxos[1]]); });