diff --git a/src/app/components/bitcoin-fees-list/bitcoin-fees-list.tsx b/src/app/components/bitcoin-fees-list/bitcoin-fees-list.tsx index 052e55dc..df65863a 100644 --- a/src/app/components/bitcoin-fees-list/bitcoin-fees-list.tsx +++ b/src/app/components/bitcoin-fees-list/bitcoin-fees-list.tsx @@ -1,17 +1,25 @@ import { Stack, Text } from '@stacks/ui'; +import { BtcFeeType } from '@shared/models/fees/bitcoin-fees.model'; + import { LoadingSpinner } from '../loading-spinner'; import { FeesCard } from './components/fees-card'; -import { useBitcoinFeesList } from './use-bitcoin-fees-list'; + +export interface FeesListItem { + label: BtcFeeType; + value: number; + btcValue: string; + time: string; + fiatValue: string; + feeRate: number; +} interface BitcoinFeesListProps { - amount: number; + feesList: FeesListItem[]; + isLoading: boolean; onChooseFee(feeRate: number, feeValue: number, time: string): Promise | void; - recipient: string; } -export function BitcoinFeesList({ amount, onChooseFee, recipient }: BitcoinFeesListProps) { - const { feesList, isLoading } = useBitcoinFeesList({ amount, recipient }); - +export function BitcoinFeesList({ feesList, isLoading, onChooseFee }: BitcoinFeesListProps) { if (isLoading) return ; if (!feesList.length) { 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 819337fb..43960b41 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 @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { BtcFeeType, btcTxTimeMap } from '@shared/models/fees/bitcoin-fees.model'; import { createMoney } from '@shared/models/money.model'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; @@ -7,11 +8,12 @@ import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format- import { btcToSat } from '@app/common/money/unit-conversion'; import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query'; -import { BtcFeeType, btcTxTimeMap } from '@app/query/bitcoin/bitcoin-client'; -import { useAverageBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks'; +import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { FeesListItem } from './bitcoin-fees-list'; + interface UseBitcoinFeesListArgs { amount: number; recipient: string; @@ -21,9 +23,9 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress); const btcMarketData = useCryptoCurrencyMarketData('BTC'); - const { avgApiFeeRates: feeRate, isLoading } = useAverageBitcoinFeeRate(); + const { avgApiFeeRates: feeRate, isLoading } = useAverageBitcoinFeeRates(); - const feesList = useMemo(() => { + const feesList: FeesListItem[] = useMemo(() => { function getFiatFeeValue(fee: number) { return `~ ${i18nFormatCurrency( baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) @@ -40,7 +42,7 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs feeRate: feeRate.fastestFee.toNumber(), }); - const { fee: standartFeeValue } = determineUtxosForSpend({ + const { fee: standardFeeValue } = determineUtxosForSpend({ utxos, recipient, amount: satAmount, @@ -65,17 +67,17 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs }, { label: BtcFeeType.Standard, - value: standartFeeValue, - btcValue: formatMoneyPadded(createMoney(standartFeeValue, 'BTC')), + value: standardFeeValue, + btcValue: formatMoneyPadded(createMoney(standardFeeValue, 'BTC')), time: btcTxTimeMap.halfHourFee, - fiatValue: getFiatFeeValue(standartFeeValue), + fiatValue: getFiatFeeValue(standardFeeValue), feeRate: feeRate.halfHourFee.toNumber(), }, { label: BtcFeeType.Low, value: lowFeeValue, btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')), - time: btcTxTimeMap.economyFee, + time: btcTxTimeMap.hourFee, fiatValue: getFiatFeeValue(lowFeeValue), feeRate: feeRate.hourFee.toNumber(), }, diff --git a/src/app/components/fees-row/components/fee-estimate-select.tsx b/src/app/components/fees-row/components/fee-estimate-select.tsx index 71d14eb0..5ff6ada1 100644 --- a/src/app/components/fees-row/components/fee-estimate-select.tsx +++ b/src/app/components/fees-row/components/fee-estimate-select.tsx @@ -1,4 +1,3 @@ -import { BitcoinFeeEstimate } from '@shared/models/fees/bitcoin-fees.model'; import { FeeTypes } from '@shared/models/fees/fees.model'; import { StacksFeeEstimate } from '@shared/models/fees/stacks-fees.model'; @@ -7,7 +6,7 @@ import { FeeEstimateSelectLayout } from './fee-estimate-select.layout'; interface FeeEstimateSelectProps { isVisible: boolean; - estimate: BitcoinFeeEstimate[] | StacksFeeEstimate[]; + estimate: StacksFeeEstimate[]; onSelectItem(index: number): void; onSetIsSelectVisible(value: boolean): void; selectedItem: number; diff --git a/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx b/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx index 3f07075b..dfb5b615 100644 --- a/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx +++ b/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx @@ -7,7 +7,7 @@ import { Money, createMoney } from '@shared/models/money.model'; import { sumNumbers } from '@app/common/math/helpers'; import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator'; import { useCurrentTaprootAccountUninscribedUtxos } from '@app/query/bitcoin/balance/bitcoin-balances.query'; -import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks'; +import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; @@ -16,17 +16,17 @@ export function useGenerateRetrieveTaprootFundsTx() { const networkMode = useBitcoinScureLibNetworkConfig(); const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos(); const createSigner = useCurrentAccountTaprootSigner(); - const { data: feeRate } = useBitcoinFeeRate(); + const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates(); const fee = useMemo(() => { - if (!feeRate) return createMoney(0, 'BTC'); + if (!feeRates) return createMoney(0, 'BTC'); const txSizer = new BtcSizeFeeEstimator(); const { txVBytes } = txSizer.calcTxSize({ input_count: uninscribedUtxos.length, p2wpkh_output_count: 1, }); - return createMoney(Math.ceil(txVBytes * feeRate.hourFee), 'BTC'); - }, [feeRate, uninscribedUtxos.length]); + return createMoney(Math.ceil(txVBytes * feeRates.hourFee.toNumber()), 'BTC'); + }, [feeRates, uninscribedUtxos.length]); const generateRetrieveTaprootFundsTx = useCallback( async ({ recipient, fee }: { recipient: string; fee: Money }) => { diff --git a/src/app/pages/rpc-send-transfer/rpc-send-transfer-choose-fee.tsx b/src/app/pages/rpc-send-transfer/rpc-send-transfer-choose-fee.tsx index 0ee53769..8a50ebce 100644 --- a/src/app/pages/rpc-send-transfer/rpc-send-transfer-choose-fee.tsx +++ b/src/app/pages/rpc-send-transfer/rpc-send-transfer-choose-fee.tsx @@ -10,6 +10,7 @@ import { noop } from '@shared/utils'; import { useGenerateSignedBitcoinTx } from '@app/common/transactions/bitcoin/use-generate-bitcoin-tx'; import { useWalletType } from '@app/common/use-wallet-type'; import { BitcoinFeesList } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; +import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list'; function useRpcSendTransferFeeState() { const location = useLocation(); @@ -24,6 +25,10 @@ export function RpcSendTransferChooseFee() { const navigate = useNavigate(); const { whenWallet } = useWalletType(); const generateTx = useGenerateSignedBitcoinTx(); + const { feesList, isLoading } = useBitcoinFeesList({ + amount: Number(amount), + recipient: address, + }); async function previewTransfer(feeRate: number, feeValue: number, time: string) { const resp = generateTx( @@ -50,6 +55,6 @@ export function RpcSendTransferChooseFee() { } return ( - + ); } diff --git a/src/app/pages/send/ordinal-inscription/select-inscription-coins.spec.ts b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.spec.ts similarity index 100% rename from src/app/pages/send/ordinal-inscription/select-inscription-coins.spec.ts rename to src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.spec.ts diff --git a/src/app/pages/send/ordinal-inscription/select-inscription-coins.ts b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts similarity index 100% rename from src/app/pages/send/ordinal-inscription/select-inscription-coins.ts rename to src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts diff --git a/src/app/pages/send/ordinal-inscription/utils.ts b/src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts similarity index 100% rename from src/app/pages/send/ordinal-inscription/utils.ts rename to src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts diff --git a/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx b/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx new file mode 100644 index 00000000..07c595ac --- /dev/null +++ b/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx @@ -0,0 +1,11 @@ +import { Outlet } from 'react-router-dom'; + +import { SendInscriptionLoader } from './send-inscription-loader'; + +export function SendInscriptionContainer() { + return ( + + {({ feeRates, inscription, utxo }) => } + + ); +} diff --git a/src/app/pages/send/ordinal-inscription/components/send-inscription-loader.tsx b/src/app/pages/send/ordinal-inscription/components/send-inscription-loader.tsx new file mode 100644 index 00000000..80d1f88d --- /dev/null +++ b/src/app/pages/send/ordinal-inscription/components/send-inscription-loader.tsx @@ -0,0 +1,40 @@ +import { Navigate, useLocation, useOutletContext } from 'react-router-dom'; + +import get from 'lodash.get'; + +import { AverageBitcoinFeeRates } from '@shared/models/fees/bitcoin-fees.model'; +import { SupportedInscription } from '@shared/models/inscription.model'; +import { RouteUrls } from '@shared/route-urls'; + +import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; +import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; + +import { useSendInscriptionRouteState } from '../hooks/use-send-inscription-route-state'; +import { createUtxoFromInscription } from './create-utxo-from-inscription'; + +interface InscriptionSendState { + feeRates: AverageBitcoinFeeRates; + inscription: SupportedInscription; + utxo: TaprootUtxo; +} +export function useInscriptionSendState() { + const location = useLocation(); + const context = useOutletContext(); + return { ...context, recipient: get(location.state, 'recipient', '') as string }; +} + +interface SendInscriptionLoaderProps { + children(data: InscriptionSendState): JSX.Element; +} +export function SendInscriptionLoader({ children }: SendInscriptionLoaderProps) { + const { inscription } = useSendInscriptionRouteState(); + const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates(); + + if (!feeRates) return null; + + if (!inscription) return ; + + const utxo = createUtxoFromInscription(inscription); + + return children({ inscription, feeRates, utxo }); +} diff --git a/src/app/pages/send/ordinal-inscription/use-generate-ordinal-tx.ts b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts similarity index 88% rename from src/app/pages/send/ordinal-inscription/use-generate-ordinal-tx.ts rename to src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts index 119cec98..0c1b2211 100644 --- a/src/app/pages/send/ordinal-inscription/use-generate-ordinal-tx.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts @@ -4,38 +4,36 @@ import { logger } from '@shared/logger'; import { OrdinalSendFormValues } from '@shared/models/form.model'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/address.hooks'; -import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks'; import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; 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 './select-inscription-coins'; +import { selectInscriptionTransferCoins } from '../coinselect/select-inscription-coins'; export function useGenerateSignedOrdinalTx(trInput: TaprootUtxo) { const createTapRootSigner = useCurrentAccountTaprootSigner(); const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); const networkMode = useBitcoinScureLibNetworkConfig(); - const { data: feeRate } = useBitcoinFeeRate(); const { data: nativeSegwitUtxos } = useCurrentNativeSegwitUtxos(); function coverFeeFromAdditionalUtxos(values: OrdinalSendFormValues) { const trSigner = createTapRootSigner?.(trInput.addressIndex); const nativeSegwitSigner = createNativeSegwitSigner?.(0); - if (!trSigner || !nativeSegwitSigner || !nativeSegwitUtxos || !feeRate) return; + if (!trSigner || !nativeSegwitSigner || !nativeSegwitUtxos || !values.feeRate) return; const result = selectInscriptionTransferCoins({ recipient: values.recipient, inscriptionInput: trInput, nativeSegwitUtxos, changeAddress: nativeSegwitSigner.payment.address!, - feeRate: feeRate.halfHourFee, + feeRate: values.feeRate, }); if (!result.success) return null; - const { inputs, outputs, txFee } = result; + const { inputs, outputs } = result; try { const tx = new btc.Transaction(); @@ -76,7 +74,7 @@ export function useGenerateSignedOrdinalTx(trInput: TaprootUtxo) { } tx.finalize(); - return { hex: tx.hex, fee: txFee }; + return { hex: tx.hex }; } catch (e) { 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 new file mode 100644 index 00000000..3191fb71 --- /dev/null +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts @@ -0,0 +1,101 @@ +import { useMemo } from 'react'; + +import { BtcFeeType, btcTxTimeMap } from '@shared/models/fees/bitcoin-fees.model'; +import { createMoney } from '@shared/models/money.model'; + +import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; +import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; +import { FeesListItem } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; +import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/address.hooks'; +import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; +import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; +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'; + +interface UseSendInscriptionFeesListArgs { + recipient: string; + utxo: TaprootUtxo; +} +export function useSendInscriptionFeesList({ recipient, utxo }: UseSendInscriptionFeesListArgs) { + const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); + const { data: nativeSegwitUtxos } = useCurrentNativeSegwitUtxos(); + + const btcMarketData = useCryptoCurrencyMarketData('BTC'); + const { avgApiFeeRates: feeRates, isLoading } = useAverageBitcoinFeeRates(); + + const feesList: FeesListItem[] = useMemo(() => { + function getFiatFeeValue(fee: number) { + return `~ ${i18nFormatCurrency( + baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData) + )}`; + } + + const nativeSegwitSigner = createNativeSegwitSigner?.(0); + + 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 [ + { + label: BtcFeeType.High, + value: highFeeValue, + btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')), + time: btcTxTimeMap.fastestFee, + fiatValue: getFiatFeeValue(highFeeValue), + feeRate: feeRates.fastestFee.toNumber(), + }, + { + label: BtcFeeType.Standard, + value: standardFeeValue, + btcValue: formatMoneyPadded(createMoney(standardFeeValue, 'BTC')), + time: btcTxTimeMap.halfHourFee, + fiatValue: getFiatFeeValue(standardFeeValue), + feeRate: feeRates.halfHourFee.toNumber(), + }, + { + 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 { + feesList, + isLoading, + }; +} 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 new file mode 100644 index 00000000..a82b56ed --- /dev/null +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import * as yup from 'yup'; + +import { logger } from '@shared/logger'; +import { OrdinalSendFormValues } from '@shared/models/form.model'; +import { RouteUrls } from '@shared/route-urls'; +import { noop } from '@shared/utils'; + +import { FormErrorMessages } from '@app/common/error-messages'; +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useWalletType } from '@app/common/use-wallet-type'; +import { + btcAddressNetworkValidator, + btcAddressValidator, + btcTaprootAddressValidator, +} from '@app/common/validation/forms/address-validators'; +import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + +import { useInscriptionSendState } from '../components/send-inscription-loader'; +import { recipeintFieldName } from '../send-inscription-form'; +import { useGenerateSignedOrdinalTx } from './use-generate-ordinal-tx'; + +export function useSendInscriptionForm() { + const [currentError, setShowError] = useState(null); + const analytics = useAnalytics(); + const navigate = useNavigate(); + const { whenWallet } = useWalletType(); + const { inscription, utxo } = useInscriptionSendState(); + const currentNetwork = useCurrentNetwork(); + + const { coverFeeFromAdditionalUtxos } = useGenerateSignedOrdinalTx(utxo); + + return { + currentError, + + async chooseTransactionFee(values: OrdinalSendFormValues) { + // Check tx with fastest fee for errors before routing and + // generating the final transaction with the chosen fee to send + const resp = coverFeeFromAdditionalUtxos(values); + + if (!resp) { + setShowError( + 'Insufficient funds to cover fee. Deposit some BTC to your Native Segwit address.' + ); + return; + } + + if (Number(inscription.offset) !== 0) { + setShowError('Sending inscriptions at non-zero offsets is unsupported'); + return; + } + + try { + const numInscriptionsOnUtxo = await getNumberOfInscriptionOnUtxo(utxo.txid, utxo.vout); + if (numInscriptionsOnUtxo > 1) { + setShowError('Sending inscription from utxo with multiple inscriptions is unsupported'); + return; + } + } catch (error) { + void analytics.track('ordinals_dot_com_unavailable', { error }); + setShowError('Unable to establish if utxo has multiple inscriptions'); + return; + } + + whenWallet({ + software: () => + navigate(RouteUrls.SendOrdinalInscriptionChooseFee, { + state: { inscription, recipient: values.recipient, utxo }, + }), + ledger: noop, + })(); + }, + + async reviewTransaction(feeValue: number, time: string, values: OrdinalSendFormValues) { + // Generate the final tx with the chosen fee to send + const resp = coverFeeFromAdditionalUtxos(values); + + if (!resp) { + logger.error('Failed to generate transaction for send'); + return; + } + + const { hex } = resp; + return navigate(RouteUrls.SendOrdinalInscriptionReview, { + state: { fee: feeValue, inscription, utxo, recipient: values.recipient, time, tx: hex }, + }); + }, + + validationSchema: yup.object({ + [recipeintFieldName]: yup + .string() + .required(FormErrorMessages.AddressRequired) + .concat(btcAddressValidator()) + .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)) + .concat(btcTaprootAddressValidator()), + }), + }; +} diff --git a/src/app/pages/send/ordinal-inscription/use-send-ordinal-inscription-route-state.ts b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-route-state.ts similarity index 63% rename from src/app/pages/send/ordinal-inscription/use-send-ordinal-inscription-route-state.ts rename to src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-route-state.ts index 1059cd15..df8e546f 100644 --- a/src/app/pages/send/ordinal-inscription/use-send-ordinal-inscription-route-state.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-route-state.ts @@ -6,10 +6,10 @@ import { SupportedInscription } from '@shared/models/inscription.model'; import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; -export function useSendOrdinalInscriptionRouteState() { +export function useSendInscriptionRouteState() { const location = useLocation(); return { - inscription: get(location, 'state.inscription', null) as SupportedInscription | null, - utxo: get(location, 'state.utxo', null) as TaprootUtxo | null, + inscription: get(location.state, 'inscription', null) as SupportedInscription | null, + utxo: get(location.state, 'utxo', null) as TaprootUtxo | null, }; } 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 430b78f4..56f58b6a 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 @@ -1,35 +1,42 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -// import get from 'lodash.get'; -// import { RouteUrls } from '@shared/route-urls'; +import { BitcoinFeesList } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; +import { BitcoinFeesListLayout } from '@app/components/bitcoin-fees-list/components/bitcoin-fees-list.layout'; import { BaseDrawer } from '@app/components/drawer/base-drawer'; +import { LoadingSpinner } from '@app/components/loading-spinner'; -// import { BitcoinChooseFee } from '../send-crypto-asset-form/family/bitcoin/components/bitcoin-choose-fee'; -// import { useInscriptionSendState } from './send-inscription-container'; - -// function useSendInscriptionChooseFeeState() { -// const location = useLocation(); -// return { -// tx: get(location.state, 'tx') as string, -// recipient: get(location.state, 'recipient', '') as string, -// }; -// } +import { useInscriptionSendState } from './components/send-inscription-loader'; +import { useSendInscriptionFeesList } from './hooks/use-send-inscription-fees-list'; +import { useSendInscriptionForm } from './hooks/use-send-inscription-form'; export function SendInscriptionChooseFee() { + const [isLoadingReview, setIsLoadingReview] = useState(false); const navigate = useNavigate(); - // const { tx, recipient } = useSendInscriptionChooseFeeState(); - // const { inscription, utxo } = useInscriptionSendState(); + const { recipient, utxo } = useInscriptionSendState(); + const { feesList, isLoading } = useSendInscriptionFeesList({ recipient, utxo }); + const { reviewTransaction } = useSendInscriptionForm(); - // function previewTransaction(feeRate: number, feeValue: number, time: string) { - // feeRate; - // navigate(RouteUrls.SendOrdinalInscriptionReview, { - // state: { fee: feeValue, inscription, utxo, recipient, tx, arrivesIn: time }, - // }); - // } + async function previewTransaction(feeRate: number, feeValue: number, time: string) { + try { + setIsLoadingReview(true); + await reviewTransaction(feeValue, time, { feeRate, recipient }); + } finally { + setIsLoadingReview(false); + } + } + + if (isLoadingReview) return ; return ( navigate(-1)}> - {/* ; */} + + + ); } diff --git a/src/app/pages/send/ordinal-inscription/send-inscription-container.tsx b/src/app/pages/send/ordinal-inscription/send-inscription-container.tsx deleted file mode 100644 index 5f6bf1a0..00000000 --- a/src/app/pages/send/ordinal-inscription/send-inscription-container.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Navigate, Outlet, useLocation, useOutletContext } from 'react-router-dom'; - -import get from 'lodash.get'; - -import { SupportedInscription } from '@shared/models/inscription.model'; -import { RouteUrls } from '@shared/route-urls'; - -import { FeeEstimateMempoolSpaceApi } from '@app/query/bitcoin/bitcoin-client'; -import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks'; - -import { useSendOrdinalInscriptionRouteState } from './use-send-ordinal-inscription-route-state'; - -interface InscriptionSendState { - fees: FeeEstimateMempoolSpaceApi; - inscription: SupportedInscription; -} - -export function useInscriptionSendState() { - const location = useLocation(); - const context = useOutletContext(); - return { ...context, recipient: get(location, 'state.recipient', '') as string }; -} - -interface SendInscriptionLoaderProps { - children(data: InscriptionSendState): JSX.Element; -} -function SendInscriptionLoader({ children }: SendInscriptionLoaderProps) { - const { inscription } = useSendOrdinalInscriptionRouteState(); - const { data: fees } = useBitcoinFeeRate(); - if (!fees) return null; - if (!inscription) return ; - return children({ inscription, fees }); -} - -export function SendInscription() { - return ( - - {({ fees, inscription }) => } - - ); -} 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 c4216251..5a7ae9e9 100644 --- a/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx +++ b/src/app/pages/send/ordinal-inscription/send-inscription-form.tsx @@ -1,84 +1,37 @@ -import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, Flex } from '@stacks/ui'; import { Form, Formik } from 'formik'; -import { OrdinalSendFormValues } from '@shared/models/form.model'; import { RouteUrls } from '@shared/route-urls'; -import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { BaseDrawer } from '@app/components/drawer/base-drawer'; import { ErrorLabel } from '@app/components/error-label'; import { OrdinalIcon } from '@app/components/icons/ordinal-icon'; import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview'; -import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; +import { InscriptionPreviewCard } from '@app/components/inscription-preview-card/inscription-preview-card'; -import { InscriptionPreviewCard } from '../../../components/inscription-preview-card/inscription-preview-card'; import { RecipientField } from '../send-crypto-asset-form/components/recipient-field'; import { CollectibleAsset } from './components/collectible-asset'; -import { useInscriptionSendState } from './send-inscription-container'; -import { useGenerateSignedOrdinalTx } from './use-generate-ordinal-tx'; -import { useOrdinalInscriptionFormValidationSchema } from './use-ordinal-inscription-form-validation-schema'; -import { createUtxoFromInscription } from './utils'; +import { useInscriptionSendState } from './components/send-inscription-loader'; +import { useSendInscriptionForm } from './hooks/use-send-inscription-form'; export const recipeintFieldName = 'recipient'; export function SendInscriptionForm() { - const [currentError, setShowError] = useState(null); const navigate = useNavigate(); - const { inscription, recipient } = useInscriptionSendState(); - const validationSchema = useOrdinalInscriptionFormValidationSchema(); - const [isLoading, setIsLoading] = useState(false); - const analytics = useAnalytics(); - const utxo = createUtxoFromInscription(inscription); - const { coverFeeFromAdditionalUtxos } = useGenerateSignedOrdinalTx(utxo); - - async function reviewTransaction(values: OrdinalSendFormValues) { - const resp = coverFeeFromAdditionalUtxos(values); - - if (!resp) { - setShowError( - 'Insufficient funds to cover fee. Deposit some BTC to your Native Segwit address.' - ); - return; - } - - if (Number(inscription.offset) !== 0) { - setShowError('Sending inscriptions at non-zero offsets is unsupported'); - return; - } - - try { - const numInscriptionsOnUtxo = await getNumberOfInscriptionOnUtxo(utxo.txid, utxo.vout); - if (numInscriptionsOnUtxo > 1) { - setShowError('Sending inscription from utxo with multiple inscriptions is unsupported'); - return; - } - } catch (error) { - void analytics.track('ordinals_dot_com_unavailable', { error }); - setShowError('Unable to establish if utxo has multiple inscriptions.'); - return; - } - - const { hex } = resp; - return navigate(RouteUrls.SendOrdinalInscriptionReview, { - state: { fee: resp.fee, inscription, recipient: values.recipient, tx: hex }, - }); - } + const { feeRates, inscription, recipient } = useInscriptionSendState(); + const { chooseTransactionFee, currentError, validationSchema } = useSendInscriptionForm(); return ( { - try { - setIsLoading(true); - await reviewTransaction(values); - } finally { - setIsLoading(false); - } + initialValues={{ + [recipeintFieldName]: recipient, + inscription, + feeRate: feeRates.fastestFee.toNumber(), }} + onSubmit={chooseTransactionFee} >
navigate(RouteUrls.Home)}> @@ -104,13 +57,12 @@ export function SendInscriptionForm() { )} diff --git a/src/app/pages/send/ordinal-inscription/send-inscription-review.tsx b/src/app/pages/send/ordinal-inscription/send-inscription-review.tsx index 15586f38..12c808c5 100644 --- a/src/app/pages/send/ordinal-inscription/send-inscription-review.tsx +++ b/src/app/pages/send/ordinal-inscription/send-inscription-review.tsx @@ -14,15 +14,15 @@ import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-c import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview'; import { PrimaryButton } from '@app/components/primary-button'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/address.hooks'; -import { btcTxTimeMap } from '@app/query/bitcoin/bitcoin-client'; import { InscriptionPreviewCard } from '../../../components/inscription-preview-card/inscription-preview-card'; import { useBitcoinBroadcastTransaction } from '../../../query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; -import { useInscriptionSendState } from './send-inscription-container'; +import { useInscriptionSendState } from './components/send-inscription-loader'; function useSendInscriptionReviewState() { const location = useLocation(); return { + arrivesIn: get(location.state, 'time') as string, signedTx: get(location.state, 'tx') as string, recipient: get(location.state, 'recipient', '') as string, fee: get(location.state, 'fee') as number, @@ -33,13 +33,12 @@ export function SendInscriptionReview() { const analytics = useAnalytics(); const navigate = useNavigate(); - const { signedTx, recipient, fee } = useSendInscriptionReviewState(); + const { arrivesIn, signedTx, recipient, fee } = useSendInscriptionReviewState(); const { inscription } = useInscriptionSendState(); const { refetch } = useCurrentNativeSegwitUtxos(); const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction(); - const arrivesIn = btcTxTimeMap.hourFee; const summaryFee = formatMoney(createMoney(Number(fee), 'BTC')); async function sendInscription() { diff --git a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx index 8d4ea05c..ab6620b6 100644 --- a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx +++ b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx @@ -47,15 +47,16 @@ export function SendInscriptionSummary() { const { handleOpenTxLink } = useExplorerLink(); const analytics = useAnalytics(); - const onClickLink = () => { + function onClickLink() { void analytics.track('view_transaction_confirmation', { symbol: 'BTC' }); handleOpenTxLink(txLink); - }; + } - const onClickCopy = () => { + function onClickCopy() { onCopy(); toast.success('ID copied!'); - }; + } + return ( navigate(RouteUrls.Home)}> diff --git a/src/app/pages/send/ordinal-inscription/use-ordinal-inscription-form-validation-schema.ts b/src/app/pages/send/ordinal-inscription/use-ordinal-inscription-form-validation-schema.ts deleted file mode 100644 index ea3fd45b..00000000 --- a/src/app/pages/send/ordinal-inscription/use-ordinal-inscription-form-validation-schema.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as yup from 'yup'; - -import { FormErrorMessages } from '@app/common/error-messages'; -import { - btcAddressNetworkValidator, - btcAddressValidator, - btcTaprootAddressValidator, -} from '@app/common/validation/forms/address-validators'; -import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; - -import { recipeintFieldName } from './send-inscription-form'; - -export function useOrdinalInscriptionFormValidationSchema() { - const currentNetwork = useCurrentNetwork(); - return yup.object({ - [recipeintFieldName]: yup - .string() - .required(FormErrorMessages.AddressRequired) - .concat(btcAddressValidator()) - .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)) - .concat(btcTaprootAddressValidator()), - }); -} diff --git a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts index 83aa4bec..03151099 100644 --- a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts +++ b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts @@ -9,18 +9,18 @@ import { satToBtc } from '@app/common/money/unit-conversion'; import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator'; import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query'; import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; -import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks'; +import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; export function useCalculateMaxBitcoinSpend() { const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero(); const balance = useCurrentNativeSegwitAddressBalance(); const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress); - const { data: feeRate } = useBitcoinFeeRate(); + const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates(); return useCallback( (address = '') => { - if (!utxos || !feeRate) + if (!utxos || !feeRates) return { spendAllFee: 0, amount: createMoney(0, 'BTC'), @@ -35,7 +35,7 @@ export function useCalculateMaxBitcoinSpend() { input_count: utxos.length, [`${addressTypeWithFallback}_output_count`]: 2, }); - const fee = Math.ceil(size.txVBytes * feeRate.fastestFee); + const fee = Math.ceil(size.txVBytes * feeRates.fastestFee.toNumber()); const spendableAmount = BigNumber.max(0, balance.amount.minus(fee)); @@ -45,6 +45,6 @@ export function useCalculateMaxBitcoinSpend() { spendableBitcoin: satToBtc(spendableAmount), }; }, - [balance.amount, feeRate, utxos] + [balance.amount, feeRates, utxos] ); } 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 a8178901..baf04cb3 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 @@ -13,6 +13,7 @@ import { useGenerateSignedBitcoinTx } from '@app/common/transactions/bitcoin/use import { useWalletType } from '@app/common/use-wallet-type'; import { BitcoinFeesList } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; import { BitcoinFeesListLayout } from '@app/components/bitcoin-fees-list/components/bitcoin-fees-list.layout'; +import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list'; import { ModalHeader } from '@app/components/modal-header'; import { useSendFormNavigate } from '../../hooks/use-send-form-navigate'; @@ -29,6 +30,10 @@ export function BtcChooseFee() { const { whenWallet } = useWalletType(); const sendFormNavigate = useSendFormNavigate(); const generateTx = useGenerateSignedBitcoinTx(); + const { feesList, isLoading } = useBitcoinFeesList({ + amount: Number(txValues.amount), + recipient: txValues.recipient, + }); async function previewTransaction(feeRate: number, feeValue: number, time: string) { const resp = generateTx( @@ -59,11 +64,7 @@ export function BtcChooseFee() { return ( - + ); } diff --git a/src/app/pages/send/sent-summary/btc-sent-summary.tsx b/src/app/pages/send/sent-summary/btc-sent-summary.tsx index 31f226d2..2e643fbc 100644 --- a/src/app/pages/send/sent-summary/btc-sent-summary.tsx +++ b/src/app/pages/send/sent-summary/btc-sent-summary.tsx @@ -39,14 +39,15 @@ export function BtcSentSummary() { const { handleOpenTxLink } = useExplorerLink(); const analytics = useAnalytics(); - const onClickLink = () => { + function onClickLink() { void analytics.track('view_transaction_confirmation', { symbol: 'BTC' }); handleOpenTxLink(txLink); - }; - const onClickCopy = () => { + } + + function onClickCopy() { onCopy(); toast.success('ID copied!'); - }; + } useRouteHeader(); diff --git a/src/app/pages/send/sent-summary/stx-sent-summary.tsx b/src/app/pages/send/sent-summary/stx-sent-summary.tsx index 4a8dfab9..43e55013 100644 --- a/src/app/pages/send/sent-summary/stx-sent-summary.tsx +++ b/src/app/pages/send/sent-summary/stx-sent-summary.tsx @@ -39,14 +39,15 @@ export function StxSentSummary() { const { handleOpenTxLink } = useExplorerLink(); const analytics = useAnalytics(); - const onClickLink = () => { + function onClickLink() { void analytics.track('view_transaction_confirmation', { symbol: 'STX' }); handleOpenTxLink(txLink); - }; - const onClickCopy = () => { + } + + function onClickCopy() { onCopy(); toast.success('ID copied!'); - }; + } useRouteHeader(); diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index 3bbc8801..15c62612 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -42,13 +42,14 @@ interface FeeEstimateEarnApiResponse { halfHourFee: number; hourFee: number; } -export interface FeeEstimateMempoolSpaceApi { +interface FeeEstimateMempoolSpaceApiResponse { fastestFee: number; halfHourFee: number; hourFee: number; economyFee: number; minimumFee: number; } + class FeeEstimatesApi { constructor(public configuration: Configuration) {} @@ -59,7 +60,7 @@ class FeeEstimatesApi { }); } - async getFeeEstimatesFromMempoolSpaceApi(): Promise { + async getFeeEstimatesFromMempoolSpaceApi(): Promise { return fetchData({ errorMsg: 'No fee estimates fetched', url: ` https://mempool.space/api/v1/fees/recommended`, @@ -85,20 +86,6 @@ class TransactionsApi { } } -export const btcTxTimeMap: Record = { - fastestFee: '~10 – 20min', - halfHourFee: '~30 min', - economyFee: '~1 hour+', - hourFee: '~1 hour+', - minimumFee: '~1 hour+', -}; - -export enum BtcFeeType { - High = 'High', - Standard = 'Standard', - Low = 'Low', -} - export class BitcoinClient { configuration: Configuration; addressApi: AddressApi; diff --git a/src/app/query/bitcoin/fees/fee-estimates.hooks.ts b/src/app/query/bitcoin/fees/fee-estimates.hooks.ts index b8d664e0..2d9bba7e 100644 --- a/src/app/query/bitcoin/fees/fee-estimates.hooks.ts +++ b/src/app/query/bitcoin/fees/fee-estimates.hooks.ts @@ -1,24 +1,16 @@ import BigNumber from 'bignumber.js'; import { logger } from '@shared/logger'; +import { AverageBitcoinFeeRates } from '@shared/models/fees/bitcoin-fees.model'; import { calculateMeanAverage } from '@app/common/math/calculate-averages'; -import { - useGetBitcoinAllFeeEstimatesQuery, - useGetBitcoinMempoolApiFeeEstimatesQuery, -} from './fee-estimates.query'; +import { useGetAllBitcoinFeeEstimatesQuery } from './fee-estimates.query'; -export function useBitcoinFeeRate() { - return useGetBitcoinMempoolApiFeeEstimatesQuery({ - onError: err => logger.error('Error getting bitcoin fee estimates', { err }), - }); -} - -export function useAverageBitcoinFeeRate() { - const { data: avgApiFeeRates, isLoading } = useGetBitcoinAllFeeEstimatesQuery({ +export function useAverageBitcoinFeeRates() { + const { data: avgApiFeeRates, isLoading } = useGetAllBitcoinFeeEstimatesQuery({ onError: err => logger.error('Error getting all apis bitcoin fee estimates', { err }), - select: resp => { + select: (resp): AverageBitcoinFeeRates | null => { if (resp[0].status === 'rejected' && resp[1].status === 'rejected') { return null; } diff --git a/src/app/query/bitcoin/fees/fee-estimates.query.ts b/src/app/query/bitcoin/fees/fee-estimates.query.ts index 27c81492..763346f6 100644 --- a/src/app/query/bitcoin/fees/fee-estimates.query.ts +++ b/src/app/query/bitcoin/fees/fee-estimates.query.ts @@ -5,34 +5,6 @@ import { useBitcoinClient } from '@app/store/common/api-clients.hooks'; import { BitcoinClient } from '../bitcoin-client'; -// Mempool api -function fetchMempoolApiBitcoinFeeEstimates(client: BitcoinClient) { - return async () => { - return client.feeEstimatesApi.getFeeEstimatesFromMempoolSpaceApi(); - }; -} - -type FetchMempoolApiBitcoinFeeEstimatesResp = Awaited< - ReturnType> ->; - -// https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-fees -// Possible alt api if needed: https://bitcoinfees.earn.com/api -export function useGetBitcoinMempoolApiFeeEstimatesQuery< - T extends unknown = FetchMempoolApiBitcoinFeeEstimatesResp ->(options?: AppUseQueryConfig) { - const client = useBitcoinClient(); - return useQuery({ - queryKey: ['mempool-api-bitcoin-fee-estimates'], - queryFn: fetchMempoolApiBitcoinFeeEstimates(client), - staleTime: 1000 * 60, - refetchOnWindowFocus: false, - refetchOnMount: false, - ...options, - }); -} - -// Earn api function fetchAllBitcoinFeeEstimates(client: BitcoinClient) { return async () => { return Promise.allSettled([ @@ -46,7 +18,7 @@ type FetchAllBitcoinFeeEstimatesResp = Awaited< ReturnType> >; -export function useGetBitcoinAllFeeEstimatesQuery< +export function useGetAllBitcoinFeeEstimatesQuery< T extends unknown = FetchAllBitcoinFeeEstimatesResp >(options?: AppUseQueryConfig) { const client = useBitcoinClient(); diff --git a/src/app/routes/app-routes.tsx b/src/app/routes/app-routes.tsx index 6b69dc3c..2668fcf9 100644 --- a/src/app/routes/app-routes.tsx +++ b/src/app/routes/app-routes.tsx @@ -39,8 +39,8 @@ import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses' import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-transfer.routes'; import { SelectNetwork } from '@app/pages/select-network/select-network'; import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error'; +import { SendInscriptionContainer } from '@app/pages/send/ordinal-inscription/components/send-inscription-container'; import { SendInscriptionChooseFee } from '@app/pages/send/ordinal-inscription/send-inscription-choose-fee'; -import { SendInscription } from '@app/pages/send/ordinal-inscription/send-inscription-container'; import { SendInscriptionForm } from '@app/pages/send/ordinal-inscription/send-inscription-form'; import { SendInscriptionReview } from '@app/pages/send/ordinal-inscription/send-inscription-review'; import { SendInscriptionSummary } from '@app/pages/send/ordinal-inscription/sent-inscription-summary'; @@ -118,16 +118,16 @@ function useAppRoutes() { } /> } /> - }> + }> } /> - } - /> } /> + } + /> } diff --git a/src/shared/models/fees/bitcoin-fees.model.ts b/src/shared/models/fees/bitcoin-fees.model.ts index 6852d03e..8b8edbcf 100644 --- a/src/shared/models/fees/bitcoin-fees.model.ts +++ b/src/shared/models/fees/bitcoin-fees.model.ts @@ -1,7 +1,19 @@ -// Source: https://github.com/Blockstream/esplora/blob/master/API.md#fee-estimates -import { Money } from '../money.model'; +import BigNumber from 'bignumber.js'; -export interface BitcoinFeeEstimate { - feeRate: number; - fee: Money; +export interface AverageBitcoinFeeRates { + fastestFee: BigNumber; + halfHourFee: BigNumber; + hourFee: BigNumber; +} + +export const btcTxTimeMap: Record = { + fastestFee: '~10 – 20min', + halfHourFee: '~30 min', + hourFee: '~1 hour+', +}; + +export enum BtcFeeType { + High = 'High', + Standard = 'Standard', + Low = 'Low', } diff --git a/src/shared/models/fees/fees.model.ts b/src/shared/models/fees/fees.model.ts index dc0d7b3a..b875d23d 100644 --- a/src/shared/models/fees/fees.model.ts +++ b/src/shared/models/fees/fees.model.ts @@ -1,5 +1,4 @@ import { Blockchains } from '../blockchain.model'; -import { BitcoinFeeEstimate } from './bitcoin-fees.model'; import { StacksFeeEstimate } from './stacks-fees.model'; export enum FeeTypes { @@ -19,6 +18,6 @@ export enum FeeCalculationTypes { export interface Fees { blockchain: Blockchains; - estimates: BitcoinFeeEstimate[] | StacksFeeEstimate[]; + estimates: StacksFeeEstimate[]; calculation: FeeCalculationTypes; } diff --git a/src/shared/models/form.model.ts b/src/shared/models/form.model.ts index 9cb26f66..98bf09a6 100644 --- a/src/shared/models/form.model.ts +++ b/src/shared/models/form.model.ts @@ -10,6 +10,7 @@ export interface BitcoinSendFormValues { } export interface OrdinalSendFormValues { + feeRate: number; recipient: string; } diff --git a/tests/specs/send/send-btc.spec.ts b/tests/specs/send/send-btc.spec.ts index ac8bec9d..63b4d7f1 100644 --- a/tests/specs/send/send-btc.spec.ts +++ b/tests/specs/send/send-btc.spec.ts @@ -3,7 +3,7 @@ import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors'; import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors'; import { getDisplayerAddress } from '@tests/utils'; -import { BtcFeeType } from '@app/query/bitcoin/bitcoin-client'; +import { BtcFeeType } from '@shared/models/fees/bitcoin-fees.model'; import { test } from '../../fixtures/fixtures';