From 45cea188f3a6dc0cddc0d1336428cfde0be12cbe Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 13:47:24 +0200 Subject: [PATCH 01/11] fix: existing tests with latest API versions --- sdk/test/esplora.test.ts | 2 +- sdk/test/ordinal-api.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/test/esplora.test.ts b/sdk/test/esplora.test.ts index 54347601..40d4d6b1 100644 --- a/sdk/test/esplora.test.ts +++ b/sdk/test/esplora.test.ts @@ -132,7 +132,7 @@ describe('Esplora Tests', () => { it('should get fee rate', async () => { const client = new EsploraClient('testnet'); const feeRate = await client.getFeeEstimate(1); - assert.isAtLeast(feeRate, 1); + assert(feeRate > 0); }); it('should get balance', async () => { diff --git a/sdk/test/ordinal-api.test.ts b/sdk/test/ordinal-api.test.ts index 52830321..67184f27 100644 --- a/sdk/test/ordinal-api.test.ts +++ b/sdk/test/ordinal-api.test.ts @@ -72,7 +72,7 @@ describe('Ordinal API Tests', () => { const expectedOutputJson: OutputJson = { value: 10737, script_pubkey: - 'OP_PUSHNUM_1 OP_PUSHBYTES_32 e18a5367c5d11ee31d10bf4c53e743a7479c70e3336e70dbdea1fd927305c022', + '5120e18a5367c5d11ee31d10bf4c53e743a7479c70e3336e70dbdea1fd927305c022', address: 'bc1pux99xe796y0wx8gshax98e6r5arecu8rxdh8pk77587eyuc9cq3q2e3nng', transaction: 'dfe942a58b7e29a3952d8d1ed6608086c66475d20bc7bdbc3d784d616f9a6a7a', sat_ranges: null, From 8027475946d181c84cb2cfc98b8f2c52a8b3b768 Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 13:47:47 +0200 Subject: [PATCH 02/11] chore: typo and formatting --- sdk/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/README.md b/sdk/README.md index adba5bba..ad3a8021 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -24,16 +24,19 @@ or npm i @gobob/bob-sdk ``` -## Building BOB sdk +## Building BOB SDK ### Clone the repository + Clone the repository and change to the `sdk` subfolder. + ```shell git clone git@github.com:bob-collective/bob.git cd bob/sdk ``` ### Install dependencies + We use `pnpm` in the examples below. But the steps below should also work when using `yarn` or `npm` instead. ```shell From 49796e17cd15b62026f3e4faa81d012564b1ebad Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 14:09:32 +0200 Subject: [PATCH 03/11] chore: use mainnet --- sdk/test/esplora.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/test/esplora.test.ts b/sdk/test/esplora.test.ts index 40d4d6b1..58cc813f 100644 --- a/sdk/test/esplora.test.ts +++ b/sdk/test/esplora.test.ts @@ -130,7 +130,7 @@ describe('Esplora Tests', () => { }); it('should get fee rate', async () => { - const client = new EsploraClient('testnet'); + const client = new EsploraClient('mainnet'); const feeRate = await client.getFeeEstimate(1); assert(feeRate > 0); }); From b13d56d184ed7358c464943b1c610a526e6f8094 Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 14:09:56 +0200 Subject: [PATCH 04/11] feat: add better fee estimation --- sdk/src/wallet/utxo.ts | 133 +++++++++++++++++++++++++++++++++++++++-- sdk/test/utxo.test.ts | 41 ++++++++++++- 2 files changed, 167 insertions(+), 7 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 7a882465..f2c8225d 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -39,6 +39,7 @@ export interface Input { * @param publicKey Optional public key needed if using P2SH-P2WPKH. * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). + * @param utxoSelectionStrategy The strategy to use for selecting UTXOs. See https://github.com/paulmillr/scure-btc-signer/tree/main#utxo-selection for options. * @returns {Promise} The Base64 encoded PSBT. */ export async function createBitcoinPsbt( @@ -47,11 +48,12 @@ export async function createBitcoinPsbt( amount: number, publicKey?: string, opReturnData?: string, - confirmationTarget: number = 3 + confirmationTarget: number = 3, + utxoSelectionStrategy: string = 'default' ): Promise { const addressInfo = getAddressInfo(fromAddress); - const network = addressInfo.network; - if (network === 'regtest') { + + if (addressInfo.network === 'regtest') { throw new Error('Bitcoin regtest not supported'); } @@ -80,7 +82,7 @@ export async function createBitcoinPsbt( confirmedUtxos.map(async (utxo) => { const hex = await esploraClient.getTransactionHex(utxo.txid); const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); - const input = getInputFromUtxoAndTx(network, utxo, transaction, addressInfo.type, publicKey); + const input = getInputFromUtxoAndTx(addressInfo.network, utxo, transaction, addressInfo.type, publicKey); possibleInputs.push(input); }) ); @@ -108,12 +110,12 @@ export async function createBitcoinPsbt( // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks // big outputs to small ones, which in the end will create a lot of outputs close to dust. - const transaction = selectUTXO(possibleInputs, outputs, 'default', { + const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy, { changeAddress: fromAddress, // Refund surplus to the payment address feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer bip69: true, // Sort inputs and outputs according to BIP69 createTx: true, // Create the transaction - network: getBtcNetwork(network), + network: getBtcNetwork(addressInfo.network), allowUnknownOutputs: true, // Required for OP_RETURN allowLegacyWitnessUtxo: true, // Required for P2SH-P2WPKH // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -179,3 +181,122 @@ export function getInputFromUtxoAndTx( return input; } + + +/** + * Estimate the tx inclusion fee for a given address or public key with an optional OP_RETURN output. + * + * @param fromAddress The Bitcoin address which is sending to the `toAddress`. + * @param amount The amount of BTC (as satoshis) to send. If no amount is specified, the fee is estimated for all UTXOs, i.e., the max amount. + * @param publicKey Optional public key needed if using P2SH-P2WPKH. + * @param opReturnData Optional OP_RETURN data to include in an output. + * @param confirmationTarget The number of blocks to include this tx (for fee estimation). + * @param utxoSelectionStrategy The strategy to use for selecting UTXOs. See https://github.com/paulmillr/scure-btc-signer/tree/main#utxo-selection for options. + * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. + * + * @dev Wtih no amount set, we estimate the fee for all UTXOs by trying to spend all inputs using strategy 'all'. If an amount is set, we use the 'default + * strategy to select the UTXOs. + */ +export async function estimateTxFee( + fromAddress: string, + amount?: number, + publicKey?: string, + opReturnData?: string, + confirmationTarget: number = 3, + utxoSelectionStrategy: string = 'default' +): Promise { + const addressInfo = getAddressInfo(fromAddress); + + if (addressInfo.network === 'regtest') { + throw new Error('Bitcoin regtest not supported'); + } + + // We need the public key to generate the redeem and witness script to spend the scripts + if (addressInfo.type === (AddressType.p2sh || AddressType.p2wsh)) { + if (!publicKey) { + throw new Error('Public key is required to spend from the selected address type'); + } + } + + // Use the from address as the toAddress for the fee estimate + const toAddress = fromAddress; + + // TODO: allow submitting the UTXOs, fee estimate and confirmed transactions + const esploraClient = new EsploraClient(addressInfo.network); + + const [confirmedUtxos, feeRate] = await Promise.all([ + esploraClient.getAddressUtxos(fromAddress), + esploraClient.getFeeEstimate(confirmationTarget), + ]); + + if (confirmedUtxos.length === 0) { + throw new Error('No confirmed UTXOs'); + } + + // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs + const possibleInputs: Input[] = []; + + await Promise.all( + confirmedUtxos.map(async (utxo) => { + const hex = await esploraClient.getTransactionHex(utxo.txid); + const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); + const input = getInputFromUtxoAndTx(addressInfo.network, utxo, transaction, addressInfo.type, publicKey); + possibleInputs.push(input); + }) + ); + + // Create transaction without outputs + const targetOutputs: Output[] = [ + { + address: toAddress, + amount: BigInt(amount? amount : 0), + }, + ]; + + if (opReturnData) { + // Strip 0x prefix from opReturn + if (opReturnData.startsWith('0x')) { + opReturnData = opReturnData.slice(2); + } + targetOutputs.push({ + // OP_RETURN https://github.com/paulmillr/scure-btc-signer/issues/26 + script: Script.encode(['RETURN', hex.decode(opReturnData)]), + amount: BigInt(0), + }); + } + + let outputs: Output[] = []; + if (amount === undefined) { + // Select all UTXOs if no amount is specified + // Add outputs to the transaction after all UTXOs are selected to prevent tx creation failures + utxoSelectionStrategy = 'all'; + } else { + // Add the target outputs to the transaction + // Tx creation might fail if the requested amount is more than the available balance plus fees + outputs = targetOutputs; + } + + // Outsource UTXO selection to btc-signer + // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection + // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks + // big outputs to small ones, which in the end will create a lot of outputs close to dust. + const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy, { + changeAddress: fromAddress, // Refund surplus to the payment address + feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer + bip69: true, // Sort inputs and outputs according to BIP69 + createTx: true, // Create the transaction + network: getBtcNetwork(addressInfo.network), + allowUnknownOutputs: true, // Required for OP_RETURN + allowLegacyWitnessUtxo: true, // Required for P2SH-P2WPKH + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dust: BigInt(546) as any, // TODO: update scure-btc-signer + }); + + // Add the target outputs after the fact + if (amount === undefined) { + transaction.tx.addOutput(targetOutputs[0]); + transaction.tx.addOutput(targetOutputs[1]); + } + + return transaction.fee; +} \ No newline at end of file diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 95b1e6f4..0ff94c9b 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -2,7 +2,7 @@ import { describe, it, assert } from 'vitest'; import { AddressType, getAddressInfo, Network } from 'bitcoin-address-validation'; import { Address, NETWORK, OutScript, Script, Transaction, p2sh, p2wpkh, selectUTXO } from '@scure/btc-signer'; import { hex, base64 } from '@scure/base'; -import { createBitcoinPsbt, getInputFromUtxoAndTx } from '../src/wallet/utxo'; +import { createBitcoinPsbt, getInputFromUtxoAndTx, estimateTxFee } from '../src/wallet/utxo'; import { TransactionOutput } from '@scure/btc-signer/psbt'; // TODO: Add more tests using https://github.com/paulmillr/scure-btc-signer/tree/5ead71ea9a873d8ba1882a9cd6aa561ad410d0d1/test/bitcoinjs-test/fixtures/bitcoinjs @@ -49,6 +49,8 @@ describe('UTXO Tests', () => { // Use a random public key for P2SH-P2WPKH pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; } + // Note: it is possible that the above addresses have spent all of their funds + // and the transaction will fail. const psbtBase64 = await createBitcoinPsbt(paymentAddress, toAddress, amount, pubkey, opReturn); const transaction = Transaction.fromPSBT(base64.decode(psbtBase64)); @@ -258,4 +260,41 @@ describe('UTXO Tests', () => { assert.isDefined(transaction); }); + + it('should estimate the fee for a transaction', async () => { + // Addresses where randomly picked from blockstream.info + const paymentAddresses = [ + // P2WPKH: https://blockstream.info/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq + 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + // P2SH-P2WPKH: https://blockstream.info/address/3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr + '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', + // P2PKH: https://blockstream.info/address/1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g + '1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g', + ]; + + const amounts = [undefined, 2000, 3000]; + + // EVM address for OP return + let opReturn = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + + // Refactor to execute in parallel + await Promise.all( + amounts.map(async (amount) => Promise.all( + paymentAddresses.map(async (paymentAddress) => { + const paymentAddressType = getAddressInfo(paymentAddress).type; + + let pubkey: string | undefined; + + if (paymentAddressType === AddressType.p2sh) { + // Use a random public key for P2SH-P2WPKH + pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; + } + + // If the amount is undefined, the fee should be estimated + const fee = await estimateTxFee(paymentAddress, undefined, pubkey, opReturn); + assert(fee > 0, 'Fee should be greater than 0'); + } + ) + ))) + }); }); From a4540ca689a033d4bd40dd77e5fa44464e1c883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Sim=C3=A3o?= Date: Mon, 14 Oct 2024 13:28:47 +0100 Subject: [PATCH 05/11] fix(sdk): types --- sdk/src/wallet/utxo.ts | 34 +++++++++++++++++++++++----------- sdk/test/ordinal-api.test.ts | 3 +-- sdk/test/utxo.test.ts | 32 +++++++++++++++++--------------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index f2c8225d..81288470 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -2,6 +2,7 @@ import { Transaction, Script, selectUTXO, TEST_NETWORK, NETWORK, p2wpkh, p2sh } import { hex, base64 } from '@scure/base'; import { AddressType, getAddressInfo, Network } from 'bitcoin-address-validation'; import { EsploraClient, UTXO } from '../esplora'; +import { SelectionStrategy } from '@scure/btc-signer/lib/utxo'; export type BitcoinNetworkName = Exclude; @@ -49,7 +50,7 @@ export async function createBitcoinPsbt( publicKey?: string, opReturnData?: string, confirmationTarget: number = 3, - utxoSelectionStrategy: string = 'default' + utxoSelectionStrategy: SelectionStrategy = 'default' ): Promise { const addressInfo = getAddressInfo(fromAddress); @@ -82,7 +83,13 @@ export async function createBitcoinPsbt( confirmedUtxos.map(async (utxo) => { const hex = await esploraClient.getTransactionHex(utxo.txid); const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); - const input = getInputFromUtxoAndTx(addressInfo.network, utxo, transaction, addressInfo.type, publicKey); + const input = getInputFromUtxoAndTx( + addressInfo.network as BitcoinNetworkName, + utxo, + transaction, + addressInfo.type, + publicKey + ); possibleInputs.push(input); }) ); @@ -182,7 +189,6 @@ export function getInputFromUtxoAndTx( return input; } - /** * Estimate the tx inclusion fee for a given address or public key with an optional OP_RETURN output. * @@ -192,8 +198,8 @@ export function getInputFromUtxoAndTx( * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). * @param utxoSelectionStrategy The strategy to use for selecting UTXOs. See https://github.com/paulmillr/scure-btc-signer/tree/main#utxo-selection for options. - * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. - * + * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. + * * @dev Wtih no amount set, we estimate the fee for all UTXOs by trying to spend all inputs using strategy 'all'. If an amount is set, we use the 'default * strategy to select the UTXOs. */ @@ -203,8 +209,8 @@ export async function estimateTxFee( publicKey?: string, opReturnData?: string, confirmationTarget: number = 3, - utxoSelectionStrategy: string = 'default' -): Promise { + utxoSelectionStrategy: SelectionStrategy = 'default' +): Promise { const addressInfo = getAddressInfo(fromAddress); if (addressInfo.network === 'regtest') { @@ -240,7 +246,13 @@ export async function estimateTxFee( confirmedUtxos.map(async (utxo) => { const hex = await esploraClient.getTransactionHex(utxo.txid); const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); - const input = getInputFromUtxoAndTx(addressInfo.network, utxo, transaction, addressInfo.type, publicKey); + const input = getInputFromUtxoAndTx( + addressInfo.network as BitcoinNetworkName, + utxo, + transaction, + addressInfo.type, + publicKey + ); possibleInputs.push(input); }) ); @@ -249,7 +261,7 @@ export async function estimateTxFee( const targetOutputs: Output[] = [ { address: toAddress, - amount: BigInt(amount? amount : 0), + amount: BigInt(amount ? amount : 0), }, ]; @@ -291,7 +303,7 @@ export async function estimateTxFee( // eslint-disable-next-line @typescript-eslint/no-explicit-any dust: BigInt(546) as any, // TODO: update scure-btc-signer }); - + // Add the target outputs after the fact if (amount === undefined) { transaction.tx.addOutput(targetOutputs[0]); @@ -299,4 +311,4 @@ export async function estimateTxFee( } return transaction.fee; -} \ No newline at end of file +} diff --git a/sdk/test/ordinal-api.test.ts b/sdk/test/ordinal-api.test.ts index 67184f27..b9726174 100644 --- a/sdk/test/ordinal-api.test.ts +++ b/sdk/test/ordinal-api.test.ts @@ -71,8 +71,7 @@ describe('Ordinal API Tests', () => { }); const expectedOutputJson: OutputJson = { value: 10737, - script_pubkey: - '5120e18a5367c5d11ee31d10bf4c53e743a7479c70e3336e70dbdea1fd927305c022', + script_pubkey: '5120e18a5367c5d11ee31d10bf4c53e743a7479c70e3336e70dbdea1fd927305c022', address: 'bc1pux99xe796y0wx8gshax98e6r5arecu8rxdh8pk77587eyuc9cq3q2e3nng', transaction: 'dfe942a58b7e29a3952d8d1ed6608086c66475d20bc7bdbc3d784d616f9a6a7a', sat_ranges: null, diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 0ff94c9b..ab8b278b 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -62,7 +62,7 @@ describe('UTXO Tests', () => { // Get all outputs and add them to array const outputs: TransactionOutput[] = []; - for (var i = 0; i < transaction.outputsLength; i++) { + for (let i = 0; i < transaction.outputsLength; i++) { const output = transaction.getOutput(i); outputs.push(output); @@ -275,26 +275,28 @@ describe('UTXO Tests', () => { const amounts = [undefined, 2000, 3000]; // EVM address for OP return - let opReturn = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + const opReturn = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; // Refactor to execute in parallel await Promise.all( - amounts.map(async (amount) => Promise.all( - paymentAddresses.map(async (paymentAddress) => { - const paymentAddressType = getAddressInfo(paymentAddress).type; + amounts.map(async () => + Promise.all( + paymentAddresses.map(async (paymentAddress) => { + const paymentAddressType = getAddressInfo(paymentAddress).type; - let pubkey: string | undefined; + let pubkey: string | undefined; - if (paymentAddressType === AddressType.p2sh) { - // Use a random public key for P2SH-P2WPKH - pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; - } + if (paymentAddressType === AddressType.p2sh) { + // Use a random public key for P2SH-P2WPKH + pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; + } - // If the amount is undefined, the fee should be estimated - const fee = await estimateTxFee(paymentAddress, undefined, pubkey, opReturn); - assert(fee > 0, 'Fee should be greater than 0'); - } + // If the amount is undefined, the fee should be estimated + const fee = await estimateTxFee(paymentAddress, undefined, pubkey, opReturn); + assert(fee > 0, 'Fee should be greater than 0'); + }) + ) ) - ))) + ); }); }); From bcc5711391db7310695f4dc919f7524350bf6e28 Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 15:05:52 +0200 Subject: [PATCH 06/11] refactor: remove the utxo selection strategy argument --- sdk/src/wallet/utxo.ts | 19 ++++++++++++------- sdk/test/utxo.test.ts | 4 +++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 81288470..16440dda 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -50,10 +50,12 @@ export async function createBitcoinPsbt( publicKey?: string, opReturnData?: string, confirmationTarget: number = 3, - utxoSelectionStrategy: SelectionStrategy = 'default' ): Promise { const addressInfo = getAddressInfo(fromAddress); + // TODO: possibly, allow other strategies to be passed to this function + const utxoSelectionStrategy: SelectionStrategy = 'default'; + if (addressInfo.network === 'regtest') { throw new Error('Bitcoin regtest not supported'); } @@ -130,6 +132,8 @@ export async function createBitcoinPsbt( }); if (!transaction || !transaction.tx) { + console.debug(`fromAddress: ${fromAddress}, toAddress: ${toAddress}, amount: ${amount}`); + console.debug(`publicKey: ${publicKey}, opReturnData: ${opReturnData}`); throw new Error('Failed to create transaction. Do you have enough funds?'); } @@ -209,7 +213,6 @@ export async function estimateTxFee( publicKey?: string, opReturnData?: string, confirmationTarget: number = 3, - utxoSelectionStrategy: SelectionStrategy = 'default' ): Promise { const addressInfo = getAddressInfo(fromAddress); @@ -228,6 +231,7 @@ export async function estimateTxFee( const toAddress = fromAddress; // TODO: allow submitting the UTXOs, fee estimate and confirmed transactions + // to avoid fetching them again. const esploraClient = new EsploraClient(addressInfo.network); const [confirmedUtxos, feeRate] = await Promise.all([ @@ -278,13 +282,14 @@ export async function estimateTxFee( } let outputs: Output[] = []; - if (amount === undefined) { - // Select all UTXOs if no amount is specified - // Add outputs to the transaction after all UTXOs are selected to prevent tx creation failures - utxoSelectionStrategy = 'all'; - } else { + // Select all UTXOs if no amount is specified + // Add outputs to the transaction after all UTXOs are selected to prevent tx creation failures + let utxoSelectionStrategy: SelectionStrategy = 'all'; + if (amount) { // Add the target outputs to the transaction // Tx creation might fail if the requested amount is more than the available balance plus fees + // TODO: allow passing other UTXO selection strategies for fee etimates + utxoSelectionStrategy = 'default'; outputs = targetOutputs; } diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index ab8b278b..af7ec53d 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -14,7 +14,9 @@ describe('UTXO Tests', () => { // P2WPKH: https://blockstream.info/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', // P2SH-P2WPKH: https://blockstream.info/address/3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr - '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', + // TODO: Use a real P2SH-P2WPKH address + // TODO: Add the pubkey to allow spending from the outputs + // '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', // P2PKH: https://blockstream.info/address/1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g '1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g', ]; From 4681435ee9a21352ba617882114153e011de1159 Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 15:10:32 +0200 Subject: [PATCH 07/11] docs: add examples --- sdk/src/wallet/utxo.ts | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 16440dda..decfa863 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -40,8 +40,22 @@ export interface Input { * @param publicKey Optional public key needed if using P2SH-P2WPKH. * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). - * @param utxoSelectionStrategy The strategy to use for selecting UTXOs. See https://github.com/paulmillr/scure-btc-signer/tree/main#utxo-selection for options. * @returns {Promise} The Base64 encoded PSBT. + * + * @example + * ```typescript + * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; + * const toAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; + * const amount = 100000; + * const publicKey = '02d4...`; // only for P2SH + * const opReturnData = 'Hello, World!'; // optional + * const confirmationTarget = 3; // optional + * + * const psbt = await createBitcoinPsbt(fromAddress, toAddress, amount, publicKey, opReturnData, confirmationTarget); + * console.log(psbt); + * + * // The PSBT can then be signed with the private key using sats-wagmi, sats-connect, ... + * ``` */ export async function createBitcoinPsbt( fromAddress: string, @@ -201,8 +215,29 @@ export function getInputFromUtxoAndTx( * @param publicKey Optional public key needed if using P2SH-P2WPKH. * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). - * @param utxoSelectionStrategy The strategy to use for selecting UTXOs. See https://github.com/paulmillr/scure-btc-signer/tree/main#utxo-selection for options. * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. + * + * @example + * ```typescript + * // Using a target amount (call might fail if amount is larger than balance plus fees) + * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; + * const amount = 100000; + * const publicKey = '02d4...`; // only for P2SH + * const opReturnData = 'Hello, World!'; // optional + * const confirmationTarget = 3; // optional + * + * const fee = await estimateTxFee(fromAddress, amount, publicKey, opReturnData, confirmationTarget); + * console.log(fee); + * + * // Using all UTXOs without a target amount (max fee for spending all UTXOs) + * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; + * const publicKey = '02d4...`; // only for P2SH + * const opReturnData = 'Hello, World!'; // optional + * const confirmationTarget = 3; // optional + * + * const fee = await estimateTxFee(fromAddress, undefined, publicKey, opReturnData, confirmationTarget); + * console.log(fee); + * ``` * * @dev Wtih no amount set, we estimate the fee for all UTXOs by trying to spend all inputs using strategy 'all'. If an amount is set, we use the 'default * strategy to select the UTXOs. From 3ef5ec3a9ac936d4440e9e501e15649e15bfaa3c Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 15:11:44 +0200 Subject: [PATCH 08/11] chore: linting --- sdk/src/wallet/utxo.ts | 18 +++++++++--------- sdk/test/utxo.test.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index decfa863..8d289635 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -41,7 +41,7 @@ export interface Input { * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). * @returns {Promise} The Base64 encoded PSBT. - * + * * @example * ```typescript * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; @@ -50,10 +50,10 @@ export interface Input { * const publicKey = '02d4...`; // only for P2SH * const opReturnData = 'Hello, World!'; // optional * const confirmationTarget = 3; // optional - * + * * const psbt = await createBitcoinPsbt(fromAddress, toAddress, amount, publicKey, opReturnData, confirmationTarget); * console.log(psbt); - * + * * // The PSBT can then be signed with the private key using sats-wagmi, sats-connect, ... * ``` */ @@ -63,7 +63,7 @@ export async function createBitcoinPsbt( amount: number, publicKey?: string, opReturnData?: string, - confirmationTarget: number = 3, + confirmationTarget: number = 3 ): Promise { const addressInfo = getAddressInfo(fromAddress); @@ -216,7 +216,7 @@ export function getInputFromUtxoAndTx( * @param opReturnData Optional OP_RETURN data to include in an output. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. - * + * * @example * ```typescript * // Using a target amount (call might fail if amount is larger than balance plus fees) @@ -225,16 +225,16 @@ export function getInputFromUtxoAndTx( * const publicKey = '02d4...`; // only for P2SH * const opReturnData = 'Hello, World!'; // optional * const confirmationTarget = 3; // optional - * + * * const fee = await estimateTxFee(fromAddress, amount, publicKey, opReturnData, confirmationTarget); * console.log(fee); - * + * * // Using all UTXOs without a target amount (max fee for spending all UTXOs) * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; * const publicKey = '02d4...`; // only for P2SH * const opReturnData = 'Hello, World!'; // optional * const confirmationTarget = 3; // optional - * + * * const fee = await estimateTxFee(fromAddress, undefined, publicKey, opReturnData, confirmationTarget); * console.log(fee); * ``` @@ -247,7 +247,7 @@ export async function estimateTxFee( amount?: number, publicKey?: string, opReturnData?: string, - confirmationTarget: number = 3, + confirmationTarget: number = 3 ): Promise { const addressInfo = getAddressInfo(fromAddress); diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index af7ec53d..799e0e3a 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -15,7 +15,7 @@ describe('UTXO Tests', () => { 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', // P2SH-P2WPKH: https://blockstream.info/address/3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr // TODO: Use a real P2SH-P2WPKH address - // TODO: Add the pubkey to allow spending from the outputs + // TODO: Add the pubkey to allow spending from the outputs // '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', // P2PKH: https://blockstream.info/address/1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g '1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g', From 6c4008408a7f6cd9019c3717f7a26f74f81b1dee Mon Sep 17 00:00:00 2001 From: Dominik Harz Date: Mon, 14 Oct 2024 15:27:57 +0200 Subject: [PATCH 09/11] feat: allow passing the fee per byte as a parameter --- sdk/src/wallet/utxo.ts | 48 +++++++++++++++++++++++++++++++----------- sdk/test/utxo.test.ts | 38 +++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 8d289635..f14ac3ba 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -39,6 +39,7 @@ export interface Input { * @param amount The amount of BTC (as satoshis) to send. * @param publicKey Optional public key needed if using P2SH-P2WPKH. * @param opReturnData Optional OP_RETURN data to include in an output. + * @param feeRate Optional fee rate in satoshis per byte. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). * @returns {Promise} The Base64 encoded PSBT. * @@ -63,6 +64,7 @@ export async function createBitcoinPsbt( amount: number, publicKey?: string, opReturnData?: string, + feeRate?: number, confirmationTarget: number = 3 ): Promise { const addressInfo = getAddressInfo(fromAddress); @@ -83,10 +85,16 @@ export async function createBitcoinPsbt( const esploraClient = new EsploraClient(addressInfo.network); - const [confirmedUtxos, feeRate] = await Promise.all([ - esploraClient.getAddressUtxos(fromAddress), - esploraClient.getFeeEstimate(confirmationTarget), - ]); + let confirmedUtxos: UTXO[] = []; + + if (feeRate) { + confirmedUtxos = await esploraClient.getAddressUtxos(fromAddress); + } else { + [confirmedUtxos, feeRate] = await Promise.all([ + esploraClient.getAddressUtxos(fromAddress), + esploraClient.getFeeEstimate(confirmationTarget), + ]); + } if (confirmedUtxos.length === 0) { throw new Error('No confirmed UTXOs'); @@ -148,6 +156,7 @@ export async function createBitcoinPsbt( if (!transaction || !transaction.tx) { console.debug(`fromAddress: ${fromAddress}, toAddress: ${toAddress}, amount: ${amount}`); console.debug(`publicKey: ${publicKey}, opReturnData: ${opReturnData}`); + console.debug(`feeRate: ${feeRate}, confirmationTarget: ${confirmationTarget}`); throw new Error('Failed to create transaction. Do you have enough funds?'); } @@ -214,8 +223,9 @@ export function getInputFromUtxoAndTx( * @param amount The amount of BTC (as satoshis) to send. If no amount is specified, the fee is estimated for all UTXOs, i.e., the max amount. * @param publicKey Optional public key needed if using P2SH-P2WPKH. * @param opReturnData Optional OP_RETURN data to include in an output. + * @param feeRate Optional fee rate in satoshis per byte. * @param confirmationTarget The number of blocks to include this tx (for fee estimation). - * @returns {Promise} The fee amount for estiamted transaction inclusion in satoshis. + * @returns {Promise} The fee amount for estimated transaction inclusion in satoshis. * * @example * ```typescript @@ -224,18 +234,19 @@ export function getInputFromUtxoAndTx( * const amount = 100000; * const publicKey = '02d4...`; // only for P2SH * const opReturnData = 'Hello, World!'; // optional + * const feeRate = 1; // optional * const confirmationTarget = 3; // optional * - * const fee = await estimateTxFee(fromAddress, amount, publicKey, opReturnData, confirmationTarget); + * const fee = await estimateTxFee(fromAddress, amount, publicKey, opReturnData, feeRate, confirmationTarget); * console.log(fee); * * // Using all UTXOs without a target amount (max fee for spending all UTXOs) * const fromAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; * const publicKey = '02d4...`; // only for P2SH * const opReturnData = 'Hello, World!'; // optional - * const confirmationTarget = 3; // optional + * const feeRate = 1; // optional * - * const fee = await estimateTxFee(fromAddress, undefined, publicKey, opReturnData, confirmationTarget); + * const fee = await estimateTxFee(fromAddress, undefined, publicKey, opReturnData, feeRate); * console.log(fee); * ``` * @@ -247,6 +258,7 @@ export async function estimateTxFee( amount?: number, publicKey?: string, opReturnData?: string, + feeRate?: number, confirmationTarget: number = 3 ): Promise { const addressInfo = getAddressInfo(fromAddress); @@ -269,10 +281,15 @@ export async function estimateTxFee( // to avoid fetching them again. const esploraClient = new EsploraClient(addressInfo.network); - const [confirmedUtxos, feeRate] = await Promise.all([ - esploraClient.getAddressUtxos(fromAddress), - esploraClient.getFeeEstimate(confirmationTarget), - ]); + let confirmedUtxos: UTXO[] = []; + if (feeRate) { + confirmedUtxos = await esploraClient.getAddressUtxos(fromAddress); + } else { + [confirmedUtxos, feeRate] = await Promise.all([ + esploraClient.getAddressUtxos(fromAddress), + esploraClient.getFeeEstimate(confirmationTarget), + ]); + } if (confirmedUtxos.length === 0) { throw new Error('No confirmed UTXOs'); @@ -344,6 +361,13 @@ export async function estimateTxFee( dust: BigInt(546) as any, // TODO: update scure-btc-signer }); + if (!transaction || !transaction.tx) { + console.debug(`fromAddress: ${fromAddress}, amount: ${amount}`); + console.debug(`publicKey: ${publicKey}, opReturnData: ${opReturnData}`); + console.debug(`feeRate: ${feeRate}, confirmationTarget: ${confirmationTarget}`); + throw new Error('Failed to create transaction. Do you have enough funds?'); + } + // Add the target outputs after the fact if (amount === undefined) { transaction.tx.addOutput(targetOutputs[0]); diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 799e0e3a..44a621b7 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -269,34 +269,40 @@ describe('UTXO Tests', () => { // P2WPKH: https://blockstream.info/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', // P2SH-P2WPKH: https://blockstream.info/address/3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr - '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', + // TODO: As above, add a correct P2SH-P2WPKH address with its pub key + // '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', // P2PKH: https://blockstream.info/address/1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g '1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g', ]; const amounts = [undefined, 2000, 3000]; + const feeRates = [undefined, 10]; // EVM address for OP return const opReturn = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; // Refactor to execute in parallel await Promise.all( - amounts.map(async () => + feeRates.map(async (feeRate) => Promise.all( - paymentAddresses.map(async (paymentAddress) => { - const paymentAddressType = getAddressInfo(paymentAddress).type; - - let pubkey: string | undefined; - - if (paymentAddressType === AddressType.p2sh) { - // Use a random public key for P2SH-P2WPKH - pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; - } - - // If the amount is undefined, the fee should be estimated - const fee = await estimateTxFee(paymentAddress, undefined, pubkey, opReturn); - assert(fee > 0, 'Fee should be greater than 0'); - }) + amounts.map(async (amount) => + Promise.all( + paymentAddresses.map(async (paymentAddress) => { + const paymentAddressType = getAddressInfo(paymentAddress).type; + + let pubkey: string | undefined; + + if (paymentAddressType === AddressType.p2sh) { + // Use a random public key for P2SH-P2WPKH + pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; + } + + // If the amount is undefined, the fee should be estimated + const fee = await estimateTxFee(paymentAddress, amount, pubkey, opReturn, feeRate); + assert(fee > 0, 'Fee should be greater than 0'); + }) + ) + ) ) ) ); From 6a92bdc69ce33a11eae482de71c1831bfeda593e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Sim=C3=A3o?= Date: Tue, 15 Oct 2024 10:57:40 +0100 Subject: [PATCH 10/11] fix: final --- sdk/src/gateway/client.ts | 1 + sdk/src/gateway/types.ts | 2 ++ sdk/src/utils.ts | 27 --------------------------- sdk/src/wallet/utxo.ts | 8 ++++---- sdk/test/utils.test.ts | 8 ++------ sdk/test/utxo.test.ts | 4 ++++ 6 files changed, 13 insertions(+), 37 deletions(-) diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index 72071d4a..c03e2a46 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -203,6 +203,7 @@ export class GatewayApiClient { gatewayQuote.satoshis, params.fromUserPublicKey, data.opReturnHash, + params.feeRate, gatewayQuote.txProofDifficultyFactor ); } diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index 19965c62..61595e35 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -60,6 +60,8 @@ export interface GatewayQuoteParams { /** @description The percentage of fee charged by partners in Basis Points (BPS) units. This will override the default fee rate configured via platform. 1 BPS = 0.01%. The maximum value is 1000 (which equals 10%). The minimum value is 1 (which equals 0.01%). */ fee?: number; + feeRate?: number; + // NOTE: the following are new fields added by us /** @description Amount of satoshis to swap for ETH */ gasRefill?: number; diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 186e17d9..2e61af6b 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -14,11 +14,6 @@ import { hash256 } from 'bitcoinjs-lib/src/crypto'; * @ignore */ import { Output, Transaction } from 'bitcoinjs-lib/src/transaction'; -/** - * @ignore - */ -import * as bitcoin from 'bitcoinjs-lib'; - /** * @ignore */ @@ -187,25 +182,3 @@ export function getMerkleProof(block: Block, txHash: string, forWitness?: boolea root: merkleAndRoot.root.toString('hex'), }; } - -/** - * Estimate the tx inclusion fee for N P2WPKH inputs and 3 P2WPKH outputs. - * - * @param feeRate - The current rate for inclusion, satoshi per byte. - * @param numInputs - The number of inputs to estimate for. - * @returns The estimated fee for transaction inclusion. - */ -export function estimateTxFee(feeRate: number, numInputs: number = 1) { - const tx = new bitcoin.Transaction(); - for (let i = 0; i < numInputs; i++) { - tx.addInput(Buffer.alloc(32, 0), 0, 0xfffffffd, Buffer.alloc(0)); - } - // https://github.com/interlay/interbtc-clients/blob/6bd3e81d695b93180c5aeae4f33910ad4395ff1a/bitcoin/src/light/wallet.rs#L80 - tx.ins.map((tx_input) => (tx_input.witness = [Buffer.alloc(33 + 32 + 7, 0), Buffer.alloc(33, 0)])); - tx.addOutput(Buffer.alloc(22, 0), 1000); // P2WPKH - tx.addOutput(Buffer.alloc(22, 0), 1000); // P2WPKH (change) - tx.addOutput(bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, Buffer.alloc(20, 0)]), 0); - const vsize = tx.virtualSize(); - const satoshis = feeRate * vsize; - return satoshis; -} diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index f14ac3ba..12082fcf 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -2,7 +2,6 @@ import { Transaction, Script, selectUTXO, TEST_NETWORK, NETWORK, p2wpkh, p2sh } import { hex, base64 } from '@scure/base'; import { AddressType, getAddressInfo, Network } from 'bitcoin-address-validation'; import { EsploraClient, UTXO } from '../esplora'; -import { SelectionStrategy } from '@scure/btc-signer/lib/utxo'; export type BitcoinNetworkName = Exclude; @@ -70,7 +69,7 @@ export async function createBitcoinPsbt( const addressInfo = getAddressInfo(fromAddress); // TODO: possibly, allow other strategies to be passed to this function - const utxoSelectionStrategy: SelectionStrategy = 'default'; + const utxoSelectionStrategy = 'default'; if (addressInfo.network === 'regtest') { throw new Error('Bitcoin regtest not supported'); @@ -336,7 +335,7 @@ export async function estimateTxFee( let outputs: Output[] = []; // Select all UTXOs if no amount is specified // Add outputs to the transaction after all UTXOs are selected to prevent tx creation failures - let utxoSelectionStrategy: SelectionStrategy = 'all'; + let utxoSelectionStrategy = 'all'; if (amount) { // Add the target outputs to the transaction // Tx creation might fail if the requested amount is more than the available balance plus fees @@ -349,7 +348,8 @@ export async function estimateTxFee( // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks // big outputs to small ones, which in the end will create a lot of outputs close to dust. - const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy as any, { changeAddress: fromAddress, // Refund surplus to the payment address feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer bip69: true, // Sort inputs and outputs according to BIP69 diff --git a/sdk/test/utils.test.ts b/sdk/test/utils.test.ts index a6cb994f..18cae26d 100644 --- a/sdk/test/utils.test.ts +++ b/sdk/test/utils.test.ts @@ -1,7 +1,7 @@ +import { Block } from 'bitcoinjs-lib'; import { assert, describe, it } from 'vitest'; import { MAINNET_ESPLORA_BASE_PATH } from '../src/esplora'; -import { Block } from 'bitcoinjs-lib'; -import { estimateTxFee, getMerkleProof } from '../src/utils'; +import { getMerkleProof } from '../src/utils'; describe('Utils Tests', () => { // NOTE: this is a bit flaky due to slow response times from electrs @@ -19,8 +19,4 @@ describe('Utils Tests', () => { root: '7cee5e99c8f0fc25fb115b7d7d00befca61f59a8544adaf3980f52132baf61ae', }); }); - - it('should estimate fee', async () => { - assert.equal(estimateTxFee(1), 172); - }); }); diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 44a621b7..a9fc673e 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -87,17 +87,20 @@ describe('UTXO Tests', () => { // Check the transfer script to the toAddress } else if (output.amount === BigInt(amount)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const scriptDecoded = OutScript.decode(output.script!) as any; // Remove "p2" from the address type as it's exluced in the OutScript type assert.equal(scriptDecoded.type, addressType.slice(2)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const address = Address(NETWORK).decode(toAddress) as any; assert.deepEqual(scriptDecoded.hash, address.hash); // Check the possible change output } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const scriptDecoded = OutScript.decode(output.script!) as any; // Remove "p2" from the address type as it's exluced in the OutScript type @@ -256,6 +259,7 @@ describe('UTXO Tests', () => { network: NETWORK, allowUnknownOutputs: true, allowLegacyWitnessUtxo: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any dust: BigInt(546) as any, } ); From 7c4edde72c9312da2a0f987e7b8a69a354b3cb98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Sim=C3=A3o?= Date: Tue, 15 Oct 2024 12:14:30 +0100 Subject: [PATCH 11/11] fix: code review --- sdk/src/wallet/utxo.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 12082fcf..573cd2f5 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -294,21 +294,18 @@ export async function estimateTxFee( throw new Error('No confirmed UTXOs'); } - // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs - const possibleInputs: Input[] = []; - - await Promise.all( + const possibleInputs = await Promise.all( confirmedUtxos.map(async (utxo) => { const hex = await esploraClient.getTransactionHex(utxo.txid); const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); - const input = getInputFromUtxoAndTx( + + return getInputFromUtxoAndTx( addressInfo.network as BitcoinNetworkName, utxo, transaction, addressInfo.type, publicKey ); - possibleInputs.push(input); }) ); @@ -322,7 +319,7 @@ export async function estimateTxFee( if (opReturnData) { // Strip 0x prefix from opReturn - if (opReturnData.startsWith('0x')) { + if (opReturnData.toLowerCase().startsWith('0x')) { opReturnData = opReturnData.slice(2); } targetOutputs.push({