mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-04-24 04:45:58 +08:00
feat: add btc set fee choice
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Box, Button, Flex, FlexProps, Stack, Text } from '@stacks/ui';
|
||||
import { Box, Button, Flex, FlexProps, Stack, StackProps, Text } from '@stacks/ui';
|
||||
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
|
||||
|
||||
import { isString } from '@shared/utils';
|
||||
|
||||
import { whenPageMode } from '@app/common/utils';
|
||||
import { figmaTheme } from '@app/common/utils/figma-theme';
|
||||
|
||||
import { SpaceBetween } from '../layout/space-between';
|
||||
@@ -54,7 +55,7 @@ export function InfoCardSeparator() {
|
||||
}
|
||||
|
||||
// InfoCardAssetValue
|
||||
interface InfoCardAssetValueProps {
|
||||
interface InfoCardAssetValueProps extends StackProps {
|
||||
value: number;
|
||||
fiatValue?: string;
|
||||
fiatSymbol?: string;
|
||||
@@ -68,35 +69,37 @@ export function InfoCardAssetValue({
|
||||
fiatSymbol,
|
||||
symbol,
|
||||
icon,
|
||||
...props
|
||||
}: InfoCardAssetValueProps) {
|
||||
return (
|
||||
<Stack
|
||||
mb="44px"
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
backgroundColor="#F9F9FA"
|
||||
py="24px"
|
||||
border="1px solid #EFEFF2"
|
||||
borderRadius="10px"
|
||||
>
|
||||
{icon && <Box as={icon} size="32px" />}
|
||||
<Box width="100%" {...props}>
|
||||
<Stack
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
backgroundColor="#F9F9FA"
|
||||
py="24px"
|
||||
border="1px solid #EFEFF2"
|
||||
borderRadius="10px"
|
||||
>
|
||||
{icon && <Box as={icon} size="32px" />}
|
||||
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<Text
|
||||
fontSize="24px"
|
||||
fontWeight="500"
|
||||
lineHeight="36px"
|
||||
data-testid={SharedComponentsSelectors.InfoCardAssetValue}
|
||||
>
|
||||
{value} {symbol}
|
||||
</Text>
|
||||
{fiatValue && (
|
||||
<Text fontSize="12px" mt="4px">
|
||||
~ {fiatValue} {fiatSymbol}
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<Text
|
||||
fontSize="24px"
|
||||
fontWeight="500"
|
||||
lineHeight="36px"
|
||||
data-testid={SharedComponentsSelectors.InfoCardAssetValue}
|
||||
>
|
||||
{value} {symbol}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
{fiatValue && (
|
||||
<Text fontSize="12px" mt="4px">
|
||||
~ {fiatValue} {fiatSymbol}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,3 +127,33 @@ export function InfoCardBtn({ icon, label, onClick }: InfoCardBtnProps) {
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// InfoCardFooter
|
||||
interface InfoCardFooterProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function InfoCardFooter({ children }: InfoCardFooterProps) {
|
||||
return (
|
||||
<Flex
|
||||
bottom="0"
|
||||
width="100%"
|
||||
bg={whenPageMode({
|
||||
full: '',
|
||||
popup: '#fff',
|
||||
})}
|
||||
borderTop="1px solid #EFEFF2"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex="999"
|
||||
py="loose"
|
||||
px="extra-loose"
|
||||
position={whenPageMode({
|
||||
full: 'unset',
|
||||
popup: 'fixed',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,13 +68,7 @@ export function ModalHeader({
|
||||
</Title>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexBasis="20%"
|
||||
isInline
|
||||
justifyContent="flex-end"
|
||||
position="relative"
|
||||
>
|
||||
<Flex alignItems="center" flexBasis="20%" justifyContent="flex-end" position="relative">
|
||||
<NetworkModeBadge position="absolute" right="35px" />
|
||||
{(onClose || defaultClose) && (
|
||||
<IconButton
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.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';
|
||||
@@ -25,7 +25,7 @@ function useSendInscriptionReviewState() {
|
||||
return {
|
||||
signedTx: get(location.state, 'tx') as string,
|
||||
recipient: get(location.state, 'recipient', '') as string,
|
||||
fee: get(location.state, 'fee'),
|
||||
fee: get(location.state, 'fee') as number,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ export function SendInscriptionReview() {
|
||||
const { refetch } = useCurrentNativeSegwitUtxos();
|
||||
const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction();
|
||||
|
||||
const { data: feeRate } = useBitcoinFeeRate();
|
||||
|
||||
const arrivesIn = feeRate ? `~${feeRate?.fastestFee} min` : '~10 – 20 min';
|
||||
const arrivesIn = btcTxTimeMap.hourFee;
|
||||
const summaryFee = formatMoney(createMoney(Number(fee), 'BTC'));
|
||||
|
||||
async function sendInscription() {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// import get from 'lodash.get';
|
||||
// import { RouteUrls } from '@shared/route-urls';
|
||||
import { BaseDrawer } from '@app/components/drawer/base-drawer';
|
||||
|
||||
// import { BitcoinSetFee } from '../send-crypto-asset-form/family/bitcoin/components/bitcoin-set-fee';
|
||||
// import { useInscriptionSendState } from './send-inscription-container';
|
||||
|
||||
// function useSendInscriptionSetFeeState() {
|
||||
// const location = useLocation();
|
||||
// return {
|
||||
// tx: get(location.state, 'tx') as string,
|
||||
// recipient: get(location.state, 'recipient', '') as string,
|
||||
// };
|
||||
// }
|
||||
|
||||
export function SendInscriptionSetFee() {
|
||||
const navigate = useNavigate();
|
||||
// const { tx, recipient } = useSendInscriptionSetFeeState();
|
||||
// const { inscription, utxo } = useInscriptionSendState();
|
||||
|
||||
// function previewTransaction(feeRate: number, feeValue: number, time: string) {
|
||||
// feeRate;
|
||||
// navigate(RouteUrls.SendOrdinalInscriptionReview, {
|
||||
// state: { fee: feeValue, inscription, utxo, recipient, tx, arrivesIn: time },
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<BaseDrawer title="Choose fee" isShowing enableGoBack onClose={() => navigate(-1)}>
|
||||
{/* <BitcoinSetFee onChooseFee={previewTransaction} recipient={recipient} amount={}/>; */}
|
||||
</BaseDrawer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Box, Flex, Text, color, transition } from '@stacks/ui';
|
||||
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
|
||||
|
||||
interface FeesCardProps {
|
||||
feeType: string;
|
||||
feeAmount: string;
|
||||
feeFiatValue: string;
|
||||
arrivesIn: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
export function FeesCard({ feeType, feeAmount, feeFiatValue, arrivesIn, ...props }: FeesCardProps) {
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
border="1px solid"
|
||||
borderColor={color('border')}
|
||||
borderRadius="16px"
|
||||
boxShadow="0px 1px 2px rgba(0, 0, 0, 0.04)"
|
||||
transition={transition}
|
||||
padding="extra-loose"
|
||||
width="100%"
|
||||
_hover={{ background: '#F9F9FA' }}
|
||||
data-testid={SharedComponentsSelectors.FeeCard}
|
||||
{...props}
|
||||
>
|
||||
<Flex justifyContent="space-between" mb="tight" fontWeight={500}>
|
||||
<Text>{feeType}</Text>
|
||||
<Text data-testid={SharedComponentsSelectors.FeeCardFeeValue}>{feeAmount}</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between" color="#74777D">
|
||||
<Text>{arrivesIn}</Text>
|
||||
<Text>{feeFiatValue}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function FormFooter(props: { balance: Money }) {
|
||||
bg={color('bg')}
|
||||
borderTop="1px solid #DCDDE2"
|
||||
bottom="0px"
|
||||
height={['106px', '116px']}
|
||||
height={['96px', '116px']}
|
||||
position={whenPageMode({
|
||||
full: 'unset',
|
||||
popup: 'absolute',
|
||||
|
||||
@@ -16,7 +16,7 @@ export function PreviewButton(props: ButtonProps) {
|
||||
width="100%"
|
||||
{...props}
|
||||
>
|
||||
Preview
|
||||
Continue
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Stack, Text } from '@stacks/ui';
|
||||
|
||||
import { FeesCard } from '../../../components/fees-card';
|
||||
import { useBitcoinSetFees } from './use-bitcoin-set-fees';
|
||||
|
||||
interface BitcoinSetFeeProps {
|
||||
onChooseFee(feeRate: number, feeValue: number, time: string): Promise<void> | void;
|
||||
recipient: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function BitcoinSetFee({ onChooseFee, recipient, amount }: BitcoinSetFeeProps) {
|
||||
const { feesList } = useBitcoinSetFees({ recipient, amount });
|
||||
|
||||
return (
|
||||
<Stack p="extra-loose" width="100%" spacing="extra-loose" alignItems="center">
|
||||
<Stack width="100%" spacing="base">
|
||||
{feesList.map(({ label, value, btcValue, fiatValue, time, feeRate }) => (
|
||||
<FeesCard
|
||||
key={label}
|
||||
feeType={label}
|
||||
feeAmount={btcValue}
|
||||
feeFiatValue={fiatValue}
|
||||
arrivesIn={time}
|
||||
onClick={() => onChooseFee(feeRate, value, time)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Text fontSize="14px" color="#74777D" textAlign="center">
|
||||
Fees are deducted from your balance,
|
||||
<br /> it won't affect your sending amount.
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { 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 { useBitcoinFeeRate } 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';
|
||||
|
||||
interface UseBitcoinSetFeesArgs {
|
||||
recipient: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function useBitcoinSetFees({ recipient, amount }: UseBitcoinSetFeesArgs) {
|
||||
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
|
||||
const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress);
|
||||
|
||||
const btcMarketData = useCryptoCurrencyMarketData('BTC');
|
||||
const { data: feeRate } = useBitcoinFeeRate();
|
||||
|
||||
const feesList = useMemo(() => {
|
||||
function getFiatFeeValue(fee: number) {
|
||||
return `~ ${i18nFormatCurrency(
|
||||
baseCurrencyAmountInQuote(createMoney(Math.ceil(fee), 'BTC'), btcMarketData)
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (!feeRate || !utxos) return [];
|
||||
|
||||
const satAmount = btcToSat(amount).toNumber();
|
||||
const { fee: highFeeValue } = determineUtxosForSpend({
|
||||
utxos,
|
||||
recipient,
|
||||
amount: satAmount,
|
||||
feeRate: feeRate.fastestFee,
|
||||
});
|
||||
|
||||
const { fee: standartFeeValue } = determineUtxosForSpend({
|
||||
utxos,
|
||||
recipient,
|
||||
amount: satAmount,
|
||||
feeRate: feeRate.halfHourFee,
|
||||
});
|
||||
|
||||
const { fee: lowFeeValue } = determineUtxosForSpend({
|
||||
utxos,
|
||||
recipient,
|
||||
amount: satAmount,
|
||||
feeRate: feeRate.economyFee,
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
label: BtcFeeType.High,
|
||||
value: highFeeValue,
|
||||
btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')),
|
||||
time: btcTxTimeMap.fastestFee,
|
||||
fiatValue: getFiatFeeValue(highFeeValue),
|
||||
feeRate: feeRate.fastestFee,
|
||||
},
|
||||
|
||||
{
|
||||
label: BtcFeeType.Standard,
|
||||
value: standartFeeValue,
|
||||
btcValue: formatMoneyPadded(createMoney(standartFeeValue, 'BTC')),
|
||||
time: btcTxTimeMap.halfHourFee,
|
||||
fiatValue: getFiatFeeValue(standartFeeValue),
|
||||
feeRate: feeRate.halfHourFee,
|
||||
},
|
||||
{
|
||||
label: BtcFeeType.Low,
|
||||
value: lowFeeValue,
|
||||
btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')),
|
||||
time: btcTxTimeMap.economyFee,
|
||||
fiatValue: getFiatFeeValue(lowFeeValue),
|
||||
feeRate: feeRate.economyFee,
|
||||
},
|
||||
];
|
||||
}, [feeRate, btcMarketData, utxos, recipient, amount]);
|
||||
|
||||
return {
|
||||
feesList,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { BitcoinSendFormValues } from '@shared/models/form.model';
|
||||
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 { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
|
||||
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
|
||||
import {
|
||||
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
|
||||
@@ -21,10 +20,9 @@ export function useGenerateSignedBitcoinTx() {
|
||||
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
|
||||
const signTx = useSignBitcoinNativeSegwitTx();
|
||||
const networkMode = useBitcoinLibNetworkConfig();
|
||||
const { data: feeRate } = useBitcoinFeeRate();
|
||||
|
||||
return useCallback(
|
||||
(values: BitcoinSendFormValues) => {
|
||||
(values: BitcoinSendFormValues, feeRate: number) => {
|
||||
if (!utxos) return;
|
||||
if (!feeRate) return;
|
||||
|
||||
@@ -35,7 +33,7 @@ export function useGenerateSignedBitcoinTx() {
|
||||
utxos,
|
||||
recipient: values.recipient,
|
||||
amount: btcToSat(values.amount).toNumber(),
|
||||
feeRate: feeRate.fastestFee,
|
||||
feeRate,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -77,13 +75,6 @@ export function useGenerateSignedBitcoinTx() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[
|
||||
currentAccountBtcAddress,
|
||||
currentAddressIndexKeychain?.publicKey,
|
||||
feeRate,
|
||||
networkMode,
|
||||
signTx,
|
||||
utxos,
|
||||
]
|
||||
[currentAccountBtcAddress, currentAddressIndexKeychain?.publicKey, networkMode, signTx, utxos]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transa
|
||||
|
||||
import { useStacksTransactionSummary } from './use-stacks-transaction-summary';
|
||||
|
||||
export function useStacksBroadcastTransaction(unsignedTx: string, token: CryptoCurrencies) {
|
||||
export function useStacksBroadcastTransaction(
|
||||
unsignedTx: string,
|
||||
token: CryptoCurrencies,
|
||||
decimals?: number
|
||||
) {
|
||||
const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
|
||||
const [isBroadcasting, setIsBroadcasting] = useState(false);
|
||||
const { formSentSummaryTxState } = useStacksTransactionSummary(token);
|
||||
@@ -32,7 +36,7 @@ export function useStacksBroadcastTransaction(unsignedTx: string, token: CryptoC
|
||||
':txId',
|
||||
`${txId}`
|
||||
),
|
||||
formSentSummaryTxState(txId, signedTx)
|
||||
formSentSummaryTxState(txId, signedTx, decimals)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,5 +89,6 @@ export function useStacksBroadcastTransaction(unsignedTx: string, token: CryptoC
|
||||
isBroadcasting,
|
||||
token,
|
||||
formSentSummaryTxState,
|
||||
decimals,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useStacksTransactionSummary(token: CryptoCurrencies) {
|
||||
const { isTestnet } = useCurrentNetworkState();
|
||||
const { data: blockTime } = useStacksBlockTime();
|
||||
|
||||
function formSentSummaryTxState(txId: string, signedTx: StacksTransaction) {
|
||||
function formSentSummaryTxState(txId: string, signedTx: StacksTransaction, decimals?: number) {
|
||||
return {
|
||||
state: {
|
||||
hasHeaderTitle: true,
|
||||
@@ -34,7 +34,7 @@ export function useStacksTransactionSummary(token: CryptoCurrencies) {
|
||||
txid: txId || '',
|
||||
},
|
||||
txId,
|
||||
...formReviewTxSummary(signedTx, token),
|
||||
...formReviewTxSummary(signedTx, token, decimals),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ import { FormAddressDisplayer } from '@app/components/address-displayer/form-add
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardAssetValue,
|
||||
InfoCardFooter,
|
||||
InfoCardRow,
|
||||
InfoCardSeparator,
|
||||
} from '@app/components/info-card/info-card';
|
||||
import { ModalHeader } from '@app/components/modal-header';
|
||||
import { PrimaryButton } from '@app/components/primary-button';
|
||||
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/address.hooks';
|
||||
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
|
||||
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';
|
||||
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
@@ -31,12 +31,19 @@ import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
|
||||
|
||||
const symbol = 'BTC';
|
||||
|
||||
export function BtcSendFormConfirmation() {
|
||||
function useBtcSendFormConfirmationState() {
|
||||
const location = useLocation();
|
||||
return {
|
||||
tx: get(location.state, 'tx') as string,
|
||||
fee: get(location.state, 'fee') as string,
|
||||
arrivesIn: get(location.state, 'time') as string,
|
||||
recipient: get(location.state, 'recipient') as string,
|
||||
};
|
||||
}
|
||||
|
||||
export function BtcSendFormConfirmation() {
|
||||
const navigate = useNavigate();
|
||||
const tx = get(location.state, 'tx');
|
||||
const recipient = get(location.state, 'recipient');
|
||||
const fee = get(location.state, 'fee');
|
||||
const { tx, recipient, fee, arrivesIn } = useBtcSendFormConfirmationState();
|
||||
|
||||
const { refetch } = useCurrentNativeSegwitUtxos();
|
||||
const analytics = useAnalytics();
|
||||
@@ -52,8 +59,6 @@ export function BtcSendFormConfirmation() {
|
||||
baseCurrencyAmountInQuote(createMoneyFromDecimal(Number(transferAmount), symbol), btcMarketData)
|
||||
);
|
||||
const txFiatValueSymbol = btcMarketData.price.symbol;
|
||||
const { data: feeRate } = useBitcoinFeeRate();
|
||||
const arrivesIn = feeRate ? `~${feeRate.fastestFee} min` : '~10 – 20 min';
|
||||
|
||||
const feeInBtc = satToBtc(fee);
|
||||
const totalSpend = formatMoney(
|
||||
@@ -107,16 +112,19 @@ export function BtcSendFormConfirmation() {
|
||||
useRouteHeader(<ModalHeader hideActions defaultClose defaultGoBack title="Review" />);
|
||||
|
||||
return (
|
||||
<InfoCard padding="extra-loose" data-testid={SendCryptoAssetSelectors.ConfirmationDetails}>
|
||||
<InfoCard data-testid={SendCryptoAssetSelectors.ConfirmationDetails}>
|
||||
<InfoCardAssetValue
|
||||
value={Number(transferAmount)}
|
||||
fiatValue={txFiatValue}
|
||||
fiatSymbol={txFiatValueSymbol}
|
||||
symbol={symbol}
|
||||
data-testid={SendCryptoAssetSelectors.ConfirmationDetailsAssetValue}
|
||||
mt="loose"
|
||||
mb="extra-loose"
|
||||
px="loose"
|
||||
/>
|
||||
|
||||
<Stack width="100%" mb="36px">
|
||||
<Stack width="100%" px="extra-loose" pb="extra-loose">
|
||||
<InfoCardRow
|
||||
title="To"
|
||||
value={<FormAddressDisplayer address={recipient} />}
|
||||
@@ -133,9 +141,11 @@ export function BtcSendFormConfirmation() {
|
||||
<InfoCardRow title="Estimated confirmation time" value={arrivesIn} />
|
||||
</Stack>
|
||||
|
||||
<PrimaryButton isLoading={isBroadcasting} width="100%" onClick={initiateTransaction}>
|
||||
Confirm and send transaction
|
||||
</PrimaryButton>
|
||||
<InfoCardFooter>
|
||||
<PrimaryButton isLoading={isBroadcasting} width="100%" onClick={initiateTransaction}>
|
||||
Confirm and send transaction
|
||||
</PrimaryButton>
|
||||
</InfoCardFooter>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Box } from '@stacks/ui';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
import { HIGH_FEE_WARNING_LEARN_MORE_URL_BTC } from '@shared/constants';
|
||||
@@ -35,7 +36,7 @@ export function BtcSendForm() {
|
||||
currentNetwork,
|
||||
formRef,
|
||||
onFormStateChange,
|
||||
previewTransaction,
|
||||
chooseTransactionFee,
|
||||
validationSchema,
|
||||
} = useBtcSendForm();
|
||||
|
||||
@@ -46,7 +47,7 @@ export function BtcSendForm() {
|
||||
...routeState,
|
||||
recipientBnsName: '',
|
||||
})}
|
||||
onSubmit={previewTransaction}
|
||||
onSubmit={chooseTransactionFee}
|
||||
validationSchema={validationSchema}
|
||||
innerRef={formRef}
|
||||
{...defaultSendFormFormikProps}
|
||||
@@ -78,6 +79,11 @@ export function BtcSendForm() {
|
||||
<FormFooter balance={btcBalance.balance} />
|
||||
<HighFeeDrawer learnMoreUrl={HIGH_FEE_WARNING_LEARN_MORE_URL_BTC} />
|
||||
<Outlet />
|
||||
|
||||
{/* This is for testing purposes only, to make sure the form is ready to be submitted */}
|
||||
{calcMaxSpend(props.values.recipient).spendableBitcoin.toNumber() > 0 ? (
|
||||
<Box data-testid={SendCryptoAssetSelectors.SendPageReady}></Box>
|
||||
) : null}
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { logger } from '@shared/logger';
|
||||
import { BitcoinSendFormValues } from '@shared/models/form.model';
|
||||
|
||||
import { useRouteHeader } from '@app/common/hooks/use-route-header';
|
||||
import { useWalletType } from '@app/common/use-wallet-type';
|
||||
import { ModalHeader } from '@app/components/modal-header';
|
||||
|
||||
import { BitcoinSetFee } from '../../family/bitcoin/components/bitcoin-set-fee';
|
||||
import { useGenerateSignedBitcoinTx } from '../../family/bitcoin/hooks/use-generate-bitcoin-tx';
|
||||
import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
|
||||
|
||||
function useBtcSetFeeState() {
|
||||
const location = useLocation();
|
||||
return {
|
||||
txValues: get(location.state, 'values') as BitcoinSendFormValues,
|
||||
};
|
||||
}
|
||||
|
||||
export function BtcSetFee() {
|
||||
const { txValues } = useBtcSetFeeState();
|
||||
const { whenWallet } = useWalletType();
|
||||
const sendFormNavigate = useSendFormNavigate();
|
||||
const generateTx = useGenerateSignedBitcoinTx();
|
||||
|
||||
async function previewTransaction(feeRate: number, feeValue: number, time: string) {
|
||||
const resp = generateTx(txValues, feeRate);
|
||||
|
||||
if (!resp) return logger.error('Attempted to generate raw tx, but no tx exists');
|
||||
|
||||
const { hex } = resp;
|
||||
|
||||
whenWallet({
|
||||
software: () =>
|
||||
sendFormNavigate.toConfirmAndSignBtcTransaction({
|
||||
tx: hex,
|
||||
recipient: txValues.recipient,
|
||||
fee: feeValue,
|
||||
time,
|
||||
}),
|
||||
ledger: () => {},
|
||||
})();
|
||||
}
|
||||
|
||||
useRouteHeader(<ModalHeader hideActions defaultGoBack title="Choose fee" />);
|
||||
|
||||
return (
|
||||
<BitcoinSetFee
|
||||
onChooseFee={previewTransaction}
|
||||
recipient={txValues.recipient}
|
||||
amount={Number(txValues.amount)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,10 @@ import { useRef } from 'react';
|
||||
import { FormikHelpers, FormikProps } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { HIGH_FEE_AMOUNT_BTC } from '@shared/constants';
|
||||
import { logger } from '@shared/logger';
|
||||
import { BitcoinSendFormValues } from '@shared/models/form.model';
|
||||
import { isEmpty } from '@shared/utils';
|
||||
|
||||
import { formatPrecisionError } from '@app/common/error-formatters';
|
||||
import { useDrawers } from '@app/common/hooks/use-drawers';
|
||||
import { useWalletType } from '@app/common/use-wallet-type';
|
||||
import {
|
||||
btcAddressNetworkValidator,
|
||||
@@ -27,7 +24,6 @@ import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/acc
|
||||
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
|
||||
|
||||
import { useCalculateMaxBitcoinSpend } from '../../family/bitcoin/hooks/use-calculate-max-spend';
|
||||
import { useGenerateSignedBitcoinTx } from '../../family/bitcoin/hooks/use-generate-bitcoin-tx';
|
||||
import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
|
||||
|
||||
export function useBtcSendForm() {
|
||||
@@ -35,10 +31,8 @@ export function useBtcSendForm() {
|
||||
const currentNetwork = useCurrentNetwork();
|
||||
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
|
||||
const btcCryptoCurrencyAssetBalance = useNativeSegwitBalance(currentAccountBtcAddress);
|
||||
const { isShowingHighFeeConfirmation, setIsShowingHighFeeConfirmation } = useDrawers();
|
||||
const { whenWallet } = useWalletType();
|
||||
const sendFormNavigate = useSendFormNavigate();
|
||||
const generateTx = useGenerateSignedBitcoinTx();
|
||||
const calcMaxSpend = useCalculateMaxBitcoinSpend();
|
||||
const { onFormStateChange } = useUpdatePersistedSendFormValues();
|
||||
|
||||
@@ -70,29 +64,16 @@ export function useBtcSendForm() {
|
||||
.concat(notCurrentAddressValidator(currentAccountBtcAddress || '')),
|
||||
}),
|
||||
|
||||
async previewTransaction(
|
||||
async chooseTransactionFee(
|
||||
values: BitcoinSendFormValues,
|
||||
formikHelpers: FormikHelpers<BitcoinSendFormValues>
|
||||
) {
|
||||
logger.debug('btc form values', values);
|
||||
// Validate and check high fee warning first
|
||||
const formErrors = await formikHelpers.validateForm();
|
||||
if (
|
||||
!isShowingHighFeeConfirmation &&
|
||||
isEmpty(formErrors) &&
|
||||
values.fee > HIGH_FEE_AMOUNT_BTC
|
||||
) {
|
||||
return setIsShowingHighFeeConfirmation(true);
|
||||
}
|
||||
|
||||
const resp = generateTx(values);
|
||||
|
||||
if (!resp) return logger.error('Attempted to generate raw tx, but no tx exists');
|
||||
|
||||
const { hex, fee } = resp;
|
||||
await formikHelpers.validateForm();
|
||||
|
||||
whenWallet({
|
||||
software: () => sendFormNavigate.toConfirmAndSignBtcTransaction(hex, values.recipient, fee),
|
||||
software: () => sendFormNavigate.toChooseTransactionFee(values),
|
||||
ledger: () => {},
|
||||
})();
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Stack } from '@stacks/ui';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
|
||||
import { whenPageMode } from '@app/common/utils';
|
||||
import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer';
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardAssetValue,
|
||||
InfoCardFooter,
|
||||
InfoCardRow,
|
||||
InfoCardSeparator,
|
||||
} from '@app/components/info-card/info-card';
|
||||
@@ -43,16 +45,28 @@ export function SendFormConfirmation({
|
||||
symbol,
|
||||
}: SendFormConfirmationProps) {
|
||||
return (
|
||||
<InfoCard padding="extra-loose" data-testid={SendCryptoAssetSelectors.ConfirmationDetails}>
|
||||
<InfoCard
|
||||
data-testid={SendCryptoAssetSelectors.ConfirmationDetails}
|
||||
pb={whenPageMode({
|
||||
full: '0px',
|
||||
popup: '80px',
|
||||
})}
|
||||
>
|
||||
<InfoCardAssetValue
|
||||
value={Number(txValue)}
|
||||
fiatValue={txFiatValue}
|
||||
fiatSymbol={txFiatValueSymbol}
|
||||
symbol={symbol}
|
||||
data-testid={SendCryptoAssetSelectors.ConfirmationDetailsAssetValue}
|
||||
my="loose"
|
||||
px="loose"
|
||||
/>
|
||||
|
||||
<Stack width="100%">
|
||||
<InfoLabel px="loose" mb="loose" title="Sending to an exchange?">
|
||||
{`Make sure you include the memo so the exchange can credit the ${symbol} to your account`}
|
||||
</InfoLabel>
|
||||
|
||||
<Stack width="100%" px="extra-loose" pb="extra-loose">
|
||||
<InfoCardRow
|
||||
title="To"
|
||||
value={<FormAddressDisplayer address={recipient} />}
|
||||
@@ -75,18 +89,16 @@ export function SendFormConfirmation({
|
||||
<InfoCardRow title="Estimated confirmation time" value={arrivesIn} />
|
||||
</Stack>
|
||||
|
||||
<InfoLabel my="extra-loose" title="Sending to an exchange?">
|
||||
{`Make sure you include the memo so the exchange can credit the ${symbol} to your account`}
|
||||
</InfoLabel>
|
||||
|
||||
<PrimaryButton
|
||||
data-testid={SendCryptoAssetSelectors.ConfirmSendTxBtn}
|
||||
width="100%"
|
||||
isLoading={isLoading}
|
||||
onClick={onBroadcastTransaction}
|
||||
>
|
||||
Confirm and send transaction
|
||||
</PrimaryButton>
|
||||
<InfoCardFooter>
|
||||
<PrimaryButton
|
||||
data-testid={SendCryptoAssetSelectors.ConfirmSendTxBtn}
|
||||
width="100%"
|
||||
isLoading={isLoading}
|
||||
onClick={onBroadcastTransaction}
|
||||
>
|
||||
Confirm and send transaction
|
||||
</PrimaryButton>
|
||||
</InfoCardFooter>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function StacksSendFormConfirmation() {
|
||||
const { symbol = 'STX' } = useParams();
|
||||
|
||||
const { stacksDeserializedTransaction, stacksBroadcastTransaction, isBroadcasting } =
|
||||
useStacksBroadcastTransaction(tx, symbol.toUpperCase() as CryptoCurrencies);
|
||||
useStacksBroadcastTransaction(tx, symbol.toUpperCase() as CryptoCurrencies, decimals);
|
||||
|
||||
const { formReviewTxSummary } = useStacksTransactionSummary(
|
||||
symbol.toUpperCase() as CryptoCurrencies
|
||||
@@ -32,6 +32,7 @@ export function StacksSendFormConfirmation() {
|
||||
const {
|
||||
txValue,
|
||||
txFiatValue,
|
||||
txFiatValueSymbol,
|
||||
recipient,
|
||||
fee,
|
||||
totalSpend,
|
||||
@@ -47,6 +48,7 @@ export function StacksSendFormConfirmation() {
|
||||
<SendFormConfirmation
|
||||
txValue={txValue}
|
||||
txFiatValue={txFiatValue}
|
||||
txFiatValueSymbol={txFiatValueSymbol}
|
||||
recipient={recipient}
|
||||
fee={fee}
|
||||
totalSpend={totalSpend}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { bytesToHex } from '@stacks/common';
|
||||
import { StacksTransaction } from '@stacks/transactions';
|
||||
|
||||
import { BitcoinSendFormValues } from '@shared/models/form.model';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
|
||||
interface ConfirmationRouteState {
|
||||
decimals?: number;
|
||||
token?: string;
|
||||
@@ -17,6 +20,13 @@ interface ConfirmationRouteStacksSip10Args {
|
||||
tx: StacksTransaction;
|
||||
}
|
||||
|
||||
interface ConfirmationRouteBtcArgs {
|
||||
tx: string;
|
||||
recipient: string;
|
||||
fee: number;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export function useSendFormNavigate() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -25,13 +35,21 @@ export function useSendFormNavigate() {
|
||||
backToSendForm(state: any) {
|
||||
return navigate('../', { relative: 'path', replace: true, state });
|
||||
},
|
||||
toConfirmAndSignBtcTransaction(tx: string, recipient: string, fee: number) {
|
||||
return navigate('confirm', {
|
||||
replace: true,
|
||||
toChooseTransactionFee(values: BitcoinSendFormValues) {
|
||||
return navigate('set-fee', {
|
||||
state: {
|
||||
values,
|
||||
hasHeaderTitle: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
toConfirmAndSignBtcTransaction({ tx, recipient, fee, time }: ConfirmationRouteBtcArgs) {
|
||||
return navigate(RouteUrls.SendBtcConfirmation, {
|
||||
state: {
|
||||
tx,
|
||||
recipient,
|
||||
fee,
|
||||
time,
|
||||
hasHeaderTitle: true,
|
||||
} as ConfirmationRouteState,
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StxSentSummary } from '../sent-summary/stx-sent-summary';
|
||||
import { RecipientAccountsDrawer } from './components/recipient-accounts-drawer/recipient-accounts-drawer';
|
||||
import { BtcSendForm } from './form/btc/btc-send-form';
|
||||
import { BtcSendFormConfirmation } from './form/btc/btc-send-form-confirmation';
|
||||
import { BtcSetFee } from './form/btc/btc-set-fee';
|
||||
import { Sip10TokenSendForm } from './form/stacks-sip10/sip10-token-send-form';
|
||||
import { StacksSendFormConfirmation } from './form/stacks/stacks-send-form-confirmation';
|
||||
import { StxSendForm } from './form/stx/stx-send-form';
|
||||
@@ -53,7 +54,7 @@ export const sendCryptoAssetFormRoutes = (
|
||||
<Route path="/send/btc/confirm" element={<BtcSendFormConfirmation />} />
|
||||
<Route path="/send/btc/disabled" element={<SendBtcDisabled />} />
|
||||
<Route path="/send/btc/error" element={<BroadcastError />} />
|
||||
|
||||
<Route path={RouteUrls.SendBtcSetFee} element={<BtcSetFee />}></Route>
|
||||
<Route path={RouteUrls.SendCryptoAssetForm.replace(':symbol', 'stx')} element={<StxSendForm />}>
|
||||
{broadcastErrorDrawerRoute}
|
||||
{editNonceDrawerRoute}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
InfoCard,
|
||||
InfoCardAssetValue,
|
||||
InfoCardBtn,
|
||||
InfoCardFooter,
|
||||
InfoCardRow,
|
||||
InfoCardSeparator,
|
||||
} from '@app/components/info-card/info-card';
|
||||
@@ -50,16 +51,18 @@ export function BtcSentSummary() {
|
||||
useRouteHeader(<ModalHeader hideActions defaultClose title="Sent" />);
|
||||
|
||||
return (
|
||||
<InfoCard pt="extra-loose" pb="base-loose" px="extra-loose">
|
||||
<InfoCard>
|
||||
<InfoCardAssetValue
|
||||
value={txValue}
|
||||
fiatValue={txFiatValue}
|
||||
fiatSymbol={txFiatValueSymbol}
|
||||
symbol={symbol}
|
||||
icon={FiCheck}
|
||||
my="loose"
|
||||
px="loose"
|
||||
/>
|
||||
|
||||
<Stack width="100%" mb="44px">
|
||||
<Stack width="100%" px="extra-loose" pb="extra-loose">
|
||||
<InfoCardRow title="To" value={<FormAddressDisplayer address={recipient} />} />
|
||||
<InfoCardSeparator />
|
||||
<InfoCardRow title="Total spend" value={totalSpend} />
|
||||
@@ -69,10 +72,12 @@ export function BtcSentSummary() {
|
||||
<InfoCardRow title="Arrives in" value={arrivesIn} />
|
||||
</Stack>
|
||||
|
||||
<Stack spacing="base" isInline width="100%">
|
||||
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
|
||||
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
|
||||
</Stack>
|
||||
<InfoCardFooter>
|
||||
<Stack spacing="base" isInline width="100%">
|
||||
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
|
||||
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
|
||||
</Stack>
|
||||
</InfoCardFooter>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
InfoCard,
|
||||
InfoCardAssetValue,
|
||||
InfoCardBtn,
|
||||
InfoCardFooter,
|
||||
InfoCardRow,
|
||||
InfoCardSeparator,
|
||||
} from '@app/components/info-card/info-card';
|
||||
@@ -50,16 +51,18 @@ export function StxSentSummary() {
|
||||
useRouteHeader(<ModalHeader hideActions defaultClose title="Sent" />);
|
||||
|
||||
return (
|
||||
<InfoCard pt="extra-loose" pb="extra-loose" px="extra-loose">
|
||||
<InfoCard>
|
||||
<InfoCardAssetValue
|
||||
value={txValue}
|
||||
fiatValue={txFiatValue}
|
||||
fiatSymbol={txFiatValueSymbol}
|
||||
symbol={symbol}
|
||||
icon={FiCheck}
|
||||
></InfoCardAssetValue>
|
||||
my="loose"
|
||||
px="loose"
|
||||
/>
|
||||
|
||||
<Stack width="100%" mb="44px">
|
||||
<Stack width="100%" px="extra-loose" pb="extra-loose">
|
||||
<InfoCardRow title="To" value={<FormAddressDisplayer address={recipient} />} />
|
||||
<InfoCardSeparator />
|
||||
<InfoCardRow title="Total spend" value={totalSpend} />
|
||||
@@ -69,10 +72,12 @@ export function StxSentSummary() {
|
||||
<InfoCardRow title="Estimated confirmation time" value={arrivesIn} />
|
||||
</Stack>
|
||||
|
||||
<Stack spacing="base" isInline width="100%">
|
||||
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
|
||||
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
|
||||
</Stack>
|
||||
<InfoCardFooter>
|
||||
<Stack spacing="base" isInline width="100%">
|
||||
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
|
||||
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
|
||||
</Stack>
|
||||
</InfoCardFooter>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,20 @@ 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;
|
||||
|
||||
@@ -38,6 +38,7 @@ import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error'
|
||||
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 { SendInscriptionSetFee } from '@app/pages/send/ordinal-inscription/send-inscription-set-fee';
|
||||
import { SendInscriptionSummary } from '@app/pages/send/ordinal-inscription/sent-inscription-summary';
|
||||
import { sendCryptoAssetFormRoutes } from '@app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes';
|
||||
import { SignOutConfirmDrawer } from '@app/pages/sign-out-confirm/sign-out-confirm';
|
||||
@@ -102,6 +103,10 @@ function AppRoutesAfterUserHasConsented() {
|
||||
path={RouteUrls.SendOrdinalInscriptionReview}
|
||||
element={<SendInscriptionReview />}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionSetFee}
|
||||
element={<SendInscriptionSetFee />}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.SendOrdinalInscriptionSent}
|
||||
element={<SendInscriptionSummary />}
|
||||
|
||||
@@ -8,7 +8,6 @@ export const gaiaUrl = 'https://hub.blockstack.org';
|
||||
export const POPUP_CENTER_WIDTH = 442;
|
||||
export const POPUP_CENTER_HEIGHT = 646;
|
||||
|
||||
export const HIGH_FEE_AMOUNT_BTC = 0.001;
|
||||
export const HIGH_FEE_AMOUNT_STX = 5;
|
||||
export const HIGH_FEE_WARNING_LEARN_MORE_URL_BTC = 'https://bitcoinfees.earn.com/';
|
||||
export const HIGH_FEE_WARNING_LEARN_MORE_URL_STX = 'https://hiro.so/questions/fee-estimates';
|
||||
|
||||
@@ -69,6 +69,7 @@ export enum RouteUrls {
|
||||
SendStacksSip10Confirmation = '/send/:symbol/confirm',
|
||||
SentBtcTxSummary = '/sent/btc/:txId',
|
||||
SentStxTxSummary = '/sent/stx/:txId',
|
||||
SendBtcSetFee = '/send/btc/set-fee',
|
||||
|
||||
// Send ordinal inscriptions
|
||||
SendOrdinalInscription = '/send/ordinal-inscription',
|
||||
@@ -77,6 +78,7 @@ export enum RouteUrls {
|
||||
SendOrdinalInscriptionSummary = '/send/ordinal-inscription/',
|
||||
SendOrdinalInscriptionSent = '/send/ordinal-inscription/sent',
|
||||
SendOrdinalInscriptionError = '/send/ordinal-inscription/error',
|
||||
SendOrdinalInscriptionSetFee = '/send/ordinal-inscription/set-fee',
|
||||
|
||||
// Request routes
|
||||
RpcGetAddresses = '/get-addresses',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
|
||||
import { createTestSelector } from '@tests/utils';
|
||||
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
|
||||
@@ -21,6 +23,7 @@ export class SendPage {
|
||||
readonly sendMaxButton: Locator;
|
||||
readonly feesRow: Locator;
|
||||
readonly memoRow: Locator;
|
||||
readonly feesCard: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
@@ -52,6 +55,7 @@ export class SendPage {
|
||||
this.memoRow = page.getByTestId(SendCryptoAssetSelectors.ConfirmationDetailsMemo);
|
||||
|
||||
this.sendMaxButton = page.getByTestId(SendCryptoAssetSelectors.SendMaxBtn);
|
||||
this.feesCard = page.getByTestId(SharedComponentsSelectors.FeeCard);
|
||||
}
|
||||
|
||||
async selectBtcAndGoToSendForm() {
|
||||
@@ -71,4 +75,10 @@ export class SendPage {
|
||||
await this.page.waitForURL('**' + `${RouteUrls.SendCryptoAsset}/stx`);
|
||||
await this.page.getByTestId(SendCryptoAssetSelectors.SendForm).waitFor();
|
||||
}
|
||||
|
||||
async waitForSendPageReady() {
|
||||
await this.page.waitForSelector(createTestSelector(SendCryptoAssetSelectors.SendPageReady), {
|
||||
state: 'attached',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ export enum SendCryptoAssetSelectors {
|
||||
RecipientBnsAddressCopyToClipboard = 'recipient-bns-address-copy-to-clipboard',
|
||||
SendForm = 'send-form',
|
||||
SendMaxBtn = 'send-max-btn',
|
||||
|
||||
SendPageReady = 'send-page-ready',
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ export enum SharedComponentsSelectors {
|
||||
FeeToBePaidLabel = 'fee-to-be-paid-label',
|
||||
LowFeeEstimateItem = 'low-fee',
|
||||
MiddleFeeEstimateItem = 'standard-fee',
|
||||
FeeCard = 'fee-card',
|
||||
FeeCardFeeValue = 'fee-card-fee-value',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS } from '@tests/mocks/constants';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
|
||||
import { getDisplayerAddress } from '@tests/utils';
|
||||
|
||||
import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import { BtcFeeType } from '@app/query/bitcoin/bitcoin-client';
|
||||
|
||||
import { test } from '../../fixtures/fixtures';
|
||||
|
||||
@@ -13,27 +14,17 @@ test.describe('send btc', () => {
|
||||
await homePage.enableTestMode();
|
||||
await homePage.sendButton.click();
|
||||
await sendPage.selectBtcAndGoToSendForm();
|
||||
await sendPage.waitForSendPageReady();
|
||||
});
|
||||
|
||||
test.describe('btc send form', () => {
|
||||
test('that it shows preview of tx details to be confirmed', async ({ sendPage }) => {
|
||||
await sendPage.amountInput.fill('0.0001');
|
||||
await sendPage.recipientInput.fill(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS);
|
||||
await sendPage.previewSendTxButton.click();
|
||||
const details = await sendPage.confirmationDetails.allInnerTexts();
|
||||
test.expect(details).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that it shows preview of tx details after validation error is resolved', async ({
|
||||
sendPage,
|
||||
}) => {
|
||||
await sendPage.amountInput.fill('0.00006');
|
||||
await sendPage.amountInput.blur();
|
||||
const errorMsg = await sendPage.amountInputErrorLabel.innerText();
|
||||
test.expect(errorMsg).toEqual(FormErrorMessages.InsufficientFunds);
|
||||
await sendPage.recipientInput.fill(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS);
|
||||
|
||||
await sendPage.amountInput.fill('0.0001');
|
||||
await sendPage.previewSendTxButton.click();
|
||||
await sendPage.feesCard.filter({ hasText: BtcFeeType.High }).click();
|
||||
|
||||
const details = await sendPage.confirmationDetails.allInnerTexts();
|
||||
test.expect(details).toBeTruthy();
|
||||
});
|
||||
@@ -48,6 +39,7 @@ test.describe('send btc', () => {
|
||||
await sendPage.page.waitForTimeout(1000);
|
||||
|
||||
await sendPage.previewSendTxButton.click();
|
||||
await sendPage.feesCard.filter({ hasText: BtcFeeType.High }).click();
|
||||
|
||||
const displayerAddress = await getDisplayerAddress(sendPage.confirmationDetailsRecipient);
|
||||
test.expect(displayerAddress).toEqual(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS);
|
||||
@@ -57,5 +49,27 @@ test.describe('send btc', () => {
|
||||
.innerText();
|
||||
test.expect(confirmationAssetValue).toEqual(`${amount} ${amountSymbol}`);
|
||||
});
|
||||
|
||||
test('that fee value on preview match chosen one', async ({ sendPage }) => {
|
||||
await sendPage.amountInput.fill('0.00006');
|
||||
await sendPage.recipientInput.fill(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS);
|
||||
|
||||
await sendPage.previewSendTxButton.click();
|
||||
|
||||
const feeType = BtcFeeType.Standard;
|
||||
const fee = await sendPage.feesCard
|
||||
.filter({ hasText: feeType })
|
||||
.getByTestId(SharedComponentsSelectors.FeeCardFeeValue)
|
||||
.innerText();
|
||||
|
||||
await sendPage.feesCard.filter({ hasText: feeType }).click();
|
||||
|
||||
const confirmationFee = await sendPage.confirmationDetails
|
||||
.getByTestId(SendCryptoAssetSelectors.ConfirmationDetailsFee)
|
||||
.getByTestId(SharedComponentsSelectors.InfoCardRowValue)
|
||||
.innerText();
|
||||
|
||||
test.expect(confirmationFee).toEqual(fee);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user