From 7bdabba97d555e9dc31eabedfde67816e80ab2d0 Mon Sep 17 00:00:00 2001 From: alter-eggo Date: Wed, 6 Mar 2024 00:31:53 +0400 Subject: [PATCH] fix: tx calc unhandled errors, closes #4941 --- .../coinselect/local-coin-selection.ts | 8 +- .../bitcoin-custom-fee-fiat.tsx | 41 +------ .../bitcoin-custom-fee-input.tsx | 110 ++++++++++++++++++ .../bitcoin-custom-fee/bitcoin-custom-fee.tsx | 51 ++------ .../use-bitcoin-fees-list.ts | 38 ++++-- .../components/increase-btc-fee-form.tsx | 27 ++--- .../select-inscription-coins.spec.ts | 8 +- .../coinselect/select-inscription-coins.ts | 9 +- .../hooks/use-generate-ordinal-tx.ts | 23 ++-- .../hooks/use-send-inscription-fees-list.ts | 94 ++++++++------- .../hooks/use-send-inscription-form.tsx | 12 +- .../send-inscription-choose-fee.tsx | 2 +- .../send-inscription-form.tsx | 6 +- .../recipient-address-type-field.tsx | 10 +- .../form/brc-20/use-brc20-send-form.tsx | 1 - 15 files changed, 262 insertions(+), 178 deletions(-) create mode 100644 src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-input.tsx 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 ade9b348719..b886597f563 100644 --- a/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +++ b/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts @@ -11,6 +11,12 @@ export interface DetermineUtxosForSpendArgs { utxos: UtxoResponseItem[]; } +export class InsufficientFundsError extends Error { + constructor() { + super('Insufficient funds'); + } +} + export function determineUtxosForSpendAll({ amount, feeRate, @@ -71,7 +77,7 @@ export function determineUtxosForSpend({ neededUtxos.push(utxo); } - if (!sizeInfo) throw new Error('Transaction size must be defined'); + if (!sizeInfo) throw new InsufficientFundsError(); const fee = Math.ceil(sizeInfo.txVBytes * feeRate); diff --git a/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat.tsx b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat.tsx index 993cb5df93d..aed27511b5a 100644 --- a/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat.tsx +++ b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat.tsx @@ -1,49 +1,18 @@ -import { useMemo } from 'react'; - -import { useField } from 'formik'; import { Flex, styled } from 'leather-styles/jsx'; -import { createMoney } from '@shared/models/money.model'; - -import { satToBtc } from '@app/common/money/unit-conversion'; - -import { useBitcoinCustomFee } from './hooks/use-bitcoin-custom-fee'; - interface BitcoinCustomFeeFiatProps { - amount: number; - isSendingMax: boolean; - recipient: string; + feeInBtc: string; + fiatFeeValue: string; } -export function BitcoinCustomFeeFiat({ - amount, - isSendingMax, - recipient, -}: BitcoinCustomFeeFiatProps) { - const [field] = useField('feeRate'); - const getCustomFeeValues = useBitcoinCustomFee({ - amount: createMoney(amount, 'BTC'), - isSendingMax, - recipient, - }); - - const feeData = useMemo(() => { - const { fee, fiatFeeValue } = getCustomFeeValues(Number(field.value)); - const feeInBtc = satToBtc(fee).toString(); - - return { fiatFeeValue, feeInBtc }; - }, [getCustomFeeValues, field.value]); - - const canShow = !feeData.feeInBtc.includes('e') && Number(field.value) > 0; - if (!canShow) return null; - +export function BitcoinCustomFeeFiat({ feeInBtc, fiatFeeValue }: BitcoinCustomFeeFiatProps) { return ( - {feeData.fiatFeeValue} + {fiatFeeValue} - {feeData.feeInBtc} BTC + {feeInBtc} BTC ); diff --git a/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-input.tsx b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-input.tsx new file mode 100644 index 00000000000..83b561d112a --- /dev/null +++ b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee-input.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; + +import { useField } from 'formik'; +import { Stack } from 'leather-styles/jsx'; + +import { createMoney } from '@shared/models/money.model'; + +import { useOnMount } from '@app/common/hooks/use-on-mount'; +import { satToBtc } from '@app/common/money/unit-conversion'; +import { InsufficientFundsError } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; +import { Input } from '@app/ui/components/input/input'; + +import { ErrorLabel } from '../error-label'; +import { BitcoinCustomFeeFiat } from './bitcoin-custom-fee-fiat'; +import { useBitcoinCustomFee } from './hooks/use-bitcoin-custom-fee'; + +interface Props { + onClick?(): void; + amount: number; + isSendingMax: boolean; + recipient: string; + hasInsufficientBalanceError: boolean; + errorMessage?: string; + setCustomFeeInitialValue?(value: string): void; + customFeeInitialValue: string; +} + +const feeInputLabel = 'sats/vB'; + +export function BitcoinCustomFeeInput({ + onClick, + amount, + isSendingMax, + recipient, + hasInsufficientBalanceError, + setCustomFeeInitialValue, + customFeeInitialValue, +}: Props) { + const [field] = useField('feeRate'); + + const [feeValue, setFeeValue] = useState(null); + + const getCustomFeeValues = useBitcoinCustomFee({ + amount: createMoney(amount, 'BTC'), + isSendingMax, + recipient, + }); + const [unknownError, setUnknownError] = useState(false); + const [customInsufficientBalanceError, setCustomInsufficientBalanceError] = useState(false); + + const hasError = hasInsufficientBalanceError || unknownError || customInsufficientBalanceError; + const errorMessage = + hasInsufficientBalanceError || customInsufficientBalanceError + ? 'Insufficient funds' + : 'Unknown error'; + + function processFeeValue(feeRate: string) { + try { + const feeValues = getCustomFeeValues(Number(feeRate)); + setFeeValue(feeValues); + + setUnknownError(false); + setCustomInsufficientBalanceError(false); + } catch (err) { + if (err instanceof InsufficientFundsError) { + return setCustomInsufficientBalanceError(true); + } + + setUnknownError(true); + } + } + + function onChange(e: React.ChangeEvent) { + const value = e.target.value; + setCustomFeeInitialValue?.(e.target.value); + processFeeValue(value); + } + + useOnMount(() => { + processFeeValue(customFeeInitialValue); + }); + return ( + + + + {feeInputLabel} + { + field.onChange(e); + onChange?.(e); + }} + /> + + {hasError && {errorMessage}} + + + {!hasError && feeValue && ( + + )} + + ); +} diff --git a/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee.tsx b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee.tsx index 0913027cca8..440fba223e0 100644 --- a/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee.tsx +++ b/src/app/components/bitcoin-custom-fee/bitcoin-custom-fee.tsx @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction, useCallback, useRef } from 'react'; -import { Form, Formik, useField } from 'formik'; +import { Form, Formik } from 'formik'; import { Stack, styled } from 'leather-styles/jsx'; import * as yup from 'yup'; @@ -9,41 +9,12 @@ import { createMoney } from '@shared/models/money.model'; import { openInNewTab } from '@app/common/utils/open-in-new-tab'; import { PreviewButton } from '@app/components/preview-button'; -import { Input } from '@app/ui/components/input/input'; import { Link } from '@app/ui/components/link/link'; import { OnChooseFeeArgs } from '../bitcoin-fees-list/bitcoin-fees-list'; -import { BitcoinCustomFeeFiat } from './bitcoin-custom-fee-fiat'; +import { BitcoinCustomFeeInput } from './bitcoin-custom-fee-input'; import { useBitcoinCustomFee } from './hooks/use-bitcoin-custom-fee'; -const feeInputLabel = 'sats/vB'; - -interface BitcoinCustomFeeInputProps { - hasInsufficientBalanceError: boolean; - onClick(): void; - onChange?(e: React.ChangeEvent): void; -} -function BitcoinCustomFeeInput({ - hasInsufficientBalanceError, - onClick, - onChange, -}: BitcoinCustomFeeInputProps) { - const [field] = useField('feeRate'); - return ( - - {feeInputLabel} - { - field.onChange(e); - onChange?.(e); - }} - /> - - ); -} - interface BitcoinCustomFeeProps { amount: number; customFeeInitialValue: string; @@ -56,6 +27,7 @@ interface BitcoinCustomFeeProps { setCustomFeeInitialValue: Dispatch>; maxCustomFeeRate: number; } + export function BitcoinCustomFee({ amount, customFeeInitialValue, @@ -123,20 +95,17 @@ export function BitcoinCustomFee({ { - feeInputRef?.current?.focus(); + feeInputRef.current?.focus(); await props.setValues({ ...props.values }); }} - onChange={e => setCustomFeeInitialValue((e.target as HTMLInputElement).value)} + customFeeInitialValue={customFeeInitialValue} + setCustomFeeInitialValue={setCustomFeeInitialValue} + recipient={recipient} + hasInsufficientBalanceError={hasInsufficientBalanceError} /> - - - 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 fd98d553812..786016ccdd6 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 @@ -21,10 +21,14 @@ function getFeeForList( determineUtxosForFeeArgs: DetermineUtxosForSpendArgs, isSendingMax?: boolean ) { - const { fee } = isSendingMax - ? determineUtxosForSpendAll(determineUtxosForFeeArgs) - : determineUtxosForSpend(determineUtxosForFeeArgs); - return fee; + try { + const { fee } = isSendingMax + ? determineUtxosForSpendAll(determineUtxosForFeeArgs) + : determineUtxosForSpend(determineUtxosForFeeArgs); + return fee; + } catch (error) { + return null; + } } interface UseBitcoinFeesListArgs { @@ -75,36 +79,46 @@ export function useBitcoinFeesList({ feeRate: feeRates.hourFee.toNumber(), }; + const feesArr = []; + const highFeeValue = getFeeForList(determineUtxosForHighFeeArgs, isSendingMax); const standardFeeValue = getFeeForList(determineUtxosForStandardFeeArgs, isSendingMax); const lowFeeValue = getFeeForList(determineUtxosForLowFeeArgs, isSendingMax); - return [ - { + 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({ label: BtcFeeType.Standard, value: standardFeeValue, btcValue: formatMoneyPadded(createMoney(standardFeeValue, 'BTC')), time: btcTxTimeMap.halfHourFee, fiatValue: getFiatFeeValue(standardFeeValue), feeRate: feeRates.halfHourFee.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]); return { diff --git a/src/app/features/increase-fee-drawer/components/increase-btc-fee-form.tsx b/src/app/features/increase-fee-drawer/components/increase-btc-fee-form.tsx index a957d4fe98b..83f8cca436e 100644 --- a/src/app/features/increase-fee-drawer/components/increase-btc-fee-form.tsx +++ b/src/app/features/increase-fee-drawer/components/increase-btc-fee-form.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; -import { Formik, useField } from 'formik'; +import { Formik } from 'formik'; import { Stack } from 'leather-styles/jsx'; import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; @@ -10,31 +10,19 @@ import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balanc import { formatMoney } from '@app/common/money/format-money'; import { btcToSat } from '@app/common/money/unit-conversion'; import { getBitcoinTxValue } from '@app/common/transactions/bitcoin/utils'; -import { BitcoinCustomFeeFiat } from '@app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat'; +import { BitcoinCustomFeeInput } from '@app/components/bitcoin-custom-fee/bitcoin-custom-fee-input'; import { BitcoinTransactionItem } from '@app/components/bitcoin-transaction-item/bitcoin-transaction-item'; import { LoadingSpinner } from '@app/components/loading-spinner'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { Input } from '@app/ui/components/input/input'; import { Caption } from '@app/ui/components/typography/caption'; import { useBtcIncreaseFee } from '../hooks/use-btc-increase-fee'; import { IncreaseFeeActions } from './increase-fee-actions'; -const feeInputLabel = 'sats/vB'; - -function BitcoinFeeIncreaseField() { - const [field] = useField('feeRate'); - return ( - - - Fee rate - - ); -} - interface IncreaseBtcFeeFormProps { btcTx: BitcoinTx; } + export function IncreaseBtcFeeForm({ btcTx }: IncreaseBtcFeeFormProps) { const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); const navigate = useNavigate(); @@ -63,13 +51,14 @@ export function IncreaseBtcFeeForm({ btcTx }: IncreaseBtcFeeFormProps) { {btcTx && } - - 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 b2713c310b0..09a84f15d52 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 @@ -1,12 +1,12 @@ import { sumNumbers } from '@app/common/math/helpers'; -import { selectInscriptionTransferCoins } from './select-inscription-coins'; +import { selectTaprootInscriptionTransferCoins } from './select-inscription-coins'; -describe(selectInscriptionTransferCoins.name, () => { +describe(selectTaprootInscriptionTransferCoins.name, () => { test('inscription coin selection', () => { const inscriptionInputAmount = 1000n; - const result = selectInscriptionTransferCoins({ + const result = selectTaprootInscriptionTransferCoins({ recipient: '', inscriptionInput: { value: Number(inscriptionInputAmount), @@ -41,7 +41,7 @@ describe(selectInscriptionTransferCoins.name, () => { }); test('when there are not enough utxo to cover fee', () => { - const result = selectInscriptionTransferCoins({ + const result = selectTaprootInscriptionTransferCoins({ recipient: '', inscriptionInput: { value: 1000, 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 ac1e6185b4a..51ac5736e97 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 @@ -6,8 +6,6 @@ import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-s import { createCounter } from '@app/common/utils/counter'; import { UtxoResponseItem, UtxoWithDerivationPath } from '@app/query/bitcoin/bitcoin-client'; -const idealInscriptionValue = 10_000; - interface SelectInscriptionCoinSuccess { success: true; inputs: UtxoResponseItem[]; @@ -28,7 +26,8 @@ interface SelectInscriptionTransferCoinsArgs { recipient: string; changeAddress: string; } -export function selectInscriptionTransferCoins( + +export function selectTaprootInscriptionTransferCoins( args: SelectInscriptionTransferCoinsArgs ): SelectInscriptionCoinResult { const { inscriptionInput, recipient, changeAddress, nativeSegwitUtxos, feeRate } = args; @@ -51,9 +50,9 @@ export function selectInscriptionTransferCoins( const indexCounter = createCounter(); function shouldContinueTryingWithMoreInputs() { - const valueOfUtxos = sumNumbers(neededInputs.map(utxo => utxo.value)); + const neededSumOfInputs = sumNumbers(neededInputs.map(utxo => utxo.value)); if (indexCounter.getValue() > nativeSegwitUtxos.length) return false; - return idealInscriptionValue + txFee > inscriptionInput.value + valueOfUtxos.toNumber(); + return txFee >= neededSumOfInputs.toNumber(); } let utxos = nativeSegwitUtxos 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 78688b02f82..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 @@ -6,7 +6,10 @@ import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config' import { logger } from '@shared/logger'; import { OrdinalSendFormValues } from '@shared/models/form.model'; -import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; +import { + InsufficientFundsError, + determineUtxosForSpend, +} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; import { createCounter } from '@app/common/utils/counter'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; import { UtxoWithDerivationPath } from '@app/query/bitcoin/bitcoin-client'; @@ -14,7 +17,7 @@ import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/ import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; -import { selectInscriptionTransferCoins } from '../coinselect/select-inscription-coins'; +import { selectTaprootInscriptionTransferCoins } from '../coinselect/select-inscription-coins'; export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivationPath) { const createTaprootSigner = useCurrentAccountTaprootSigner(); @@ -37,7 +40,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio if (!taprootSigner || !nativeSegwitSigner || !nativeSegwitUtxos || !values.feeRate) return; - const result = selectInscriptionTransferCoins({ + const result = selectTaprootInscriptionTransferCoins({ recipient: values.recipient, inscriptionInput, nativeSegwitUtxos, @@ -51,7 +54,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio if (!result.success) return null; - const { inputs, outputs } = result; + const { inputs, outputs, txFee } = result; try { const tx = new btc.Transaction(); @@ -96,8 +99,11 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio tx.toPSBT(); - return { psbt: tx.toPSBT(), signingConfig }; + return { psbt: tx.toPSBT(), signingConfig, txFee }; } catch (e) { + if (e instanceof InsufficientFundsError) { + throw new InsufficientFundsError(); + } logger.error('Unable to sign transaction'); return null; } @@ -116,7 +122,7 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio utxos: nativeSegwitUtxos, }; - const { inputs, outputs } = determineUtxosForSpend(determineUtxosArgs); + const { inputs, outputs, fee } = determineUtxosForSpend(determineUtxosArgs); try { const tx = new btc.Transaction(); @@ -148,8 +154,11 @@ export function useGenerateUnsignedOrdinalTx(inscriptionInput: UtxoWithDerivatio tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode); }); - return { psbt: tx.toPSBT(), signingConfig: undefined }; + return { psbt: tx.toPSBT(), signingConfig: undefined, txFee: fee }; } catch (e) { + if (e instanceof InsufficientFundsError) { + throw new InsufficientFundsError(); + } logger.error('Unable to sign transaction'); return null; } diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts index 3003240cb52..52435aa4d3f 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts @@ -1,6 +1,7 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { BtcFeeType, btcTxTimeMap } from '@shared/models/fees/bitcoin-fees.model'; +import type { SupportedInscription } from '@shared/models/inscription.model'; import { createMoney } from '@shared/models/money.model'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; @@ -12,19 +13,44 @@ import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { selectInscriptionTransferCoins } from '../coinselect/select-inscription-coins'; +import { useGenerateUnsignedOrdinalTx } from './use-generate-ordinal-tx'; interface UseSendInscriptionFeesListArgs { recipient: string; utxo: UtxoWithDerivationPath; + inscription: SupportedInscription; } -export function useSendInscriptionFeesList({ recipient, utxo }: UseSendInscriptionFeesListArgs) { + +export function useSendInscriptionFeesList({ + recipient, + utxo, + inscription, +}: UseSendInscriptionFeesListArgs) { const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); const { data: nativeSegwitUtxos } = useCurrentNativeSegwitUtxos(); const btcMarketData = useCryptoCurrencyMarketData('BTC'); const { data: feeRates, isLoading } = useAverageBitcoinFeeRates(); + const { coverFeeFromAdditionalUtxos } = useGenerateUnsignedOrdinalTx(utxo); + + const getTransactionFee = useCallback( + (feeRate: number) => { + try { + const tx = coverFeeFromAdditionalUtxos({ + recipient, + feeRate, + inscription, + }); + + return tx?.txFee; + } catch (error) { + return null; + } + }, + [coverFeeFromAdditionalUtxos, recipient, inscription] + ); + const feesList: FeesListItem[] = useMemo(() => { function getFiatFeeValue(fee: number) { return `~ ${i18nFormatCurrency( @@ -36,63 +62,47 @@ export function useSendInscriptionFeesList({ recipient, utxo }: UseSendInscripti if (!feeRates || !nativeSegwitUtxos || !nativeSegwitSigner) return []; - const highFeeResult = selectInscriptionTransferCoins({ - recipient, - inscriptionInput: utxo, - nativeSegwitUtxos, - changeAddress: nativeSegwitSigner.payment.address!, - feeRate: feeRates.fastestFee.toNumber(), - }); - - const standardFeeResult = selectInscriptionTransferCoins({ - recipient, - inscriptionInput: utxo, - nativeSegwitUtxos, - changeAddress: nativeSegwitSigner.payment.address!, - feeRate: feeRates.halfHourFee.toNumber(), - }); - - const lowFeeResult = selectInscriptionTransferCoins({ - recipient, - inscriptionInput: utxo, - nativeSegwitUtxos, - changeAddress: nativeSegwitSigner.payment.address!, - feeRate: feeRates.hourFee.toNumber(), - }); - - if (!highFeeResult.success || !standardFeeResult.success || !lowFeeResult.success) return []; - - const { txFee: highFeeValue } = highFeeResult; - const { txFee: standardFeeValue } = standardFeeResult; - const { txFee: lowFeeValue } = lowFeeResult; - - return [ - { + const highFeeValue = getTransactionFee(feeRates.fastestFee.toNumber()); + const standardFeeValue = getTransactionFee(feeRates.halfHourFee.toNumber()); + const lowFeeValue = getTransactionFee(feeRates.hourFee.toNumber()); + + const feesArr = []; + + 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({ label: BtcFeeType.Standard, value: standardFeeValue, btcValue: formatMoneyPadded(createMoney(standardFeeValue, 'BTC')), time: btcTxTimeMap.halfHourFee, fiatValue: getFiatFeeValue(standardFeeValue), feeRate: feeRates.halfHourFee.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(), - }, - ]; - }, [createNativeSegwitSigner, feeRates, nativeSegwitUtxos, recipient, utxo, btcMarketData]); + }); + } + + return feesArr; + }, [feeRates, nativeSegwitUtxos, btcMarketData, createNativeSegwitSigner, getTransactionFee]); return { feesList, diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx index f60660bf7d2..9342d37bde6 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx @@ -11,6 +11,7 @@ import { isError } from '@shared/utils'; import { FormErrorMessages } from '@app/common/error-messages'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { formFeeRowValue } from '@app/common/send/utils'; +import { InsufficientFundsError } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; import { btcAddressNetworkValidator, btcAddressValidator, @@ -20,7 +21,7 @@ import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; import { useSendInscriptionState } from '../components/send-inscription-container'; -import { recipeintFieldName } from '../send-inscription-form'; +import { recipientFieldName } from '../send-inscription-form'; import { useGenerateUnsignedOrdinalTx } from './use-generate-ordinal-tx'; export function useSendInscriptionForm() { @@ -66,6 +67,13 @@ export function useSendInscriptionForm() { } catch (error) { void analytics.track('ordinals_dot_com_unavailable', { error }); + if (error instanceof InsufficientFundsError) { + setShowError( + 'Insufficient funds to cover fee. Deposit some BTC to your Native Segwit address.' + ); + return; + } + let message = 'Unable to establish if utxo has multiple inscriptions'; if (isError(error)) { message = error.message; @@ -127,7 +135,7 @@ export function useSendInscriptionForm() { }, validationSchema: yup.object({ - [recipeintFieldName]: yup + [recipientFieldName]: yup .string() .required(FormErrorMessages.AddressRequired) .concat(btcAddressValidator()) diff --git a/src/app/pages/send/ordinal-inscription/send-inscription-choose-fee.tsx b/src/app/pages/send/ordinal-inscription/send-inscription-choose-fee.tsx index 9b90eb08bed..15b572ecbb1 100644 --- a/src/app/pages/send/ordinal-inscription/send-inscription-choose-fee.tsx +++ b/src/app/pages/send/ordinal-inscription/send-inscription-choose-fee.tsx @@ -22,7 +22,7 @@ export function SendInscriptionChooseFee() { const navigate = useNavigate(); const { recipient, selectedFeeType, setSelectedFeeType, utxo, inscription } = useSendInscriptionState(); - const { feesList, isLoading } = useSendInscriptionFeesList({ recipient, utxo }); + const { feesList, isLoading } = useSendInscriptionFeesList({ recipient, utxo, inscription }); const recommendedFeeRate = feesList[1]?.feeRate.toString() || ''; const { reviewTransaction } = useSendInscriptionForm(); diff --git a/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx b/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx index 59d3e4f0afb..6e73330b073 100644 --- a/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx +++ b/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx @@ -18,7 +18,7 @@ import { useSendInscriptionState } from './components/send-inscription-container import { useSendInscriptionForm } from './hooks/use-send-inscription-form'; import { SendInscriptionFormLoader } from './send-indcription-form-loader'; -export const recipeintFieldName = 'recipient'; +export const recipientFieldName = 'recipient'; export function SendInscriptionForm() { const navigate = useNavigate(); @@ -30,7 +30,7 @@ export function SendInscriptionForm() { } name="Ordinal inscription" /> diff --git a/src/app/pages/send/send-crypto-asset-form/components/recipient-address-type-field.tsx b/src/app/pages/send/send-crypto-asset-form/components/recipient-address-type-field.tsx index 23da077496a..3dfbde755ea 100644 --- a/src/app/pages/send/send-crypto-asset-form/components/recipient-address-type-field.tsx +++ b/src/app/pages/send/send-crypto-asset-form/components/recipient-address-type-field.tsx @@ -32,10 +32,12 @@ export function RecipientAddressTypeField({ return ( - - {rightLabel} - - {topInputOverlay} + {rightLabel && ( + + {rightLabel} + + )} + {topInputOverlay && {topInputOverlay}}