diff --git a/public/assets/avatars/stx20-avatar-icon.png b/public/assets/avatars/stx20-avatar-icon.png new file mode 100644 index 00000000000..7a686f283d5 Binary files /dev/null and b/public/assets/avatars/stx20-avatar-icon.png differ 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..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 @@ -21,25 +21,74 @@ 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); + }); + + 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 (biggest first and no dust)', () => { + 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', () => { 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,29 +99,29 @@ 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'); + const legacy = generate10kSpendWithDummyUtxoSet('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj'); + const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); expect(legacy.fee).toBeGreaterThan(segwit.fee); }); test('that given a set of utxos, wrapped segwit is more expensive than native', () => { - const segwit = generate10kSpendWithTestData('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); - const native = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); + const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH'); + const native = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m'); expect(segwit.fee).toBeGreaterThan(native.fee); }); @@ -80,8 +129,8 @@ describe(determineUtxosForSpend.name, () => { // 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); 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..711c73e18a3 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,9 +35,9 @@ 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, }); @@ -52,6 +54,10 @@ export function determineUtxosForSpendAll({ }; } +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 fee = 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(fee) }, ]; return { filteredUtxos, inputs: neededUtxos, outputs, - size: sizeInfo.txVBytes, + size: estimateTransactionSize().txVBytes, fee, + ...estimateTransactionSize(), }; } 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..b90640fc5b2 100644 --- a/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts +++ b/src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts @@ -166,7 +166,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: 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/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/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/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/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 ( - - + + ); 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/coinselect/select-inscription-coins.spec.ts b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.spec.ts index 67978daf466..0a66c13d460 100644 --- a/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.spec.ts +++ b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.spec.ts @@ -37,7 +37,7 @@ describe(selectTaprootInscriptionTransferCoins.name, () => { Number(inscriptionInputAmount) ); - expect(result.txFee).toEqual(5048); + expect(result.txFee).toEqual(6608); }); test('when there are not enough utxo to cover fee', () => { diff --git a/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts index 51ac5736e97..eab76a0ca44 100644 --- a/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts +++ b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts @@ -37,10 +37,11 @@ export function selectTaprootInscriptionTransferCoins( const txSizer = new BtcSizeFeeEstimator(); const initialTxSize = txSizer.calcTxSize({ - input_script: 'p2tr', + input_script: 'p2wpkh', input_count: 1, // From the address of the recipient, we infer the output type p2tr_output_count: 1, + p2wpkh_output_count: 1, }); const neededInputs: UtxoResponseItem[] = []; @@ -65,9 +66,10 @@ export function selectTaprootInscriptionTransferCoins( if (nextUtxo) neededInputs.push(nextUtxo); utxos = remainingUtxos; txSize = txSizer.calcTxSize({ - input_script: 'p2tr', + input_script: 'p2wpkh', input_count: neededInputs.length + 1, p2tr_output_count: 1, + p2wpkh_output_count: 1, }); indexCounter.increment(); } 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..7a7258f003a 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 @@ -104,7 +104,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio if (e instanceof InsufficientFundsError) { throw new InsufficientFundsError(); } - logger.error('Unable to sign transaction'); + logger.error('Unable to sign transaction', e); return null; } } @@ -159,7 +159,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio if (e instanceof InsufficientFundsError) { throw new InsufficientFundsError(); } - logger.error('Unable to sign transaction'); + logger.error('Unable to sign transaction', e); return null; } } 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..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 @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { hexToBytes } from '@noble/hashes/utils'; @@ -50,13 +51,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/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() {