mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
feat: send inscription choose fee, closes #3544
This commit is contained in:
@@ -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> | 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 <LoadingSpinner />;
|
||||
|
||||
if (!feesList.length) {
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 (
|
||||
<BitcoinFeesList amount={Number(amount)} onChooseFee={previewTransfer} recipient={address} />
|
||||
<BitcoinFeesList feesList={feesList} isLoading={isLoading} onChooseFee={previewTransfer} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { SendInscriptionLoader } from './send-inscription-loader';
|
||||
|
||||
export function SendInscriptionContainer() {
|
||||
return (
|
||||
<SendInscriptionLoader>
|
||||
{({ feeRates, inscription, utxo }) => <Outlet context={{ feeRates, inscription, utxo }} />}
|
||||
</SendInscriptionLoader>
|
||||
);
|
||||
}
|
||||
@@ -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<InscriptionSendState>();
|
||||
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 <Navigate to={RouteUrls.Home} />;
|
||||
|
||||
const utxo = createUtxoFromInscription(inscription);
|
||||
|
||||
return children({ inscription, feeRates, utxo });
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 | string>(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()),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<BaseDrawer title="Choose fee" isShowing enableGoBack onClose={() => navigate(-1)}>
|
||||
{/* <BitcoinChooseFee onChooseFee={previewTransaction} recipient={recipient} amount={}/>; */}
|
||||
<BitcoinFeesListLayout>
|
||||
<BitcoinFeesList
|
||||
feesList={feesList}
|
||||
isLoading={isLoading}
|
||||
onChooseFee={previewTransaction}
|
||||
/>
|
||||
</BitcoinFeesListLayout>
|
||||
</BaseDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<InscriptionSendState>();
|
||||
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 <Navigate to={RouteUrls.Home} />;
|
||||
return children({ inscription, fees });
|
||||
}
|
||||
|
||||
export function SendInscription() {
|
||||
return (
|
||||
<SendInscriptionLoader>
|
||||
{({ fees, inscription }) => <Outlet context={{ fees, inscription }} />}
|
||||
</SendInscriptionLoader>
|
||||
);
|
||||
}
|
||||
@@ -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 | string>(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 (
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={{ [recipeintFieldName]: recipient, inscription }}
|
||||
onSubmit={async values => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await reviewTransaction(values);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
initialValues={{
|
||||
[recipeintFieldName]: recipient,
|
||||
inscription,
|
||||
feeRate: feeRates.fastestFee.toNumber(),
|
||||
}}
|
||||
onSubmit={chooseTransactionFee}
|
||||
>
|
||||
<Form>
|
||||
<BaseDrawer title="Send" enableGoBack isShowing onClose={() => navigate(RouteUrls.Home)}>
|
||||
@@ -104,13 +57,12 @@ export function SendInscriptionForm() {
|
||||
</ErrorLabel>
|
||||
)}
|
||||
<Button
|
||||
borderRadius="10px"
|
||||
height="48px"
|
||||
mb="extra-loose"
|
||||
mt="tight"
|
||||
type="submit"
|
||||
borderRadius="10px"
|
||||
height="48px"
|
||||
width="100%"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
<BaseDrawer title="Sent" isShowing onClose={() => navigate(RouteUrls.Home)}>
|
||||
<Box px="extra-loose" mt="extra-loose">
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<BitcoinFeesListLayout>
|
||||
<BitcoinFeesList
|
||||
amount={Number(txValues.amount)}
|
||||
onChooseFee={previewTransaction}
|
||||
recipient={txValues.recipient}
|
||||
/>
|
||||
<BitcoinFeesList feesList={feesList} isLoading={isLoading} onChooseFee={previewTransaction} />
|
||||
</BitcoinFeesListLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(<ModalHeader hideActions defaultClose title="Sent" />);
|
||||
|
||||
|
||||
@@ -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(<ModalHeader hideActions defaultClose title="Sent" />);
|
||||
|
||||
|
||||
@@ -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<FeeEstimateMempoolSpaceApi> {
|
||||
async getFeeEstimatesFromMempoolSpaceApi(): Promise<FeeEstimateMempoolSpaceApiResponse> {
|
||||
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<keyof FeeEstimateMempoolSpaceApi, string> = {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<ReturnType<typeof fetchMempoolApiBitcoinFeeEstimates>>
|
||||
>;
|
||||
|
||||
// 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<FetchMempoolApiBitcoinFeeEstimatesResp, T>) {
|
||||
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<ReturnType<typeof fetchAllBitcoinFeeEstimates>>
|
||||
>;
|
||||
|
||||
export function useGetBitcoinAllFeeEstimatesQuery<
|
||||
export function useGetAllBitcoinFeeEstimatesQuery<
|
||||
T extends unknown = FetchAllBitcoinFeeEstimatesResp
|
||||
>(options?: AppUseQueryConfig<FetchAllBitcoinFeeEstimatesResp, T>) {
|
||||
const client = useBitcoinClient();
|
||||
|
||||
@@ -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() {
|
||||
<Route path={RouteUrls.ReceiveStx} element={<ReceiveStxModal />} />
|
||||
<Route path={RouteUrls.ReceiveBtc} element={<ReceiveBtcModal />} />
|
||||
|
||||
<Route path={RouteUrls.SendOrdinalInscription} element={<SendInscription />}>
|
||||
<Route path={RouteUrls.SendOrdinalInscription} element={<SendInscriptionContainer />}>
|
||||
<Route index element={<SendInscriptionForm />} />
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionReview}
|
||||
element={<SendInscriptionReview />}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionChooseFee}
|
||||
element={<SendInscriptionChooseFee />}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionReview}
|
||||
element={<SendInscriptionReview />}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionSent}
|
||||
element={<SendInscriptionSummary />}
|
||||
|
||||
@@ -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<keyof AverageBitcoinFeeRates, string> = {
|
||||
fastestFee: '~10 – 20min',
|
||||
halfHourFee: '~30 min',
|
||||
hourFee: '~1 hour+',
|
||||
};
|
||||
|
||||
export enum BtcFeeType {
|
||||
High = 'High',
|
||||
Standard = 'Standard',
|
||||
Low = 'Low',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface BitcoinSendFormValues {
|
||||
}
|
||||
|
||||
export interface OrdinalSendFormValues {
|
||||
feeRate: number;
|
||||
recipient: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user