From b215b7d63f99a73a3dabf81e5e5e462bfeb13e97 Mon Sep 17 00:00:00 2001 From: Norman Wilde Date: Wed, 19 Apr 2023 19:05:50 +0200 Subject: [PATCH] feat: send STX from Ledger account --- .../index.tsx | 206 +++++++++++++++++ src/app/routes/index.tsx | 16 +- src/app/screens/coinDashboard/coinHeader.tsx | 6 + src/app/screens/home/index.tsx | 8 +- .../index.tsx | 100 +++++++-- .../ledger/importLedgerAccount/index.tsx | 6 +- .../screens/ledger/ledgerSendStx/index.tsx | 171 ++++++++++++++ .../reviewLedgerBtcTransaction/index.tsx | 2 +- .../reviewLedgerStxTransaction/index.tsx | 211 ++++++++++++++++++ 9 files changed, 695 insertions(+), 31 deletions(-) create mode 100644 src/app/components/ledger/reviewLedgerStxTransactionComponent/index.tsx rename src/app/screens/ledger/{confirmLedgerBtcTransaction => confirmLedgerTransaction}/index.tsx (79%) create mode 100644 src/app/screens/ledger/ledgerSendStx/index.tsx create mode 100644 src/app/screens/ledger/reviewLedgerStxTransaction/index.tsx diff --git a/src/app/components/ledger/reviewLedgerStxTransactionComponent/index.tsx b/src/app/components/ledger/reviewLedgerStxTransactionComponent/index.tsx new file mode 100644 index 000000000..dcc359b22 --- /dev/null +++ b/src/app/components/ledger/reviewLedgerStxTransactionComponent/index.tsx @@ -0,0 +1,206 @@ +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { ReactNode, useEffect, useState } from 'react'; +import BigNumber from 'bignumber.js'; +import ActionButton from '@components/button'; +import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; +import TransactionSettingAlert from '@components/transactionSetting'; +import { microstacksToStx, stxToMicrostacks } from '@secretkeylabs/xverse-core/currency'; +import { StacksTransaction } from '@secretkeylabs/xverse-core/types'; +import TransferFeeView from '@components/transferFeeView'; +import { + setFee, + setNonce, + getNonce, + signMultiStxTransactions, + signTransaction, +} from '@secretkeylabs/xverse-core'; +import useWalletSelector from '@hooks/useWalletSelector'; +import useNetworkSelector from '@hooks/useNetwork'; + +const Container = styled.div` + display: flex; + flex-direction: column; + flex: 1; + margin-top: 22px; + margin-left: 16px; + margin-right: 16px; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; + +const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + marginBottom: props.theme.spacing(20), + marginTop: props.theme.spacing(12), + marginLeft: props.theme.spacing(8), + marginRight: props.theme.spacing(8), +})); + +const TransparentButtonContainer = styled.div((props) => ({ + marginLeft: props.theme.spacing(2), + marginRight: props.theme.spacing(2), + width: '100%', +})); + +const Button = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + borderRadius: props.theme.radius(1), + backgroundColor: 'transparent', + width: '100%', + marginTop: props.theme.spacing(10), +})); + +const ButtonText = styled.div((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['0'], + textAlign: 'center', +})); + +const TransferFeeContainer = styled.div((props) => ({ + marginBottom: props.theme.spacing(12), +})); + +const ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +const SponsoredInfoText = styled.h1((props) => ({ + ...props.theme.body_m, + color: props.theme.colors.white['400'], +})); + +interface Props { + initialStxTransactions: StacksTransaction[]; + loading: boolean; + onCancelClick: () => void; + onConfirmClick: (transactions: StacksTransaction[]) => void; + children: ReactNode; + isSponsored?: boolean; +} + +function ReviewLedgerStxTransactionComponent({ + initialStxTransactions, + loading, + isSponsored, + children, + onConfirmClick, + onCancelClick, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const selectedNetwork = useNetworkSelector(); + const { selectedAccount, seedPhrase } = useWalletSelector(); + const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); + const [buttonLoading, setButtonLoading] = useState(loading); + + useEffect(() => { + setButtonLoading(loading); + }, [loading]); + + const getFee = () => + isSponsored + ? new BigNumber(0) + : new BigNumber( + initialStxTransactions + .map((tx) => tx?.auth?.spendingCondition?.fee ?? BigInt(0)) + .reduce((prev, curr) => prev + curr, BigInt(0)) + .toString(10) + ); + + const getTxNonce = (): string => { + const nonce = getNonce(initialStxTransactions[0]); + return nonce.toString(); + }; + + const onAdvancedSettingClick = () => { + setOpenTransactionSettingModal(true); + }; + + const closeTransactionSettingAlert = () => { + setOpenTransactionSettingModal(false); + }; + + const onConfirmButtonClick = async () => { + let signedTxs: StacksTransaction[] = []; + if (initialStxTransactions.length === 1) { + const signedContractCall = await signTransaction( + initialStxTransactions[0], + seedPhrase, + selectedAccount?.id ?? 0, + selectedNetwork + ); + signedTxs.push(signedContractCall); + } else if (initialStxTransactions.length === 2) { + signedTxs = await signMultiStxTransactions( + initialStxTransactions, + selectedAccount?.id ?? 0, + selectedNetwork, + seedPhrase + ); + } + onConfirmClick(signedTxs); + }; + + const applyTxSettings = (settingFee: string, nonce?: string) => { + const fee = stxToMicrostacks(new BigNumber(settingFee)); + setFee(initialStxTransactions[0], BigInt(fee.toString())); + if (nonce && nonce !== '') { + setNonce(initialStxTransactions[0], BigInt(nonce)); + } + setOpenTransactionSettingModal(false); + }; + + return ( + <> + + {children} + + + + + {!isSponsored && ( + + )} + {isSponsored && {t('SPONSORED_TX_INFO')}} + + + + + + + + + + ); +} + +export default ReviewLedgerStxTransactionComponent; diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 6a082fac3..b78401cae 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -49,8 +49,10 @@ import RestoreBtc from '@screens/restoreFunds/restoreBtc'; import RestoreOrdinals from '@screens/restoreFunds/restoreOrdinals'; import ImportLedger from '@screens/ledger/importLedgerAccount'; import ReviewLedgerBtcTransaction from '@screens/ledger/reviewLedgerBtcTransaction'; -import ConfirmLedgerBtcTransaction from '@screens/ledger/confirmLedgerBtcTransaction'; +import ConfirmLedgerTransaction from '@screens/ledger/confirmLedgerTransaction'; import LedgerSendBtcScreen from '@screens/ledger/ledgerSendBtc'; +import LedgerSendStxScreen from '@screens/ledger/ledgerSendStx'; +import ReviewLedgerStxTransaction from '@screens/ledger/reviewLedgerStxTransaction'; const router = createHashRouter([ { @@ -110,6 +112,10 @@ const router = createHashRouter([ path: 'send-btc-ledger', element: , }, + { + path: 'send-stx-ledger', + element: , + }, { path: 'confirm-stx-tx', element: , @@ -127,8 +133,12 @@ const router = createHashRouter([ element: , }, { - path: 'confirm-ledger-btc-tx', - element: , + path: 'review-ledger-stx-tx', + element: , + }, + { + path: 'confirm-ledger-tx', + element: , }, { path: 'backup', diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 1cfd85bb0..21b880a9c 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -332,6 +332,12 @@ export default function CoinHeader(props: CoinBalanceProps) { }); return; } + if (coin === 'STX') { + await chrome.tabs.create({ + url: chrome.runtime.getURL('options.html#/send-stx-ledger'), + }); + return; + } } if (coin === 'STX' || coin === 'BTC') { navigate(`/send-${coin}`); diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index e8a49a832..5bccb953c 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -174,7 +174,13 @@ function Home() { navigate('/manage-tokens'); }; - const onStxSendClick = () => { + const onStxSendClick = async () => { + if (isLedgerAccount) { + await chrome.tabs.create({ + url: chrome.runtime.getURL('options.html#/send-stx-ledger'), + }); + return; + } navigate('/send-stx'); }; diff --git a/src/app/screens/ledger/confirmLedgerBtcTransaction/index.tsx b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx similarity index 79% rename from src/app/screens/ledger/confirmLedgerBtcTransaction/index.tsx rename to src/app/screens/ledger/confirmLedgerTransaction/index.tsx index 008fd7e1f..d0e3f9023 100644 --- a/src/app/screens/ledger/confirmLedgerBtcTransaction/index.tsx +++ b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx @@ -7,7 +7,9 @@ import Transport from '@ledgerhq/hw-transport-webusb'; import ActionButton from '@components/button'; import { broadcastRawBtcTransaction, + broadcastSignedTransaction, signLedgerNestedSegwitBtcTransaction, + signStxTransaction, } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; import { LedgerTransactionType } from '../reviewLedgerBtcTransaction'; @@ -15,11 +17,13 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { Recipient } from '@secretkeylabs/xverse-core/transactions/btc'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import { ledgerDelay } from '@common/utils/ledger'; -import { getBtcTxStatusUrl } from '@utils/helper'; +import { getBtcTxStatusUrl, getStxTxStatusUrl } from '@utils/helper'; import FullScreenHeader from '@components/ledger/fullScreenHeader'; import LedgerConnectDefaultSVG from '@assets/img/ledger/ledger_connect_default.svg'; import CheckCircleSVG from '@assets/img/ledger/check_circle.svg'; +import { StacksTransaction } from '@stacks/transactions'; +import useNetworkSelector from '@hooks/useNetwork'; const Container = styled.div` display: flex; @@ -70,7 +74,7 @@ const TxConfirmedDescription = styled.p((props) => ({ color: props.theme.colors.white[200], })); -function ConfirmLedgerBtcTransaction(): JSX.Element { +function ConfirmLedgerTransaction(): JSX.Element { const [currentStepIndex, setCurrentStepIndex] = useState(0); const [txId, setTxId] = useState(undefined); @@ -82,12 +86,20 @@ function ConfirmLedgerBtcTransaction(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'LEDGER_CONFIRM_TRANSACTION_SCREEN' }); const location = useLocation(); + const selectedNetwork = useNetworkSelector(); + const { network, selectedAccount } = useWalletSelector(); const { recipient, type, - }: { amount: BigNumber; recipient: Recipient; type: LedgerTransactionType } = location.state; + unsignedTx, + }: { + amount: BigNumber; + recipient: Recipient; + type: LedgerTransactionType; + unsignedTx: StacksTransaction; + } = location.state; const transition = useTransition(currentStepIndex, { from: { @@ -100,6 +112,45 @@ function ConfirmLedgerBtcTransaction(): JSX.Element { }, }); + const signAndBroadcastBtcTx = async (transport: Transport, accountId: number) => { + try { + const result = await signLedgerNestedSegwitBtcTransaction( + transport, + network.type, + accountId, + recipient + ); + setIsTxApproved(true); + await ledgerDelay(1500); + const transactionId = await broadcastRawBtcTransaction(result, network.type); + setTxId(transactionId.tx.hash); + setCurrentStepIndex(2); + } catch (err) { + console.error(err); + setIsTxRejected(true); + setIsButtonDisabled(false); + } finally { + transport.close(); + } + }; + + const signAndBroadcastStxTx = async (transport: Transport, accountId: number) => { + try { + const result = await signStxTransaction(transport, unsignedTx, accountId); + setIsTxApproved(true); + await ledgerDelay(1500); + const transactionHash = await broadcastSignedTransaction(result, selectedNetwork); + setTxId(transactionHash); + setCurrentStepIndex(2); + } catch (err) { + console.error(err); + setIsTxRejected(true); + setIsButtonDisabled(false); + } finally { + transport.close(); + } + }; + const handleConnectAndConfirm = async () => { if (!selectedAccount) { console.error('No account selected'); @@ -120,24 +171,16 @@ function ConfirmLedgerBtcTransaction(): JSX.Element { await ledgerDelay(1500); setCurrentStepIndex(1); - try { - const result = await signLedgerNestedSegwitBtcTransaction( - transport, - network.type, - selectedAccount.id, - recipient - ); - setIsTxApproved(true); - await ledgerDelay(1500); - const transactionId = await broadcastRawBtcTransaction(result, network.type); - setTxId(transactionId.tx.hash); - setCurrentStepIndex(2); - } catch (err) { - console.error(err); - setIsTxRejected(true); - setIsButtonDisabled(false); - } finally { - transport.close(); + switch (type) { + case 'BTC': + await signAndBroadcastBtcTx(transport as Transport, selectedAccount.id); + break; + case 'STX': + await signAndBroadcastStxTx(transport as Transport, selectedAccount.id); + break; + case 'ORDINALS': + default: + break; } }; @@ -158,7 +201,18 @@ function ConfirmLedgerBtcTransaction(): JSX.Element { console.error('No txId found'); return; } - window.open(getBtcTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer'); + + switch (type) { + case 'BTC': + window.open(getBtcTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer'); + break; + case 'STX': + window.open(getStxTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer'); + break; + case 'ORDINALS': + default: + break; + } }; return ( @@ -230,4 +284,4 @@ function ConfirmLedgerBtcTransaction(): JSX.Element { ); } -export default ConfirmLedgerBtcTransaction; +export default ConfirmLedgerTransaction; diff --git a/src/app/screens/ledger/importLedgerAccount/index.tsx b/src/app/screens/ledger/importLedgerAccount/index.tsx index bdb0b7984..48c2db9e6 100644 --- a/src/app/screens/ledger/importLedgerAccount/index.tsx +++ b/src/app/screens/ledger/importLedgerAccount/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { animated, useTransition } from '@react-spring/web'; @@ -268,13 +268,13 @@ function ImportLedger(): JSX.Element { (account) => account.stxAddress !== '' ).length; setAddressIndex(newAddressIndex); - const { address, publicKey } = await importStacksAccountFromLedger( + const { address, publicKey, testnetAddress } = await importStacksAccountFromLedger( transport, network, 0, newAddressIndex ); - setStacksCredentials({ address, publicKey }); + setStacksCredentials({ address: testnetAddress, publicKey }); await transport.close(); }; diff --git a/src/app/screens/ledger/ledgerSendStx/index.tsx b/src/app/screens/ledger/ledgerSendStx/index.tsx new file mode 100644 index 000000000..f4deb937b --- /dev/null +++ b/src/app/screens/ledger/ledgerSendStx/index.tsx @@ -0,0 +1,171 @@ +import { useMutation } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { generateUnsignedStxTokenTransferTransaction } from '@secretkeylabs/xverse-core/transactions'; +import { microstacksToStx, stxToMicrostacks } from '@secretkeylabs/xverse-core/currency'; +import { StacksTransaction } from '@secretkeylabs/xverse-core/types'; +import { validateStxAddress } from '@secretkeylabs/xverse-core/wallet'; +import SendForm from '@components/sendForm'; +import TopRow from '@components/topRow'; +import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; +import { StoreState } from '@stores/index'; +import { replaceCommaByDot } from '@utils/helper'; +import BottomBar from '@components/tabBar'; +import useNetworkSelector from '@hooks/useNetwork'; + +function LedgerSendStxScreen() { + const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); + const navigate = useNavigate(); + const { + stxAddress, + stxAvailableBalance, + stxPublicKey, + feeMultipliers, + network, + isLedgerAccount, + } = useSelector((state: StoreState) => state.walletState); + const [amountError, setAmountError] = useState(''); + const [addressError, setAddressError] = useState(''); + const [memoError, setMemoError] = useState(''); + const selectedNetwork = useNetworkSelector(); + const { data: stxPendingTxData } = useStxPendingTxData(); + const location = useLocation(); + let recipientAddress: string | undefined; + let amountToSend: string | undefined; + let stxMemo: string | undefined; + + useEffect(() => { + if (!isLedgerAccount) { + //TODO - Handle window close or navigate to home + console.warn('Not Ledger Account'); + } + }, [isLedgerAccount]); + + if (location.state) { + recipientAddress = location.state.recipientAddress; + amountToSend = location.state.amountToSend; + stxMemo = location.state.stxMemo; + } + const { isLoading, data, mutate } = useMutation< + StacksTransaction, + Error, + { associatedAddress: string; amount: string; memo?: string } + >(async ({ associatedAddress, amount, memo }) => { + const unsignedSendStxTx: StacksTransaction = await generateUnsignedStxTokenTransferTransaction( + associatedAddress, + stxToMicrostacks(new BigNumber(amount)).toString(), + memo!, + stxPendingTxData?.pendingTransactions ?? [], + stxPublicKey, + selectedNetwork + ); + // increasing the fees with multiplication factor + const fee: bigint = + BigInt(unsignedSendStxTx.auth.spendingCondition.fee.toString()) ?? BigInt(0); + if (feeMultipliers?.stxSendTxMultiplier) { + unsignedSendStxTx.setFee(fee * BigInt(feeMultipliers.stxSendTxMultiplier)); + } + return unsignedSendStxTx; + }); + + useEffect(() => { + // FIXME route doesn't get updated in url bar + if (data) { + navigate('/review-ledger-stx-tx', { + state: { + unsignedTx: data, + }, + }); + } + }, [data]); + + const handleBackButtonClick = () => { + navigate(-1); + }; + + function validateFields(associatedAddress: string, amount: string, memo: string): boolean { + if (!associatedAddress) { + setAddressError(t('ERRORS.ADDRESS_REQUIRED')); + return false; + } + + if (!amount) { + setAmountError(t('ERRORS.AMOUNT_REQUIRED')); + return false; + } + if (!validateStxAddress({ stxAddress: associatedAddress, network: network.type })) { + setAddressError(t('ERRORS.ADDRESS_INVALID')); + return false; + } + + if (associatedAddress === stxAddress) { + setAddressError(t('ERRORS.SEND_TO_SELF')); + return false; + } + + let parsedAmount = new BigNumber(0); + try { + if (!Number.isNaN(Number(amount))) { + parsedAmount = new BigNumber(amount); + } else { + setAmountError(t('ERRORS.INVALID_AMOUNT')); + return false; + } + } catch (e) { + setAmountError(t('ERRORS.INVALID_AMOUNT')); + return false; + } + + if (stxToMicrostacks(parsedAmount).lt(1)) { + setAmountError(t('ERRORS.MINIMUM_AMOUNT')); + return false; + } + + if (stxToMicrostacks(parsedAmount).gt(stxAvailableBalance)) { + setAmountError(t('ERRORS.INSUFFICIENT_BALANCE')); + return false; + } + + if (memo) { + if (Buffer.from(memo).byteLength >= 34) { + setMemoError(t('ERRORS.MEMO_LENGTH')); + return false; + } + } + return true; + } + + const onPressSendSTX = async (associatedAddress: string, amount: string, memo?: string) => { + const modifyAmount = replaceCommaByDot(amount); + const addMemo = memo ?? ''; + if (validateFields(associatedAddress.trim(), modifyAmount, memo!)) { + setAddressError(''); + setMemoError(''); + setAmountError(''); + mutate({ amount, associatedAddress, memo: addMemo }); + } + }; + + return ( + <> + {}} showBackButton={false} /> + + + ); +} + +export default LedgerSendStxScreen; diff --git a/src/app/screens/ledger/reviewLedgerBtcTransaction/index.tsx b/src/app/screens/ledger/reviewLedgerBtcTransaction/index.tsx index 9648691a3..170d21c8e 100644 --- a/src/app/screens/ledger/reviewLedgerBtcTransaction/index.tsx +++ b/src/app/screens/ledger/reviewLedgerBtcTransaction/index.tsx @@ -25,7 +25,7 @@ function ReviewLedgerBtcTransaction() { const handleOnConfirmClick = async () => { const txType: LedgerTransactionType = 'BTC'; - navigate('/confirm-ledger-btc-tx', { state: { amount, recipient, type: txType } }); + navigate('/confirm-ledger-tx', { state: { amount, recipient, type: txType } }); }; const goBackToScreen = () => { diff --git a/src/app/screens/ledger/reviewLedgerStxTransaction/index.tsx b/src/app/screens/ledger/reviewLedgerStxTransaction/index.tsx new file mode 100644 index 000000000..b81940f26 --- /dev/null +++ b/src/app/screens/ledger/reviewLedgerStxTransaction/index.tsx @@ -0,0 +1,211 @@ +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { useMutation } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { getStxFiatEquivalent, microstacksToStx } from '@secretkeylabs/xverse-core/currency'; +import { StacksTransaction, TokenTransferPayload } from '@secretkeylabs/xverse-core/types'; +import { + addressToString, + broadcastSignedTransaction, +} from '@secretkeylabs/xverse-core/transactions'; +import Seperator from '@components/seperator'; +import BottomBar from '@components/tabBar'; +import RecipientAddressView from '@components/recipinetAddressView'; +import TransferAmountView from '@components/transferAmountView'; +import TopRow from '@components/topRow'; +import AccountHeaderComponent from '@components/accountHeader'; +import finalizeTxSignature from '@components/transactionsRequests/utils'; +import InfoContainer from '@components/infoContainer'; +import useOnOriginTabClose from '@hooks/useOnTabClosed'; +import useNetworkSelector from '@hooks/useNetwork'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useWalletSelector from '@hooks/useWalletSelector'; +import ConfirmStxTransationComponent from '@components/confirmStxTransactionComponent'; +import ReviewLedgerStxTransactionComponent from '@components/ledger/reviewLedgerStxTransactionComponent'; +import { LedgerTransactionType } from '../reviewLedgerBtcTransaction'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.spacing(12), + marginBottom: props.theme.spacing(4), +})); + +const AlertContainer = styled.div((props) => ({ + marginTop: props.theme.spacing(12), +})); + +const TitleText = styled.h1((props) => ({ + ...props.theme.headline_category_s, + color: props.theme.colors.white['400'], + textTransform: 'uppercase', +})); + +const ValueText = styled.h1((props) => ({ + ...props.theme.body_m, + marginTop: props.theme.spacing(2), + wordBreak: 'break-all', +})); + +function ReviewLedgerStxTransaction() { + const { t } = useTranslation('translation'); + const [fee, setStateFee] = useState(new BigNumber(0)); + const [amount, setAmount] = useState(new BigNumber(0)); + const [fiatAmount, setFiatAmount] = useState(new BigNumber(0)); + const [total, setTotal] = useState(new BigNumber(0)); + const [fiatTotal, setFiatTotal] = useState(new BigNumber(0)); + const [hasTabClosed, setHasTabClosed] = useState(false); + const [recipient, setRecipient] = useState(''); + const [txRaw, setTxRaw] = useState(''); + const [memo, setMemo] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + const selectedNetwork = useNetworkSelector(); + const { unsignedTx, sponsored, isBrowserTx, tabId, requestToken } = location.state; + useOnOriginTabClose(Number(tabId), () => { + setHasTabClosed(true); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + const { stxBtcRate, btcFiatRate, network } = useWalletSelector(); + const { refetch } = useStxWalletData(); + const { + isLoading, + error: txError, + data: stxTxBroadcastData, + mutate, + } = useMutation(async ({ signedTx }) => + broadcastSignedTransaction(signedTx, selectedNetwork) + ); + + useEffect(() => { + if (stxTxBroadcastData) { + if (isBrowserTx) { + finalizeTxSignature({ + requestPayload: requestToken, + tabId: Number(tabId), + data: { txId: stxTxBroadcastData, txRaw }, + }); + } + navigate('/tx-status', { + state: { + txid: stxTxBroadcastData, + currency: 'STX', + error: '', + browserTx: isBrowserTx, + }, + }); + setTimeout(() => { + refetch(); + }, 1000); + } + }, [stxTxBroadcastData]); + + useEffect(() => { + if (txError) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'STX', + error: txError.toString(), + browserTx: isBrowserTx, + }, + }); + } + }, [txError]); + + function updateUI() { + const txPayload = unsignedTx.payload as TokenTransferPayload; + + if (txPayload.recipient.address) { + setRecipient(addressToString(txPayload.recipient.address)); + } + + const txAmount = new BigNumber(txPayload.amount.toString(10)); + const txFee = new BigNumber(unsignedTx.auth.spendingCondition.fee.toString()); + const txTotal = amount.plus(fee); + const txFiatAmount = getStxFiatEquivalent(amount, stxBtcRate, btcFiatRate); + const txFiatTotal = getStxFiatEquivalent(amount, stxBtcRate, btcFiatRate); + const { memo: txMemo } = txPayload; + + setAmount(txAmount); + setStateFee(txFee); + setFiatAmount(txFiatAmount); + setTotal(txTotal); + setFiatTotal(txFiatTotal); + setMemo(txMemo.content); + } + + useEffect(() => { + if (recipient === '' || !fee || !amount || !fiatAmount || !total || !fiatTotal) { + updateUI(); + } + }); + + const networkInfoSection = ( + + {t('CONFIRM_TRANSACTION.NETWORK')} + {network.type} + + ); + + const memoInfoSection = !!memo && ( + <> + + {t('CONFIRM_TRANSACTION.MEMO')} + {memo} + + + + ); + + const getAmount = () => { + const txPayload = unsignedTx?.payload as TokenTransferPayload; + const amountToTransfer = new BigNumber(txPayload?.amount?.toString(10)); + return microstacksToStx(amountToTransfer); + }; + + const handleOnConfirmClick = () => { + const txType: LedgerTransactionType = 'STX'; + navigate('/confirm-ledger-tx', { state: { unsignedTx, type: txType } }); + }; + + const handleOnCancelClick = () => { + navigate('/send-stx-ledger', { + state: { + recipientAddress: recipient, + amountToSend: getAmount().toString(), + stxMemo: memo, + }, + }); + }; + + return ( + <> + + + + {hasTabClosed && ( + + + + )} + + {networkInfoSection} + + {memoInfoSection} + + + ); +} +export default ReviewLedgerStxTransaction;