feat: implement increase btc pending tx fee, closes #3416

This commit is contained in:
alter-eggo
2023-07-25 15:55:04 +04:00
committed by Anastasios
parent fa9d13bf51
commit 11614ad15e
44 changed files with 636 additions and 200 deletions

View File

@@ -61,6 +61,7 @@ export function useGenerateSignedNativeSegwitTx() {
tx.addInput({
txid: input.txid,
index: input.vout,
sequence: 0,
witnessUtxo: {
// script = 0014 + pubKeyHash
script: p2wpkh.script,

View File

@@ -1,25 +1,57 @@
import { truncateMiddle } from '@stacks/ui-utils';
import { getAddressInfo } from 'bitcoin-address-validation';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTransactionVectorOutput } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { sumNumbers } from '@app/common/math/helpers';
import { satToBtc } from '@app/common/money/unit-conversion';
export const getBitcoinTxCaption = (transaction?: BitcoinTransaction) =>
import { BtcSizeFeeEstimator } from './fees/btc-size-fee-estimator';
export function getSizeInfo(payload: {
inputLength: number;
recipient: string;
outputLength: number;
}) {
const { inputLength, recipient, outputLength } = payload;
const addressInfo = getAddressInfo(recipient);
const txSizer = new BtcSizeFeeEstimator();
const sizeInfo = txSizer.calcTxSize({
// Only p2wpkh is supported by the wallet
input_script: 'p2wpkh',
input_count: inputLength,
// From the address of the recipient, we infer the output type
[addressInfo.type + '_output_count']: outputLength,
});
return sizeInfo;
}
export function getRecipientAddressFromOutput(
vout: BitcoinTransactionVectorOutput[],
currentBitcoinAddress: string
) {
return vout.find(output => output.scriptpubkey_address !== currentBitcoinAddress)
?.scriptpubkey_address;
}
export const getBitcoinTxCaption = (transaction?: BitcoinTx) =>
transaction ? truncateMiddle(transaction.txid, 4) : '';
// If vin array contains a prevout with a scriptpubkey_address equal to
// the address, then that is the current address making a `Sent` tx (-)
// and the value of the prevout is the tx amount
const transactionsSentByAddress = (address: string, transaction: BitcoinTransaction) =>
const transactionsSentByAddress = (address: string, transaction: BitcoinTx) =>
transaction.vin.filter(input => input.prevout.scriptpubkey_address === address);
// If vout array contains a scriptpubkey_address equal to the address,
// then that is a `Receive` tx (+) and the value is the tx amount
const transactionsReceivedByAddress = (address: string, transaction: BitcoinTransaction) =>
const transactionsReceivedByAddress = (address: string, transaction: BitcoinTx) =>
transaction.vout.filter(output => output.scriptpubkey_address === address);
export function isBitcoinTxInbound(address: string, transaction: BitcoinTransaction) {
export function isBitcoinTxInbound(address: string, transaction: BitcoinTx) {
const inputs = transactionsSentByAddress(address, transaction);
const outputs = transactionsReceivedByAddress(address, transaction);
@@ -28,7 +60,7 @@ export function isBitcoinTxInbound(address: string, transaction: BitcoinTransact
return true;
}
export function getBitcoinTxValue(address: string, transaction?: BitcoinTransaction) {
export function getBitcoinTxValue(address: string, transaction?: BitcoinTx) {
if (!transaction) return '';
const inputs = transactionsSentByAddress(address, transaction);
const outputs = transactionsReceivedByAddress(address, transaction);

View File

@@ -28,6 +28,9 @@ export function BitcoinCustomFeeFiat({
return { fiatFeeValue, feeInBtc };
}, [getCustomFeeValues, field.value]);
const canShow = !feeData.feeInBtc.includes('e') && Number(field.value) > 0;
if (!canShow) return null;
return (
<Flex justifyContent="space-between" color="#74777D" fontSize="14px">
<Text>{feeData.fiatFeeValue}</Text>

View File

@@ -10,8 +10,8 @@ import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { Link } from '@app/components/link';
import { PreviewButton } from '@app/components/preview-button';
import { OnChooseFeeArgs } from '../../../components/bitcoin-fees-list/bitcoin-fees-list';
import { TextInputField } from '../../../components/text-input-field';
import { OnChooseFeeArgs } from '../bitcoin-fees-list/bitcoin-fees-list';
import { TextInputField } from '../text-input-field';
import { BitcoinCustomFeeFiat } from './bitcoin-custom-fee-fiat';
import { useBitcoinCustomFee } from './hooks/use-bitcoin-custom-fee';
@@ -27,6 +27,7 @@ interface BitcoinCustomFeeProps {
onValidateBitcoinSpend(value: number): boolean;
recipient: string;
setCustomFeeInitialValue: Dispatch<SetStateAction<string>>;
maxCustomFeeRate: number;
}
export function BitcoinCustomFee({
amount,
@@ -38,6 +39,7 @@ export function BitcoinCustomFee({
onValidateBitcoinSpend,
recipient,
setCustomFeeInitialValue,
maxCustomFeeRate,
}: BitcoinCustomFeeProps) {
const feeInputRef = useRef<HTMLInputElement | null>(null);
const getCustomFeeValues = useBitcoinCustomFee({ amount, isSendingMax, recipient });
@@ -54,7 +56,16 @@ export function BitcoinCustomFee({
);
const validationSchema = yup.object({
feeRate: yup.string().required('Fee is required'),
feeRate: yup
.number()
.required('Fee is required')
.integer('Fee must be a whole number')
.test({
message: 'Fee is too high',
test: value => {
return value <= maxCustomFeeRate;
},
}),
});
return (

View File

@@ -12,6 +12,8 @@ import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/
import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
export const MAX_FEE_RATE_MULTIPLIER = 50;
interface UseBitcoinCustomFeeArgs {
amount: number;
isSendingMax: boolean;

View File

@@ -2,26 +2,25 @@ import { FiArrowDown as IconArrowDown, FiArrowUp as IconArrowUp } from 'react-ic
import { Box, BoxProps, Circle, ColorsStringLiteral, Flex, color } from '@stacks/ui';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { isBitcoinTxInbound } from '@app/common/transactions/bitcoin/utils';
import { BtcIcon } from '@app/components/icons/btc-icon';
import { isBitcoinTxInbound } from './bitcoin-transaction.utils';
interface TransactionIconProps extends BoxProps {
transaction: BitcoinTransaction;
transaction: BitcoinTx;
btcAddress: string;
}
type BtcTxStatus = 'pending' | 'success';
type BtcStatusColorMap = Record<BtcTxStatus, ColorsStringLiteral>;
const statusFromTx = (tx: BitcoinTransaction): BtcTxStatus => {
const statusFromTx = (tx: BitcoinTx): BtcTxStatus => {
if (tx.status.confirmed) return 'success';
return 'pending';
};
const colorFromTx = (tx: BitcoinTransaction): ColorsStringLiteral => {
const colorFromTx = (tx: BitcoinTx): ColorsStringLiteral => {
const colorMap: BtcStatusColorMap = {
pending: 'feedback-alert',
success: 'brand',
@@ -30,7 +29,7 @@ const colorFromTx = (tx: BitcoinTransaction): ColorsStringLiteral => {
return colorMap[statusFromTx(tx)] ?? 'feedback-error';
};
function IconForTx(address: string, tx: BitcoinTransaction) {
function IconForTx(address: string, tx: BitcoinTx) {
if (isBitcoinTxInbound(address, tx)) return IconArrowDown;
return IconArrowUp;
}

View File

@@ -1,25 +1,39 @@
import { useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { BoxProps } from '@stacks/ui';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
import {
getBitcoinTxCaption,
getBitcoinTxValue,
isBitcoinTxInbound,
} from '@app/common/transactions/bitcoin/utils';
import { useWalletType } from '@app/common/use-wallet-type';
import { usePressable } from '@app/components/item-hover';
import { IncreaseFeeButton } from '@app/components/stacks-transaction-item/increase-fee-button';
import { TransactionTitle } from '@app/components/transaction/transaction-title';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
import { BitcoinTransactionCaption } from './bitcoin-transaction-caption';
import { BitcoinTransactionIcon } from './bitcoin-transaction-icon';
import { BitcoinTransactionItemLayout } from './bitcoin-transaction-item.layout';
import { BitcoinTransactionStatus } from './bitcoin-transaction-status';
import { BitcoinTransactionValue } from './bitcoin-transaction-value';
import { getBitcoinTxCaption, getBitcoinTxValue } from './bitcoin-transaction.utils';
interface BitcoinTransactionItemProps extends BoxProps {
transaction?: BitcoinTransaction;
transaction?: BitcoinTx;
}
export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransactionItemProps) {
const [component, bind, { isHovered }] = usePressable(true);
const { pathname } = useLocation();
const navigate = useNavigate();
const { whenWallet } = useWalletType();
const bitcoinAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const { handleOpenTxLink } = useExplorerLink();
const analytics = useAnalytics();
@@ -31,6 +45,15 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
if (!transaction) return null;
const onIncreaseFee = () => {
whenWallet({
ledger: () => {
// TO-DO when implement BTC in Ledger
},
software: () => navigate(RouteUrls.IncreaseBtcFee, { state: { btcTx: transaction } }),
})();
};
const openTxLink = () => {
void analytics.track('view_bitcoin_transaction');
handleOpenTxLink({
@@ -39,18 +62,33 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
});
};
const isOriginator = !isBitcoinTxInbound(bitcoinAddress, transaction);
const isEnabled = isOriginator && !transaction.status.confirmed;
const txCaption = <BitcoinTransactionCaption>{caption}</BitcoinTransactionCaption>;
const txValue = <BitcoinTransactionValue>{value}</BitcoinTransactionValue>;
const increaseFeeButton = (
<IncreaseFeeButton
isEnabled={isEnabled}
isHovered={isHovered}
isSelected={pathname === RouteUrls.IncreaseBtcFee}
onIncreaseFee={onIncreaseFee}
/>
);
return (
<BitcoinTransactionItemLayout
<TransactionItemLayout
openTxLink={openTxLink}
txCaption={txCaption}
txIcon={<BitcoinTransactionIcon transaction={transaction} btcAddress={bitcoinAddress} />}
txStatus={<BitcoinTransactionStatus transaction={transaction} />}
txTitle={<TransactionTitle title="Bitcoin" />}
txValue={txValue}
belowCaptionEl={increaseFeeButton}
{...bind}
{...rest}
/>
>
{component}
</TransactionItemLayout>
);
}

View File

@@ -1,9 +1,9 @@
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { PendingLabel } from '@app/components/transaction/pending-label';
interface BitcoinTransactionStatusProps {
transaction: BitcoinTransaction;
transaction: BitcoinTx;
}
const pendingWaitingMessage =
'This transaction is waiting to be confirmed. The average (median) confirmation time on Bitcoin is 5-10 minutes';

View File

@@ -1,7 +1,7 @@
import { createSearchParams, useLocation, useNavigate } from 'react-router-dom';
import type { MempoolTransaction } from '@stacks/stacks-blockchain-api-types';
import { Box, BoxProps, Flex, Stack, Text, color, useMediaQuery } from '@stacks/ui';
import { BoxProps, Text, color } from '@stacks/ui';
import { isPendingTx } from '@stacks/ui-utils';
import { StacksTx, TxTransferDetails } from '@shared/models/transactions/stacks-transaction.model';
@@ -18,12 +18,12 @@ import { useWalletType } from '@app/common/use-wallet-type';
import { whenPageMode } from '@app/common/utils';
import { openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab';
import { usePressable } from '@app/components/item-hover';
import { SpaceBetween } from '@app/components/layout/space-between';
import { TransactionTitle } from '@app/components/transaction/transaction-title';
import { Title } from '@app/components/typography';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useRawTxIdState } from '@app/store/transactions/raw.hooks';
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
import { IncreaseFeeButton } from './increase-fee-button';
import { StacksTransactionIcon } from './stacks-transaction-icon';
import { StacksTransactionStatus } from './stacks-transaction-status';
@@ -46,8 +46,6 @@ export function StacksTransactionItem({
const navigate = useNavigate();
const { whenWallet } = useWalletType();
const [hideIncreaseFeeButton] = useMediaQuery('(max-width: 355px)');
if (!transaction && !transferDetails) return null;
const openTxLink = () => {
@@ -67,10 +65,10 @@ export function StacksTransactionItem({
whenWallet({
ledger: () =>
whenPageMode({
full: () => navigate(RouteUrls.IncreaseFee),
popup: () => openIndexPageInNewTab(RouteUrls.IncreaseFee, urlSearchParams),
full: () => navigate(RouteUrls.IncreaseStxFee),
popup: () => openIndexPageInNewTab(RouteUrls.IncreaseStxFee, urlSearchParams),
})(),
software: () => navigate(RouteUrls.IncreaseFee),
software: () => navigate(RouteUrls.IncreaseStxFee),
})();
};
@@ -78,42 +76,42 @@ export function StacksTransactionItem({
const isPending = transaction && isPendingTx(transaction as MempoolTransaction);
const caption = transaction ? getTxCaption(transaction) : transferDetails?.caption || '';
const icon = transaction ? (
const txIcon = transaction ? (
<StacksTransactionIcon transaction={transaction} />
) : (
transferDetails?.icon
);
const title = transaction ? getTxTitle(transaction) : transferDetails?.title || '';
const value = transaction ? getTxValue(transaction, isOriginator) : transferDetails?.value;
const increaseFeeButton = (
<IncreaseFeeButton
isEnabled={isOriginator && isPending}
isHovered={isHovered}
isSelected={pathname === RouteUrls.IncreaseStxFee}
onIncreaseFee={onIncreaseFee}
/>
);
const txStatus = transaction && <StacksTransactionStatus transaction={transaction} />;
const txCaption = (
<Text color={color('text-caption')} fontSize={0} whiteSpace="nowrap">
{caption}
</Text>
);
const txValue = <Title fontWeight="normal">{value}</Title>;
return (
<Box position="relative" cursor="pointer" onClick={openTxLink} {...bind} {...rest}>
<Stack alignItems="center" isInline position="relative" spacing="base-loose" zIndex={2}>
{icon}
<Flex flexDirection="column" flexGrow={1} minWidth="0px">
<SpaceBetween spacing="extra-loose">
<TransactionTitle title={title} />
{value && <Title fontWeight="normal">{value}</Title>}
</SpaceBetween>
<SpaceBetween minHeight="loose" minWidth="0px" mt="extra-tight">
<Stack alignItems="center" isInline>
<Text color={color('text-caption')} fontSize={0} whiteSpace="nowrap">
{caption}
</Text>
{transaction ? <StacksTransactionStatus transaction={transaction} /> : null}
</Stack>
{!hideIncreaseFeeButton ? (
<IncreaseFeeButton
isEnabled={isOriginator && isPending}
isHovered={isHovered}
isSelected={pathname === RouteUrls.IncreaseFee}
onIncreaseFee={onIncreaseFee}
/>
) : null}
</SpaceBetween>
</Flex>
</Stack>
<TransactionItemLayout
openTxLink={openTxLink}
txCaption={txCaption}
txIcon={txIcon}
txStatus={txStatus}
txTitle={<TransactionTitle title={title} />}
txValue={txValue}
belowCaptionEl={increaseFeeButton}
{...bind}
{...rest}
>
{component}
</Box>
</TransactionItemLayout>
);
}

View File

@@ -0,0 +1,50 @@
import { Box, Flex, Stack } from '@stacks/ui';
import { SpaceBetween } from '@app/components/layout/space-between';
interface TransactionItemLayoutProps {
openTxLink(): void;
txCaption: JSX.Element;
txTitle: JSX.Element;
txValue: JSX.Element;
txIcon?: JSX.Element;
txStatus?: JSX.Element;
belowCaptionEl?: JSX.Element;
children?: JSX.Element;
}
export function TransactionItemLayout({
openTxLink,
txCaption,
txIcon,
txStatus,
txTitle,
txValue,
belowCaptionEl,
children,
...rest
}: TransactionItemLayoutProps) {
return (
<Box position="relative" cursor="pointer" {...rest}>
<Stack
alignItems="center"
isInline
onClick={openTxLink}
position="relative"
spacing="base-loose"
zIndex={2}
>
{txIcon && txIcon}
<Flex flexDirection="column" justifyContent="space-between" flexGrow={1} minWidth="0px">
<SpaceBetween spacing="extra-loose">
{txTitle} {txValue}
</SpaceBetween>
<Stack alignItems="center" isInline mt="4px">
{txCaption} {txStatus && txStatus}
{belowCaptionEl ? belowCaptionEl : null}
</Stack>
</Flex>
</Stack>
{children}
</Box>
);
}

View File

@@ -1,13 +1,13 @@
import { AddressTransactionWithTransfers } from '@stacks/stacks-blockchain-api-types';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import {
TransactionListBitcoinTx,
TransactionListStacksTx,
} from './components/transaction-list/transaction-list.model';
function createBitcoinTxTypeWrapper(tx: BitcoinTransaction): TransactionListBitcoinTx {
function createBitcoinTxTypeWrapper(tx: BitcoinTx): TransactionListBitcoinTx {
return {
blockchain: 'bitcoin',
transaction: tx,
@@ -21,7 +21,7 @@ function createStacksTxTypeWrapper(tx: AddressTransactionWithTransfers): Transac
};
}
export function convertBitcoinTxsToListType(txs?: BitcoinTransaction[]) {
export function convertBitcoinTxsToListType(txs?: BitcoinTx[]) {
if (!txs) return [];
const confirmedTxs = txs.filter(tx => tx.status.confirmed);
return confirmedTxs.map(tx => createBitcoinTxTypeWrapper(tx));

View File

@@ -1,14 +1,14 @@
import { MempoolTransaction } from '@stacks/stacks-blockchain-api-types';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item';
import { BitcoinTransactionItem } from '../transaction-list/bitcoin-transaction/bitcoin-transaction-item';
import { BitcoinTransactionItem } from '../../../../components/bitcoin-transaction-item/bitcoin-transaction-item';
import { PendingTransactionListLayout } from './pending-transaction-list.layout';
interface PendingTransactionListProps {
bitcoinTxs: BitcoinTransaction[];
bitcoinTxs: BitcoinTx[];
stacksTxs: MempoolTransaction[];
}
export function PendingTransactionList({ bitcoinTxs, stacksTxs }: PendingTransactionListProps) {

View File

@@ -1,41 +0,0 @@
import { Box, Flex, Stack } from '@stacks/ui';
import { usePressable } from '@app/components/item-hover';
import { SpaceBetween } from '@app/components/layout/space-between';
interface BitcoinTransactionItemLayoutProps {
openTxLink(): void;
txCaption: React.JSX.Element;
txIcon: React.JSX.Element;
txStatus: React.JSX.Element;
txTitle: React.JSX.Element;
txValue: React.JSX.Element;
}
export function BitcoinTransactionItemLayout({
openTxLink,
txCaption,
txIcon,
txStatus,
txTitle,
txValue,
...rest
}: BitcoinTransactionItemLayoutProps) {
const [component, bind] = usePressable(true);
return (
<Box position="relative" cursor="pointer" onClick={openTxLink} {...bind} {...rest}>
<Stack alignItems="center" isInline position="relative" spacing="base-loose" zIndex={2}>
{txIcon}
<Flex flexDirection="column" flexGrow={1} minWidth="0px">
<SpaceBetween spacing="extra-loose">
{txTitle} {txValue}
</SpaceBetween>
<Stack alignItems="center" isInline>
{txCaption} {txStatus}
</Stack>
</Flex>
</Stack>
{component}
</Box>
);
}

View File

@@ -0,0 +1,3 @@
import { BitcoinTransactionItem } from '@app/components/bitcoin-transaction-item/bitcoin-transaction-item';
export const BitcoinTransaction = BitcoinTransactionItem;

View File

@@ -1,5 +1,4 @@
import { BitcoinTransactionItem } from '@app/features/activity-list/components/transaction-list/bitcoin-transaction/bitcoin-transaction-item';
import { BitcoinTransaction } from './bitcoin-transaction/bitcoin-transaction';
import { StacksTransaction } from './stacks-transaction/stacks-transaction';
import { TransactionListTxs } from './transaction-list.model';
@@ -9,7 +8,7 @@ interface TransactionListItemProps {
export function TransactionListItem({ tx }: TransactionListItemProps) {
switch (tx.blockchain) {
case 'bitcoin':
return <BitcoinTransactionItem transaction={tx.transaction} />;
return <BitcoinTransaction transaction={tx.transaction} />;
case 'stacks':
return <StacksTransaction transaction={tx.transaction} />;
default:

View File

@@ -1,11 +1,11 @@
import { AddressTransactionWithTransfers } from '@stacks/stacks-blockchain-api-types';
import type { Blockchains } from '@shared/models/blockchain.model';
import type { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import type { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
export interface TransactionListBitcoinTx {
blockchain: Extract<Blockchains, 'bitcoin'>;
transaction: BitcoinTransaction;
transaction: BitcoinTx;
}
export interface TransactionListStacksTx {

View File

@@ -7,8 +7,9 @@ import { Money } from '@shared/models/money.model';
import { formatMoney } from '@app/common/money/format-money';
import { AvailableBalance } from '@app/components/available-balance';
import { BitcoinCustomFee } from '@app/components/bitcoin-custom-fee/bitcoin-custom-fee';
import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee';
import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-list';
import { BitcoinCustomFee } from '@app/features/bitcoin-choose-fee/bitcoin-custom-fee/bitcoin-custom-fee';
import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
@@ -28,6 +29,7 @@ interface BitcoinChooseFeeProps {
recipient: string;
recommendedFeeRate: string;
showError: boolean;
maxRecommendedFeeRate?: number;
}
export function BitcoinChooseFee({
amount,
@@ -40,6 +42,7 @@ export function BitcoinChooseFee({
recipient,
recommendedFeeRate,
showError,
maxRecommendedFeeRate = 0,
}: BitcoinChooseFeeProps) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const btcBalance = useNativeSegwitBalance(nativeSegwitSigner.address);
@@ -71,6 +74,7 @@ export function BitcoinChooseFee({
onValidateBitcoinSpend={onValidateBitcoinSpend}
recipient={recipient}
setCustomFeeInitialValue={setCustomFeeInitialValue}
maxCustomFeeRate={maxRecommendedFeeRate * MAX_FEE_RATE_MULTIPLIER}
/>
}
feesList={feesList}

View File

@@ -0,0 +1,77 @@
import { useNavigate } from 'react-router-dom';
import { Stack } from '@stacks/ui';
import { Formik } from 'formik';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { RouteUrls } from '@shared/route-urls';
import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance';
import { formatMoney } from '@app/common/money/format-money';
import { btcToSat } from '@app/common/money/unit-conversion';
import { getBitcoinTxValue } from '@app/common/transactions/bitcoin/utils';
import { BitcoinCustomFeeFiat } from '@app/components/bitcoin-custom-fee/bitcoin-custom-fee-fiat';
import { BitcoinTransactionItem } from '@app/components/bitcoin-transaction-item/bitcoin-transaction-item';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { TextInputField } from '@app/components/text-input-field';
import { Caption } from '@app/components/typography';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useBtcIncreaseFee } from '../hooks/use-btc-increase-fee';
import { IncreaseFeeActions } from './increase-fee-actions';
const feeInputLabel = 'sats/vB';
interface IncreaseBtcFeeFormProps {
btcTx: BitcoinTx;
}
export function IncreaseBtcFeeForm({ btcTx }: IncreaseBtcFeeFormProps) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const navigate = useNavigate();
const currentBitcoinAddress = nativeSegwitSigner.address;
const { btcAvailableAssetBalance } = useBtcAssetBalance(currentBitcoinAddress);
const { isBroadcasting, sizeInfo, onSubmit, validationSchema, recipient } =
useBtcIncreaseFee(btcTx);
const balance = formatMoney(btcAvailableAssetBalance.balance);
if (isBroadcasting) {
return <LoadingSpinner />;
}
const initialFeeRate = `${(btcTx.fee / sizeInfo.txVBytes).toFixed(0)}`;
return (
<Formik
initialValues={{ feeRate: initialFeeRate }}
onSubmit={onSubmit}
validateOnChange={false}
validateOnBlur={false}
validateOnMount={false}
validationSchema={validationSchema}
>
<Stack spacing="extra-loose">
{btcTx && <BitcoinTransactionItem position="relative" transaction={btcTx} zIndex={99} />}
<Stack spacing="base">
<Stack spacing="extra-tight">
<TextInputField label={feeInputLabel} name="feeRate" placeholder={feeInputLabel} />
<BitcoinCustomFeeFiat
recipient={recipient}
isSendingMax={false}
amount={Math.abs(
btcToSat(getBitcoinTxValue(currentBitcoinAddress, btcTx)).toNumber()
)}
/>
</Stack>
{btcAvailableAssetBalance && <Caption>Balance: {balance}</Caption>}
</Stack>
<IncreaseFeeActions
isDisabled={false}
onCancel={() => {
navigate(RouteUrls.Home);
}}
/>
</Stack>
</Formik>
);
}

View File

@@ -1,36 +1,22 @@
import { useNavigate } from 'react-router-dom';
import { Button, Stack } from '@stacks/ui';
import { useField, useFormikContext } from 'formik';
import { RouteUrls } from '@shared/route-urls';
import { useFormikContext } from 'formik';
import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { stxToMicroStx } from '@app/common/money/unit-conversion';
import { useWalletType } from '@app/common/use-wallet-type';
import { useRawTxIdState } from '@app/store/transactions/raw.hooks';
interface IncreaseFeeActionsProps {
currentFee: number;
isDisabled: boolean;
onCancel: () => void;
}
export function IncreaseFeeActions(props: IncreaseFeeActionsProps) {
const { currentFee } = props;
const [field] = useField('fee');
const { onCancel, isDisabled } = props;
const { handleSubmit } = useFormikContext();
const { isLoading } = useLoading(LoadingKeys.INCREASE_FEE_DRAWER);
const [, setRawTxId] = useRawTxIdState();
const { whenWallet } = useWalletType();
const navigate = useNavigate();
const newFee = field.value;
const isSame = currentFee === stxToMicroStx(newFee).toNumber();
const actionText = whenWallet({ ledger: 'Confirm on Ledger', software: 'Submit' });
const onCancel = () => {
setRawTxId(null);
navigate(RouteUrls.Home);
};
return (
<Stack isInline>
<Button onClick={onCancel} flexGrow={1} borderRadius="10px" mode="tertiary">
@@ -42,7 +28,7 @@ export function IncreaseFeeActions(props: IncreaseFeeActionsProps) {
onClick={handleSubmit}
isLoading={isLoading}
borderRadius="10px"
isDisabled={isSame}
isDisabled={isDisabled}
>
{actionText}
</Button>

View File

@@ -1,11 +1,14 @@
import { useCallback, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import { Stack } from '@stacks/ui';
import BigNumber from 'bignumber.js';
import { Formik } from 'formik';
import * as yup from 'yup';
import { RouteUrls } from '@shared/route-urls';
import { useRefreshAllAccountData } from '@app/common/hooks/account/use-refresh-all-account-data';
import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance';
import { microStxToStx, stxToMicroStx } from '@app/common/money/unit-conversion';
@@ -26,9 +29,10 @@ import { useSelectedTx } from '../hooks/use-selected-tx';
import { IncreaseFeeActions } from './increase-fee-actions';
import { IncreaseFeeField } from './increase-fee-field';
export function IncreaseFeeForm() {
export function IncreaseStxFeeForm() {
const refreshAccountData = useRefreshAllAccountData();
const tx = useSelectedTx();
const navigate = useNavigate();
const [, setTxId] = useRawTxIdState();
const replaceByFee = useReplaceByFeeSoftwareWalletSubmitCallBack();
const { data: balances } = useCurrentStacksAccountAnchoredBalances();
@@ -87,7 +91,7 @@ export function IncreaseFeeForm() {
validateOnMount={false}
validationSchema={validationSchema}
>
{() => (
{props => (
<Stack spacing="extra-loose">
{tx && <StacksTransactionItem position="relative" transaction={tx} zIndex={99} />}
<Stack spacing="base">
@@ -98,7 +102,13 @@ export function IncreaseFeeForm() {
</Caption>
)}
</Stack>
<IncreaseFeeActions currentFee={fee} />
<IncreaseFeeActions
onCancel={() => {
setTxId(null);
navigate(RouteUrls.Home);
}}
isDisabled={stxToMicroStx(props.values.fee).isEqualTo(fee)}
/>
</Stack>
)}
</Formik>

View File

@@ -0,0 +1,172 @@
import { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import * as btc from '@scure/btc-signer';
import BigNumber from 'bignumber.js';
import * as yup from 'yup';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance';
import { queryClient } from '@app/common/persistence';
import {
getBitcoinTxValue,
getRecipientAddressFromOutput,
getSizeInfo,
} from '@app/common/transactions/bitcoin/utils';
import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee';
import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
export function useBtcIncreaseFee(btcTx: BitcoinTx) {
const navigate = useNavigate();
const networkMode = useBitcoinScureLibNetworkConfig();
const analytics = useAnalytics();
const {
address: currentBitcoinAddress,
sign,
publicKeychain: currentAddressIndexKeychain,
} = useCurrentAccountNativeSegwitIndexZeroSigner();
const { data: utxos = [], refetch } = useCurrentNativeSegwitUtxos();
const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction();
const recipient = getRecipientAddressFromOutput(btcTx.vout, currentBitcoinAddress) || '';
const sizeInfo = getSizeInfo({
inputLength: btcTx.vin.length,
recipient,
outputLength: btcTx.vout.length,
});
const { btcAvailableAssetBalance } = useBtcAssetBalance(currentBitcoinAddress);
const sendingAmount = getBitcoinTxValue(currentBitcoinAddress, btcTx);
const { feesList } = useBitcoinFeesList({
amount: Number(sendingAmount),
isSendingMax: false,
recipient,
utxos,
});
function generateTx(payload: { feeRate: string; tx: BitcoinTx }) {
const newTx = new btc.Transaction();
const { vin, vout, fee: prevFee } = payload.tx;
const p2wpkh = btc.p2wpkh(currentAddressIndexKeychain.publicKey!, networkMode);
vin.forEach(input => {
newTx.addInput({
txid: input.txid,
index: input.vout,
sequence: input.sequence + 1,
witnessUtxo: {
// script = 0014 + pubKeyHash
script: p2wpkh.script,
amount: BigInt(input.prevout.value),
},
});
});
const newFee = Math.ceil(sizeInfo.txVBytes * Number(payload.feeRate));
const feeDiff = newFee - prevFee;
vout.forEach(output => {
if (output.scriptpubkey_address === currentBitcoinAddress) {
const outputDiff = output.value - feeDiff;
if (outputDiff < 0) {
void analytics.track('bitcoin_rbf_fee_increase_error', {
outputDiff,
});
throw new Error('Previous tx inputs cannot cover new fee');
}
newTx.addOutputAddress(currentBitcoinAddress, BigInt(outputDiff), networkMode);
return;
}
newTx.addOutputAddress(recipient, BigInt(output.value), networkMode);
});
sign(newTx);
newTx.finalize();
return { hex: newTx.hex };
}
async function initiateTransaction(tx: string) {
await broadcastTx({
tx,
async onSuccess(txid) {
navigate(RouteUrls.IncreaseFeeSent);
void analytics.track('increase_fee_transaction', {
symbol: 'btc',
txid,
});
await refetch();
void queryClient.invalidateQueries({ queryKey: ['btc-txs-by-address'] });
},
onError,
delayTime: 5000,
});
}
async function onSubmit(values: { feeRate: string }) {
try {
const { hex } = generateTx({ feeRate: values.feeRate, tx: btcTx });
await initiateTransaction(hex);
} catch (e) {
onError(e);
}
}
function onError(error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
toast.error(message);
navigate(RouteUrls.Home);
}
const validationSchema = yup.object({
feeRate: yup
.number()
.integer('Fee must be a whole number')
.required('Fee is required')
.test({
message: 'Fee rate cannot be less or equal to previous',
test(value) {
const bnValue = new BigNumber(value);
const prevFee = new BigNumber(btcTx.fee);
return prevFee.isLessThan(bnValue.multipliedBy(sizeInfo.txVBytes));
},
})
.test({
message: 'Fee is too high',
test(value) {
const bnValue = new BigNumber(value);
// check if fee is higher than 50 times the highest fee
if (
feesList.length > 0 &&
bnValue.isGreaterThan(feesList[0].feeRate * MAX_FEE_RATE_MULTIPLIER)
) {
return false;
}
// check if fee is higher than the available balance
return bnValue.isLessThanOrEqualTo(btcAvailableAssetBalance.balance.amount);
},
}),
});
return {
generateTx,
initiateTransaction,
isBroadcasting,
sizeInfo,
onSubmit,
validationSchema,
recipient,
};
}

View File

@@ -0,0 +1,36 @@
import { useLocation, useNavigate } from 'react-router-dom';
import get from 'lodash.get';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { RouteUrls } from '@shared/route-urls';
import { IncreaseBtcFeeForm } from './components/increase-btc-fee-form';
import { IncreaseFeeDrawer } from './increase-fee-drawer';
function useIncreaseBtcFeeDrawerState() {
const location = useLocation();
return {
tx: get(location.state, 'btcTx') as BitcoinTx,
};
}
export function IncreaseBtcFeeDrawer() {
const { tx } = useIncreaseBtcFeeDrawerState();
const navigate = useNavigate();
const location = useLocation();
const onClose = () => {
navigate(RouteUrls.Home);
};
return (
tx && (
<IncreaseFeeDrawer
feeForm={<IncreaseBtcFeeForm btcTx={tx} />}
onClose={onClose}
isShowing={location.pathname === RouteUrls.IncreaseBtcFee}
/>
)
);
}

View File

@@ -1,46 +1,21 @@
import { Suspense, useEffect } from 'react';
import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { Flex, Spinner, Stack } from '@stacks/ui';
import { RouteUrls } from '@shared/route-urls';
import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { Caption } from '@app/components/typography';
import { useRawTxIdState } from '@app/store/transactions/raw.hooks';
import { IncreaseFeeForm } from './components/increase-fee-form';
interface IncreaseFeeDrawerProps {
feeForm: JSX.Element;
onClose: () => void;
isShowing: boolean;
}
export function IncreaseFeeDrawer() {
const [rawTxId, setRawTxId] = useRawTxIdState();
const { isLoading, setIsIdle } = useLoading(LoadingKeys.INCREASE_FEE_DRAWER);
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const txIdFromParams = searchParams.get('txId');
useEffect(() => {
if (!rawTxId && txIdFromParams) {
setRawTxId(txIdFromParams);
}
if (isLoading && !rawTxId) {
setIsIdle();
}
}, [isLoading, rawTxId, setIsIdle, setRawTxId, txIdFromParams]);
const onClose = () => {
setRawTxId(null);
navigate(RouteUrls.Home);
};
return rawTxId ? (
export function IncreaseFeeDrawer({ feeForm, onClose, isShowing }: IncreaseFeeDrawerProps) {
return (
<>
<BaseDrawer
isShowing={location.pathname === RouteUrls.IncreaseFee}
onClose={onClose}
title="Increase transaction fee"
>
<BaseDrawer isShowing={isShowing} onClose={onClose} title="Increase transaction fee">
<Stack px="loose" spacing="loose" pb="extra-loose">
<Suspense
fallback={
@@ -50,14 +25,14 @@ export function IncreaseFeeDrawer() {
}
>
<Caption>
If your transaction has been pending for a long time, its fee might not be high enough
to be included in a block. Increase the fee and try again.
If your transaction is pending for a long time, its fee might not be high enough to be
included in a clock. Update the fee for a higher value and try again.
</Caption>
<IncreaseFeeForm />
{feeForm && feeForm}
</Suspense>
</Stack>
</BaseDrawer>
<Outlet />
</>
) : null;
);
}

View File

@@ -0,0 +1,25 @@
import { FiCheck } from 'react-icons/fi';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { Box, Flex } from '@stacks/ui';
import { RouteUrls } from '@shared/route-urls';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
export function IncreaseFeeSentDrawer() {
const location = useLocation();
const navigate = useNavigate();
const isShowing = location.pathname === RouteUrls.IncreaseFeeSent;
return (
<>
<BaseDrawer isShowing={isShowing} onClose={() => navigate(RouteUrls.Home)} title="Confirmed">
<Flex px="loose" pb="extra-loose" justifyContent="center">
<Box size="32px" as={FiCheck} mt="2px" />
</Flex>
</BaseDrawer>
<Outlet />
</>
);
}

View File

@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { RouteUrls } from '@shared/route-urls';
import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { useRawTxIdState } from '@app/store/transactions/raw.hooks';
import { IncreaseStxFeeForm } from './components/increase-stx-fee-form';
import { IncreaseFeeDrawer } from './increase-fee-drawer';
export function IncreaseStxFeeDrawer() {
const [rawTxId, setRawTxId] = useRawTxIdState();
const { isLoading, setIsIdle } = useLoading(LoadingKeys.INCREASE_FEE_DRAWER);
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const txIdFromParams = searchParams.get('txId');
const onClose = () => {
setRawTxId(null);
navigate(RouteUrls.Home);
};
useEffect(() => {
if (!rawTxId && txIdFromParams) {
setRawTxId(txIdFromParams);
}
if (isLoading && !rawTxId) {
setIsIdle();
}
}, [isLoading, rawTxId, setIsIdle, setRawTxId, txIdFromParams]);
return (
<IncreaseFeeDrawer
feeForm={<IncreaseStxFeeForm />}
onClose={onClose}
isShowing={location.pathname === RouteUrls.IncreaseStxFee}
/>
);
}

View File

@@ -93,6 +93,7 @@ export function RpcSendTransferChooseFee() {
recipient={address}
recommendedFeeRate={recommendedFeeRate}
showError={showInsufficientBalanceError}
maxRecommendedFeeRate={feesList[0]?.feeRate}
/>
);
}

View File

@@ -19,7 +19,7 @@ export function useRpcSendTransferState() {
}
export function RpcSendTransferContainer() {
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(BtcFeeType.Standard);
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(null);
const { origin } = useRpcSendTransfer();
useRouteHeader(<PopupHeader displayAddresssBalanceOf="all" />);

View File

@@ -24,7 +24,7 @@ export function useSendInscriptionState() {
}
export function SendInscriptionContainer() {
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(BtcFeeType.Standard);
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(null);
return (
<SendInscriptionLoader>

View File

@@ -59,6 +59,7 @@ export function SendInscriptionChooseFee() {
recipient={recipient}
recommendedFeeRate={recommendedFeeRate}
showError={showInsufficientBalanceError}
maxRecommendedFeeRate={feesList[0]?.feeRate}
/>
</BaseDrawer>
);

View File

@@ -13,6 +13,6 @@ export function useSendBitcoinAssetContextState() {
}
export function SendBitcoinAssetContainer() {
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(BtcFeeType.Standard);
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(null);
return <Outlet context={{ selectedFeeType, setSelectedFeeType }} />;
}

View File

@@ -144,6 +144,7 @@ export function BrcChooseFee() {
recommendedFeeRate={recommendedFeeRate}
recipient={recipient}
showError={showInsufficientBalanceError}
maxRecommendedFeeRate={feesList[0]?.feeRate}
/>
);
}

View File

@@ -64,6 +64,7 @@ export function BtcChooseFee() {
recipient={txValues.recipient}
recommendedFeeRate={recommendedFeeRate}
showError={showInsufficientBalanceError}
maxRecommendedFeeRate={feesList[0]?.feeRate}
/>
);
}

View File

@@ -13,6 +13,7 @@ import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { queryClient } from '@app/common/persistence';
import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer';
import {
InfoCard,
@@ -82,6 +83,11 @@ export function BtcSendFormConfirmation() {
navigate(RouteUrls.SentBtcTxSummary.replace(':txId', `${txid}`), {
state: formBtcTxSummaryState(txid),
});
// invalidate txs query after some time to ensure that the new tx will be shown in the list
setTimeout(() => {
void queryClient.invalidateQueries({ queryKey: ['btc-txs-by-address'] });
}, 2000);
},
onError(e) {
nav.toErrorPage(e);

View File

@@ -1,9 +1,9 @@
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
export const mockAddress = 'tb1qxy5r9rlmpcxgwp92x2594q3gg026y4kdv2rsl8';
// multiple inputs and outputs
export const mockPendingTxs1: BitcoinTransaction[] = [
export const mockPendingTxs1: BitcoinTx[] = [
{
txid: '7438bd24579108a85fbf77756e7b9c87238b947dd0f858f6e30bad4f4d6d557a',
version: 2,
@@ -74,7 +74,7 @@ export const mockPendingTxs1: BitcoinTransaction[] = [
];
// multiple transactions
export const mockPendingTxs2: BitcoinTransaction[] = [
export const mockPendingTxs2: BitcoinTx[] = [
{
txid: '7438bd24579108a85fbf77756e7b9c87238b947dd0f858f6e30bad4f4d6d557a',
version: 2,
@@ -212,7 +212,7 @@ export const mockPendingTxs2: BitcoinTransaction[] = [
];
// one input and many outputs
export const mockPendingTxs3: BitcoinTransaction[] = [
export const mockPendingTxs3: BitcoinTx[] = [
{
txid: '7438bd24579108a85fbf77756e7b9c87238b947dd0f858f6e30bad4f4d6d557a',
version: 2,

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { createMoney } from '@shared/models/money.model';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { sumNumbers } from '@app/common/math/helpers';
@@ -10,7 +10,7 @@ import { useGetBitcoinTransactionsByAddressQuery } from './transactions-by-addre
import { useAllSpendableNativeSegwitUtxos } from './utxos-by-address.hooks';
function useFilterAddressPendingTransactions() {
return useCallback((txs: BitcoinTransaction[]) => {
return useCallback((txs: BitcoinTx[]) => {
return txs.filter(tx => !tx.status.confirmed);
}, []);
}
@@ -35,10 +35,7 @@ export function useBitcoinPendingTransactionsInputs(address: string) {
});
}
export function calculateOutboundPendingTxsValue(
pendingTxs: BitcoinTransaction[],
address: string
) {
export function calculateOutboundPendingTxsValue(pendingTxs: BitcoinTx[], address: string) {
// sum all inputs
const sumInputs = sumNumbers(pendingTxs.flatMap(tx => tx.vin.map(input => input.prevout.value)));
@@ -56,7 +53,7 @@ export function calculateOutboundPendingTxsValue(
// filter out pending txs that have inputs that are not in the utxos list to prevent double extraction
function filterMissingUtxosPendingTxs(
pendingTxs: BitcoinTransaction[],
pendingTxs: BitcoinTx[],
utxos: UtxoResponseItem[],
address: string
) {

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { AppUseQueryConfig } from '@app/query/query-config';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
@@ -9,9 +9,9 @@ const staleTime = 5 * 1000;
const queryOptions = { staleTime, refetchInterval: staleTime };
export function useGetBitcoinTransactionsByAddressQuery<T extends unknown = BitcoinTransaction[]>(
export function useGetBitcoinTransactionsByAddressQuery<T extends unknown = BitcoinTx[]>(
address: string,
options?: AppUseQueryConfig<BitcoinTransaction[], T>
options?: AppUseQueryConfig<BitcoinTx[], T>
) {
const client = useBitcoinClient();

View File

@@ -2,7 +2,7 @@ import * as btc from '@scure/btc-signer';
import { bytesToHex } from '@stacks/common';
import { UseQueryResult, useQueries, useQuery } from '@tanstack/react-query';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { AppUseQueryConfig } from '@app/query/query-config';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
@@ -42,7 +42,7 @@ const queryOptions = {
export function useGetBitcoinTransactionQueries(
inputs: btc.TransactionInputRequired[]
): UseQueryResult<BitcoinTransaction>[] {
): UseQueryResult<BitcoinTx>[] {
const client = useBitcoinClient();
return useQueries({

View File

@@ -6,6 +6,7 @@ import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
interface BroadcastCallbackArgs {
tx: string;
delayTime?: number;
onSuccess?(txid: string): void;
onError?(error: Error): void;
onFinally?(): void;
@@ -17,12 +18,12 @@ export function useBitcoinBroadcastTransaction() {
const analytics = useAnalytics();
const broadcastTx = useCallback(
async ({ tx, onSuccess, onError, onFinally }: BroadcastCallbackArgs) => {
async ({ tx, onSuccess, onError, onFinally, delayTime = 700 }: BroadcastCallbackArgs) => {
try {
setIsBroadcasting(true);
const resp = await client.transactionsApi.broadcastTransaction(tx);
// simulate slower broadcast time to allow mempool refresh
await delay(700);
await delay(delayTime);
if (!resp.ok) throw new Error(await resp.text());
const txid = await resp.text();
onSuccess?.(txid);

View File

@@ -15,7 +15,9 @@ import { ActivityList } from '@app/features/activity-list/activity-list';
import { BalancesList } from '@app/features/balances-list/balances-list';
import { Container } from '@app/features/container/container';
import { EditNonceDrawer } from '@app/features/edit-nonce-drawer/edit-nonce-drawer';
import { IncreaseFeeDrawer } from '@app/features/increase-fee-drawer/increase-fee-drawer';
import { IncreaseBtcFeeDrawer } from '@app/features/increase-fee-drawer/increase-btc-fee-drawer';
import { IncreaseFeeSentDrawer } from '@app/features/increase-fee-drawer/increase-fee-sent-drawer';
import { IncreaseStxFeeDrawer } from '@app/features/increase-fee-drawer/increase-stx-fee-drawer';
import { ledgerJwtSigningRoutes } from '@app/features/ledger/flows/jwt-signing/ledger-sign-jwt.routes';
import { requestBitcoinKeysRoutes } from '@app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys';
import { requestStacksKeysRoutes } from '@app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys';
@@ -108,9 +110,12 @@ function useAppRoutes() {
{requestStacksKeysRoutes}
<Route path={RouteUrls.RetriveTaprootFunds} element={<RetrieveTaprootToNativeSegwit />} />
<Route path={RouteUrls.IncreaseFee} element={<IncreaseFeeDrawer />}>
<Route path={RouteUrls.IncreaseStxFee} element={<IncreaseStxFeeDrawer />}>
{ledgerStacksTxSigningRoutes}
</Route>
<Route path={RouteUrls.IncreaseBtcFee} element={<IncreaseBtcFeeDrawer />} />
<Route path={RouteUrls.IncreaseFeeSent} element={<IncreaseFeeSentDrawer />} />
<Route path={RouteUrls.Receive} element={<ReceiveModal />} />
<Route path={RouteUrls.ReceiveCollectible} element={<ReceiveCollectibleModal />} />
<Route

View File

@@ -29,7 +29,7 @@ export const useReplaceByFeeSoftwareWalletSubmitCallBack = () => {
await submitTransaction({
onSuccess() {
setTxId(null);
navigate(RouteUrls.Home);
navigate(RouteUrls.IncreaseFeeSent);
},
onError() {
logger.error('Error submitting transaction');

View File

@@ -52,7 +52,7 @@ interface BitcoinTransactionVectorInput {
witness: string[];
}
export interface BitcoinTransaction {
export interface BitcoinTx {
fee: number;
locktime: number;
size: number;

View File

@@ -33,7 +33,9 @@ export enum RouteUrls {
FundReceive = '/fund/receive',
FundReceiveStx = '/fund/receive/stx',
FundReceiveBtc = '/fund/receive/btc',
IncreaseFee = '/increase-fee',
IncreaseStxFee = '/increase-fee/stx',
IncreaseBtcFee = '/increase-fee/btc',
IncreaseFeeSent = '/increase-fee/sent',
Receive = '/receive',
ReceiveCollectible = '/receive/collectible',
ReceiveCollectibleOrdinal = '/receive/collectible/ordinal',