From 0722324c34d8a1f431d4cde55cd3962f36ad87ba Mon Sep 17 00:00:00 2001 From: Pete Watters <2938440+pete-watters@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:14:49 +0100 Subject: [PATCH 1/8] chore: prevent runtime error from lottie --- .../features/ledger/animations/plugging-in-cable.lottie.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/ledger/animations/plugging-in-cable.lottie.tsx b/src/app/features/ledger/animations/plugging-in-cable.lottie.tsx index 59604db514a..ceb12981511 100644 --- a/src/app/features/ledger/animations/plugging-in-cable.lottie.tsx +++ b/src/app/features/ledger/animations/plugging-in-cable.lottie.tsx @@ -25,8 +25,8 @@ export default function PluggingInLedgerCableAnimation(props: BoxProps) { return ( - - + + ); From df936ea708c7bdaa3631ce1ce165504f61bbd700 Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Tue, 23 Apr 2024 13:04:04 +0200 Subject: [PATCH 2/8] fix: bitcoin fees underestimation, closes #4777 --- .../coinselect/local-coin-selection.spec.ts | 73 +++++++++++------ .../coinselect/local-coin-selection.ts | 78 ++++++++++++------- .../bitcoin/use-generate-bitcoin-tx.ts | 12 ++- src/app/common/transactions/bitcoin/utils.ts | 18 ++--- .../hooks/use-bitcoin-custom-fee.tsx | 12 +-- ...e-bitcoin-fees-list-multiple-recipients.ts | 4 +- .../use-bitcoin-fees-list.ts | 66 ++++++++-------- .../hooks/use-btc-increase-fee.ts | 18 +++-- .../hooks/use-generate-ordinal-tx.ts | 2 +- .../form/btc/btc-choose-fee.tsx | 1 - .../form/btc/btc-send-form-confirmation.tsx | 53 ++++++++++++- .../bitcoin/transaction/transaction.query.ts | 14 +--- src/shared/utils.ts | 2 +- 13 files changed, 225 insertions(+), 128 deletions(-) diff --git a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts index efe3f638dcb..2db33f7ad8e 100644 --- a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts +++ b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts @@ -21,25 +21,52 @@ const demoUtxos = [ { value: 909 }, ]; +function generate10kSpendWithDummyUtxoSet(recipient: string) { + return determineUtxosForSpend({ + utxos: demoUtxos as any, + amount: 10_000, + feeRate: 20, + recipient, + }); +} + describe(determineUtxosForSpend.name, () => { - function generate10kSpendWithTestData(recipient: string) { - return determineUtxosForSpend({ - utxos: demoUtxos as any, - amount: 10_000, - feeRate: 20, - recipient, + describe('Estimated size', () => { + test('that Native Segwit, 1 input 2 outputs weighs 140 vBytes', () => { + const estimation = determineUtxosForSpend({ + utxos: [{ value: 50_000 }] as any[], + amount: 40_000, + recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m', + feeRate: 20, + }); + console.log(estimation); + expect(estimation.txVBytes).toBeGreaterThan(140); + expect(estimation.txVBytes).toBeLessThan(142); }); - } - describe('sorting algorithm (biggest first and no dust)', () => { + test('that Native Segwit, 2 input 2 outputs weighs 200vBytes', () => { + const estimation = determineUtxosForSpend({ + utxos: [{ value: 50_000 }, { value: 50_000 }] as any[], + amount: 60_000, + recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m', + feeRate: 20, + }); + console.log(estimation); + expect(estimation.txVBytes).toBeGreaterThan(208); + expect(estimation.txVBytes).toBeLessThan(209); + }); + }); + + describe('sorting algorithm', () => { test('that it filters out dust utxos', () => { - const result = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + const result = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + console.log(result); const hasDust = result.filteredUtxos.some(utxo => utxo.value <= BTC_P2WPKH_DUST_AMOUNT); expect(hasDust).toBeFalsy(); }); test('that it sorts utxos in decending order', () => { - const result = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + const result = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); result.inputs.forEach((u, i) => { const nextUtxo = result.inputs[i + 1]; if (!nextUtxo) return; @@ -50,41 +77,41 @@ describe(determineUtxosForSpend.name, () => { test('that it accepts a wrapped segwit address', () => expect(() => - generate10kSpendWithTestData('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH') + generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH') ).not.toThrowError()); test('that it accepts a legacy addresses', () => expect(() => - generate10kSpendWithTestData('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj') + generate10kSpendWithDummyUtxoSet('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj') ).not.toThrowError()); test('that it throws an error with non-legit address', () => { expect(() => - generate10kSpendWithTestData('whoop-de-da-boop-da-de-not-a-bitcoin-address') + generate10kSpendWithDummyUtxoSet('whoop-de-da-boop-da-de-not-a-bitcoin-address') ).toThrowError(); }); test('that given a set of utxos, legacy is more expensive', () => { - const legacy = generate10kSpendWithTestData('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj'); - const segwit = generate10kSpendWithTestData('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); - expect(legacy.fee).toBeGreaterThan(segwit.fee); + const legacy = generate10kSpendWithDummyUtxoSet('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj'); + const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); + expect(legacy.estimatedFee).toBeGreaterThan(segwit.estimatedFee); }); test('that given a set of utxos, wrapped segwit is more expensive than native', () => { - const segwit = generate10kSpendWithTestData('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); - const native = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); - expect(segwit.fee).toBeGreaterThan(native.fee); + const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); + const native = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + expect(segwit.estimatedFee).toBeGreaterThan(native.estimatedFee); }); test('that given a set of utxos, taproot is more expensive than native segwit', () => { // Non-obvious behaviour. // P2TR outputs = 34 vBytes // P2WPKH outputs = 22 vBytes - const native = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); - const taproot = generate10kSpendWithTestData( + const native = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + const taproot = generate10kSpendWithDummyUtxoSet( 'tb1parwmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd' ); - expect(taproot.fee).toBeGreaterThan(native.fee); + expect(taproot.estimatedFee).toBeGreaterThan(native.estimatedFee); }); test('against a random set of generated utxos', () => { @@ -102,7 +129,7 @@ describe(determineUtxosForSpend.name, () => { expect(result.outputs[1].value.toString()).toEqual( sumNumbers(result.inputs.map(i => i.value)) - .minus(result.fee) + .minus(result.estimatedFee) .minus(amount.toString()) .toString() ); diff --git a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts index 342390eb336..04c754e29eb 100644 --- a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +++ b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts @@ -1,13 +1,15 @@ +import BigNumber from 'bignumber.js'; import { validate } from 'bitcoin-address-validation'; import type { RpcSendTransferRecipient } from '@shared/rpc/methods/send-transfer'; +import { sumNumbers } from '@app/common/math/helpers'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { filterUneconomicalUtxos, filterUneconomicalUtxosMultipleRecipients, - getSizeInfo, + getBitcoinTxSizeEstimation, getSizeInfoMultipleRecipients, } from '../utils'; @@ -33,25 +35,29 @@ export function determineUtxosForSpendAll({ if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type'); const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, address: recipient }); - const sizeInfo = getSizeInfo({ - inputLength: filteredUtxos.length, - outputLength: 1, + const sizeInfo = getBitcoinTxSizeEstimation({ + inputCount: filteredUtxos.length, + outputCount: 1, recipient, }); // Fee has already been deducted from the amount with send all const outputs = [{ value: BigInt(amount), address: recipient }]; - const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); return { inputs: filteredUtxos, outputs, size: sizeInfo.txVBytes, - fee, + estimatedFee, }; } +function getUtxoTotal(utxos: UtxoResponseItem[]) { + return sumNumbers(utxos.map(utxo => utxo.value)); +} + export function determineUtxosForSpend({ amount, feeRate, @@ -60,47 +66,59 @@ export function determineUtxosForSpend({ }: DetermineUtxosForSpendArgs) { if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type'); - const orderedUtxos = utxos.sort((a, b) => b.value - a.value); - - const filteredUtxos = filterUneconomicalUtxos({ - utxos: orderedUtxos, + const filteredUtxos: UtxoResponseItem[] = filterUneconomicalUtxos({ + utxos: utxos.sort((a, b) => b.value - a.value), feeRate, address: recipient, }); - const neededUtxos = []; - let sum = 0n; - let sizeInfo = null; + if (!filteredUtxos.length) throw new InsufficientFundsError(); - for (const utxo of filteredUtxos) { - sizeInfo = getSizeInfo({ - inputLength: neededUtxos.length, - outputLength: 2, + // Prepopulate with first UTXO, at least one is needed + const neededUtxos: UtxoResponseItem[] = [filteredUtxos[0]]; + + function estimateTransactionSize() { + return getBitcoinTxSizeEstimation({ + inputCount: neededUtxos.length, + outputCount: 2, recipient, }); - if (sum >= BigInt(amount) + BigInt(Math.ceil(sizeInfo.txVBytes * feeRate))) break; + } - sum += BigInt(utxo.value); - neededUtxos.push(utxo); + function hasSufficientUtxosForTx() { + const txEstimation = estimateTransactionSize(); + const neededAmount = new BigNumber(txEstimation.txVBytes * feeRate).plus(amount); + return getUtxoTotal(neededUtxos).isGreaterThanOrEqualTo(neededAmount); } - if (!sizeInfo) throw new InsufficientFundsError(); + function getRemainingUnspentUtxos() { + return filteredUtxos.filter(utxo => !neededUtxos.includes(utxo)); + } - const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + while (!hasSufficientUtxosForTx()) { + const [nextUtxo] = getRemainingUnspentUtxos(); + if (!nextUtxo) throw new InsufficientFundsError(); + neededUtxos.push(nextUtxo); + } + + const estimatedFee = Math.ceil( + new BigNumber(estimateTransactionSize().txVBytes).multipliedBy(feeRate).toNumber() + ); const outputs = [ // outputs[0] = the desired amount going to recipient { value: BigInt(amount), address: recipient }, // outputs[1] = the remainder to be returned to a change address - { value: sum - BigInt(amount) - BigInt(fee) }, + { value: BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount) - BigInt(estimatedFee) }, ]; return { filteredUtxos, inputs: neededUtxos, outputs, - size: sizeInfo.txVBytes, - fee, + size: estimateTransactionSize().txVBytes, + ...estimateTransactionSize(), + estimatedFee, }; } @@ -140,13 +158,13 @@ export function determineUtxosForSpendAllMultipleRecipients({ address, })); - const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); return { inputs: filteredUtxos, outputs, size: sizeInfo.txVBytes, - fee, + estimatedFee, }; } @@ -186,7 +204,7 @@ export function determineUtxosForSpendMultipleRecipients({ if (!sizeInfo) throw new InsufficientFundsError(); - const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); const outputs: { value: bigint; @@ -198,7 +216,7 @@ export function determineUtxosForSpendMultipleRecipients({ address, })), // outputs[recipients.length] = the remainder to be returned to a change address - { value: sum - BigInt(amount) - BigInt(fee) }, + { value: sum - BigInt(amount) - BigInt(estimatedFee) }, ]; return { @@ -206,6 +224,6 @@ export function determineUtxosForSpendMultipleRecipients({ inputs: neededUtxos, outputs, size: sizeInfo.txVBytes, - fee, + estimatedFee, }; } diff --git a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts index 0b4a68eacc6..ad87e61dc61 100644 --- a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts +++ b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts @@ -47,7 +47,11 @@ export function useGenerateUnsignedNativeSegwitSingleRecipientTx() { utxos, }; - const { inputs, outputs, fee } = isSendingMax + const { + inputs, + outputs, + estimatedFee: fee, + } = isSendingMax ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); @@ -127,11 +131,11 @@ export function useGenerateUnsignedNativeSegwitMultipleRecipientsTx() { utxos, }; - const { inputs, outputs, fee } = isSendingMax + const { inputs, outputs, estimatedFee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosArgs); - logger.info('Coin selection', { inputs, outputs, fee }); + logger.info('Coin selection', { inputs, outputs, estimatedFee }); if (!inputs.length) throw new Error('No inputs to sign'); if (!outputs.length) throw new Error('No outputs to sign'); @@ -166,7 +170,7 @@ export function useGenerateUnsignedNativeSegwitMultipleRecipientsTx() { tx.addOutputAddress(output.address, BigInt(output.value), networkMode); }); - return { hex: tx.hex, fee, psbt: tx.toPSBT(), inputs }; + return { hex: tx.hex, fee: estimatedFee, psbt: tx.toPSBT(), inputs }; } catch (e) { // eslint-disable-next-line no-console console.log('Error signing bitcoin transaction', e); diff --git a/src/app/common/transactions/bitcoin/utils.ts b/src/app/common/transactions/bitcoin/utils.ts index 4a2845ed9e7..ae47d2c3a37 100644 --- a/src/app/common/transactions/bitcoin/utils.ts +++ b/src/app/common/transactions/bitcoin/utils.ts @@ -34,9 +34,9 @@ export function getSpendableAmount({ }) { const balance = utxos.map(utxo => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0); - const size = getSizeInfo({ - inputLength: utxos.length, - outputLength: 1, + const size = getBitcoinTxSizeEstimation({ + inputCount: utxos.length, + outputCount: 1, recipient: address, }); const fee = Math.ceil(size.txVBytes * feeRate); @@ -80,12 +80,12 @@ export function filterUneconomicalUtxos({ return filteredUtxos; } -export function getSizeInfo(payload: { - inputLength: number; - outputLength: number; +export function getBitcoinTxSizeEstimation(payload: { + inputCount: number; + outputCount: number; recipient: string; }) { - const { inputLength, recipient, outputLength } = payload; + const { inputCount, recipient, outputCount } = payload; const addressInfo = validate(recipient) ? getAddressInfo(recipient) : null; const outputAddressTypeWithFallback = addressInfo ? addressInfo.type : 'p2wpkh'; @@ -93,9 +93,9 @@ export function getSizeInfo(payload: { const sizeInfo = txSizer.calcTxSize({ // Only p2wpkh is supported by the wallet input_script: 'p2wpkh', - input_count: inputLength, + input_count: inputCount, // From the address of the recipient, we infer the output type - [outputAddressTypeWithFallback + '_output_count']: outputLength, + [outputAddressTypeWithFallback + '_output_count']: outputCount, }); return sizeInfo; diff --git a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx index 710f1e4feb3..35a5c88f4e2 100644 --- a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx +++ b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx @@ -39,14 +39,14 @@ export function useBitcoinCustomFee({ amount, isSendingMax, recipient }: UseBitc utxos, feeRate, }; - const { fee } = isSendingMax + const { estimatedFee } = isSendingMax ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); return { - fee, + estimatedFee, fiatFeeValue: `~ ${i18nFormatCurrency( - baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) + baseCurrencyAmountInQuote(createMoney(Math.ceil(estimatedFee), 'BTC'), btcMarketData) )}`, }; }, @@ -81,14 +81,14 @@ export function useBitcoinCustomFeeMultipleRecipients({ utxos, feeRate, }; - const { fee } = isSendingMax + const { estimatedFee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosArgs); return { - fee, + estimatedFee, fiatFeeValue: `~ ${i18nFormatCurrency( - baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) + baseCurrencyAmountInQuote(createMoney(Math.ceil(estimatedFee), 'BTC'), btcMarketData) )}`, }; }, diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts index 7532c67a224..c29decc3198 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts @@ -22,10 +22,10 @@ function getFeeForList( isSendingMax?: boolean ) { try { - const { fee } = isSendingMax + const { estimatedFee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosForFeeArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosForFeeArgs); - return fee; + return estimatedFee; } catch (error) { return null; } diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts index b7e90374b4b..e377063bc33 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts @@ -22,10 +22,10 @@ function getFeeForList( isSendingMax?: boolean ) { try { - const { fee } = isSendingMax + const { estimatedFee } = isSendingMax ? determineUtxosForSpendAll(determineUtxosForFeeArgs) : determineUtxosForSpend(determineUtxosForFeeArgs); - return fee; + return estimatedFee; } catch (error) { return null; } @@ -64,37 +64,37 @@ export function useBitcoinFeesList({ utxos, }; - const determineUtxosForHighFeeArgs = { - ...determineUtxosDefaultArgs, - feeRate: feeRates.fastestFee.toNumber(), - }; + // const determineUtxosForHighFeeArgs = { + // ...determineUtxosDefaultArgs, + // feeRate: feeRates.fastestFee.toNumber(), + // }; const determineUtxosForStandardFeeArgs = { ...determineUtxosDefaultArgs, feeRate: feeRates.halfHourFee.toNumber(), }; - const determineUtxosForLowFeeArgs = { - ...determineUtxosDefaultArgs, - feeRate: feeRates.hourFee.toNumber(), - }; + // const determineUtxosForLowFeeArgs = { + // ...determineUtxosDefaultArgs, + // feeRate: feeRates.hourFee.toNumber(), + // }; const feesArr = []; - const highFeeValue = getFeeForList(determineUtxosForHighFeeArgs, isSendingMax); + // const highFeeValue = getFeeForList(determineUtxosForHighFeeArgs, isSendingMax); const standardFeeValue = getFeeForList(determineUtxosForStandardFeeArgs, isSendingMax); - const lowFeeValue = getFeeForList(determineUtxosForLowFeeArgs, isSendingMax); - - if (highFeeValue) { - feesArr.push({ - label: BtcFeeType.High, - value: highFeeValue, - btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')), - time: btcTxTimeMap.fastestFee, - fiatValue: getFiatFeeValue(highFeeValue), - feeRate: feeRates.fastestFee.toNumber(), - }); - } + // const lowFeeValue = getFeeForList(determineUtxosForLowFeeArgs, isSendingMax); + + // if (highFeeValue) { + // feesArr.push({ + // label: BtcFeeType.High, + // value: highFeeValue, + // btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')), + // time: btcTxTimeMap.fastestFee, + // fiatValue: getFiatFeeValue(highFeeValue), + // feeRate: feeRates.fastestFee.toNumber(), + // }); + // } if (standardFeeValue) { feesArr.push({ @@ -107,16 +107,16 @@ export function useBitcoinFeesList({ }); } - if (lowFeeValue) { - feesArr.push({ - label: BtcFeeType.Low, - value: lowFeeValue, - btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')), - time: btcTxTimeMap.hourFee, - fiatValue: getFiatFeeValue(lowFeeValue), - feeRate: feeRates.hourFee.toNumber(), - }); - } + // if (lowFeeValue) { + // feesArr.push({ + // label: BtcFeeType.Low, + // value: lowFeeValue, + // btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')), + // time: btcTxTimeMap.hourFee, + // fiatValue: getFiatFeeValue(lowFeeValue), + // feeRate: feeRates.hourFee.toNumber(), + // }); + // } return feesArr; }, [feeRates, utxos, isSendingMax, balance.amount, amount.amount, recipient, btcMarketData]); diff --git a/src/app/features/dialogs/increase-fee-dialog/hooks/use-btc-increase-fee.ts b/src/app/features/dialogs/increase-fee-dialog/hooks/use-btc-increase-fee.ts index 48d2a3528c3..91d9666d902 100644 --- a/src/app/features/dialogs/increase-fee-dialog/hooks/use-btc-increase-fee.ts +++ b/src/app/features/dialogs/increase-fee-dialog/hooks/use-btc-increase-fee.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import * as btc from '@scure/btc-signer'; @@ -14,9 +15,9 @@ import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balanc import { btcToSat } from '@app/common/money/unit-conversion'; import { queryClient } from '@app/common/persistence'; import { + getBitcoinTxSizeEstimation, getBitcoinTxValue, getRecipientAddressFromOutput, - getSizeInfo, } from '@app/common/transactions/bitcoin/utils'; import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee'; import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list'; @@ -39,11 +40,16 @@ export function useBtcIncreaseFee(btcTx: BitcoinTx) { const signTransaction = useSignBitcoinTx(); const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction(); const recipient = getRecipientAddressFromOutput(btcTx.vout, currentBitcoinAddress) || ''; - const sizeInfo = getSizeInfo({ - inputLength: btcTx.vin.length, - recipient, - outputLength: btcTx.vout.length, - }); + + const sizeInfo = useMemo( + () => + getBitcoinTxSizeEstimation({ + inputCount: btcTx.vin.length, + recipient, + outputCount: btcTx.vout.length, + }), + [btcTx.vin.length, btcTx.vout.length, recipient] + ); const { btcAvailableAssetBalance } = useBtcAssetBalance(currentBitcoinAddress); const sendingAmount = getBitcoinTxValue(currentBitcoinAddress, btcTx); diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts index cff9efcdab6..d48b0073386 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts @@ -122,7 +122,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio utxos: nativeSegwitUtxos, }; - const { inputs, outputs, fee } = determineUtxosForSpend(determineUtxosArgs); + const { inputs, outputs, estimatedFee: fee } = determineUtxosForSpend(determineUtxosArgs); try { const tx = new btc.Transaction(); diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-choose-fee.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-choose-fee.tsx index 0104ce58810..864fac42002 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-choose-fee.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-choose-fee.tsx @@ -17,7 +17,6 @@ export function useBtcChooseFeeState() { const isSendingMax = useLocationStateWithCache('isSendingMax') as boolean; const txValues = useLocationStateWithCache('values') as BitcoinSendFormValues; const utxos = useLocationStateWithCache('utxos') as UtxoResponseItem[]; - return { isSendingMax, txValues, utxos }; } diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx index 7d0f68256fe..499dd28077f 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx @@ -1,18 +1,27 @@ +import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { hexToBytes } from '@noble/hashes/utils'; import * as btc from '@scure/btc-signer'; +import { bytesToHex } from '@stacks/common'; import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors'; import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors'; +import BigNumber from 'bignumber.js'; import { Stack } from 'leather-styles/jsx'; import get from 'lodash.get'; -import { decodeBitcoinTx } from '@shared/crypto/bitcoin/bitcoin.utils'; +import { + decodeBitcoinTx, + getPsbtTxInputs, + getPsbtTxOutputs, +} from '@shared/crypto/bitcoin/bitcoin.utils'; import { CryptoCurrencies } from '@shared/models/currencies.model'; import { createMoney, createMoneyFromDecimal } from '@shared/models/money.model'; import { RouteUrls } from '@shared/route-urls'; +import { isDefined } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { sumNumbers } from '@app/common/math/helpers'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; import { satToBtc } from '@app/common/money/unit-conversion'; @@ -24,6 +33,7 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { useGetBitcoinTransactionQueries } from '@app/query/bitcoin/transaction/transaction.query'; import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; import { Button } from '@app/ui/components/button/button'; @@ -50,13 +60,52 @@ export function BtcSendFormConfirmation() { const navigate = useNavigate(); const { tx, recipient, fee, arrivesIn, feeRowValue } = useBtcSendFormConfirmationState(); + const transaction = useMemo(() => btc.Transaction.fromRaw(hexToBytes(tx)), [tx]); + const inputs = useMemo(() => getPsbtTxInputs(transaction), [transaction]); + + const inputTransactions = useGetBitcoinTransactionQueries( + inputs + .map(input => input.txid) + .filter(isDefined) + .map(txid => bytesToHex(txid)) + ); + const { refetch } = useCurrentNativeSegwitUtxos(); const analytics = useAnalytics(); const btcMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction(); - const transaction = btc.Transaction.fromRaw(hexToBytes(tx)); + console.log({ transaction }); + + useMemo(() => { + if (inputTransactions.some(query => !query.data)) return null; + + const inputTotal = sumNumbers( + inputs + .map((input, index) => inputTransactions[index].data?.vout[input.index ?? 0].value) + .filter(isDefined) + ); + + const outputs = getPsbtTxOutputs(transaction); + + const outputTotal = sumNumbers( + outputs + .map(output => output.amount) + .filter(isDefined) + .map(val => Number(val)) + ); + + // console.log('Presented fee', fee); + // console.log('fee === ', inputTotal.minus(outputTotal).toNumber()); + + console.log('Actual vsize ', transaction.vsize); + console.log('Fee ', fee); + console.log('Fee row value', feeRowValue); + console.log('Sats per vbytes ', new BigNumber(fee).dividedBy(transaction.vsize).toNumber()); + }, [fee, feeRowValue, inputTransactions, inputs, transaction]); + + // console.log({ inputs, outputs }); const decodedTx = decodeBitcoinTx(transaction.hex); diff --git a/src/app/query/bitcoin/transaction/transaction.query.ts b/src/app/query/bitcoin/transaction/transaction.query.ts index 6a3127312c1..cb63d7c98e6 100644 --- a/src/app/query/bitcoin/transaction/transaction.query.ts +++ b/src/app/query/bitcoin/transaction/transaction.query.ts @@ -1,5 +1,3 @@ -import * as btc from '@scure/btc-signer'; -import { bytesToHex } from '@stacks/common'; import { UseQueryResult, useQueries, useQuery } from '@tanstack/react-query'; import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; @@ -40,18 +38,14 @@ const queryOptions = { refetchOnWindowFocus: false, } as const; -// ts-unused-exports:disable-next-line -export function useGetBitcoinTransactionQueries( - inputs: btc.TransactionInput[] -): UseQueryResult[] { +export function useGetBitcoinTransactionQueries(txids: string[]): UseQueryResult[] { const client = useBitcoinClient(); return useQueries({ - queries: inputs.map(input => { - const txId = input.txid ? bytesToHex(input.txid) : ''; + queries: txids.map(txid => { return { - queryKey: ['bitcoin-transaction', txId], - queryFn: () => fetchBitcoinTransaction(client)(txId), + queryKey: ['bitcoin-transaction', txid], + queryFn: () => fetchBitcoinTransaction(client)(txid), ...queryOptions, }; }), diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 64a143b5247..cedfcf76b4e 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -67,7 +67,7 @@ export function isEmptyArray(data: unknown[]) { return data.length === 0; } -export const defaultWalletKeyId = 'default' as const; +export const defaultWalletKeyId = 'default'; export function closeWindow() { if (process.env.DEBUG_PREVENT_WINDOW_CLOSE === 'true') { From d12bb827e0a73a3f9bec6744540b88a970b0808b Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Tue, 23 Apr 2024 16:04:04 +0200 Subject: [PATCH 3/8] chore: rename estimated fee --- .../coinselect/local-coin-selection.spec.ts | 30 ++++++-- .../coinselect/local-coin-selection.ts | 20 +++--- .../bitcoin/use-generate-bitcoin-tx.ts | 12 ++-- .../hooks/use-bitcoin-custom-fee.tsx | 12 ++-- ...e-bitcoin-fees-list-multiple-recipients.ts | 4 +- .../use-bitcoin-fees-list.ts | 66 +++++++++--------- .../pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx | 4 +- .../hooks/use-generate-ordinal-tx.ts | 2 +- .../form/btc/btc-send-form-confirmation.tsx | 69 ++++++++----------- .../bitcoin/transaction/transaction.query.ts | 1 + 10 files changed, 114 insertions(+), 106 deletions(-) diff --git a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts index 2db33f7ad8e..3403cd95625 100644 --- a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts +++ b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.spec.ts @@ -55,6 +55,28 @@ describe(determineUtxosForSpend.name, () => { expect(estimation.txVBytes).toBeGreaterThan(208); expect(estimation.txVBytes).toBeLessThan(209); }); + + test('that Native Segwit, 10 input 2 outputs weighs 200vBytes', () => { + const estimation = determineUtxosForSpend({ + utxos: [ + { value: 20_000 }, + { value: 20_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + { value: 10_000 }, + ] as any[], + amount: 100_000, + recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m', + feeRate: 20, + }); + expect(estimation.txVBytes).toBeGreaterThan(750); + expect(estimation.txVBytes).toBeLessThan(751); + }); }); describe('sorting algorithm', () => { @@ -94,13 +116,13 @@ describe(determineUtxosForSpend.name, () => { test('that given a set of utxos, legacy is more expensive', () => { const legacy = generate10kSpendWithDummyUtxoSet('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj'); const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); - expect(legacy.estimatedFee).toBeGreaterThan(segwit.estimatedFee); + expect(legacy.fee).toBeGreaterThan(segwit.fee); }); test('that given a set of utxos, wrapped segwit is more expensive than native', () => { const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); const native = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); - expect(segwit.estimatedFee).toBeGreaterThan(native.estimatedFee); + expect(segwit.fee).toBeGreaterThan(native.fee); }); test('that given a set of utxos, taproot is more expensive than native segwit', () => { @@ -111,7 +133,7 @@ describe(determineUtxosForSpend.name, () => { const taproot = generate10kSpendWithDummyUtxoSet( 'tb1parwmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd' ); - expect(taproot.estimatedFee).toBeGreaterThan(native.estimatedFee); + expect(taproot.fee).toBeGreaterThan(native.fee); }); test('against a random set of generated utxos', () => { @@ -129,7 +151,7 @@ describe(determineUtxosForSpend.name, () => { expect(result.outputs[1].value.toString()).toEqual( sumNumbers(result.inputs.map(i => i.value)) - .minus(result.estimatedFee) + .minus(result.fee) .minus(amount.toString()) .toString() ); diff --git a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts index 04c754e29eb..711c73e18a3 100644 --- a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +++ b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts @@ -44,13 +44,13 @@ export function determineUtxosForSpendAll({ // Fee has already been deducted from the amount with send all const outputs = [{ value: BigInt(amount), address: recipient }]; - const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); return { inputs: filteredUtxos, outputs, size: sizeInfo.txVBytes, - estimatedFee, + fee, }; } @@ -101,7 +101,7 @@ export function determineUtxosForSpend({ neededUtxos.push(nextUtxo); } - const estimatedFee = Math.ceil( + const fee = Math.ceil( new BigNumber(estimateTransactionSize().txVBytes).multipliedBy(feeRate).toNumber() ); @@ -109,7 +109,7 @@ export function determineUtxosForSpend({ // outputs[0] = the desired amount going to recipient { value: BigInt(amount), address: recipient }, // outputs[1] = the remainder to be returned to a change address - { value: BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount) - BigInt(estimatedFee) }, + { value: BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount) - BigInt(fee) }, ]; return { @@ -117,8 +117,8 @@ export function determineUtxosForSpend({ inputs: neededUtxos, outputs, size: estimateTransactionSize().txVBytes, + fee, ...estimateTransactionSize(), - estimatedFee, }; } @@ -158,13 +158,13 @@ export function determineUtxosForSpendAllMultipleRecipients({ address, })); - const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); return { inputs: filteredUtxos, outputs, size: sizeInfo.txVBytes, - estimatedFee, + fee, }; } @@ -204,7 +204,7 @@ export function determineUtxosForSpendMultipleRecipients({ if (!sizeInfo) throw new InsufficientFundsError(); - const estimatedFee = Math.ceil(sizeInfo.txVBytes * feeRate); + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); const outputs: { value: bigint; @@ -216,7 +216,7 @@ export function determineUtxosForSpendMultipleRecipients({ address, })), // outputs[recipients.length] = the remainder to be returned to a change address - { value: sum - BigInt(amount) - BigInt(estimatedFee) }, + { value: sum - BigInt(amount) - BigInt(fee) }, ]; return { @@ -224,6 +224,6 @@ export function determineUtxosForSpendMultipleRecipients({ inputs: neededUtxos, outputs, size: sizeInfo.txVBytes, - estimatedFee, + fee, }; } diff --git a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts index ad87e61dc61..b90640fc5b2 100644 --- a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts +++ b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts @@ -47,11 +47,7 @@ export function useGenerateUnsignedNativeSegwitSingleRecipientTx() { utxos, }; - const { - inputs, - outputs, - estimatedFee: fee, - } = isSendingMax + const { inputs, outputs, fee } = isSendingMax ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); @@ -131,11 +127,11 @@ export function useGenerateUnsignedNativeSegwitMultipleRecipientsTx() { utxos, }; - const { inputs, outputs, estimatedFee } = isSendingMax + const { inputs, outputs, fee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosArgs); - logger.info('Coin selection', { inputs, outputs, estimatedFee }); + logger.info('Coin selection', { inputs, outputs, fee }); if (!inputs.length) throw new Error('No inputs to sign'); if (!outputs.length) throw new Error('No outputs to sign'); @@ -170,7 +166,7 @@ export function useGenerateUnsignedNativeSegwitMultipleRecipientsTx() { tx.addOutputAddress(output.address, BigInt(output.value), networkMode); }); - return { hex: tx.hex, fee: estimatedFee, psbt: tx.toPSBT(), inputs }; + return { hex: tx.hex, fee: fee, psbt: tx.toPSBT(), inputs }; } catch (e) { // eslint-disable-next-line no-console console.log('Error signing bitcoin transaction', e); diff --git a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx index 35a5c88f4e2..710f1e4feb3 100644 --- a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx +++ b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx @@ -39,14 +39,14 @@ export function useBitcoinCustomFee({ amount, isSendingMax, recipient }: UseBitc utxos, feeRate, }; - const { estimatedFee } = isSendingMax + const { fee } = isSendingMax ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); return { - estimatedFee, + fee, fiatFeeValue: `~ ${i18nFormatCurrency( - baseCurrencyAmountInQuote(createMoney(Math.ceil(estimatedFee), 'BTC'), btcMarketData) + baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) )}`, }; }, @@ -81,14 +81,14 @@ export function useBitcoinCustomFeeMultipleRecipients({ utxos, feeRate, }; - const { estimatedFee } = isSendingMax + const { fee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosArgs); return { - estimatedFee, + fee, fiatFeeValue: `~ ${i18nFormatCurrency( - baseCurrencyAmountInQuote(createMoney(Math.ceil(estimatedFee), 'BTC'), btcMarketData) + baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) )}`, }; }, diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts index c29decc3198..7532c67a224 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts @@ -22,10 +22,10 @@ function getFeeForList( isSendingMax?: boolean ) { try { - const { estimatedFee } = isSendingMax + const { fee } = isSendingMax ? determineUtxosForSpendAllMultipleRecipients(determineUtxosForFeeArgs) : determineUtxosForSpendMultipleRecipients(determineUtxosForFeeArgs); - return estimatedFee; + return fee; } catch (error) { return null; } diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts index e377063bc33..b7e90374b4b 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts @@ -22,10 +22,10 @@ function getFeeForList( isSendingMax?: boolean ) { try { - const { estimatedFee } = isSendingMax + const { fee } = isSendingMax ? determineUtxosForSpendAll(determineUtxosForFeeArgs) : determineUtxosForSpend(determineUtxosForFeeArgs); - return estimatedFee; + return fee; } catch (error) { return null; } @@ -64,37 +64,37 @@ export function useBitcoinFeesList({ utxos, }; - // const determineUtxosForHighFeeArgs = { - // ...determineUtxosDefaultArgs, - // feeRate: feeRates.fastestFee.toNumber(), - // }; + const determineUtxosForHighFeeArgs = { + ...determineUtxosDefaultArgs, + feeRate: feeRates.fastestFee.toNumber(), + }; const determineUtxosForStandardFeeArgs = { ...determineUtxosDefaultArgs, feeRate: feeRates.halfHourFee.toNumber(), }; - // const determineUtxosForLowFeeArgs = { - // ...determineUtxosDefaultArgs, - // feeRate: feeRates.hourFee.toNumber(), - // }; + const determineUtxosForLowFeeArgs = { + ...determineUtxosDefaultArgs, + feeRate: feeRates.hourFee.toNumber(), + }; const feesArr = []; - // const highFeeValue = getFeeForList(determineUtxosForHighFeeArgs, isSendingMax); + const highFeeValue = getFeeForList(determineUtxosForHighFeeArgs, isSendingMax); const standardFeeValue = getFeeForList(determineUtxosForStandardFeeArgs, isSendingMax); - // const lowFeeValue = getFeeForList(determineUtxosForLowFeeArgs, isSendingMax); - - // if (highFeeValue) { - // feesArr.push({ - // label: BtcFeeType.High, - // value: highFeeValue, - // btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')), - // time: btcTxTimeMap.fastestFee, - // fiatValue: getFiatFeeValue(highFeeValue), - // feeRate: feeRates.fastestFee.toNumber(), - // }); - // } + const lowFeeValue = getFeeForList(determineUtxosForLowFeeArgs, isSendingMax); + + if (highFeeValue) { + feesArr.push({ + label: BtcFeeType.High, + value: highFeeValue, + btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')), + time: btcTxTimeMap.fastestFee, + fiatValue: getFiatFeeValue(highFeeValue), + feeRate: feeRates.fastestFee.toNumber(), + }); + } if (standardFeeValue) { feesArr.push({ @@ -107,16 +107,16 @@ export function useBitcoinFeesList({ }); } - // if (lowFeeValue) { - // feesArr.push({ - // label: BtcFeeType.Low, - // value: lowFeeValue, - // btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')), - // time: btcTxTimeMap.hourFee, - // fiatValue: getFiatFeeValue(lowFeeValue), - // feeRate: feeRates.hourFee.toNumber(), - // }); - // } + if (lowFeeValue) { + feesArr.push({ + label: BtcFeeType.Low, + value: lowFeeValue, + btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')), + time: btcTxTimeMap.hourFee, + fiatValue: getFiatFeeValue(lowFeeValue), + feeRate: feeRates.hourFee.toNumber(), + }); + } return feesArr; }, [feeRates, utxos, isSendingMax, balance.amount, amount.amount, recipient, btcMarketData]); diff --git a/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx b/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx index 79963033b10..7dbdcec1373 100644 --- a/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx +++ b/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx @@ -73,9 +73,7 @@ export function useRpcSignPsbt() { txValue: formatMoney(transferTotalAsMoney), }; - navigate(RouteUrls.RpcSignPsbtSummary, { - state: psbtTxSummaryState, - }); + navigate(RouteUrls.RpcSignPsbtSummary, { state: psbtTxSummaryState }); }, onError(e) { navigate(RouteUrls.RequestError, { diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts index d48b0073386..cff9efcdab6 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts @@ -122,7 +122,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio utxos: nativeSegwitUtxos, }; - const { inputs, outputs, estimatedFee: fee } = determineUtxosForSpend(determineUtxosArgs); + const { inputs, outputs, fee } = determineUtxosForSpend(determineUtxosArgs); try { const tx = new btc.Transaction(); diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx index 499dd28077f..00b9fb55e04 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx @@ -3,25 +3,17 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { hexToBytes } from '@noble/hashes/utils'; import * as btc from '@scure/btc-signer'; -import { bytesToHex } from '@stacks/common'; import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors'; import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors'; -import BigNumber from 'bignumber.js'; import { Stack } from 'leather-styles/jsx'; import get from 'lodash.get'; -import { - decodeBitcoinTx, - getPsbtTxInputs, - getPsbtTxOutputs, -} from '@shared/crypto/bitcoin/bitcoin.utils'; +import { decodeBitcoinTx } from '@shared/crypto/bitcoin/bitcoin.utils'; import { CryptoCurrencies } from '@shared/models/currencies.model'; import { createMoney, createMoneyFromDecimal } from '@shared/models/money.model'; import { RouteUrls } from '@shared/route-urls'; -import { isDefined } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; -import { sumNumbers } from '@app/common/math/helpers'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; import { satToBtc } from '@app/common/money/unit-conversion'; @@ -33,7 +25,6 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; -import { useGetBitcoinTransactionQueries } from '@app/query/bitcoin/transaction/transaction.query'; import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; import { Button } from '@app/ui/components/button/button'; @@ -61,14 +52,14 @@ export function BtcSendFormConfirmation() { const { tx, recipient, fee, arrivesIn, feeRowValue } = useBtcSendFormConfirmationState(); const transaction = useMemo(() => btc.Transaction.fromRaw(hexToBytes(tx)), [tx]); - const inputs = useMemo(() => getPsbtTxInputs(transaction), [transaction]); + // const inputs = useMemo(() => getPsbtTxInputs(transaction), [transaction]); - const inputTransactions = useGetBitcoinTransactionQueries( - inputs - .map(input => input.txid) - .filter(isDefined) - .map(txid => bytesToHex(txid)) - ); + // const inputTransactions = useGetBitcoinTransactionQueries( + // inputs + // .map(input => input.txid) + // .filter(isDefined) + // .map(txid => bytesToHex(txid)) + // ); const { refetch } = useCurrentNativeSegwitUtxos(); const analytics = useAnalytics(); @@ -76,34 +67,34 @@ export function BtcSendFormConfirmation() { const btcMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction(); - console.log({ transaction }); + // console.log({ transaction }); - useMemo(() => { - if (inputTransactions.some(query => !query.data)) return null; + // useMemo(() => { + // if (inputTransactions.some(query => !query.data)) return null; - const inputTotal = sumNumbers( - inputs - .map((input, index) => inputTransactions[index].data?.vout[input.index ?? 0].value) - .filter(isDefined) - ); + // const inputTotal = sumNumbers( + // inputs + // .map((input, index) => inputTransactions[index].data?.vout[input.index ?? 0].value) + // .filter(isDefined) + // ); - const outputs = getPsbtTxOutputs(transaction); + // const outputs = getPsbtTxOutputs(transaction); - const outputTotal = sumNumbers( - outputs - .map(output => output.amount) - .filter(isDefined) - .map(val => Number(val)) - ); + // const outputTotal = sumNumbers( + // outputs + // .map(output => output.amount) + // .filter(isDefined) + // .map(val => Number(val)) + // ); - // console.log('Presented fee', fee); - // console.log('fee === ', inputTotal.minus(outputTotal).toNumber()); + // // console.log('Presented fee', fee); + // // console.log('fee === ', inputTotal.minus(outputTotal).toNumber()); - console.log('Actual vsize ', transaction.vsize); - console.log('Fee ', fee); - console.log('Fee row value', feeRowValue); - console.log('Sats per vbytes ', new BigNumber(fee).dividedBy(transaction.vsize).toNumber()); - }, [fee, feeRowValue, inputTransactions, inputs, transaction]); + // console.log('Actual vsize ', transaction.vsize); + // console.log('Fee ', fee); + // console.log('Fee row value', feeRowValue); + // console.log('Sats per vbytes ', new BigNumber(fee).dividedBy(transaction.vsize).toNumber()); + // }, [fee, feeRowValue, inputTransactions, inputs, transaction]); // console.log({ inputs, outputs }); diff --git a/src/app/query/bitcoin/transaction/transaction.query.ts b/src/app/query/bitcoin/transaction/transaction.query.ts index cb63d7c98e6..329e68c1a45 100644 --- a/src/app/query/bitcoin/transaction/transaction.query.ts +++ b/src/app/query/bitcoin/transaction/transaction.query.ts @@ -38,6 +38,7 @@ const queryOptions = { refetchOnWindowFocus: false, } as const; +// ts-unused-exports:disable-next-line export function useGetBitcoinTransactionQueries(txids: string[]): UseQueryResult[] { const client = useBitcoinClient(); From c5a32cfd69667982e9930af1ca8756d77ddbb49d Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Mon, 29 Apr 2024 19:01:28 -0500 Subject: [PATCH 4/8] feat: stx-20 balances, closes #5077 --- public/assets/avatars/stx20-avatar-icon.png | Bin 0 -> 19817 bytes .../src20-token-asset-list.tsx | 6 +-- .../stx20-token-asset-item.layout.tsx | 37 ++++++++++++++++++ .../stx20-token-asset-list.tsx | 12 ++++++ .../loaders/brc20-tokens-loader.tsx | 6 +-- .../loaders/src20-tokens-loader.tsx | 6 +-- .../loaders/stx20-tokens-loader.tsx | 11 ++++++ src/app/features/asset-list/asset-list.tsx | 27 ++++++++++--- .../bitcoin-fungible-tokens-asset-list.tsx | 29 -------------- src/app/query/bitcoin/bitcoin-client.ts | 10 ++--- .../ordinals/brc20/brc20-tokens.hooks.ts | 5 ++- .../ordinals/brc20/brc20-tokens.query.ts | 4 +- ...ils.spec.ts => brc20-tokens.utils.spec.ts} | 2 +- ...{brc-20.utils.ts => brc20-tokens.utils.ts} | 0 .../runes/runes-outputs-by-address.query.ts | 2 +- .../bitcoin/runes/runes-ticker-info.query.ts | 2 +- .../runes/runes-wallet-balances.query.ts | 2 +- .../bitcoin/transaction/use-check-utxos.ts | 4 +- src/app/query/stacks/stacks-client.ts | 37 +++++++++++++++++- .../query/stacks/stx20/stx20-tokens.hooks.ts | 20 ++++++++++ .../query/stacks/stx20/stx20-tokens.query.ts | 23 +++++++++++ src/app/query/stacks/token-metadata-client.ts | 15 ------- .../fungible-token-metadata.query.ts | 10 ++--- .../non-fungible-token-metadata.query.ts | 4 +- src/app/query/stacks/utils.ts | 15 ------- src/app/store/common/api-clients.hooks.ts | 27 +++---------- .../components/avatar/stx20-avatar-icon.tsx | 14 +++++++ src/shared/constants.ts | 2 + 28 files changed, 215 insertions(+), 117 deletions(-) create mode 100644 public/assets/avatars/stx20-avatar-icon.png create mode 100644 src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx create mode 100644 src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx create mode 100644 src/app/components/loaders/stx20-tokens-loader.tsx delete mode 100644 src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx rename src/app/query/bitcoin/ordinals/brc20/{brc-20.utils.spec.ts => brc20-tokens.utils.spec.ts} (86%) rename src/app/query/bitcoin/ordinals/brc20/{brc-20.utils.ts => brc20-tokens.utils.ts} (100%) create mode 100644 src/app/query/stacks/stx20/stx20-tokens.hooks.ts create mode 100644 src/app/query/stacks/stx20/stx20-tokens.query.ts delete mode 100644 src/app/query/stacks/token-metadata-client.ts delete mode 100644 src/app/query/stacks/utils.ts create mode 100644 src/app/ui/components/avatar/stx20-avatar-icon.tsx diff --git a/public/assets/avatars/stx20-avatar-icon.png b/public/assets/avatars/stx20-avatar-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7a686f283d518388bd295e1f746c48a94f0934e7 GIT binary patch literal 19817 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
ccco* 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 U3Vg5KO#lD@07*qoM6N<$f_(*MyZ`_I literal 0 HcmV?d00001 diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx index 022aec7a158..706537735a8 100644 --- a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx +++ b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx @@ -3,10 +3,10 @@ import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.que import { Src20TokenAssetItemLayout } from './src20-token-asset-item.layout'; interface Src20TokenAssetListProps { - src20Tokens: Src20Token[]; + tokens: Src20Token[]; } -export function Src20TokenAssetList({ src20Tokens }: Src20TokenAssetListProps) { - return src20Tokens.map((token, i) => ( +export function Src20TokenAssetList({ tokens }: Src20TokenAssetListProps) { + return tokens.map((token, i) => ( )); } diff --git a/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx b/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx new file mode 100644 index 00000000000..6cc4b7d2f1d --- /dev/null +++ b/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx @@ -0,0 +1,37 @@ +import { styled } from 'leather-styles/jsx'; + +import { formatBalance } from '@app/common/format-balance'; +import type { Stx20Token } from '@app/query/stacks/stacks-client'; +import { Stx20AvatarIcon } from '@app/ui/components/avatar/stx20-avatar-icon'; +import { ItemLayout } from '@app/ui/components/item-layout/item-layout'; +import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; +import { Pressable } from '@app/ui/pressable/pressable'; + +interface Stx20TokenAssetItemLayoutProps { + token: Stx20Token; +} +export function Stx20TokenAssetItemLayout({ token }: Stx20TokenAssetItemLayoutProps) { + const balanceAsString = token.balance.amount.toString(); + const formattedBalance = formatBalance(balanceAsString); + + return ( + + } + titleLeft={token.tokenData.ticker} + captionLeft="STX-20" + titleRight={ + + + {formattedBalance.value} + + + } + /> + + ); +} diff --git a/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx b/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx new file mode 100644 index 00000000000..18462051c5c --- /dev/null +++ b/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx @@ -0,0 +1,12 @@ +import type { Stx20Token } from '@app/query/stacks/stacks-client'; + +import { Stx20TokenAssetItemLayout } from './stx20-token-asset-item.layout'; + +interface Stx20TokenAssetListProps { + tokens: Stx20Token[]; +} +export function Stx20TokenAssetList({ tokens }: Stx20TokenAssetListProps) { + return tokens.map((token, i) => ( + + )); +} diff --git a/src/app/components/loaders/brc20-tokens-loader.tsx b/src/app/components/loaders/brc20-tokens-loader.tsx index b5769cd74da..cbc9af4a770 100644 --- a/src/app/components/loaders/brc20-tokens-loader.tsx +++ b/src/app/components/loaders/brc20-tokens-loader.tsx @@ -2,9 +2,9 @@ import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; interface Brc20TokensLoaderProps { - children(brc20Tokens: Brc20Token[]): React.ReactNode; + children(tokens: Brc20Token[]): React.ReactNode; } export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) { - const brc20Tokens = useBrc20Tokens(); - return children(brc20Tokens); + const tokens = useBrc20Tokens(); + return children(tokens); } diff --git a/src/app/components/loaders/src20-tokens-loader.tsx b/src/app/components/loaders/src20-tokens-loader.tsx index 170fdbfc3d1..1ce829f6beb 100644 --- a/src/app/components/loaders/src20-tokens-loader.tsx +++ b/src/app/components/loaders/src20-tokens-loader.tsx @@ -3,9 +3,9 @@ import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.que interface Src20TokensLoaderProps { address: string; - children(src20Tokens: Src20Token[]): React.ReactNode; + children(tokens: Src20Token[]): React.ReactNode; } export function Src20TokensLoader({ address, children }: Src20TokensLoaderProps) { - const { data: src20Tokens = [] } = useSrc20TokensByAddress(address); - return children(src20Tokens); + const { data: tokens = [] } = useSrc20TokensByAddress(address); + return children(tokens); } diff --git a/src/app/components/loaders/stx20-tokens-loader.tsx b/src/app/components/loaders/stx20-tokens-loader.tsx new file mode 100644 index 00000000000..986d214f6e6 --- /dev/null +++ b/src/app/components/loaders/stx20-tokens-loader.tsx @@ -0,0 +1,11 @@ +import type { Stx20Token } from '@app/query/stacks/stacks-client'; +import { useStx20Tokens } from '@app/query/stacks/stx20/stx20-tokens.hooks'; + +interface Stx20TokensLoaderProps { + address: string; + children(tokens: Stx20Token[]): React.ReactNode; +} +export function Stx20TokensLoader({ address, children }: Stx20TokensLoaderProps) { + const { data: tokens = [] } = useStx20Tokens(address); + return children(tokens); +} diff --git a/src/app/features/asset-list/asset-list.tsx b/src/app/features/asset-list/asset-list.tsx index 4963ad8bcd9..0324823ece5 100644 --- a/src/app/features/asset-list/asset-list.tsx +++ b/src/app/features/asset-list/asset-list.tsx @@ -10,8 +10,16 @@ import { BitcoinTaprootAccountLoader, } from '@app/components/account/bitcoin-account-loader'; import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point'; +import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list'; +import { RunesAssetList } from '@app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list'; +import { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list'; import { CryptoCurrencyAssetItemLayout } from '@app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout'; +import { Stx20TokenAssetList } from '@app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list'; +import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader'; +import { RunesLoader } from '@app/components/loaders/runes-loader'; +import { Src20TokensLoader } from '@app/components/loaders/src20-tokens-loader'; import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader'; +import { Stx20TokensLoader } from '@app/components/loaders/stx20-tokens-loader'; import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -20,7 +28,6 @@ import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; import { Collectibles } from '../collectibles/collectibles'; import { PendingBrc20TransferList } from '../pending-brc-20-transfers/pending-brc-20-transfers'; import { AddStacksLedgerKeysItem } from './components/add-stacks-ledger-keys-item'; -import { BitcoinFungibleTokenAssetList } from './components/bitcoin-fungible-tokens-asset-list'; import { ConnectLedgerAssetBtn } from './components/connect-ledger-asset-button'; import { StacksBalanceListItem } from './components/stacks-balance-list-item'; import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token-asset-list'; @@ -74,6 +81,9 @@ export function AssetsList() { <> + + {tokens => } + )} @@ -82,10 +92,17 @@ export function AssetsList() { {nativeSegwitAccount => ( {taprootAccount => ( - + <> + + {tokens => } + + + {tokens => } + + + {runes => } + + )} )} diff --git a/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx b/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx deleted file mode 100644 index ceacfafe001..00000000000 --- a/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list'; -import { RunesAssetList } from '@app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list'; -import { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list'; -import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader'; -import { RunesLoader } from '@app/components/loaders/runes-loader'; -import { Src20TokensLoader } from '@app/components/loaders/src20-tokens-loader'; - -interface BitcoinFungibleTokenAssetListProps { - btcAddressNativeSegwit: string; - btcAddressTaproot: string; -} -export function BitcoinFungibleTokenAssetList({ - btcAddressNativeSegwit, - btcAddressTaproot, -}: BitcoinFungibleTokenAssetListProps) { - return ( - <> - - {brc20Tokens => } - - - {src20Tokens => } - - - {runes => } - - - ); -} diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index 5882888ac1c..aee6896925b 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -179,7 +179,7 @@ interface RunesOutputsByAddressResponse { data: RunesOutputsByAddress[]; } -class BestinslotApi { +class BestinSlotApi { url = BESTINSLOT_API_BASE_URL_MAINNET; testnetUrl = BESTINSLOT_API_BASE_URL_TESTNET; @@ -407,13 +407,13 @@ export class BitcoinClient { addressApi: AddressApi; feeEstimatesApi: FeeEstimatesApi; transactionsApi: TransactionsApi; - BestinslotApi: BestinslotApi; + bestinSlotApi: BestinSlotApi; - constructor(basePath: string) { - this.configuration = new Configuration(basePath); + constructor(baseUrl: string) { + this.configuration = new Configuration(baseUrl); this.addressApi = new AddressApi(this.configuration); this.feeEstimatesApi = new FeeEstimatesApi(this.configuration); this.transactionsApi = new TransactionsApi(this.configuration); - this.BestinslotApi = new BestinslotApi(this.configuration); + this.bestinSlotApi = new BestinSlotApi(this.configuration); } } diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts index cb3e43ddab9..6e7bd527808 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts @@ -15,7 +15,10 @@ import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice'; import type { Brc20Token } from '../../bitcoin-client'; import { useAverageBitcoinFeeRates } from '../../fees/fee-estimates.hooks'; import { useOrdinalsbotClient } from '../../ordinalsbot-client'; -import { createBrc20TransferInscription, encodeBrc20TransferInscription } from './brc-20.utils'; +import { + createBrc20TransferInscription, + encodeBrc20TransferInscription, +} from './brc20-tokens.utils'; // ts-unused-exports:disable-next-line export function useBrc20FeatureFlag() { diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts index c5a294cc309..6440524a788 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts @@ -51,11 +51,11 @@ export function useGetBrc20TokensQuery() { } const brc20TokensPromises = addressesData.map(async address => { - const brc20Tokens = await client.BestinslotApi.getBrc20Balances(address); + const brc20Tokens = await client.bestinSlotApi.getBrc20Balances(address); const tickerPromises = await Promise.all( brc20Tokens.data.map(token => { - return client.BestinslotApi.getBrc20TickerInfo(token.ticker); + return client.bestinSlotApi.getBrc20TickerInfo(token.ticker); }) ); diff --git a/src/app/query/bitcoin/ordinals/brc20/brc-20.utils.spec.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.utils.spec.ts similarity index 86% rename from src/app/query/bitcoin/ordinals/brc20/brc-20.utils.spec.ts rename to src/app/query/bitcoin/ordinals/brc20/brc20-tokens.utils.spec.ts index d9f2b8f4e59..d916ffc4a03 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc-20.utils.spec.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.utils.spec.ts @@ -1,4 +1,4 @@ -import { encodeBrc20TransferInscription } from './brc-20.utils'; +import { encodeBrc20TransferInscription } from './brc20-tokens.utils'; describe(encodeBrc20TransferInscription.name, () => { test('that it encodes the BRC-20 transfer correctly', () => { diff --git a/src/app/query/bitcoin/ordinals/brc20/brc-20.utils.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.utils.ts similarity index 100% rename from src/app/query/bitcoin/ordinals/brc20/brc-20.utils.ts rename to src/app/query/bitcoin/ordinals/brc20/brc20-tokens.utils.ts diff --git a/src/app/query/bitcoin/runes/runes-outputs-by-address.query.ts b/src/app/query/bitcoin/runes/runes-outputs-by-address.query.ts index 5d92ffb56df..a38fa9425bc 100644 --- a/src/app/query/bitcoin/runes/runes-outputs-by-address.query.ts +++ b/src/app/query/bitcoin/runes/runes-outputs-by-address.query.ts @@ -21,7 +21,7 @@ export function useGetRunesOutputsByAddressQuery - client.BestinslotApi.getRunesOutputsByAddress({ + client.bestinSlotApi.getRunesOutputsByAddress({ address, network: network.chain.bitcoin.bitcoinNetwork, }), diff --git a/src/app/query/bitcoin/runes/runes-ticker-info.query.ts b/src/app/query/bitcoin/runes/runes-ticker-info.query.ts index 36223438ced..ed9d2804d5e 100644 --- a/src/app/query/bitcoin/runes/runes-ticker-info.query.ts +++ b/src/app/query/bitcoin/runes/runes-ticker-info.query.ts @@ -19,7 +19,7 @@ export function useGetRunesTickerInfoQuery(runeNames: string[]): UseQueryResult< enabled: !!runeName && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled), queryKey: ['runes-ticker-info', runeName], queryFn: () => - client.BestinslotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork), + client.bestinSlotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork), ...queryOptions, }; }), diff --git a/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts b/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts index 16e8177c169..c33fcf5536b 100644 --- a/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts +++ b/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts @@ -23,7 +23,7 @@ export function useGetRunesWalletBalancesByAddressesQuery - client.BestinslotApi.getRunesWalletBalances( + client.bestinSlotApi.getRunesWalletBalances( address, network.chain.bitcoin.bitcoinNetwork ), diff --git a/src/app/query/bitcoin/transaction/use-check-utxos.ts b/src/app/query/bitcoin/transaction/use-check-utxos.ts index 71fe959a921..09fe190bdab 100644 --- a/src/app/query/bitcoin/transaction/use-check-utxos.ts +++ b/src/app/query/bitcoin/transaction/use-check-utxos.ts @@ -55,7 +55,7 @@ async function checkInscribedUtxosByBestinslot({ * @see https://docs.bestinslot.xyz/reference/api-reference/ordinals-and-brc-20-and-bitmap-v3-api-mainnet+testnet/inscriptions */ const inscriptionIdsList = await Promise.all( - txids.map(id => client.BestinslotApi.getInscriptionsByTransactionId(id)) + txids.map(id => client.bestinSlotApi.getInscriptionsByTransactionId(id)) ); const inscriptionIds = inscriptionIdsList.flatMap(inscription => @@ -63,7 +63,7 @@ async function checkInscribedUtxosByBestinslot({ ); const inscriptionsList = await Promise.all( - inscriptionIds.map(id => client.BestinslotApi.getInscriptionById(id)) + inscriptionIds.map(id => client.bestinSlotApi.getInscriptionById(id)) ); const hasInscribedUtxos = inscriptionsList.some(resp => { diff --git a/src/app/query/stacks/stacks-client.ts b/src/app/query/stacks/stacks-client.ts index e86c6f26497..a06231c957f 100644 --- a/src/app/query/stacks/stacks-client.ts +++ b/src/app/query/stacks/stacks-client.ts @@ -1,8 +1,9 @@ +import { TokensApi } from '@hirosystems/token-metadata-api-client'; import { AccountsApi, BlocksApi, Configuration, - ConfigurationParameters, + type ConfigurationParameters, FaucetsApi, FeesApi, FungibleTokensApi, @@ -14,6 +15,36 @@ import { SmartContractsApi, TransactionsApi, } from '@stacks/blockchain-api-client'; +import axios from 'axios'; + +import { STX20_API_BASE_URL_MAINNET } from '@shared/constants'; +import type { Money } from '@shared/models/money.model'; + +export interface Stx20Balance { + ticker: string; + balance: string; + updateDate: string; +} + +interface Stx20BalanceResponse { + address: string; + balances: Stx20Balance[]; +} + +export interface Stx20Token { + balance: Money; + marketData: null; + tokenData: Stx20Balance; +} + +class Stx20Api { + url = STX20_API_BASE_URL_MAINNET; + + async getStx20Balances(address: string) { + const resp = await axios.get(`${this.url}/balance/${address}`); + return resp.data.balances; + } +} export class StacksClient { configuration: Configuration; @@ -29,6 +60,8 @@ export class StacksClient { rosettaApi: RosettaApi; fungibleTokensApi: FungibleTokensApi; nonFungibleTokensApi: NonFungibleTokensApi; + tokensApi: TokensApi; + stx20Api: Stx20Api; constructor(config: ConfigurationParameters) { this.configuration = new Configuration(config); @@ -44,5 +77,7 @@ export class StacksClient { this.rosettaApi = new RosettaApi(this.configuration); this.fungibleTokensApi = new FungibleTokensApi(this.configuration); this.nonFungibleTokensApi = new NonFungibleTokensApi(this.configuration); + this.tokensApi = new TokensApi({ basePath: config.basePath }); + this.stx20Api = new Stx20Api(); } } diff --git a/src/app/query/stacks/stx20/stx20-tokens.hooks.ts b/src/app/query/stacks/stx20/stx20-tokens.hooks.ts new file mode 100644 index 00000000000..f3394d4a650 --- /dev/null +++ b/src/app/query/stacks/stx20/stx20-tokens.hooks.ts @@ -0,0 +1,20 @@ +import BigNumber from 'bignumber.js'; + +import { createMoney } from '@shared/models/money.model'; + +import type { Stx20Balance, Stx20Token } from '../stacks-client'; +import { useStx20BalancesQuery } from './stx20-tokens.query'; + +function makeStx20Token(token: Stx20Balance): Stx20Token { + return { + balance: createMoney(new BigNumber(token.balance), token.ticker, 0), + marketData: null, + tokenData: token, + }; +} + +export function useStx20Tokens(address: string) { + return useStx20BalancesQuery(address, { + select: resp => resp.map(balance => makeStx20Token(balance)), + }); +} diff --git a/src/app/query/stacks/stx20/stx20-tokens.query.ts b/src/app/query/stacks/stx20/stx20-tokens.query.ts new file mode 100644 index 00000000000..529922547fb --- /dev/null +++ b/src/app/query/stacks/stx20/stx20-tokens.query.ts @@ -0,0 +1,23 @@ +import { ChainID } from '@stacks/transactions'; +import { useQuery } from '@tanstack/react-query'; + +import { AppUseQueryConfig } from '@app/query/query-config'; +import { useStacksClient } from '@app/store/common/api-clients.hooks'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + +import type { Stx20Balance } from '../stacks-client'; + +export function useStx20BalancesQuery( + address: string, + options?: AppUseQueryConfig +) { + const client = useStacksClient(); + const network = useCurrentNetwork(); + + return useQuery({ + enabled: network.chain.stacks.chainId === ChainID.Mainnet, + queryKey: ['stx20-balances', address], + queryFn: () => client.stx20Api.getStx20Balances(address), + ...options, + }); +} diff --git a/src/app/query/stacks/token-metadata-client.ts b/src/app/query/stacks/token-metadata-client.ts deleted file mode 100644 index d5fc9c8c75f..00000000000 --- a/src/app/query/stacks/token-metadata-client.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - Configuration, - ConfigurationParameters, - TokensApi, -} from '@hirosystems/token-metadata-api-client'; - -export class TokenMetadataClient { - configuration: Configuration; - tokensApi: TokensApi; - - constructor(config: ConfigurationParameters) { - this.configuration = new Configuration(config); - this.tokensApi = new TokensApi(this.configuration); - } -} diff --git a/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts b/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts index 7e53a2db505..8bdaa6e8b7a 100644 --- a/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts +++ b/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts @@ -1,11 +1,11 @@ import { UseQueryResult, useQueries, useQuery } from '@tanstack/react-query'; import PQueue from 'p-queue'; -import { useTokenMetadataClient } from '@app/store/common/api-clients.hooks'; +import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; import { useHiroApiRateLimiter } from '../../hiro-rate-limiter'; -import { TokenMetadataClient } from '../../token-metadata-client'; +import type { StacksClient } from '../../stacks-client'; import { FtAssetResponse } from '../token-metadata.utils'; const staleTime = 12 * 60 * 60 * 1000; @@ -21,7 +21,7 @@ const queryOptions = { retry: 0, } as const; -function fetchFungibleTokenMetadata(client: TokenMetadataClient, limiter: PQueue) { +function fetchFungibleTokenMetadata(client: StacksClient, limiter: PQueue) { return (principal: string) => async () => { return limiter.add(() => client.tokensApi.getFtMetadata(principal), { throwOnTimeout: true, @@ -32,7 +32,7 @@ function fetchFungibleTokenMetadata(client: TokenMetadataClient, limiter: PQueue export function useGetFungibleTokenMetadataQuery( principal: string ): UseQueryResult { - const client = useTokenMetadataClient(); + const client = useStacksClient(); const network = useCurrentNetworkState(); const limiter = useHiroApiRateLimiter(); @@ -46,7 +46,7 @@ export function useGetFungibleTokenMetadataQuery( export function useGetFungibleTokenMetadataListQuery( principals: string[] ): UseQueryResult[] { - const client = useTokenMetadataClient(); + const client = useStacksClient(); const network = useCurrentNetworkState(); const limiter = useHiroApiRateLimiter(); diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts index 02311940d5f..04cdd650d08 100644 --- a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts +++ b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts @@ -4,7 +4,7 @@ import { UseQueryResult, useQueries } from '@tanstack/react-query'; import { pullContractIdFromIdentity } from '@app/common/utils'; import { QueryPrefixes } from '@app/query/query-prefixes'; import { StacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.models'; -import { useTokenMetadataClient } from '@app/store/common/api-clients.hooks'; +import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useHiroApiRateLimiter } from '../../hiro-rate-limiter'; import { NftAssetResponse } from '../token-metadata.utils'; @@ -24,7 +24,7 @@ function getTokenId(hex: string) { export function useGetNonFungibleTokenMetadataListQuery( account: StacksAccount ): UseQueryResult[] { - const client = useTokenMetadataClient(); + const client = useStacksClient(); const limiter = useHiroApiRateLimiter(); const nftHoldings = useGetNonFungibleTokenHoldingsQuery(account.address); diff --git a/src/app/query/stacks/utils.ts b/src/app/query/stacks/utils.ts deleted file mode 100644 index 390bcbee57b..00000000000 --- a/src/app/query/stacks/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Configuration as TokenMetadataConfiguration } from '@hirosystems/token-metadata-api-client'; -import { Configuration } from '@stacks/blockchain-api-client'; - -import { wrappedFetch as fetchApi } from '@app/common/api/fetch-wrapper'; - -export function createStacksClientConfig(basePath: string) { - return new Configuration({ - basePath, - fetchApi, - }); -} - -export function createTokenMetadataConfig(basePath: string) { - return new TokenMetadataConfiguration({ basePath }); -} diff --git a/src/app/store/common/api-clients.hooks.ts b/src/app/store/common/api-clients.hooks.ts index 8103caf66cd..33c3a59934c 100644 --- a/src/app/store/common/api-clients.hooks.ts +++ b/src/app/store/common/api-clients.hooks.ts @@ -1,14 +1,8 @@ import { useMemo } from 'react'; -import { ChainID } from '@stacks/transactions'; - -import { HIRO_API_BASE_URL_MAINNET, HIRO_API_BASE_URL_TESTNET } from '@shared/constants'; -import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; - +import { wrappedFetch as fetchApi } from '@app/common/api/fetch-wrapper'; import { BitcoinClient } from '@app/query/bitcoin/bitcoin-client'; import { StacksClient } from '@app/query/stacks/stacks-client'; -import { TokenMetadataClient } from '@app/query/stacks/token-metadata-client'; -import { createStacksClientConfig, createTokenMetadataConfig } from '@app/query/stacks/utils'; import { useCurrentNetworkState } from '../networks/networks.hooks'; @@ -21,20 +15,9 @@ export function useStacksClient() { const network = useCurrentNetworkState(); return useMemo(() => { - const config = createStacksClientConfig(network.chain.stacks.url); - return new StacksClient(config); + return new StacksClient({ + basePath: network.chain.stacks.url, + fetchApi, + }); }, [network.chain.stacks.url]); } - -export function useTokenMetadataClient() { - const currentNetwork = useCurrentNetworkState(); - - const basePath = whenStacksChainId(currentNetwork.chain.stacks.chainId)({ - [ChainID.Mainnet]: HIRO_API_BASE_URL_MAINNET, - [ChainID.Testnet]: HIRO_API_BASE_URL_TESTNET, - }); - - const config = createTokenMetadataConfig(basePath); - - return new TokenMetadataClient(config); -} diff --git a/src/app/ui/components/avatar/stx20-avatar-icon.tsx b/src/app/ui/components/avatar/stx20-avatar-icon.tsx new file mode 100644 index 00000000000..05e6aefe9e6 --- /dev/null +++ b/src/app/ui/components/avatar/stx20-avatar-icon.tsx @@ -0,0 +1,14 @@ +import Stx20AvatarIconSrc from '@assets/avatars/stx20-avatar-icon.png'; + +import { Avatar, type AvatarProps } from './avatar'; + +const fallback = 'ST'; + +export function Stx20AvatarIcon(props: AvatarProps) { + return ( + + + {fallback} + + ); +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 9b5b770fcb8..8f95f08798c 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -78,6 +78,8 @@ export interface NetworkConfiguration { export const BESTINSLOT_API_BASE_URL_MAINNET = 'https://leatherapi.bestinslot.xyz/v3'; export const BESTINSLOT_API_BASE_URL_TESTNET = 'https://leatherapi_testnet.bestinslot.xyz/v3'; +export const STX20_API_BASE_URL_MAINNET = 'https://api.stx20.com/api/v1'; + export const HIRO_API_BASE_URL_MAINNET = 'https://api.hiro.so'; export const HIRO_API_BASE_URL_TESTNET = 'https://api.testnet.hiro.so'; export const HIRO_INSCRIPTIONS_API_URL = 'https://api.hiro.so/ordinals/v1/inscriptions'; From 7db0d9755e39269885472beb1e68c6479a4c7521 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Tue, 30 Apr 2024 11:10:53 -0500 Subject: [PATCH 5/8] fix: swap test --- src/app/pages/swap/components/swap-review.tsx | 2 +- tests/page-object-models/swap.page.ts | 8 +++--- tests/selectors/swap.selectors.ts | 2 +- tests/specs/swap/swap.spec.ts | 26 +++++++------------ 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/app/pages/swap/components/swap-review.tsx b/src/app/pages/swap/components/swap-review.tsx index e4e5e1ab8dd..e99767770a8 100644 --- a/src/app/pages/swap/components/swap-review.tsx +++ b/src/app/pages/swap/components/swap-review.tsx @@ -23,7 +23,7 @@ export function SwapReview() {