diff --git a/package-lock.json b/package-lock.json index 9238349aa..9844dbdec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.14.1", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.14.1", + "version": "0.15.0", "dependencies": { "@ledgerhq/hw-transport-webusb": "^6.27.13", "@react-spring/web": "^9.6.1", @@ -22,7 +22,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "alex-sdk": "^0.1.14", + "alex-sdk": "^0.1.18", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", @@ -4665,9 +4665,9 @@ } }, "node_modules/alex-sdk": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.16.tgz", - "integrity": "sha512-btCCv+L3fCdULhmIKabj+E6Aeusc0/3C745L8YErk+JnYZTIjh5G90ihppziJq/8e9WV4o2slutunp5SUCRbzg==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.18.tgz", + "integrity": "sha512-TEBlO9Xiw/LRBZMM98o6eUqZv6q/qlkDOHQACc0kFGCdqfspcVhE1X83vMVpZYbjKlXnko6mFW6bi+RyMx2L3g==", "dependencies": { "clarity-codegen": "^0.2.0" }, @@ -22454,9 +22454,9 @@ "requires": {} }, "alex-sdk": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.16.tgz", - "integrity": "sha512-btCCv+L3fCdULhmIKabj+E6Aeusc0/3C745L8YErk+JnYZTIjh5G90ihppziJq/8e9WV4o2slutunp5SUCRbzg==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.18.tgz", + "integrity": "sha512-TEBlO9Xiw/LRBZMM98o6eUqZv6q/qlkDOHQACc0kFGCdqfspcVhE1X83vMVpZYbjKlXnko6mFW6bi+RyMx2L3g==", "requires": { "clarity-codegen": "^0.2.0" } diff --git a/package.json b/package.json index 169684b0b..7e9bfe50b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.14.1", + "version": "0.15.0", "private": true, "dependencies": { "@ledgerhq/hw-transport-webusb": "^6.27.13", @@ -18,7 +18,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "alex-sdk": "^0.1.14", + "alex-sdk": "^0.1.18", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", diff --git a/src/app/components/button/index.tsx b/src/app/components/button/index.tsx index f766692ad..d136ee581 100644 --- a/src/app/components/button/index.tsx +++ b/src/app/components/button/index.tsx @@ -18,33 +18,31 @@ const Button = styled.button((props) => ({ width: '100%', height: 44, transition: 'all 0.1s ease', - ...(props.disabled - ? { - cursor: 'not-allowed', - opacity: 0.4, - } - : { - ':hover': { opacity: 0.8 }, - ':active': { opacity: 0.6 }, - }), + ':disabled': { + opacity: 0.4, + cursor: 'not-allowed', + }, + ':hover:enabled': { + opacity: 0.8, + }, + ':active:enabled': { + opacity: 0.6, + }, })); const TransparentButton = styled(Button)((props) => ({ border: `1px solid ${props.theme.colors.background.elevation6}`, backgroundColor: 'transparent', - ...(props.disabled - ? { - cursor: 'not-allowed', - opacity: 0.4, - } - : { - ':hover': { - backgroundColor: props.theme.colors.background.elevation6_800, - }, - ':active': { - backgroundColor: props.theme.colors.background.elevation6_600, - }, - }), + ':disabled': { + cursor: 'not-allowed', + opacity: 0.4, + }, + ':hover:enabled': { + backgroundColor: props.theme.colors.background.elevation6_800, + }, + ':active:enabled': { + backgroundColor: props.theme.colors.background.elevation6_600, + }, })); interface TextProps { diff --git a/src/app/components/transactions/stxTransaction.tsx b/src/app/components/transactions/stxTransaction.tsx index 617ae37ce..9f164b243 100644 --- a/src/app/components/transactions/stxTransaction.tsx +++ b/src/app/components/transactions/stxTransaction.tsx @@ -30,10 +30,11 @@ import StxTransferTransaction from './stxTransferTransaction'; interface TransactionHistoryItemProps { transaction: AddressTransactionWithTransfers | Tx; transactionCoin: CurrencyTypes; + txFilter: string | null; } export default function StxTransactionHistoryItem(props: TransactionHistoryItemProps) { - const { transaction, transactionCoin } = props; + const { transaction, transactionCoin, txFilter } = props; const { selectedAccount } = useWalletSelector(); // const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); if (!isAddressTransactionWithTransfers(transaction)) { @@ -68,7 +69,7 @@ export default function StxTransactionHistoryItem(props: TransactionHistoryItemP } return ( <> - + ({ display: 'flex', @@ -51,11 +53,12 @@ const TransactionValue = styled.p((props) => ({ interface TxTransfersProps { transaction: AddressTransactionWithTransfers; coin: CurrencyTypes; + txFilter: string | null; } export default function TxTransfers(props: TxTransfersProps) { - const { transaction, coin } = props; - const { selectedAccount } = useWalletSelector(); + const { transaction, coin, txFilter } = props; + const { selectedAccount, coinsList } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); function formatAddress(addr: string): string { @@ -73,28 +76,49 @@ export default function TxTransfers(props: TxTransfersProps) { selectedAccount?.stxAddress === transfer.recipient ? t('TRANSACTION_RECEIVED') : t('TRANSACTION_SENT'); - return ( - <> - {transaction.stx_transfers.map((stxTransfer) => ( + + function renderTransaction(transactionList) { + return transactionList.map((transfer) => { + const isFT = coin === 'FT'; + const ft = coinsList?.find((ftCoin) => ftCoin.principal === txFilter!.split('::')[0]); + const isSentTransaction = selectedAccount?.stxAddress !== transfer.recipient; + if (isFT && transfer.asset_identifier !== txFilter) { + return null; + } + + return ( - {renderTransactionIcon(stxTransfer)} + {renderTransactionIcon(transfer)} - {getTokenTransferTitle(stxTransfer)} + {getTokenTransferTitle(transfer)} ( - {`${value} ${coin}`} + + {`${value} ${isFT && ft ? getFtTicker(ft) : coin}`} + )} /> - {formatAddress(stxTransfer.recipient as string)} + {formatAddress(transfer.recipient as string)} - ))} + ); + }); + } + return ( + <> + {coin === 'FT' && transaction.ft_transfers + ? renderTransaction(transaction.ft_transfers) + : renderTransaction(transaction.stx_transfers)} ); } diff --git a/src/app/screens/coinDashboard/transactionsHistoryList.tsx b/src/app/screens/coinDashboard/transactionsHistoryList.tsx index f1954c2c2..3e463f0fb 100644 --- a/src/app/screens/coinDashboard/transactionsHistoryList.tsx +++ b/src/app/screens/coinDashboard/transactionsHistoryList.tsx @@ -200,6 +200,7 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr transaction={transaction} transactionCoin={coin} key={transaction.tx_id} + txFilter={txFilter} /> ); })} diff --git a/src/app/screens/swap/error.ts b/src/app/screens/swap/error.ts deleted file mode 100644 index 5ac40f444..000000000 --- a/src/app/screens/swap/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const swapErrorList: string[] = [ - 'Transaction fee (2605779) exceeds max sponsor fee (500000)', -]; diff --git a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx index 98e5847ca..fdf41f4ad 100644 --- a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx +++ b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx @@ -40,7 +40,7 @@ export function AdvanceSettings({ swap }: Props) { const onApplyClick = useCallback(({ fee, nonce }: { fee: string; nonce?: string }) => { const settingFee = BigInt(stxToMicrostacks(new BigNumber(fee) as any).toString()); - swap.unsignedTx.setFee(settingFee); + swap.onFeeUpdate(settingFee); if (nonce != null) { swap.unsignedTx.setNonce(BigInt(nonce)); } diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index a89861974..8f075c130 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -12,14 +12,15 @@ import { useCallback, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useConfirmSwap } from '@screens/swap/swapConfirmation/useConfirmSwap'; import { AdvanceSettings } from '@screens/swap/swapConfirmation/advanceSettings'; -import { useSponsoredTransaction } from '@hooks/useSponsoredTransaction'; import SponsoredTransactionIcon from '@assets/img/transactions/CircleWavyCheck.svg'; +import InfoContainer from '@components/infoContainer'; +import { SUPPORT_URL_TAB_TARGET, SWAP_SPONSOR_DISABLED_SUPPORT_URL } from '@utils/constants'; const TitleText = styled.div((props) => ({ fontSize: 21, fontWeight: 700, color: props.theme.colors.white['0'], - marginBottom: props.theme.spacing(16), + marginBottom: props.theme.spacing(12), marginTop: props.theme.spacing(12), })); @@ -53,12 +54,15 @@ const Icon = styled.img((props) => ({ height: 24, })); +const StyledInfoContainer = styled.div((props) => ({ + marginBottom: props.theme.spacing(4), +})); + export default function SwapConfirmation() { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); const location = useLocation(); const navigate = useNavigate(); const swap = useConfirmSwap(location.state); - const { isSponsored } = useSponsoredTransaction(swap.isSponsorOptionSelected); const onCancel = useCallback(() => { navigate('/swap'); @@ -72,21 +76,37 @@ export default function SwapConfirmation() { }); }, [swap]); + const handleClickLearnMore = () => { + window.open(SWAP_SPONSOR_DISABLED_SUPPORT_URL, SUPPORT_URL_TAB_TARGET, 'noreferrer noopener'); + }; + return ( <> {t('TOKEN_SWAP')} + {swap.isSponsorDisabled && ( + + + + )} - - {isSponsored ? ( + {!swap.isSponsored && ( + + )} + {swap.isSponsored ? ( {t('THIS_IS_A_SPONSORED_TRANSACTION')} diff --git a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx index d3a139695..a16e33edb 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -1,6 +1,8 @@ +import { Currency, SponsoredTxErrorCode, SponsoredTxError } from 'alex-sdk'; +import { useNavigate } from 'react-router-dom'; import { SwapToken } from '@screens/swap/useSwap'; -import { Currency } from 'alex-sdk'; import useWalletSelector from '@hooks/useWalletSelector'; +import { microstacksToStx } from '@secretkeylabs/xverse-core'; import { broadcastSignedTransaction, signTransaction, @@ -8,12 +10,12 @@ import { } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; import useNetworkSelector from '@hooks/useNetwork'; -import { useNavigate } from 'react-router-dom'; -import useSponsoredTransaction from '@hooks/useSponsoredTransaction'; import { ApiResponseError } from '@secretkeylabs/xverse-core/types'; import { TokenImageProps } from '@components/tokenImage'; -import { XVERSE_SPONSOR_2_URL } from '@utils/constants'; -import { swapErrorList } from '../error'; +import { useAlexSponsoredTransaction } from '../useAlexSponsoredTransaction'; +import { useState } from 'react'; +import BigNumber from 'bignumber.js'; +import { useCurrencyConversion } from '../useCurrencyConversion'; export type SwapConfirmationInput = { from: Currency; @@ -28,30 +30,46 @@ export type SwapConfirmationInput = { routers: { image: TokenImageProps; name: string }[]; unsignedTx: string; // serialized hex StacksTransaction functionName: string; - isSponsorOptionSelected: boolean; + userOverrideSponsorValue: boolean; }; export type SwapConfirmationOutput = Omit & { onConfirm: () => Promise; + onFeeUpdate: (settingFee: bigint) => void; unsignedTx: StacksTransaction; // deserialized StacksTransaction + isSponsored: boolean; + isSponsorDisabled: boolean; }; export function useConfirmSwap(input: SwapConfirmationInput): SwapConfirmationOutput { const { selectedAccount, seedPhrase } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); - const { isSponsored, sponsorTransaction } = useSponsoredTransaction( - input.isSponsorOptionSelected, - XVERSE_SPONSOR_2_URL, + const { isSponsored, sponsorTransaction, isSponsorDisabled } = useAlexSponsoredTransaction( + input.userOverrideSponsorValue, ); + const { currencyToToken } = useCurrencyConversion(); const navigate = useNavigate(); - const unsignedTx = deserializeTransaction(input.unsignedTx); + const [unsignedTx, setUnsignedTx] = useState( + deserializeTransaction(input.unsignedTx), + ); + const [feeAmount, setFeeAmount] = useState(input.lpFeeAmount); + const [feeFiatAmount, setFeeFiatAmount] = useState(input.lpFeeFiatAmount); return { ...input, - lpFeeAmount: isSponsored ? 0 : input.lpFeeAmount, - lpFeeFiatAmount: isSponsored ? 0 : input.lpFeeFiatAmount, + lpFeeAmount: isSponsored ? 0 : feeAmount, + lpFeeFiatAmount: isSponsored ? 0 : feeFiatAmount, unsignedTx, - isSponsorOptionSelected: input.isSponsorOptionSelected, + userOverrideSponsorValue: input.userOverrideSponsorValue, + onFeeUpdate: (settingFee: bigint) => { + const fee = microstacksToStx(new BigNumber(settingFee)); + unsignedTx.setFee(settingFee); + setUnsignedTx(unsignedTx); + setFeeAmount(Number(fee)); + setFeeFiatAmount(currencyToToken(input.from, Number(fee))?.fiatAmount); + }, + isSponsored, + isSponsorDisabled, onConfirm: async () => { const signed = await signTransaction( unsignedTx, @@ -78,7 +96,20 @@ export function useConfirmSwap(input: SwapConfirmationInput): SwapConfirmationOu }); } } catch (e) { - if (e instanceof Error) { + if (e instanceof SponsoredTxError) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'STX', + error: + e.code !== SponsoredTxErrorCode.unknown_error ? e.message : 'Unknown sponsor error', + sponsored: isSponsored, + browserTx: true, + isSponsorServiceError: true, + isSwapTransaction: true, + }, + }); + } else if (e instanceof Error) { navigate('/tx-status', { state: { txid: '', @@ -86,7 +117,7 @@ export function useConfirmSwap(input: SwapConfirmationInput): SwapConfirmationOu error: e instanceof ApiResponseError ? e.data.message : e.message, sponsored: isSponsored, browserTx: true, - isSponsorServiceError: swapErrorList.includes(e.message), + isSwapTransaction: true, }, }); } diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index 73580bb92..f1491e3d4 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -7,6 +7,7 @@ import ChevronIcon from '@assets/img/swap/chevron.svg'; import BottomModal from '@components/bottomModal'; import { SlippageModalContent } from '@screens/swap/slippageModal'; import Switch from 'react-switch'; +import { SUPPORT_URL_TAB_TARGET, SWAP_SPONSOR_DISABLED_SUPPORT_URL } from '@utils/constants'; const CustomSwitch = styled(Switch)` .react-switch-handle { @@ -55,36 +56,49 @@ const DD = styled.dd((props) => ({ textAlign: 'right', })); -const ToggleContainer = styled.div({ - flex: '30%', - display: 'flex', - justifyContent: 'flex-end', +const ChevronImage = styled.img<{ rotated: boolean }>(({ rotated }) => ({ + transform: `rotate(${rotated ? 180 : 0}deg)`, + transition: 'transform 0.1s ease-in-out', +})); + +const SlippageImg = styled.img(() => ({ + width: 16, + height: 16, +})); + +const CannotBeSponsored = styled.p((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['200'], +})); + +const SponsorTransactionSwitchLabel = styled(DT)<{ disabled: boolean }>((props) => ({ + color: props.disabled ? props.theme.colors.white['400'] : props.theme.colors.white['200'], +})); + +const ToggleContainer = styled(DD)({ + flex: 0, }); +const LearnMoreAnchor = styled.a((props) => ({ + ...props.theme.body_bold_m, + color: props.theme.colors.white['0'], + marginTop: props.theme.spacing(2), + display: 'block', +})); + export function SwapInfoBlock({ swap }: { swap: UseSwap }) { const [expandDetail, setExpandDetail] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); const [showSlippageModal, setShowSlippageModal] = useState(false); const theme = useTheme(); - const toggleFunction = () => { - swap.setIsSponsorOptionSelected(!swap.isSponsored); - }; - return ( <>
setExpandDetail(!expandDetail)}> {t('DETAILS')} - {t('DETAILS')} +
{swap.swapInfo?.exchangeRate ?? '--'}
@@ -94,12 +108,9 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) {
{swap.minReceived ?? '--'}
{t('SLIPPAGE')}
- setShowSlippageModal(true)} - > + setShowSlippageModal(true)}> {swap.slippage * 100}% - {t('SLIPPAGE')} +
{t('LP_FEE')}
@@ -108,19 +119,39 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) {
{swap.swapInfo?.route ?? '--'}
{swap.isServiceRunning && ( <> -
{t('SPONSOR_TRANSACTION')}
- - - + <> + + {t('SPONSOR_TRANSACTION')} + + + + + + {swap.isSponsorDisabled && ( +
+ + {t('SWAP_TRANSACTION_CANNOT_BE_SPONSORED')} + + + {t('LEARN_MORE')} + {' →'} + +
+ )} )} @@ -143,3 +174,4 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { ); } +export default SwapInfoBlock; diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 982632b58..d5dc08922 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -83,6 +83,7 @@ export const AmountInput = styled(NumberInput)<{ error?: boolean }>((props) => ( textAlign: 'right', backgroundColor: 'transparent', border: 'transparent', + width: '100%', })); const CoinText = styled.div((props) => ({ diff --git a/src/app/screens/swap/useAlexSponsoredTransaction.ts b/src/app/screens/swap/useAlexSponsoredTransaction.ts new file mode 100644 index 000000000..0bdd9a52e --- /dev/null +++ b/src/app/screens/swap/useAlexSponsoredTransaction.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { StacksTransaction } from '@secretkeylabs/xverse-core'; +import { AlexSDK } from 'alex-sdk'; +import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; + +const useAlexSponsorSwapEnabledQuery = (alexSDK: AlexSDK) => + useQuery({ + queryKey: ['alexSponsoredSwapEnabled'], + queryFn: alexSDK.isSponsoredSwapEnabled, + }); + +export const useAlexSponsoredTransaction = (userOverrideSponsorValue: boolean) => { + const alexSDK = useRef(new AlexSDK()).current; + const [isServiceRunning, setIsServiceRunning] = useState(false); + const { error, data: isEnabled, isLoading } = useAlexSponsorSwapEnabledQuery(alexSDK); + + useEffect(() => { + if (!isLoading && !error) { + setIsServiceRunning(!!isEnabled); + } + }, [isEnabled, error, isLoading]); + + const sponsorTransaction = async (signed: StacksTransaction) => + alexSDK.broadcastSponsoredTx(signed.serialize().toString('hex')); + + const { data: stxPendingTxData } = useStxPendingTxData(); + const hasPendingTransactions = stxPendingTxData?.pendingTransactions?.length > 0; + + return { + isSponsored: userOverrideSponsorValue && isServiceRunning && !hasPendingTransactions, + isServiceRunning, + sponsorTransaction, + isSponsorDisabled: hasPendingTransactions, + }; +}; + +export default useAlexSponsoredTransaction; diff --git a/src/app/screens/swap/useCurrencyConversion.tsx b/src/app/screens/swap/useCurrencyConversion.tsx new file mode 100644 index 000000000..52fa2bbeb --- /dev/null +++ b/src/app/screens/swap/useCurrencyConversion.tsx @@ -0,0 +1,75 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { FungibleToken, getFiatEquivalent, microstacksToStx } from '@secretkeylabs/xverse-core'; +import { LoaderSize } from '@utils/constants'; +import { ftDecimals } from '@utils/helper'; +import { AlexSDK, Currency } from 'alex-sdk'; +import BigNumber from 'bignumber.js'; +import { SwapToken } from './useSwap'; + +export function useCurrencyConversion() { + const alexSDK = new AlexSDK(); + const { + coins: supportedCoins = [], + coinsList: visibleCoins = [], + stxAvailableBalance, + stxBtcRate, + btcFiatRate, + } = useWalletSelector(); + + const acceptableCoinList = supportedCoins + .filter((sc) => alexSDK.getCurrencyFrom(sc.contract) != null) + // TODO tim: remove this once alexsdk fix issue here + // https://github.com/alexgo-io/alex-sdk/issues/2 + .filter((sc) => sc.contract !== 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.brc20-db20') + .map((sc) => { + const ft = (visibleCoins || []).find((vc) => vc.principal === sc.contract); + return { + ...ft, + ...sc, + principal: sc.contract, + assetName: '', + total_sent: ft?.total_sent ?? '0', + total_received: ft?.total_received ?? '0', + balance: ft?.balance ?? '0', + }; + }); + + function currencyToToken(currency?: Currency, amount?: number): SwapToken | undefined { + if (currency == null) { + return undefined; + } + if (currency === Currency.STX) { + return { + balance: Number(microstacksToStx(BigNumber(stxAvailableBalance) as any)), + image: { token: 'STX', size: 28, loaderSize: LoaderSize.SMALL }, + name: 'STX', + amount, + fiatAmount: + amount != null + ? Number(getFiatEquivalent(amount, 'STX', stxBtcRate as any, btcFiatRate as any)) + : undefined, + }; + } + const token = acceptableCoinList.find( + (c) => alexSDK.getCurrencyFrom(c.principal) === currency, + )!; + if (token == null) { + return undefined; + } + return { + amount, + image: { fungibleToken: token, size: 28, loaderSize: LoaderSize.SMALL }, + name: (token.ticker ?? token.name).toUpperCase(), + balance: Number(ftDecimals(token.balance, token.decimals ?? 0)), + fiatAmount: + amount != null + ? Number(getFiatEquivalent(amount, 'FT', stxBtcRate as any, btcFiatRate as any, token)) + : undefined, + }; + } + + return { + acceptableCoinList, + currencyToToken, + }; +} diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index af2ba84d4..e1cf1a048 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -9,18 +9,15 @@ import { import { useTranslation } from 'react-i18next'; import useWalletSelector from '@hooks/useWalletSelector'; import { TokenImageProps } from '@components/tokenImage'; -import { LoaderSize, XVERSE_SPONSOR_2_URL } from '@utils/constants'; import { AlexSDK, Currency } from 'alex-sdk'; -import { ftDecimals } from '@utils/helper'; import BigNumber from 'bignumber.js'; -import { getFiatEquivalent } from '@secretkeylabs/xverse-core/transactions'; import { useNavigate } from 'react-router-dom'; import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; import { AnchorMode, makeUnsignedContractCall, PostConditionMode } from '@stacks/transactions'; -import useSponsoredTransaction from '@hooks/useSponsoredTransaction'; import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; +import { useAlexSponsoredTransaction } from './useAlexSponsoredTransaction'; +import { useCurrencyConversion } from './useCurrencyConversion'; -// const noop = () => null; const isNotNull = (t: T | null | undefined): t is T => t != null; export type STXOrFungibleToken = 'STX' | FungibleToken; @@ -56,7 +53,8 @@ export type UseSwap = { onSwap?: () => Promise; isSponsored: boolean; isServiceRunning: boolean; - setIsSponsorOptionSelected: (isSponsored: boolean) => void; + handleChangeUserOverrideSponsorValue: (checked: boolean) => void; + isSponsorDisabled: boolean; }; export type SelectedCurrencyState = { @@ -120,40 +118,12 @@ export function useSwap(): UseSwap { const navigate = useNavigate(); const alexSDK = useState(() => new AlexSDK())[0]; const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { - coins: supportedCoins = [], - coinsList: visibleCoins = [], - stxAvailableBalance, - stxBtcRate, - btcFiatRate, - // fiatCurrency, - stxAddress, - stxPublicKey, - } = useWalletSelector(); - const [isSponsorOptionSelected, setIsSponsorOptionSelected] = useState(true); - const { isSponsored, isServiceRunning } = useSponsoredTransaction( - isSponsorOptionSelected, - XVERSE_SPONSOR_2_URL, - ); + const { stxAddress, stxPublicKey } = useWalletSelector(); + const { acceptableCoinList, currencyToToken } = useCurrencyConversion(); + const [userOverrideSponsorValue, setUserOverrideSponsorValue] = useState(true); const { data: stxPendingTxData } = useStxPendingTxData(); - - const acceptableCoinList = supportedCoins - .filter((sc) => alexSDK.getCurrencyFrom(sc.contract) != null) - // TODO tim: remove this once alexsdk fix issue here - // https://github.com/alexgo-io/alex-sdk/issues/2 - .filter((sc) => sc.contract !== 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.brc20-db20') - .map((sc) => { - const ft = (visibleCoins || []).find((vc) => vc.principal === sc.contract); - return { - ...ft, - ...sc, - principal: sc.contract, - assetName: '', - total_sent: ft?.total_sent ?? '0', - total_received: ft?.total_received ?? '0', - balance: ft?.balance ?? '0', - }; - }); + const { isSponsored, isServiceRunning, isSponsorDisabled } = + useAlexSponsoredTransaction(userOverrideSponsorValue); const [inputAmount, setInputAmount] = useState(''); const [slippage, setSlippage] = useState(0.04); @@ -166,40 +136,6 @@ export function useSwap(): UseSwap { const fromAmount = Number.isNaN(Number(inputAmount)) ? undefined : Number(inputAmount); - function currencyToToken(currency?: Currency, amount?: number): SwapToken | undefined { - if (currency == null) { - return undefined; - } - if (currency === Currency.STX) { - return { - balance: Number(microstacksToStx(BigNumber(stxAvailableBalance) as any)), - image: { token: 'STX', size: 28, loaderSize: LoaderSize.SMALL }, - name: 'STX', - amount, - fiatAmount: - amount != null - ? Number(getFiatEquivalent(amount, 'STX', stxBtcRate as any, btcFiatRate as any)) - : undefined, - }; - } - const token = acceptableCoinList.find( - (c) => alexSDK.getCurrencyFrom(c.principal) === currency, - )!; - if (token == null) { - return undefined; - } - return { - amount, - image: { fungibleToken: token, size: 28, loaderSize: LoaderSize.SMALL }, - name: (token.ticker ?? token.name).toUpperCase(), - balance: Number(ftDecimals(token.balance, token.decimals ?? 0)), - fiatAmount: - amount != null - ? Number(getFiatEquivalent(amount, 'FT', stxBtcRate as any, btcFiatRate as any, token)) - : undefined, - }; - } - function getCurrencyName(currency: Currency) { if (currency === Currency.STX) { return 'STX'; @@ -372,7 +308,9 @@ export function useSwap(): UseSwap { unsignedTx, getNewNonce(stxPendingTxData?.pendingTransactions || [], getNonce(unsignedTx)), ); - + const fee = microstacksToStx( + new BigNumber(unsignedTx.auth.spendingCondition.fee.toString()), + ).toNumber(); const state: SwapConfirmationInput = { from: selectedCurrency.from!, to: selectedCurrency.to!, @@ -381,13 +319,12 @@ export function useSwap(): UseSwap { address: stxAddress, fromAmount: fromAmount!, minToAmount: toAmount! * (1 - slippage), - lpFeeAmount: info.feeRate * fromAmount!, - lpFeeFiatAmount: currencyToToken(selectedCurrency.from!, info.feeRate * fromAmount!) - ?.fiatAmount, + lpFeeAmount: fee, + lpFeeFiatAmount: currencyToToken(selectedCurrency.from!, fee)?.fiatAmount, routers: info.route.map(currencyToToken).filter(isNotNull), unsignedTx: unsignedTx.serialize().toString('hex'), functionName: `${tx.contractName}\n${tx.functionName}`, - isSponsorOptionSelected, + userOverrideSponsorValue, }; navigate('/swap-confirm', { state, @@ -396,6 +333,9 @@ export function useSwap(): UseSwap { : undefined, isSponsored, isServiceRunning, - setIsSponsorOptionSelected, + handleChangeUserOverrideSponsorValue: (checked: boolean) => { + setUserOverrideSponsorValue(checked); + }, + isSponsorDisabled, }; } diff --git a/src/app/screens/transactionStatus/index.tsx b/src/app/screens/transactionStatus/index.tsx index a6d69e4c6..fcc85233a 100644 --- a/src/app/screens/transactionStatus/index.tsx +++ b/src/app/screens/transactionStatus/index.tsx @@ -45,7 +45,9 @@ const TransactionIDContainer = styled.div((props) => ({ const ButtonContainer = styled.div((props) => ({ flex: 1, display: 'flex', + flexDirection: 'column', alignItems: 'flex-end', + gap: props.theme.spacing(6), marginTop: props.theme.spacing(15), marginBottom: props.theme.spacing(32), marginLeft: props.theme.spacing(8), @@ -93,6 +95,9 @@ const BodyText = styled.h1((props) => ({ color: props.theme.colors.white['400'], marginTop: props.theme.spacing(8), textAlign: 'center', + overflowWrap: 'break-word', + wordWrap: 'break-word', + wordBreak: 'break-word', marginLeft: props.theme.spacing(5), marginRight: props.theme.spacing(5), })); @@ -138,6 +143,7 @@ function TransactionStatus() { const navigate = useNavigate(); const location = useLocation(); const { network } = useWalletSelector(); + // TODO tim: refactor to use react context const { txid, currency, @@ -149,6 +155,7 @@ function TransactionStatus() { errorTitle, isBrc20TokenFlow, isSponsorServiceError, + isSwapTransaction, } = location.state; const renderTransactionSuccessStatus = ( @@ -184,6 +191,10 @@ function TransactionStatus() { else navigate(-3); }; + const handleClickTrySwapAgain = () => { + navigate('/swap'); + }; + const renderLink = ( {t('SEE_ON')} @@ -224,9 +235,16 @@ function TransactionStatus() { )} - - - + {isSwapTransaction && isSponsorServiceError ? ( + + + + + ) : ( + + + + )} ); } diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index 226a1044d..34df53114 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -17,7 +17,6 @@ export const BTC_TRANSACTION_STATUS_URL = 'https://mempool.space/tx/'; export const BTC_TRANSACTION_TESTNET_STATUS_URL = 'https://mempool.space/testnet/tx/'; export const TRANSACTION_STATUS_URL = 'https://explorer.stacks.co/txid/'; export const XVERSE_WEB_POOL_URL = 'https://pool.xverse.app'; -export const XVERSE_SPONSOR_2_URL = 'https://stacks-transaction-sponsor-swaps.onrender.com'; export const TRANSAC_URL = 'https://global.transak.com'; export const TRANSAC_API_KEY = process.env.TRANSAC_API_KEY; @@ -56,3 +55,7 @@ export const initialNetworksList: SettingsNetwork[] = [ */ export const SEND_MANY_TOKEN_TRANSFER_CONTRACT_PRINCIPAL = 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-many-memo'; + +export const SWAP_SPONSOR_DISABLED_SUPPORT_URL = + 'https://support.xverse.app/hc/en-us/articles/18319388355981'; +export const SUPPORT_URL_TAB_TARGET = 'SupportURLTabTarget'; diff --git a/src/locales/en.json b/src/locales/en.json index a445f8447..078525792 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -805,7 +805,9 @@ "INVALID_AMOUNT": "Invalid amount", "INSUFFICIENT_BALANCE_FEES": "Insufficient balance", "SLIPPAGE_TOLERANCE_CANNOT_EXCEED": "The slippage tolerance cannot exceed 100%" - } + }, + "SWAP_TRANSACTION_CANNOT_BE_SPONSORED": "Swap transaction cannot be sponsored while you have a pending transaction.", + "LEARN_MORE": "Learn more" }, "SWAP_CONFIRM_SCREEN": { "TOKEN_SWAP": "Token swap", @@ -824,6 +826,8 @@ "COPIED": "Copied", "COPY_YOUR_ADDRESS": "copy your address", "ADVANCED_SETTING": "Advanced settings", - "THIS_IS_A_SPONSORED_TRANSACTION": "This is a sponsored transaction, no transaction fees will be deducted from your account." + "THIS_IS_A_SPONSORED_TRANSACTION": "This is a sponsored transaction, no transaction fees will be deducted from your account.", + "SWAP_TRANSACTION_CANNOT_BE_SPONSORED": "Swap transaction cannot be sponsored while you have a pending transaction.", + "LEARN_MORE": "Learn more" } }