From 31f92237f2fffaa1851b1e00a77b9e4f227a67d7 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Mon, 3 Apr 2023 13:44:21 +0800 Subject: [PATCH 01/52] feat(Dashboard): update new action button style and add swap button --- src/app/screens/home/index.tsx | 9 ++-- src/app/screens/home/squareButton/index.tsx | 52 +++++++++++++++++++++ src/assets/img/dashboard/swap.svg | 6 +++ src/locales/en.json | 1 + 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 src/app/screens/home/squareButton/index.tsx create mode 100644 src/assets/img/dashboard/swap.svg diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index e24e21fee..d1317f329 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -8,6 +8,7 @@ import CreditCard from '@assets/img/dashboard/credit_card.svg'; import ListDashes from '@assets/img/dashboard/list_dashes.svg'; import OrdinalsIcon from '@assets/img/dashboard/ordinalBRC20.svg'; import IconStacks from '@assets/img/dashboard/stack_icon.svg'; +import Swap from '@assets/img/swap/swap.svg'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ReceiveCardComponent from '@components/receiveCardComponent'; @@ -30,9 +31,6 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import Theme from 'theme'; -import AlertMessage from '@components/alertMessage'; -import { useDispatch } from 'react-redux'; -import { ChangeShowBtcReceiveAlertAction, ChangeShowOrdinalReceiveAlertAction } from '@stores/wallet/actions/actionCreators'; import BalanceCard from './balanceCard'; import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; @@ -105,6 +103,7 @@ const RowButtonContainer = styled.div((props) => ({ const ButtonContainer = styled.div((props) => ({ marginRight: props.theme.spacing(11), + columnGap: props.theme.spacing(11), })); const TokenListButtonContainer = styled.div((props) => ({ @@ -130,7 +129,6 @@ function Home() { keyPrefix: 'DASHBOARD_SCREEN', }); const navigate = useNavigate(); - const dispatch = useDispatch(); const [openReceiveModal, setOpenReceiveModal] = useState(false); const [openSendModal, setOpenSendModal] = useState(false); const [openBuyModal, setOpenBuyModal] = useState(false); @@ -309,6 +307,9 @@ function Home() { onPress={onReceiveModalOpen} /> + + alert('wip')} /> + diff --git a/src/app/screens/home/squareButton/index.tsx b/src/app/screens/home/squareButton/index.tsx new file mode 100644 index 000000000..14b85e497 --- /dev/null +++ b/src/app/screens/home/squareButton/index.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components'; + +const Icon = styled.img` + width: 20px; + height: 20px; +`; + +const Button = styled.button` + display: flex; + width: 48px; + height: 48px; + border-radius: 16px; + justify-content: center; + align-items: center; + background-color: ${(props) => props.theme.colors.action.classic}; + :hover { + background-color: ${(props) => props.theme.colors.action.classicLight}; + opacity: 0.6; + } +`; + +const ButtonText = styled.div((props) => ({ + fontFamily: 'Satoshi-Medium', + color: props.theme.colors.white['0'], + textAlign: 'center', +})); + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + row-gap: 8px; +`; + +interface Props { + text: string; + src: string; + onPress: () => void; +} + +function SquareButton({ src, text, onPress }: Props) { + return ( + + + {text} + + ); +} + +export default SquareButton; diff --git a/src/assets/img/dashboard/swap.svg b/src/assets/img/dashboard/swap.svg new file mode 100644 index 000000000..30b8b21c6 --- /dev/null +++ b/src/assets/img/dashboard/swap.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index d40d8d489..c668fc7eb 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -22,6 +22,7 @@ "ACCOUNT_NAME": "Account", "RECEIVE": "Receive", "SEND": "Send", + "SWAP": "Swap", "BUY": "Buy", "MANAGE_TOKEN": "Manage token list", "STACKS_AND_TOKEN": "Stacks NFTs & SIP-10 tokens", From 0f0b86503452481b9ec7908d4d45e4fd5d792207 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Mon, 3 Apr 2023 14:04:02 +0800 Subject: [PATCH 02/52] feat(Swap): add swap screen placeholder --- src/app/routes/index.tsx | 5 +++++ src/app/screens/home/index.tsx | 26 +++++++++----------------- src/app/screens/swap/index.tsx | 24 ++++++++++++++++++++++++ src/locales/en.json | 3 +++ 4 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 src/app/screens/swap/index.tsx diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index b7fc77e86..06c5783f3 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -53,6 +53,7 @@ import TransactionRequest from '@screens/transactionRequest'; import TransactionStatus from '@screens/transactionStatus'; import WalletExists from '@screens/walletExists'; import { createHashRouter } from 'react-router-dom'; +import SwapScreen from '@screens/swap'; const router = createHashRouter([ { @@ -112,6 +113,10 @@ const router = createHashRouter([ path: 'send-btc', element: , }, + { + path: 'swap', + element: , + }, { path: 'confirm-stx-tx', element: , diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index d1317f329..8505e3ab5 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -12,7 +12,6 @@ import Swap from '@assets/img/swap/swap.svg'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ReceiveCardComponent from '@components/receiveCardComponent'; -import SmallActionButton from '@components/smallActionButton'; import BottomBar from '@components/tabBar'; import TokenTile from '@components/tokenTile'; import useAppConfig from '@hooks/queries/useAppConfig'; @@ -32,6 +31,7 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import Theme from 'theme'; import BalanceCard from './balanceCard'; +import SquareButton from './squareButton'; import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; @@ -253,6 +253,10 @@ function Home() { navigate('/receive/ORD'); }; + const onSwapPressed = () => { + navigate('/swap'); + }; + const receiveContent = ( - - - - - - - - alert('wip')} /> - - - - + + + + diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx new file mode 100644 index 000000000..9a215ea32 --- /dev/null +++ b/src/app/screens/swap/index.tsx @@ -0,0 +1,24 @@ +import { useNavigate } from 'react-router-dom'; +import TopRow from '@components/topRow'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import BottomBar from '@components/tabBar'; + +const Container = styled.div` + display: flex; + flex: 1; +`; + +function SwapScreen() { + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + return ( + <> + navigate('/')} /> + + + + ); +} + +export default SwapScreen; diff --git a/src/locales/en.json b/src/locales/en.json index c668fc7eb..c88dd147a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -557,5 +557,8 @@ "SKIP_BUTTON": "Do it later", "CONFIRM_BUTTON": "Confirm", "CLOSE_TAB": "Continue" + }, + "SWAP_SCREEN": { + "SWAP": "Swap" } } From 5eaebb36ec1b463024aa07b3f9b34ff61f0dae68 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 09:59:24 +0800 Subject: [PATCH 03/52] feat(swap): add swap basic UI --- src/app/screens/swap/index.tsx | 36 +++++- src/app/screens/swap/swapTokenBlock/index.tsx | 110 ++++++++++++++++++ src/assets/img/swap/arrow_swap.svg | 5 + src/assets/img/swap/chevron.svg | 3 + src/locales/en.json | 6 +- 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 src/app/screens/swap/swapTokenBlock/index.tsx create mode 100644 src/assets/img/swap/arrow_swap.svg create mode 100644 src/assets/img/swap/chevron.svg diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 9a215ea32..ce3c5e59a 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -3,19 +3,51 @@ import TopRow from '@components/topRow'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import BottomBar from '@components/tabBar'; +import SwapTokenBlock from '@screens/swap/swapTokenBlock'; +import ArrowDown from '@assets/img/swap/arrow_swap.svg'; +import useCoinsData from '@hooks/queries/useCoinData'; -const Container = styled.div` +const ScrollContainer = styled.div` display: flex; flex: 1; + flex-direction: column; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } + margin-left: 5%; + margin-right: 5%; `; +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + rowGap: props.theme.spacing(8), + marginTop: props.theme.spacing(16), +})); + +const DownArrow = styled.img((props) => ({ + alignSelf: 'center', + width: props.theme.spacing(18), + height: props.theme.spacing(18), +})); + function SwapScreen() { const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + const { data } = useCoinsData(); + console.log(data?.sortedFtList); + return ( <> navigate('/')} /> - + + + + + + + ); diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx new file mode 100644 index 000000000..f8e0b9c5c --- /dev/null +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -0,0 +1,110 @@ +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import { Coin } from '@secretkeylabs/xverse-core/types/api/xverse/coins'; +import ChevronIcon from '../../../../assets/img/swap/chevron.svg'; +import { FungibleToken } from '@secretkeylabs/xverse-core'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + rowGap: props.theme.spacing(4), +})); + +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +const TitleText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + flex: 1, + display: 'flex', +})); + +const Text = styled.h1((props) => ({ + ...props.theme.body_medium_m, +})); + +const BalanceText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['400'], + marginRight: props.theme.spacing(2), +})); + +const CardContainer = styled.div((props) => ({ + background: props.theme.colors.background.elevation1, + border: `1px solid ${props.theme.colors.background.elevation2}`, + borderRadius: 8, + padding: `${props.theme.spacing(10)}px ${props.theme.spacing(8)}px`, + ':focus-within': { + border: `1px solid ${props.theme.colors.background.elevation6}`, + }, +})); + +const CoinButtonContainer = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + columnGap: props.theme.spacing(2), + background: 'transparent', + alignItems: 'center', +})); + +const CoinButtonArrow = styled.img((props) => ({ + width: 12, + height: 12, +})); + +const AmountTex = styled.input((props) => ({ + ...props.theme.body_bold_l, + flex: 1, + color: props.theme.colors.white['0'], + marginLeft: props.theme.spacing(2), + textAlign: 'right', + backgroundColor: 'transparent', + border: 'transparent', +})); + +const CoinText = styled.div((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['0'], +})); + +const EstimateUSDText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['400'], + marginLeft: 'auto', +})); + +type SwapTokenBlockProps = { + title: string; + selectedCoin?: FungibleToken; +}; + +function SwapTokenBlock(props: SwapTokenBlockProps) { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + + return ( + + + {props.title} + {t('BALANCE')}: + {props.selectedCoin?.balance} + + + + + {props.selectedCoin?.name ?? t('SELECT_COIN')} + + + + + + -- + + + + ); +} + +export default SwapTokenBlock; diff --git a/src/assets/img/swap/arrow_swap.svg b/src/assets/img/swap/arrow_swap.svg new file mode 100644 index 000000000..ebb3cd9ab --- /dev/null +++ b/src/assets/img/swap/arrow_swap.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/img/swap/chevron.svg b/src/assets/img/swap/chevron.svg new file mode 100644 index 000000000..c7b3b88a3 --- /dev/null +++ b/src/assets/img/swap/chevron.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/locales/en.json b/src/locales/en.json index c88dd147a..c91fedef9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -559,6 +559,10 @@ "CLOSE_TAB": "Continue" }, "SWAP_SCREEN": { - "SWAP": "Swap" + "SWAP": "Swap", + "CONVERT": "Convert", + "BALANCE": "Balance", + "TO": "To", + "SELECT_COIN": "Select asset" } } From bccda0b570590fba067a1b2f912ea108c1824fe2 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 13:15:39 +0800 Subject: [PATCH 04/52] feat(swap): add more UI components --- .../components/recipientComponent/index.tsx | 2 +- src/app/components/tokenImage/index.tsx | 58 +++++------ src/app/screens/swap/index.tsx | 41 +++++++- src/app/screens/swap/swapInfoBlock/index.tsx | 97 +++++++++++++++++++ src/app/screens/swap/swapTokenBlock/index.tsx | 35 +++++-- src/app/screens/swap/useSwap.tsx | 79 +++++++++++++++ src/assets/img/swap/slippageEdit.svg | 8 ++ src/locales/en.json | 9 +- 8 files changed, 281 insertions(+), 48 deletions(-) create mode 100644 src/app/screens/swap/swapInfoBlock/index.tsx create mode 100644 src/app/screens/swap/useSwap.tsx create mode 100644 src/assets/img/swap/slippageEdit.svg diff --git a/src/app/components/recipientComponent/index.tsx b/src/app/components/recipientComponent/index.tsx index baf59d288..ef7d75ec1 100644 --- a/src/app/components/recipientComponent/index.tsx +++ b/src/app/components/recipientComponent/index.tsx @@ -170,7 +170,7 @@ function RecipientComponent({ diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index ca5d76bfc..194b4d1eb 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -12,57 +12,50 @@ interface TokenImageProps { token?: string; loading?: boolean; fungibleToken?: FungibleToken; - isSmallSize?: boolean; + size?: number; + loaderSize?: LoaderSize; } -interface ImageProps { - isSmallSize?: boolean; -} -interface TextProps { - isSmallSize?: boolean; -} - -const TickerImage = styled.img((props) => ({ - height: props.isSmallSize ? 32 : 44, - width: props.isSmallSize ? 32 : 44, - borderRadius: 30, +const TickerImage = styled.img<{ size?: number }>((props) => ({ + height: props.size ?? 44, + width: props.size ?? 44, })); const LoaderImageContainer = styled.div({ flex: 0.5, }); -const TickerIconContainer = styled.div((props) => ({ +const TickerIconContainer = styled.div<{ size?: number }>((props) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - height: props.isSmallSize ? 32 : 40, - width: props.isSmallSize ? 32 : 40, + height: props.size ?? 44, + width: props.size ?? 44, marginRight: props.theme.spacing(3), borderRadius: 30, backgroundColor: props.color, })); -const TickerIconText = styled.h1((props) => ({ +const TickerIconText = styled.h1((props) => ({ ...props.theme.body_bold_m, color: props.theme.colors.white['0'], textAlign: 'center', wordBreak: 'break-all', - fontSize: props.isSmallSize ? 10 : 13, + fontSize: 13, })); -export default function TokenImage(props: TokenImageProps) { - const { - token, - loading, - fungibleToken, - isSmallSize, - } = props; - +export default function TokenImage({ + token, + loading, + fungibleToken, + size, + loaderSize, +}: TokenImageProps) { const getCoinIcon = useCallback(() => { if (token === 'STX') { return IconStacks; - } if (token === 'BTC') { + } + if (token === 'BTC') { return IconBitcoin; } }, [token]); @@ -70,7 +63,7 @@ export default function TokenImage(props: TokenImageProps) { if (fungibleToken) { if (!loading) { if (fungibleToken?.image) { - return ; + return ; } let ticker = fungibleToken?.ticker; if (!ticker && fungibleToken?.name) { @@ -79,19 +72,16 @@ export default function TokenImage(props: TokenImageProps) { const background = stc(ticker); ticker = ticker && ticker.substring(0, 4); return ( - - {ticker} + + {ticker} ); } return ( - + ); } - - return ( - - ); + return ; } diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index ce3c5e59a..bce3a16c6 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -6,17 +6,24 @@ import BottomBar from '@components/tabBar'; import SwapTokenBlock from '@screens/swap/swapTokenBlock'; import ArrowDown from '@assets/img/swap/arrow_swap.svg'; import useCoinsData from '@hooks/queries/useCoinData'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { useSwap } from '@screens/swap/useSwap'; +import { useState } from 'react'; +import { SwapInfoBlock } from '@screens/swap/swapInfoBlock'; +import ActionButton from '@components/button'; const ScrollContainer = styled.div` display: flex; flex: 1; flex-direction: column; + row-gap: 16px; overflow-y: auto; &::-webkit-scrollbar { display: none; } margin-left: 5%; margin-right: 5%; + padding-bottom: 16px; `; const Container = styled.div((props) => ({ @@ -32,22 +39,48 @@ const DownArrow = styled.img((props) => ({ height: props.theme.spacing(18), })); +interface ButtonProps { + enabled: boolean; +} + +const SendButtonContainer = styled.div((props) => ({ + paddingBottom: props.theme.spacing(12), + paddingTop: props.theme.spacing(4), + marginLeft: '5%', + marginRight: '5%', + opacity: props.enabled ? 1 : 0.6, +})); + function SwapScreen() { const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { data } = useCoinsData(); - console.log(data?.sortedFtList); + const swap = useSwap(); return ( <> navigate('/')} /> - + - + + + + { + alert('test'); + }} + /> + ); diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx new file mode 100644 index 000000000..a97c69f9d --- /dev/null +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -0,0 +1,97 @@ +import { UseSwap } from '@screens/swap/useSwap'; +import { useState } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import SlippageEditIcon from '@assets/img/swap/slippageEdit.svg'; +import ChevronIcon from '@assets/img/swap/chevron.svg'; + +const PoweredByAlexText = styled.span((props) => ({ + ...props.theme.body_xs, + color: props.theme.colors.white['400'], +})); + +const DetailButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + columnGap: props.theme.spacing(2), + background: 'transparent', + alignItems: 'center', + ...props.theme.body_medium_m, + color: props.theme.colors.white['200'], +})); + +const EditSlippageButton = styled.button((props) => ({ + display: 'flex', + justifyContent: 'flex-end', + flexDirection: 'row', + columnGap: props.theme.spacing(2), + background: 'transparent', + alignItems: 'center', + ...props.theme.body_medium_m, + color: props.theme.colors.white['0'], +})); + +const DL = styled.dl((props) => ({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + rowGap: props.theme.spacing(4), +})); + +const DT = styled.dt((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['200'], + flex: '60%', +})); + +const DD = styled.dd((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['0'], + flex: '40%', + display: 'flex', + justifyContent: 'flex-end', +})); + +export function SwapInfoBlock({ swap }: { swap: UseSwap }) { + const [expandDetail, setExpandDetail] = useState(false); + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + + return ( + <> +
+
+ setExpandDetail(!expandDetail)}> + {t('DETAILS')} + {t('DETAILS')} + +
+ {expandDetail && ( + <> +
{swap.swapInfo?.exchangeRate ?? '--'}
+
{t('MIN_RECEIVE')}
+
{swap.minReceived ?? '--'}
+
{t('SLIPPAGE')}
+
+ + {swap.slippage * 100}% + {t('SLIPPAGE')} + +
+
{t('LP_FEE')}
+
{swap.swapInfo?.lpFee ?? '--'}
+
{t('ROUTE')}
+
{swap.swapInfo?.route ?? '--'}
+ + )} +
+ {t('POWERED_BY_ALEX')} + + ); +} diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index f8e0b9c5c..a452849da 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -1,8 +1,12 @@ import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; import { Coin } from '@secretkeylabs/xverse-core/types/api/xverse/coins'; -import ChevronIcon from '../../../../assets/img/swap/chevron.svg'; +import ChevronIcon from '@assets/img/swap/chevron.svg'; import { FungibleToken } from '@secretkeylabs/xverse-core'; +import { ftDecimals } from '@utils/helper'; +import { microstacksToStx } from '@secretkeylabs/xverse-core/currency'; +import BigNumber from 'bignumber.js'; +import { SwapToken } from '@screens/swap/useSwap'; const Container = styled.div((props) => ({ display: 'flex', @@ -33,10 +37,13 @@ const BalanceText = styled.h1((props) => ({ })); const CardContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + rowGap: props.theme.spacing(3), background: props.theme.colors.background.elevation1, border: `1px solid ${props.theme.colors.background.elevation2}`, borderRadius: 8, - padding: `${props.theme.spacing(10)}px ${props.theme.spacing(8)}px`, + padding: props.theme.spacing(8), ':focus-within': { border: `1px solid ${props.theme.colors.background.elevation6}`, }, @@ -55,10 +62,10 @@ const CoinButtonArrow = styled.img((props) => ({ height: 12, })); -const AmountTex = styled.input((props) => ({ +const AmountTex = styled.input<{ error?: boolean }>((props) => ({ ...props.theme.body_bold_l, flex: 1, - color: props.theme.colors.white['0'], + color: props.error ? props.theme.colors.feedback.error : props.theme.colors.white['0'], marginLeft: props.theme.spacing(2), textAlign: 'right', backgroundColor: 'transparent', @@ -78,7 +85,10 @@ const EstimateUSDText = styled.h1((props) => ({ type SwapTokenBlockProps = { title: string; - selectedCoin?: FungibleToken; + selectedCoin?: SwapToken; + amount?: string; + onAmountChange?: (amount: string) => void; + error?: boolean; }; function SwapTokenBlock(props: SwapTokenBlockProps) { @@ -89,18 +99,27 @@ function SwapTokenBlock(props: SwapTokenBlockProps) { {props.title} {t('BALANCE')}: - {props.selectedCoin?.balance} + {props.selectedCoin?.balance ?? '--'} + {props.selectedCoin?.image} {props.selectedCoin?.name ?? t('SELECT_COIN')} - + props.onAmountChange?.(e.target.value)} + /> - -- + + {props.selectedCoin ? `≈ $ ${props.selectedCoin.fiatAmount} USD` : '--'} +
diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx new file mode 100644 index 000000000..81aa4399b --- /dev/null +++ b/src/app/screens/swap/useSwap.tsx @@ -0,0 +1,79 @@ +import { ReactNode } from 'react'; +import { FungibleToken } from '@secretkeylabs/xverse-core'; +import { useTranslation } from 'react-i18next'; +import useWalletSelector from '@hooks/useWalletSelector'; +import TokenImage from '@components/tokenImage'; +import { LoaderSize } from '@utils/constants'; + +// function tokenName(coin: SelectedToken) { +// if (coin.type === 'STX') { +// return coin.type; +// } +// return coin.token.ticker?.toUpperCase() ?? coin.token.name.toUpperCase(); +// } +// +// function balance(coin: SelectedToken) { +// if (coin.type === 'STX') { +// return Number(microstacksToStx(coin.balance as any)); +// } +// if (coin.token.decimals) { +// return ftDecimals(coin.token.balance, coin.token.decimals); +// } +// return coin.token.balance; +// } + +export type SwapToken = { + name: string; + image: ReactNode; + balance: number; + amount: number; + fiatAmount: number; +}; + +export type UseSwap = { + selectedFromToken?: SwapToken; + selectedToToken?: SwapToken; + onSelectFromToken: (token: 'STX' | FungibleToken) => void; + onSelectToToken: (token: 'STX' | FungibleToken) => void; + inputAmount: string; + inputAmountInvalid?: boolean; + onInputAmountChanged: (amount: string) => void; + submitError?: string; + swapInfo?: { + exchangeRate: string; + lpFee: string; + route: string; + }; + slippage: number; + minReceived?: string; +}; + +export function useSwap(): UseSwap { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + const { coinsList, stxAvailableBalance } = useWalletSelector(); + + return { + onInputAmountChanged: noop, + selectedFromToken: { + amount: 123, + fiatAmount: 12312, + balance: 321, + image: , + name: 'STX', + }, + selectedToToken: { + amount: 123, + fiatAmount: 12312, + balance: 321, + image: , + name: 'STX', + }, + inputAmount: '123', + slippage: 0.03, + onSelectFromToken: noop, + onSelectToToken: noop, + inputAmountInvalid: true, + }; +} + +const noop = () => null; diff --git a/src/assets/img/swap/slippageEdit.svg b/src/assets/img/swap/slippageEdit.svg new file mode 100644 index 000000000..dc9d82f3a --- /dev/null +++ b/src/assets/img/swap/slippageEdit.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index c91fedef9..a3626a890 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -563,6 +563,13 @@ "CONVERT": "Convert", "BALANCE": "Balance", "TO": "To", - "SELECT_COIN": "Select asset" + "SELECT_COIN": "Select asset", + "DETAILS": "Details", + "MIN_RECEIVE": "Minimum received", + "SLIPPAGE": "Slippage Tolerance", + "LP_FEE": "Liquidity Provider Fee", + "ROUTE": "Route", + "POWERED_BY_ALEX": "Powered by ALEX", + "CONTINUE": "Continue" } } From 99f1fccf44329220d37340fd86410ced52638e77 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 15:27:01 +0800 Subject: [PATCH 05/52] feat(swap): add slippage --- src/app/screens/swap/index.tsx | 34 ++++++++- src/app/screens/swap/slippageModal/index.tsx | 69 +++++++++++++++++++ src/app/screens/swap/swapInfoBlock/index.tsx | 22 +++++- src/app/screens/swap/swapTokenBlock/index.tsx | 3 +- src/app/screens/swap/useSwap.tsx | 6 ++ src/locales/en.json | 5 +- 6 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 src/app/screens/swap/slippageModal/index.tsx diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index bce3a16c6..ae32a6868 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -11,6 +11,7 @@ import { useSwap } from '@screens/swap/useSwap'; import { useState } from 'react'; import { SwapInfoBlock } from '@screens/swap/swapInfoBlock'; import ActionButton from '@components/button'; +import CoinSelectModal from '@screens/home/coinSelectModal'; const ScrollContainer = styled.div` display: flex; @@ -56,6 +57,8 @@ function SwapScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); const swap = useSwap(); + const [selecting, setSelecting] = useState<'from' | 'to'>(); + return ( <> navigate('/')} /> @@ -67,12 +70,41 @@ function SwapScreen() { amount={swap.inputAmount} error={swap.inputAmountInvalid} onAmountChange={swap.onInputAmountChanged} + onSelectCoin={() => setSelecting('from')} /> - + setSelecting('to')} + />
+ {selecting != null && ( + null} + onSelectStacks={() => { + if (selecting === 'from') { + swap.onSelectFromToken('STX'); + } else { + swap.onSelectToToken('STX'); + } + }} + onClose={() => setSelecting(undefined)} + onSelectCoin={(coin) => { + if (selecting === 'from') { + swap.onSelectFromToken(coin); + } else { + swap.onSelectToToken(coin); + } + }} + visible={!!selecting} + coins={swap.coinsList} + title={selecting === 'from' ? t('FROM') : t('TO')} + loadingWalletData={swap.isLoadingWalletData} + /> + )} ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['0'], +})); + +const Input = styled.input((props) => ({ + ...props.theme.body_medium_m, + height: 48, + backgroundColor: props.theme.colors.background.elevation1, + borderColor: 'transparent', + borderStyle: 'solid', + borderWidth: 1, + borderRadius: 8, + color: props.theme.colors.white['0'], + padding: '14px 16px', + outline: 'none', + ':focus': { + borderColor: props.theme.colors.background.elevation6, + }, +})); + +const Description = styled.p((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['400'], +})); + +export function SlippageModalContent(props: { + slippage: number; + onChange: (slippage: number) => void; +}) { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + const [percentage, setPercentage] = useState((props.slippage * 100).toString() + '%'); + const result = Number(percentage.replace('%', '')); + let invalid = isNaN(result) || result >= 100 || result <= 0; + return ( + + {t('SLIPPAGE')} + setPercentage(e.target.value)} + onFocus={(e) => { + const current = e.target.value.replace('%', ''); + e.target.setSelectionRange(0, current.length); + }} + /> + {t('SLIPPAGE_DESC')} + props.onChange(result)} + /> + + ); +} diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index a97c69f9d..4eef1b067 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -4,6 +4,8 @@ import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; import SlippageEditIcon from '@assets/img/swap/slippageEdit.svg'; import ChevronIcon from '@assets/img/swap/chevron.svg'; +import BottomModal from '@components/bottomModal'; +import { SlippageModalContent } from '@screens/swap/slippageModal'; const PoweredByAlexText = styled.span((props) => ({ ...props.theme.body_xs, @@ -56,6 +58,8 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { const [expandDetail, setExpandDetail] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); + const [showSlippageModal, setShowSlippageModal] = useState(false); + return ( <>
@@ -79,7 +83,10 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) {
{swap.minReceived ?? '--'}
{t('SLIPPAGE')}
- + setShowSlippageModal(true)} + > {swap.slippage * 100}% {t('SLIPPAGE')} @@ -92,6 +99,19 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { )}
{t('POWERED_BY_ALEX')} + setShowSlippageModal(false)} + > + { + swap.onSlippageChanged(slippage); + setShowSlippageModal(false); + }} + /> + ); } diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index a452849da..d3b916e20 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -88,6 +88,7 @@ type SwapTokenBlockProps = { selectedCoin?: SwapToken; amount?: string; onAmountChange?: (amount: string) => void; + onSelectCoin?: () => void; error?: boolean; }; @@ -103,7 +104,7 @@ function SwapTokenBlock(props: SwapTokenBlockProps) { - + {props.selectedCoin?.image} {props.selectedCoin?.name ?? t('SELECT_COIN')} diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 81aa4399b..3c6fdae71 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -31,6 +31,8 @@ export type SwapToken = { }; export type UseSwap = { + coinsList: FungibleToken[]; + isLoadingWalletData: boolean; selectedFromToken?: SwapToken; selectedToToken?: SwapToken; onSelectFromToken: (token: 'STX' | FungibleToken) => void; @@ -45,6 +47,7 @@ export type UseSwap = { route: string; }; slippage: number; + onSlippageChanged: (slippage: number) => void; minReceived?: string; }; @@ -53,6 +56,8 @@ export function useSwap(): UseSwap { const { coinsList, stxAvailableBalance } = useWalletSelector(); return { + coinsList: coinsList ?? [], + isLoadingWalletData: false, onInputAmountChanged: noop, selectedFromToken: { amount: 123, @@ -73,6 +78,7 @@ export function useSwap(): UseSwap { onSelectFromToken: noop, onSelectToToken: noop, inputAmountInvalid: true, + onSlippageChanged: noop, }; } diff --git a/src/locales/en.json b/src/locales/en.json index a3626a890..e7fb1bb7b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -567,9 +567,12 @@ "DETAILS": "Details", "MIN_RECEIVE": "Minimum received", "SLIPPAGE": "Slippage Tolerance", + "SLIPPAGE_TITLE": "Slippage", "LP_FEE": "Liquidity Provider Fee", "ROUTE": "Route", "POWERED_BY_ALEX": "Powered by ALEX", - "CONTINUE": "Continue" + "CONTINUE": "Continue", + "SLIPPAGE_DESC": "Your transaction will reverse if the price changes unfavorably by more of this percentage.", + "APPLY": "Apply" } } From 2c08d758fe321e2528d1b6b1b0ccf26ae6120a85 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 15:41:39 +0800 Subject: [PATCH 06/52] feat(swap): make BitCoin and Stacks option in the coin select modal --- .../screens/home/coinSelectModal/index.tsx | 52 ++++++++++--------- src/app/screens/swap/index.tsx | 1 - 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/app/screens/home/coinSelectModal/index.tsx b/src/app/screens/home/coinSelectModal/index.tsx index 0a5d8bd93..450e1322a 100644 --- a/src/app/screens/home/coinSelectModal/index.tsx +++ b/src/app/screens/home/coinSelectModal/index.tsx @@ -15,8 +15,8 @@ interface Props { visible: boolean; coins: FungibleToken[]; title: string; - onSelectBitcoin: () => void; - onSelectStacks: () => void; + onSelectBitcoin?: () => void; + onSelectStacks?: () => void; onSelectCoin: (coin: FungibleToken) => void; onClose: () => void; loadingWalletData: boolean; @@ -36,39 +36,43 @@ function CoinSelectModal({ const theme = useTheme(); const handleOnBitcoinPress = () => { - onSelectBitcoin(); + onSelectBitcoin?.(); onClose(); }; const handleOnStackPress = () => { - onSelectStacks(); + onSelectStacks?.(); onClose(); }; function renderFixedCoins() { return ( <> - + {onSelectBitcoin != null && ( + + )} - + {onSelectStacks != null && ( + + )} ); } diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index ae32a6868..58167c557 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -83,7 +83,6 @@ function SwapScreen() { {selecting != null && ( null} onSelectStacks={() => { if (selecting === 'from') { swap.onSelectFromToken('STX'); From 8acb918823d964fa3681ea36f20b5953b51b8093 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 16:45:25 +0800 Subject: [PATCH 07/52] feat(swap): add token selection logic --- package-lock.json | 145 ++++++++++++++++-- package.json | 1 + src/app/screens/swap/index.tsx | 12 +- src/app/screens/swap/swapTokenBlock/index.tsx | 4 +- src/app/screens/swap/useSwap.tsx | 124 +++++++++------ 5 files changed, 214 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b2e3fc55..f8c05f8da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "alex-sdk": "^0.0.4", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", @@ -4238,6 +4239,60 @@ "ajv": "^6.9.1" } }, + "node_modules/alex-sdk": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.0.4.tgz", + "integrity": "sha512-a5HTTPHp8s2dmSFZLocsJvAHIStkauAJr8RHgD89JDF0FeK8FcKne8X8RMtc8tQUA1Vmu6xCDZqtLr9/DIEXyQ==", + "dependencies": { + "@stacks/transactions": "^6.2.0", + "clarity-codegen": "^0.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/alex-sdk/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/alex-sdk/node_modules/@stacks/common": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.0.0.tgz", + "integrity": "sha512-tETwccvbYvaZ7u3ZucWNMOIPN97r6IPeZXKIFhLc1KSVaWSGEPTtZcwVp+Rz3mu2XgI2pg37SUrOWXSL7OOkDw==", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "node_modules/alex-sdk/node_modules/@stacks/network": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.3.0.tgz", + "integrity": "sha512-573ZldQ+Iy0nCCxprXLLvkAo1AMEXncfmMUvqQ+5TN3m7VqCVADtb5G5WzMZsyR4m/k9oPsv076Lmqyl8AtR2A==", + "dependencies": { + "@stacks/common": "^6.0.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/alex-sdk/node_modules/@stacks/transactions": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.5.0.tgz", + "integrity": "sha512-kwE8cZq+QdAum4/LC+lSlAXVvzkdsSHTkCbfg4+VCWPBqA+gdXEqZe6R9SNBtMb8yGQrqUY8uIGRLVCWcXJ8zQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.0.0", + "@stacks/network": "^6.3.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6375,6 +6430,76 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/clarity-codegen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clarity-codegen/-/clarity-codegen-0.2.0.tgz", + "integrity": "sha512-puhmo5pzCAGI8dBjt87jaM4fTuZHMLLxb9Se+lcAS+scfzkHa3NA91dpX+Vgv1bm3iFl/V8m8DvREKLhflXXVw==", + "dependencies": { + "@stacks/stacks-blockchain-api-types": "^5.0.1", + "axios": "^0.27.2", + "lodash": "^4.17.21", + "yargs": "^17.6.0", + "yqueue": "^1.0.1" + }, + "bin": { + "clarity-codegen": "lib/generate/cli.js" + }, + "peerDependencies": { + "@stacks/transactions": "*" + } + }, + "node_modules/clarity-codegen/node_modules/@stacks/stacks-blockchain-api-types": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-5.0.3.tgz", + "integrity": "sha512-8vL+bPLTK0Sio3aJyIYITmdkwCCj01C5MqOQisC/GwthQvqf53uceAIsSsSQmVpFwvL5rqG0KGAu4rosW7QUwQ==" + }, + "node_modules/clarity-codegen/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/clarity-codegen/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clarity-codegen/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clarity-codegen/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -9019,7 +9144,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -10038,7 +10162,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -14677,7 +14800,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -15475,7 +15597,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -15488,8 +15609,7 @@ "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string.prototype.matchall": { "version": "4.0.8", @@ -15559,7 +15679,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -17169,7 +17288,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -17186,7 +17304,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -17201,7 +17318,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -17212,8 +17328,7 @@ "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrappy": { "version": "1.0.2", @@ -17269,7 +17384,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -17326,6 +17440,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yqueue": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/yqueue/-/yqueue-1.0.1.tgz", + "integrity": "sha512-DBxJZBRafFLA/tCc5uO8ZTGFr+sQgn1FRJkZ4cVrIQIk6bv2bInraE3mbpLAJw9z93JGrLkqDoyTLrrZaCNq5w==" + }, "node_modules/zone-file": { "version": "2.0.0-beta.3", "resolved": "https://registry.npmjs.org/zone-file/-/zone-file-2.0.0-beta.3.tgz", diff --git a/package.json b/package.json index 5895019a3..f9cf54445 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "alex-sdk": "^0.0.4", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 58167c557..09fa4d1da 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -84,19 +84,11 @@ function SwapScreen() { {selecting != null && ( { - if (selecting === 'from') { - swap.onSelectFromToken('STX'); - } else { - swap.onSelectToToken('STX'); - } + swap.onSelectToken('STX', selecting); }} onClose={() => setSelecting(undefined)} onSelectCoin={(coin) => { - if (selecting === 'from') { - swap.onSelectFromToken(coin); - } else { - swap.onSelectToToken(coin); - } + swap.onSelectToken(coin, selecting); }} visible={!!selecting} coins={swap.coinsList} diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index d3b916e20..dc67067d3 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -113,13 +113,13 @@ function SwapTokenBlock(props: SwapTokenBlockProps) { error={props.error} placeholder="0" disabled={props.onAmountChange == null} - value={props.amount ?? props.selectedCoin?.amount.toString() ?? ''} + value={props.amount ?? props.selectedCoin?.amount?.toString() ?? ''} onChange={(e) => props.onAmountChange?.(e.target.value)} /> - {props.selectedCoin ? `≈ $ ${props.selectedCoin.fiatAmount} USD` : '--'} + {props.selectedCoin?.fiatAmount ? `≈ $ ${props.selectedCoin.fiatAmount} USD` : '--'} diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 3c6fdae71..62cb4cab7 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -1,33 +1,20 @@ -import { ReactNode } from 'react'; -import { FungibleToken } from '@secretkeylabs/xverse-core'; +import { ReactNode, useState } from 'react'; +import { FungibleToken, microstacksToStx } from '@secretkeylabs/xverse-core'; import { useTranslation } from 'react-i18next'; import useWalletSelector from '@hooks/useWalletSelector'; import TokenImage from '@components/tokenImage'; import { LoaderSize } from '@utils/constants'; - -// function tokenName(coin: SelectedToken) { -// if (coin.type === 'STX') { -// return coin.type; -// } -// return coin.token.ticker?.toUpperCase() ?? coin.token.name.toUpperCase(); -// } -// -// function balance(coin: SelectedToken) { -// if (coin.type === 'STX') { -// return Number(microstacksToStx(coin.balance as any)); -// } -// if (coin.token.decimals) { -// return ftDecimals(coin.token.balance, coin.token.decimals); -// } -// return coin.token.balance; -// } +import { AlexSDK, Currency } from 'alex-sdk'; +import { ftDecimals } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { getFiatEquivalent } from '@secretkeylabs/xverse-core/transactions'; export type SwapToken = { name: string; image: ReactNode; - balance: number; - amount: number; - fiatAmount: number; + balance?: number; + amount?: number; + fiatAmount?: number; }; export type UseSwap = { @@ -35,8 +22,7 @@ export type UseSwap = { isLoadingWalletData: boolean; selectedFromToken?: SwapToken; selectedToToken?: SwapToken; - onSelectFromToken: (token: 'STX' | FungibleToken) => void; - onSelectToToken: (token: 'STX' | FungibleToken) => void; + onSelectToken: (token: 'STX' | FungibleToken, side: 'from' | 'to') => void; inputAmount: string; inputAmountInvalid?: boolean; onInputAmountChanged: (amount: string) => void; @@ -52,33 +38,77 @@ export type UseSwap = { }; export function useSwap(): UseSwap { + const alexSDK = useState(() => new AlexSDK())[0]; const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { coinsList, stxAvailableBalance } = useWalletSelector(); + const { coinsList, stxAvailableBalance, stxBtcRate, btcFiatRate, fiatCurrency } = + useWalletSelector(); + + const acceptableCoinList = + coinsList?.filter((c) => alexSDK.getCurrencyFrom(c.principal) != null) ?? []; + + const [inputAmount, setInputAmount] = useState(''); + const [slippage, setSlippage] = useState(0.04); + const [from, setFrom] = useState(); + const [to, setTo] = useState(); + + const fromAmount = 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: , + 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: , + 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 onSelectToken(token: 'STX' | FungibleToken, side: 'from' | 'to') { + (side === 'from' ? setFrom : setTo)( + token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)! + ); + } + const fromToken = currencyToToken(from, fromAmount); + const inputAmountInvalid = + isNaN(Number(inputAmount)) || + (fromAmount != null && + (fromAmount < 0 || (fromToken?.balance != null && fromToken.balance < fromAmount))); return { - coinsList: coinsList ?? [], + coinsList: acceptableCoinList, isLoadingWalletData: false, - onInputAmountChanged: noop, - selectedFromToken: { - amount: 123, - fiatAmount: 12312, - balance: 321, - image: , - name: 'STX', - }, - selectedToToken: { - amount: 123, - fiatAmount: 12312, - balance: 321, - image: , - name: 'STX', - }, - inputAmount: '123', - slippage: 0.03, - onSelectFromToken: noop, - onSelectToToken: noop, - inputAmountInvalid: true, - onSlippageChanged: noop, + inputAmount: inputAmount, + onInputAmountChanged: setInputAmount, + selectedFromToken: fromToken, + selectedToToken: currencyToToken(to), + slippage: slippage, + onSlippageChanged: setSlippage, + onSelectToken, + inputAmountInvalid, }; } From accf1a8e118df038bae52ea0d7186005db5585d9 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 17:03:38 +0800 Subject: [PATCH 08/52] feat: add swap data connection --- src/app/screens/swap/useSwap.tsx | 91 +++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 62cb4cab7..accd72d2d 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { FungibleToken, microstacksToStx } from '@secretkeylabs/xverse-core'; import { useTranslation } from 'react-i18next'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -28,13 +28,13 @@ export type UseSwap = { onInputAmountChanged: (amount: string) => void; submitError?: string; swapInfo?: { - exchangeRate: string; - lpFee: string; - route: string; + exchangeRate?: string; + lpFee?: string; + route?: string; }; slippage: number; onSlippageChanged: (slippage: number) => void; - minReceived?: string; + minReceived?: number; }; export function useSwap(): UseSwap { @@ -87,6 +87,19 @@ export function useSwap(): UseSwap { }; } + function getCurrencyName(currency: Currency) { + if (currency === Currency.STX) { + return 'STX'; + } + const token = acceptableCoinList.find( + (c) => alexSDK.getCurrencyFrom(c.principal) === currency + )!; + if (token == null) { + return currency; + } + return (token.ticker ?? token.name).toUpperCase(); + } + function onSelectToken(token: 'STX' | FungibleToken, side: 'from' | 'to') { (side === 'from' ? setFrom : setTo)( token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)! @@ -98,17 +111,83 @@ export function useSwap(): UseSwap { (fromAmount != null && (fromAmount < 0 || (fromToken?.balance != null && fromToken.balance < fromAmount))); + const [info, setInfo] = useState<{ + route: Currency[]; + feeRate: number; + }>(); + + useEffect(() => { + if (from == null || to == null) { + setInfo(undefined); + } else { + let cancelled = false; + Promise.all([alexSDK.getFeeRate(from, to), alexSDK.getRouter(from, to)]).then((a) => { + if (cancelled) { + return; + } + setInfo({ + route: a[1], + feeRate: Number(a[0]) / 1e8, + }); + }); + return () => { + cancelled = true; + }; + } + }, [from, to]); + + const [exchangeRate, setExchangeRate] = useState(); + + useEffect(() => { + if (from == null || to == null || fromAmount == null || fromAmount == 0) { + setExchangeRate(undefined); + } else { + let cancelled = false; + alexSDK.getAmountTo(from, BigInt(fromAmount * 1e8), to).then((result) => { + if (cancelled) { + return; + } + setExchangeRate(Number(result) / 1e8 / fromAmount); + }); + return () => { + cancelled = true; + }; + } + }, [from, to]); + + function roundForDisplay(input?: number) { + if (input == null) { + return undefined; + } + return Math.floor(input * 1000) / 1000; + } + const toAmount = + exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; + + const toToken = currencyToToken(to, roundForDisplay(toAmount)); return { coinsList: acceptableCoinList, isLoadingWalletData: false, inputAmount: inputAmount, onInputAmountChanged: setInputAmount, selectedFromToken: fromToken, - selectedToToken: currencyToToken(to), + selectedToToken: toToken, slippage: slippage, onSlippageChanged: setSlippage, onSelectToken, inputAmountInvalid, + minReceived: toAmount != null ? roundForDisplay(toAmount * (1 - slippage)) : undefined, + swapInfo: { + exchangeRate: + exchangeRate != null && fromToken != null && toToken != null + ? `1 ${fromToken.name} = ${roundForDisplay(exchangeRate)} ${toToken.name}` + : undefined, + route: info?.route.map(getCurrencyName).join(' -> '), + lpFee: + info?.feeRate != null && fromAmount != null && fromToken != null + ? `${info.feeRate * fromAmount} ${fromToken.name}` + : undefined, + }, }; } From 358f98f83d43b54ea4f4bbe4db9b67e4f9e9fcca Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 17:45:08 +0800 Subject: [PATCH 09/52] feat(swap): add runSwap --- src/app/screens/swap/index.tsx | 17 ++++++--------- src/app/screens/swap/swapInfoBlock/index.tsx | 7 ++++--- src/app/screens/swap/useSwap.tsx | 22 +++++++++++++++++--- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 09fa4d1da..209d6dfc0 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -40,16 +40,11 @@ const DownArrow = styled.img((props) => ({ height: props.theme.spacing(18), })); -interface ButtonProps { - enabled: boolean; -} - -const SendButtonContainer = styled.div((props) => ({ +const SendButtonContainer = styled.div((props) => ({ paddingBottom: props.theme.spacing(12), paddingTop: props.theme.spacing(4), marginLeft: '5%', marginRight: '5%', - opacity: props.enabled ? 1 : 0.6, })); function SwapScreen() { @@ -96,12 +91,12 @@ function SwapScreen() { loadingWalletData={swap.isLoadingWalletData} /> )} - + { - alert('test'); - }} + disabled={swap.onSwap == null} + warning={!!swap.submitError} + text={swap.submitError ?? t('CONTINUE')} + onPress={swap.onSwap!} /> diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index 4eef1b067..c1492632b 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -43,15 +43,16 @@ const DL = styled.dl((props) => ({ const DT = styled.dt((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white['200'], - flex: '60%', + flex: '50%', })); const DD = styled.dd((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white['0'], - flex: '40%', + flex: '50%', display: 'flex', justifyContent: 'flex-end', + textAlign: 'right', })); export function SwapInfoBlock({ swap }: { swap: UseSwap }) { @@ -76,9 +77,9 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { /> +
{swap.swapInfo?.exchangeRate ?? '--'}
{expandDetail && ( <> -
{swap.swapInfo?.exchangeRate ?? '--'}
{t('MIN_RECEIVE')}
{swap.minReceived ?? '--'}
{t('SLIPPAGE')}
diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index accd72d2d..591638ca6 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -35,12 +35,13 @@ export type UseSwap = { slippage: number; onSlippageChanged: (slippage: number) => void; minReceived?: number; + onSwap?: () => void; }; export function useSwap(): UseSwap { const alexSDK = useState(() => new AlexSDK())[0]; const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { coinsList, stxAvailableBalance, stxBtcRate, btcFiatRate, fiatCurrency } = + const { coinsList, stxAvailableBalance, stxBtcRate, btcFiatRate, fiatCurrency, stxAddress } = useWalletSelector(); const acceptableCoinList = @@ -153,7 +154,7 @@ export function useSwap(): UseSwap { cancelled = true; }; } - }, [from, to]); + }, [from, to, fromAmount]); function roundForDisplay(input?: number) { if (input == null) { @@ -185,9 +186,24 @@ export function useSwap(): UseSwap { route: info?.route.map(getCurrencyName).join(' -> '), lpFee: info?.feeRate != null && fromAmount != null && fromToken != null - ? `${info.feeRate * fromAmount} ${fromToken.name}` + ? `${roundForDisplay(info.feeRate * fromAmount)} ${fromToken.name}` : undefined, }, + submitError: inputAmountInvalid ? 'Invalid amount' : undefined, + onSwap: + fromAmount != null && toAmount != null && from != null && to != null && info != null + ? async () => { + const tx = await alexSDK.runSwap( + stxAddress, + from, + to, + BigInt(fromAmount * 1e8), + BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), + info.route + ); + alert(tx.contractName + ':' + tx.functionName); + } + : undefined, }; } From 9a9daef8aec3ed2488b7bb6fa99679d21a63dff7 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 18:33:15 +0800 Subject: [PATCH 10/52] chore(swap): bump alex-sdk version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9cf54445..9edad87f2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "alex-sdk": "^0.0.4", + "alex-sdk": "^0.0.5", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", From c115639f56d387b7cd9caef34647aa3ab3c582dd Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sat, 15 Apr 2023 21:34:04 +0800 Subject: [PATCH 11/52] fix(swap): slippage setting --- src/app/screens/swap/slippageModal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/slippageModal/index.tsx b/src/app/screens/swap/slippageModal/index.tsx index 2e46d93a8..f83137594 100644 --- a/src/app/screens/swap/slippageModal/index.tsx +++ b/src/app/screens/swap/slippageModal/index.tsx @@ -62,7 +62,7 @@ export function SlippageModalContent(props: { disabled={invalid} warning={invalid} text={t('APPLY')} - onPress={() => props.onChange(result)} + onPress={() => props.onChange(result / 100)} /> ); From 558e1c159c9a0912442de7161508f04892e7cf40 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Thu, 20 Apr 2023 10:04:22 +0800 Subject: [PATCH 12/52] feat: swap confirm layout --- src/app/components/accountRow/index.tsx | 2 +- .../confirmStxTransactionComponent/index.tsx | 24 +-- src/app/components/tokenImage/index.tsx | 13 +- src/app/routes/index.tsx | 5 + src/app/screens/home/index.tsx | 2 +- .../swapConfirmation/freesBlock/index.tsx | 34 ++++ .../swapConfirmation/functionBlock/index.tsx | 25 +++ .../screens/swap/swapConfirmation/index.tsx | 62 +++++++ .../swapConfirmation/routeBlock/index.tsx | 83 +++++++++ .../swapConfirmation/stxInfoBlock/index.tsx | 172 ++++++++++++++++++ src/app/screens/swap/swapInfoBlock/index.tsx | 12 -- src/app/screens/swap/swapTokenBlock/index.tsx | 6 +- src/app/screens/swap/useSwap.tsx | 7 +- src/assets/img/swap/address.svg | 4 + src/assets/img/swap/copy.svg | 4 + src/assets/img/swap/fold_arrow_down.svg | 4 + src/assets/img/swap/fold_arrow_up.svg | 3 + src/locales/en.json | 17 ++ 18 files changed, 445 insertions(+), 34 deletions(-) create mode 100644 src/app/screens/swap/swapConfirmation/freesBlock/index.tsx create mode 100644 src/app/screens/swap/swapConfirmation/functionBlock/index.tsx create mode 100644 src/app/screens/swap/swapConfirmation/index.tsx create mode 100644 src/app/screens/swap/swapConfirmation/routeBlock/index.tsx create mode 100644 src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx create mode 100644 src/assets/img/swap/address.svg create mode 100644 src/assets/img/swap/copy.svg create mode 100644 src/assets/img/swap/fold_arrow_down.svg create mode 100644 src/assets/img/swap/fold_arrow_up.svg diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index 6c7157d66..03beee969 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -73,7 +73,7 @@ const CopyImage = styled.img` margin-right: 4px; `; -const StyledToolTip = styled(Tooltip)` +export const StyledToolTip = styled(Tooltip)` background-color: #ffffff; color: #12151e; border-radius: 8px; diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 79ce36845..b31b829b1 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -26,12 +26,13 @@ const Container = styled.div` margin-left: 16px; margin-right: 16px; overflow-y: auto; + &::-webkit-scrollbar { display: none; } `; -const ButtonContainer = styled.div((props) => ({ +export const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', marginBottom: props.theme.spacing(12), @@ -125,14 +126,15 @@ function ConfirmStxTransationComponent({ 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 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]); @@ -154,7 +156,7 @@ function ConfirmStxTransationComponent({ initialStxTransactions[0], seedPhrase, selectedAccount?.id ?? 0, - selectedNetwork, + selectedNetwork ); signedTxs.push(signedContractCall); } else if (initialStxTransactions.length === 2) { @@ -162,7 +164,7 @@ function ConfirmStxTransationComponent({ initialStxTransactions, selectedAccount?.id ?? 0, selectedNetwork, - seedPhrase, + seedPhrase ); } onConfirmClick(signedTxs); diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index 194b4d1eb..57a793ce6 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -14,25 +14,27 @@ interface TokenImageProps { fungibleToken?: FungibleToken; size?: number; loaderSize?: LoaderSize; + round?: boolean; } -const TickerImage = styled.img<{ size?: number }>((props) => ({ +const TickerImage = styled.img<{ size?: number; round?: boolean }>((props) => ({ height: props.size ?? 44, width: props.size ?? 44, + borderRadius: props.round ? '50%' : 'none', })); const LoaderImageContainer = styled.div({ flex: 0.5, }); -const TickerIconContainer = styled.div<{ size?: number }>((props) => ({ +const TickerIconContainer = styled.div<{ size?: number; round?: boolean }>((props) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', height: props.size ?? 44, width: props.size ?? 44, marginRight: props.theme.spacing(3), - borderRadius: 30, + borderRadius: props.round ? '50%' : props.theme.radius(2), backgroundColor: props.color, })); @@ -50,6 +52,7 @@ export default function TokenImage({ fungibleToken, size, loaderSize, + round, }: TokenImageProps) { const getCoinIcon = useCallback(() => { if (token === 'STX') { @@ -72,7 +75,7 @@ export default function TokenImage({ const background = stc(ticker); ticker = ticker && ticker.substring(0, 4); return ( - + {ticker} ); @@ -83,5 +86,5 @@ export default function TokenImage({ ); } - return ; + return ; } diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 06c5783f3..1a4f12422 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -54,6 +54,7 @@ import TransactionStatus from '@screens/transactionStatus'; import WalletExists from '@screens/walletExists'; import { createHashRouter } from 'react-router-dom'; import SwapScreen from '@screens/swap'; +import SwapConfirmScreen from '@screens/swap/swapConfirmation'; const router = createHashRouter([ { @@ -117,6 +118,10 @@ const router = createHashRouter([ path: 'swap', element: , }, + { + path: 'swap-confirm', + element: , + }, { path: 'confirm-stx-tx', element: , diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 8505e3ab5..a8405c33f 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -35,7 +35,7 @@ import SquareButton from './squareButton'; import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; -const Container = styled.div` +export const Container = styled.div` display: flex; flex-direction: column; flex: 1; diff --git a/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx new file mode 100644 index 000000000..2410792c2 --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { Container, TitleText } from '@screens/swap/swapConfirmation/stxInfoBlock'; +import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; + +const RowContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +})); + +const FeeText = styled.p((props) => ({ + ...props.theme.body_m, + fontSize: 14, + fontWeight: 500, +})); + +interface FeeTextProps { + fee: number; + currency: string; +} + +export default function FeesBlock({ fee, currency }: FeeTextProps) { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + return ( + + + {t('FEES')} + {`${fee.toString()} ${currency}`} + + {` ~ $${0.45} USD`} + + ); +} diff --git a/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx new file mode 100644 index 000000000..c84bad1a9 --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { Container, TitleContainer, TitleText } from '@screens/swap/swapConfirmation/stxInfoBlock'; + +const FunctionName = styled.div((props) => ({ + color: props.theme.colors.white[0], + fontSize: 14, + fontWeight: 500, +})); + +interface FunctionBlockProps { + name: string; +} + +export default function FunctionBlock({ name }: FunctionBlockProps) { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + return ( + + + {t('FUNCTION')} + {name} + + + ); +} diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx new file mode 100644 index 000000000..c98c58056 --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -0,0 +1,62 @@ +import BottomBar from '@components/tabBar'; +import AccountHeaderComponent from '@components/accountHeader'; +import { Container } from '@screens/home'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import FunctionBlock from '@screens/swap/swapConfirmation/functionBlock'; +import ActionButton from '@components/button'; +import FreesBlock from '@screens/swap/swapConfirmation/freesBlock'; +import RouteBlock from '@screens/swap/swapConfirmation/routeBlock'; +import StxInfoBlock from '@screens/swap/swapConfirmation/stxInfoBlock'; +import { useCallback } from 'react'; + +const TitleText = styled.div((props) => ({ + fontSize: 21, + fontWeight: 700, + color: props.theme.colors.white['0'], + marginBottom: props.theme.spacing(16), + marginTop: props.theme.spacing(12), +})); + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + marginBottom: props.theme.spacing(20), + marginTop: props.theme.spacing(16), +})); + +export const ActionButtonWrap = styled.div((props) => ({ + marginRight: props.theme.spacing(8), + width: '100%', +})); + +export default function SwapConfirmation() { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + + const onCancel = useCallback(() => { + window.close(); + }, []); + + const onConfirm = useCallback(() => {}, []); + + return ( + <> + + + {t('TOKEN_SWAP')} + + + + + + + + + + + + + + + ); +} diff --git a/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx new file mode 100644 index 000000000..f52661d43 --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx @@ -0,0 +1,83 @@ +import { + Container, + FoldButton, + TitleContainer, + TitleText, +} from '@screens/swap/swapConfirmation/stxInfoBlock'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import TokenImage from '@components/tokenImage'; +import { useState } from 'react'; + +const RouteDescription = styled.p((props) => ({ + ...props.theme.body_m, + fontWeight: 400, + color: props.theme.colors.white[400], + marginTop: props.theme.spacing(8), +})); + +const RouteProgress = styled.div((props) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flex: 1, + marginTop: props.theme.spacing(8), + marginLeft: -props.theme.spacing(3.5), + marginRight: -props.theme.spacing(3.5), +})); + +const DashLine = styled.div((props) => ({ + borderTop: `1.5px dashed ${props.theme.colors.white[800]}`, + width: '100%', + position: 'absolute', + left: 0, + top: 12, +})); + +const ProgressItem = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + position: 'relative', + zIndex: '2', + paddingRight: props.theme.spacing(3.5), + paddingLeft: props.theme.spacing(3.5), + background: props.theme.colors.background.elevation1, +})); + +const ProgressItemText = styled.p((props) => ({ + ...props.theme.body_m, + fontWeight: 400, + color: props.theme.colors.white[0], + marginTop: props.theme.spacing(4), +})); + +const mockData = ['STX', 'STX']; + +export default function RouteBlock() { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + const [isFold, setIsFold] = useState(false); + return ( + + + {t('ROUTE')} + setIsFold((prev) => !prev)} /> + + {isFold ? null : ( + <> + + + {mockData.map((name) => ( + + + {name} + + ))} + + {t('ROUTE_DESC')} + + )} + + ); +} diff --git a/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx new file mode 100644 index 000000000..7eb32f81e --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx @@ -0,0 +1,172 @@ +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import FoldIconUp from '@assets/img/swap/fold_arrow_up.svg'; +import FoldDownIcon from '@assets/img/swap/fold_arrow_down.svg'; +import AddressIcon from '@assets/img/swap/address.svg'; +import CopyIcon from '@assets/img/swap/copy.svg'; +import { getTruncatedAddress } from '@utils/helper'; +import { StyledToolTip } from '@components/accountRow'; +import { useCallback, useState } from 'react'; +import TokenImage from '@components/tokenImage'; +import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + padding: props.theme.spacing(10), + marginBottom: props.theme.spacing(6), + background: props.theme.colors.background.elevation1, + borderRadius: 12, +})); + +export const TitleContainer = styled.div(() => ({ + display: 'flex', + flex: '1', + alignItems: 'center', + justifyContent: 'space-between', +})); + +export const TitleText = styled.h3((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white[200], + fontSize: 14, + fontWeight: 500, +})); + +export const FoldArrow = styled.img(() => ({ + width: 14, + height: 14, + cursor: 'pointer', +})); + +const DescriptionText = styled.h3((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white[400], + fontSize: 14, + fontWeight: 500, + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(4), +})); + +const AmountContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', +})); + +const AmountLabel = styled.p((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white[200], + fontSize: 14, + fontWeight: 500, + marginLeft: props.theme.spacing(5), +})); + +const SpaceBetweenContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', +})); + +const AddressImg = styled.img(() => ({ + width: 32, + height: 32, +})); + +const ItemsCenterContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', +})); + +const AddressLabelText = styled.h3((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white[200], + marginLeft: props.theme.spacing(5), +})); + +const CopyButton = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', +})); + +const CopyImg = styled.img((props) => ({ + width: 20, + height: 20, +})); + +const AddressText = styled.p((props) => ({ + ...props.theme.body_medium_m, + marginRight: props.theme.spacing(4), +})); + +const CurrencyText = styled.p((props) => ({ + ...props.theme.body_medium_m, + marginLeft: props.theme.spacing(4), +})); + +interface StxInfoCardProps { + type: 'transfer' | 'receive'; +} + +export function FoldButton({ isFold, onSwitch }: { isFold: boolean; onSwitch: () => void }) { + return onSwitch()} />; +} + +export default function StxInfoBlock({ type }: StxInfoCardProps) { + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + const [isCopied, setIsCopied] = useState(false); + const [isFold, setIsFold] = useState(false); + const copyId = `address-${type}`; + const onCopy = useCallback(() => { + navigator.clipboard.writeText('address ......'); + setIsCopied(true); + }, []); + return ( + + + + {type === 'transfer' ? t('YOU_WILL_TRANSFER') : t('YOU_WILL_RECEIVE')} + + setIsFold((prev) => !prev)} /> + + {isFold ? null : ( + <> + {t('LESS_THAN_OR_EQUAL_TO')} + + + + + {t('AMOUNT')} + + + 33 + STX + + + {` ~ $${100} USD`} + + {t('TO')} + + + + {t('YOUR_ADDRESS')} + + + {getTruncatedAddress('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX')} + + + + + + )} + + ); +} diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index c1492632b..c21c1a5a9 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -22,17 +22,6 @@ const DetailButton = styled.button((props) => ({ color: props.theme.colors.white['200'], })); -const EditSlippageButton = styled.button((props) => ({ - display: 'flex', - justifyContent: 'flex-end', - flexDirection: 'row', - columnGap: props.theme.spacing(2), - background: 'transparent', - alignItems: 'center', - ...props.theme.body_medium_m, - color: props.theme.colors.white['0'], -})); - const DL = styled.dl((props) => ({ display: 'flex', flexWrap: 'wrap', @@ -58,7 +47,6 @@ const DD = styled.dd((props) => ({ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { const [expandDetail, setExpandDetail] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const [showSlippageModal, setShowSlippageModal] = useState(false); return ( diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index dc67067d3..79346c9be 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -14,7 +14,7 @@ const Container = styled.div((props) => ({ rowGap: props.theme.spacing(4), })); -const RowContainer = styled.div({ +export const RowContainer = styled.div({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -62,7 +62,7 @@ const CoinButtonArrow = styled.img((props) => ({ height: 12, })); -const AmountTex = styled.input<{ error?: boolean }>((props) => ({ +export const AmountTex = styled.input<{ error?: boolean }>((props) => ({ ...props.theme.body_bold_l, flex: 1, color: props.error ? props.theme.colors.feedback.error : props.theme.colors.white['0'], @@ -77,7 +77,7 @@ const CoinText = styled.div((props) => ({ color: props.theme.colors.white['0'], })); -const EstimateUSDText = styled.h1((props) => ({ +export const EstimateUSDText = styled.h1((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white['400'], marginLeft: 'auto', diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 591638ca6..3bb868ef4 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -8,6 +8,7 @@ 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'; export type SwapToken = { name: string; @@ -39,6 +40,7 @@ export type UseSwap = { }; export function useSwap(): UseSwap { + const navigate = useNavigate(); const alexSDK = useState(() => new AlexSDK())[0]; const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); const { coinsList, stxAvailableBalance, stxBtcRate, btcFiatRate, fiatCurrency, stxAddress } = @@ -106,6 +108,7 @@ export function useSwap(): UseSwap { token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)! ); } + const fromToken = currencyToToken(from, fromAmount); const inputAmountInvalid = isNaN(Number(inputAmount)) || @@ -162,6 +165,7 @@ export function useSwap(): UseSwap { } return Math.floor(input * 1000) / 1000; } + const toAmount = exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; @@ -201,7 +205,8 @@ export function useSwap(): UseSwap { BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), info.route ); - alert(tx.contractName + ':' + tx.functionName); + // TODO: 跳转传参数 + navigate('/swap-confirm'); } : undefined, }; diff --git a/src/assets/img/swap/address.svg b/src/assets/img/swap/address.svg new file mode 100644 index 000000000..c97859d45 --- /dev/null +++ b/src/assets/img/swap/address.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/img/swap/copy.svg b/src/assets/img/swap/copy.svg new file mode 100644 index 000000000..78b20d771 --- /dev/null +++ b/src/assets/img/swap/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/img/swap/fold_arrow_down.svg b/src/assets/img/swap/fold_arrow_down.svg new file mode 100644 index 000000000..92faa4bec --- /dev/null +++ b/src/assets/img/swap/fold_arrow_down.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/assets/img/swap/fold_arrow_up.svg b/src/assets/img/swap/fold_arrow_up.svg new file mode 100644 index 000000000..62dfdf5dd --- /dev/null +++ b/src/assets/img/swap/fold_arrow_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/locales/en.json b/src/locales/en.json index e7fb1bb7b..339f89274 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -574,5 +574,22 @@ "CONTINUE": "Continue", "SLIPPAGE_DESC": "Your transaction will reverse if the price changes unfavorably by more of this percentage.", "APPLY": "Apply" + }, + "SWAP_CONFIRM_SCREEN": { + "TOKEN_SWAP": "Token swap", + "FUNCTION": "Function", + "YOU_WILL_TRANSFER": "You will transfer", + "YOU_WILL_RECEIVE": "You will receive", + "AMOUNT": "Amount", + "LESS_THAN_OR_EQUAL_TO": "Less than or equal to", + "TO": "To", + "YOUR_ADDRESS": "Your address", + "CANCEL": "Cancel", + "CONFIRM": "Confirm", + "FEES": "Fees", + "ROUTE": "Route", + "COPIED": "Copied", + "COPY_YOUR_ADDRESS": "copy your address", + "ROUTE_DESC": "For the transaction to proceed, your BTC will be swapped for xBTC and STX. There is no additional cost for you and no STX will be added to your wallet." } } From e5f5e0d72b0fe763c002f99ed7fc9bad053414d5 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Thu, 20 Apr 2023 23:32:15 +0800 Subject: [PATCH 13/52] feat: add data bindings --- .../swapConfirmation/freesBlock/index.tsx | 9 ++- .../screens/swap/swapConfirmation/index.tsx | 31 ++++++-- .../swapConfirmation/routeBlock/index.tsx | 9 ++- .../swapConfirmation/stxInfoBlock/index.tsx | 19 +++-- .../swap/swapConfirmation/useConfirmSwap.tsx | 76 +++++++++++++++++++ src/app/screens/swap/useSwap.tsx | 26 ++++++- 6 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx diff --git a/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx index 2410792c2..4c53bc213 100644 --- a/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx @@ -16,19 +16,20 @@ const FeeText = styled.p((props) => ({ })); interface FeeTextProps { - fee: number; + lpFee: number; + lpFeeFiatAmount?: number; currency: string; } -export default function FeesBlock({ fee, currency }: FeeTextProps) { +export default function FeesBlock({ lpFee, lpFeeFiatAmount, currency }: FeeTextProps) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); return ( {t('FEES')} - {`${fee.toString()} ${currency}`} + {`${lpFee.toString()} ${currency}`} - {` ~ $${0.45} USD`} + {` ~ $${lpFeeFiatAmount} USD`} ); } diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index c98c58056..92f77644a 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -8,7 +8,9 @@ import ActionButton from '@components/button'; import FreesBlock from '@screens/swap/swapConfirmation/freesBlock'; import RouteBlock from '@screens/swap/swapConfirmation/routeBlock'; import StxInfoBlock from '@screens/swap/swapConfirmation/stxInfoBlock'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; +import { useHref, useLocation, useNavigate } from 'react-router-dom'; +import { useConfirmSwap } from '@screens/swap/swapConfirmation/useConfirmSwap'; const TitleText = styled.div((props) => ({ fontSize: 21, @@ -32,28 +34,41 @@ export const ActionButtonWrap = styled.div((props) => ({ export default function SwapConfirmation() { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + const location = useLocation(); + const navigate = useNavigate(); + const swap = useConfirmSwap(location.state); const onCancel = useCallback(() => { - window.close(); + navigate('/swap'); }, []); - const onConfirm = useCallback(() => {}, []); + const [confirming, setConfirming] = useState(false); + const onConfirm = useCallback(() => { + setConfirming(true); + swap.onConfirm().finally(() => { + setConfirming(false); + }); + }, []); return ( <> {t('TOKEN_SWAP')} - - + + - - + + - + diff --git a/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx index f52661d43..54d660e46 100644 --- a/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx @@ -7,7 +7,8 @@ import { import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import TokenImage from '@components/tokenImage'; -import { useState } from 'react'; +import { cloneElement, useState } from 'react'; +import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; const RouteDescription = styled.p((props) => ({ ...props.theme.body_m, @@ -55,7 +56,7 @@ const ProgressItemText = styled.p((props) => ({ const mockData = ['STX', 'STX']; -export default function RouteBlock() { +export default function RouteBlock({ swap }: { swap: SwapConfirmationInput }) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); const [isFold, setIsFold] = useState(false); return ( @@ -68,9 +69,9 @@ export default function RouteBlock() { <> - {mockData.map((name) => ( + {swap.routers.map(({ name, image }) => ( - + {cloneElement(image as any, { size: 24 })} {name} ))} diff --git a/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx index 7eb32f81e..978a2191f 100644 --- a/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx @@ -9,6 +9,7 @@ import { StyledToolTip } from '@components/accountRow'; import { useCallback, useState } from 'react'; import TokenImage from '@components/tokenImage'; import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; +import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; export const Container = styled.div((props) => ({ display: 'flex', @@ -108,21 +109,23 @@ const CurrencyText = styled.p((props) => ({ interface StxInfoCardProps { type: 'transfer' | 'receive'; + swap: SwapConfirmationInput; } export function FoldButton({ isFold, onSwitch }: { isFold: boolean; onSwitch: () => void }) { return onSwitch()} />; } -export default function StxInfoBlock({ type }: StxInfoCardProps) { +export default function StxInfoBlock({ type, swap }: StxInfoCardProps) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); const [isCopied, setIsCopied] = useState(false); const [isFold, setIsFold] = useState(false); const copyId = `address-${type}`; const onCopy = useCallback(() => { - navigator.clipboard.writeText('address ......'); + void navigator.clipboard.writeText('address ......'); setIsCopied(true); }, []); + const token = type === 'transfer' ? swap.fromToken : swap.toToken; return ( @@ -137,15 +140,15 @@ export default function StxInfoBlock({ type }: StxInfoCardProps) { - - {t('AMOUNT')} + {token.image} + {token.name} - 33 - STX + {token.amount} + {token.name} - {` ~ $${100} USD`} + {` ~ $${token.fiatAmount} USD`} {t('TO')} @@ -154,7 +157,7 @@ export default function StxInfoBlock({ type }: StxInfoCardProps) { {t('YOUR_ADDRESS')} - {getTruncatedAddress('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX')} + {getTruncatedAddress(swap.address)} Promise } { + const { selectedAccount, seedPhrase, stxPublicKey } = useWalletSelector(); + const selectedNetwork = useNetworkSelector(); + const navigate = useNavigate(); + return { + ...input, + onConfirm: async () => { + const tx = await makeUnsignedContractCall({ + publicKey: stxPublicKey, + contractAddress: input.txToBroadcast.contractAddress, + contractName: input.txToBroadcast.contractName, + functionName: input.txToBroadcast.functionName, + functionArgs: input.txToBroadcast.functionArgs as any, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Deny, + postConditions: input.txToBroadcast.postConditions, + }); + const signed = await signTransaction( + tx, + seedPhrase, + selectedAccount?.id ?? 0, + selectedNetwork + ); + try { + const broadcastResult: string = await broadcastSignedTransaction(signed, selectedNetwork); + if (broadcastResult) { + navigate('/tx-status', { + state: { + txid: broadcastResult, + currency: 'STX', + error: '', + browserTx: true, + }, + }); + } + } catch (e) { + if (e instanceof Error) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'STX', + error: e.message, + browserTx: true, + }, + }); + } + } + }, + }; +} diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 3bb868ef4..90eea3ed8 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -9,6 +9,7 @@ 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'; export type SwapToken = { name: string; @@ -63,7 +64,7 @@ export function useSwap(): UseSwap { if (currency === Currency.STX) { return { balance: Number(microstacksToStx(BigNumber(stxAvailableBalance) as any)), - image: , + image: , name: 'STX', amount, fiatAmount: @@ -80,7 +81,9 @@ export function useSwap(): UseSwap { } return { amount, - image: , + image: ( + + ), name: (token.ticker ?? token.name).toUpperCase(), balance: Number(ftDecimals(token.balance, token.decimals ?? 0)), fiatAmount: @@ -205,11 +208,26 @@ export function useSwap(): UseSwap { BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), info.route ); - // TODO: 跳转传参数 - navigate('/swap-confirm'); + const state: SwapConfirmationInput = { + from: from!, + to: to!, + fromToken: fromToken!, + toToken: toToken!, + address: stxAddress, + fromAmount: fromAmount!, + minToAmount: toAmount! * (1 - slippage), + lpFeeAmount: info.feeRate * fromAmount!, + lpFeeFiatAmount: currencyToToken(from!, info.feeRate * fromAmount!)?.fiatAmount, + routers: info.route.map(currencyToToken).filter(isNotNull), + txToBroadcast: tx, + }; + navigate('/swap-confirm', { + state, + }); } : undefined, }; } const noop = () => null; +const isNotNull = (t: T | null | undefined): t is T => t != null; From d71dc7ff18458d87a319c7b9797b1b8dc15d5759 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Fri, 21 Apr 2023 11:09:33 +0800 Subject: [PATCH 14/52] fix: function name and copy address --- src/app/screens/swap/index.tsx | 2 -- src/app/screens/swap/swapConfirmation/index.tsx | 4 ++-- src/app/screens/swap/swapConfirmation/routeBlock/index.tsx | 3 --- src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx | 5 ++--- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 209d6dfc0..850b8411a 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -5,8 +5,6 @@ import styled from 'styled-components'; import BottomBar from '@components/tabBar'; import SwapTokenBlock from '@screens/swap/swapTokenBlock'; import ArrowDown from '@assets/img/swap/arrow_swap.svg'; -import useCoinsData from '@hooks/queries/useCoinData'; -import useWalletSelector from '@hooks/useWalletSelector'; import { useSwap } from '@screens/swap/useSwap'; import { useState } from 'react'; import { SwapInfoBlock } from '@screens/swap/swapInfoBlock'; diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index 92f77644a..b86aeb020 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -9,7 +9,7 @@ import FreesBlock from '@screens/swap/swapConfirmation/freesBlock'; import RouteBlock from '@screens/swap/swapConfirmation/routeBlock'; import StxInfoBlock from '@screens/swap/swapConfirmation/stxInfoBlock'; import { useCallback, useState } from 'react'; -import { useHref, useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useConfirmSwap } from '@screens/swap/swapConfirmation/useConfirmSwap'; const TitleText = styled.div((props) => ({ @@ -57,7 +57,7 @@ export default function SwapConfirmation() { {t('TOKEN_SWAP')} - + ({ marginTop: props.theme.spacing(4), })); -const mockData = ['STX', 'STX']; - export default function RouteBlock({ swap }: { swap: SwapConfirmationInput }) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); const [isFold, setIsFold] = useState(false); diff --git a/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx index 978a2191f..d9da1b9ec 100644 --- a/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/stxInfoBlock/index.tsx @@ -7,7 +7,6 @@ import CopyIcon from '@assets/img/swap/copy.svg'; import { getTruncatedAddress } from '@utils/helper'; import { StyledToolTip } from '@components/accountRow'; import { useCallback, useState } from 'react'; -import TokenImage from '@components/tokenImage'; import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; @@ -121,8 +120,8 @@ export default function StxInfoBlock({ type, swap }: StxInfoCardProps) { const [isCopied, setIsCopied] = useState(false); const [isFold, setIsFold] = useState(false); const copyId = `address-${type}`; - const onCopy = useCallback(() => { - void navigator.clipboard.writeText('address ......'); + const onCopy = useCallback(async () => { + await navigator.clipboard.writeText(swap.address); setIsCopied(true); }, []); const token = type === 'transfer' ? swap.fromToken : swap.toToken; From 084ab9f356364dfbe78937650e2147a9afbfc5aa Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Fri, 21 Apr 2023 14:21:36 +0800 Subject: [PATCH 15/52] feat: add advanced settings to the confirmation page --- src/app/screens/swap/index.tsx | 11 ++- .../advanceSettings/index.tsx | 70 +++++++++++++++++++ .../screens/swap/swapConfirmation/index.tsx | 8 ++- .../swap/swapConfirmation/useConfirmSwap.tsx | 21 +++--- src/app/screens/swap/useSwap.tsx | 25 ++++++- src/locales/en.json | 1 + 6 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 850b8411a..dd575be43 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -51,6 +51,7 @@ function SwapScreen() { const swap = useSwap(); const [selecting, setSelecting] = useState<'from' | 'to'>(); + const [loading, setLoading] = useState(false); return ( <> @@ -94,7 +95,15 @@ function SwapScreen() { disabled={swap.onSwap == null} warning={!!swap.submitError} text={swap.submitError ?? t('CONTINUE')} - onPress={swap.onSwap!} + processing={loading} + onPress={async () => { + try { + setLoading(true); + await swap.onSwap?.(); + } finally { + setLoading(false); + } + }} />
diff --git a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx new file mode 100644 index 000000000..ef8c0f2ac --- /dev/null +++ b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx @@ -0,0 +1,70 @@ +import { useCallback, useState } from 'react'; +import { microstacksToStx, stxToMicrostacks } from '@secretkeylabs/xverse-core/currency'; +import TransactionSettingAlert from '@components/transactionSetting'; +import BigNumber from 'bignumber.js'; +import { setFee, setNonce } from '@secretkeylabs/xverse-core'; +import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; +import styled from 'styled-components'; +import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; +import { useTranslation } from 'react-i18next'; + +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 ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +type Props = { + swap: SwapConfirmationInput; +}; + +export function AdvanceSettings({ swap }: Props) { + const [showModal, setShowModal] = useState(false); + const onApplyClick = useCallback((settingFee: string, nonce?: string) => { + const fee = BigInt(stxToMicrostacks(new BigNumber(settingFee) as any).toString()); + swap.unsignedTx.setFee(fee); + if (nonce != null) { + swap.unsignedTx.setNonce(BigInt(nonce)); + } + setShowModal(false); + }, []); + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); + return ( + <> + + setShowModal(false)} + /> + + ); +} diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index b86aeb020..d27ab0aeb 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -5,12 +5,13 @@ import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; import FunctionBlock from '@screens/swap/swapConfirmation/functionBlock'; import ActionButton from '@components/button'; -import FreesBlock from '@screens/swap/swapConfirmation/freesBlock'; +import FeesBlock from '@screens/swap/swapConfirmation/freesBlock'; import RouteBlock from '@screens/swap/swapConfirmation/routeBlock'; import StxInfoBlock from '@screens/swap/swapConfirmation/stxInfoBlock'; 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'; const TitleText = styled.div((props) => ({ fontSize: 21, @@ -57,13 +58,14 @@ export default function SwapConfirmation() { {t('TOKEN_SWAP')} - + - + diff --git a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx index 02a07f044..c3f82fb4a 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -3,7 +3,12 @@ import { ReactNode } from 'react'; import { Currency } from 'alex-sdk'; import useWalletSelector from '@hooks/useWalletSelector'; import { broadcastSignedTransaction, signTransaction } from '@secretkeylabs/xverse-core'; -import { makeUnsignedContractCall, AnchorMode, PostConditionMode } from '@stacks/transactions'; +import { + makeUnsignedContractCall, + AnchorMode, + PostConditionMode, + StacksTransaction, +} from '@stacks/transactions'; import type { TxToBroadCast } from 'alex-sdk/dist/helpers/SwapHelper'; import useNetworkSelector from '@hooks/useNetwork'; import { useNavigate } from 'react-router-dom'; @@ -19,7 +24,7 @@ export type SwapConfirmationInput = { lpFeeFiatAmount?: number; address: string; routers: { image: ReactNode; name: string }[]; - txToBroadcast: TxToBroadCast; + unsignedTx: StacksTransaction; }; export function useConfirmSwap( @@ -31,18 +36,8 @@ export function useConfirmSwap( return { ...input, onConfirm: async () => { - const tx = await makeUnsignedContractCall({ - publicKey: stxPublicKey, - contractAddress: input.txToBroadcast.contractAddress, - contractName: input.txToBroadcast.contractName, - functionName: input.txToBroadcast.functionName, - functionArgs: input.txToBroadcast.functionArgs as any, - anchorMode: AnchorMode.Any, - postConditionMode: PostConditionMode.Deny, - postConditions: input.txToBroadcast.postConditions, - }); const signed = await signTransaction( - tx, + input.unsignedTx, seedPhrase, selectedAccount?.id ?? 0, selectedNetwork diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 90eea3ed8..735dcc98c 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -10,6 +10,7 @@ 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'; export type SwapToken = { name: string; @@ -44,8 +45,15 @@ export function useSwap(): UseSwap { const navigate = useNavigate(); const alexSDK = useState(() => new AlexSDK())[0]; const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { coinsList, stxAvailableBalance, stxBtcRate, btcFiatRate, fiatCurrency, stxAddress } = - useWalletSelector(); + const { + coinsList, + stxAvailableBalance, + stxBtcRate, + btcFiatRate, + fiatCurrency, + stxAddress, + stxPublicKey, + } = useWalletSelector(); const acceptableCoinList = coinsList?.filter((c) => alexSDK.getCurrencyFrom(c.principal) != null) ?? []; @@ -208,6 +216,17 @@ export function useSwap(): UseSwap { BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), info.route ); + const unsignedTx = await makeUnsignedContractCall({ + publicKey: stxPublicKey, + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs as any, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Deny, + postConditions: tx.postConditions, + }); + const state: SwapConfirmationInput = { from: from!, to: to!, @@ -219,7 +238,7 @@ export function useSwap(): UseSwap { lpFeeAmount: info.feeRate * fromAmount!, lpFeeFiatAmount: currencyToToken(from!, info.feeRate * fromAmount!)?.fiatAmount, routers: info.route.map(currencyToToken).filter(isNotNull), - txToBroadcast: tx, + unsignedTx, }; navigate('/swap-confirm', { state, diff --git a/src/locales/en.json b/src/locales/en.json index 339f89274..a9200cca8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -590,6 +590,7 @@ "ROUTE": "Route", "COPIED": "Copied", "COPY_YOUR_ADDRESS": "copy your address", + "ADVANCED_SETTING": "Advanced settings", "ROUTE_DESC": "For the transaction to proceed, your BTC will be swapped for xBTC and STX. There is no additional cost for you and no STX will be added to your wallet." } } From 2a2afa76f26e338df9e525dd33c544d57b01ee56 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Wed, 26 Apr 2023 17:07:03 +0800 Subject: [PATCH 16/52] feat: bump alex-sdk to latest and fix the issue where from is the same as to --- package.json | 2 +- src/app/screens/swap/useSwap.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9edad87f2..65415c735 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "alex-sdk": "^0.0.5", + "alex-sdk": "^0.1.9", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 735dcc98c..2d8249013 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -132,7 +132,7 @@ export function useSwap(): UseSwap { }>(); useEffect(() => { - if (from == null || to == null) { + if (from == null || to == null || from === to) { setInfo(undefined); } else { let cancelled = false; @@ -154,7 +154,7 @@ export function useSwap(): UseSwap { const [exchangeRate, setExchangeRate] = useState(); useEffect(() => { - if (from == null || to == null || fromAmount == null || fromAmount == 0) { + if (from == null || to == null || fromAmount == null || fromAmount == 0 || from === to) { setExchangeRate(undefined); } else { let cancelled = false; From 7c2243c2d78b5b6a3be3ee3e91dd66dceae12275 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Wed, 26 Apr 2023 17:19:58 +0800 Subject: [PATCH 17/52] feat: add math.floor before converting to bigInt --- src/app/screens/swap/useSwap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 2d8249013..c36a55074 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -158,7 +158,7 @@ export function useSwap(): UseSwap { setExchangeRate(undefined); } else { let cancelled = false; - alexSDK.getAmountTo(from, BigInt(fromAmount * 1e8), to).then((result) => { + alexSDK.getAmountTo(from, BigInt(Math.floor(fromAmount * 1e8)), to).then((result) => { if (cancelled) { return; } @@ -212,7 +212,7 @@ export function useSwap(): UseSwap { stxAddress, from, to, - BigInt(fromAmount * 1e8), + BigInt(Math.floor(fromAmount * 1e8)), BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), info.route ); From 7b44f27d12362434b3e2661ccfa86b0a18ab63fb Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Wed, 26 Apr 2023 17:29:13 +0800 Subject: [PATCH 18/52] fix: skip action when input is invalid --- src/app/screens/swap/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index dd575be43..781b96f35 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -97,6 +97,9 @@ function SwapScreen() { text={swap.submitError ?? t('CONTINUE')} processing={loading} onPress={async () => { + if (swap.submitError) { + return; + } try { setLoading(true); await swap.onSwap?.(); From d8152ac9f1fff244053870f0067313748f527a8d Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Fri, 5 May 2023 16:05:57 +0800 Subject: [PATCH 19/52] chore: update package.lock --- package-lock.json | 55 +++++++---------------------------------------- 1 file changed, 8 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8c05f8da..6511cf152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "alex-sdk": "^0.0.4", + "alex-sdk": "^0.1.9", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", @@ -4240,57 +4240,18 @@ } }, "node_modules/alex-sdk": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.0.4.tgz", - "integrity": "sha512-a5HTTPHp8s2dmSFZLocsJvAHIStkauAJr8RHgD89JDF0FeK8FcKne8X8RMtc8tQUA1Vmu6xCDZqtLr9/DIEXyQ==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.11.tgz", + "integrity": "sha512-hJvcwWLBcqQuXMwZCGLnQXaBYewInadLajdp/Pwbl/sQ2qFmjt2tHcOvqG6MBGH2YBQadxpRnmTAoLIakX02Og==", "dependencies": { - "@stacks/transactions": "^6.2.0", "clarity-codegen": "^0.2.0" }, "engines": { "node": ">=10" - } - }, - "node_modules/alex-sdk/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/alex-sdk/node_modules/@stacks/common": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.0.0.tgz", - "integrity": "sha512-tETwccvbYvaZ7u3ZucWNMOIPN97r6IPeZXKIFhLc1KSVaWSGEPTtZcwVp+Rz3mu2XgI2pg37SUrOWXSL7OOkDw==", - "dependencies": { - "@types/bn.js": "^5.1.0", - "@types/node": "^18.0.4" - } - }, - "node_modules/alex-sdk/node_modules/@stacks/network": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.3.0.tgz", - "integrity": "sha512-573ZldQ+Iy0nCCxprXLLvkAo1AMEXncfmMUvqQ+5TN3m7VqCVADtb5G5WzMZsyR4m/k9oPsv076Lmqyl8AtR2A==", - "dependencies": { - "@stacks/common": "^6.0.0", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/alex-sdk/node_modules/@stacks/transactions": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.5.0.tgz", - "integrity": "sha512-kwE8cZq+QdAum4/LC+lSlAXVvzkdsSHTkCbfg4+VCWPBqA+gdXEqZe6R9SNBtMb8yGQrqUY8uIGRLVCWcXJ8zQ==", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.0.0", - "@stacks/network": "^6.3.0", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" + }, + "peerDependencies": { + "@stacks/network": "*", + "@stacks/transactions": "*" } }, "node_modules/ansi-escapes": { From f3318b38d84ac02ffe0c7b632053dd885bc7a677 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Sun, 7 May 2023 14:39:51 +0800 Subject: [PATCH 20/52] feat(swap): update to support amm v1_1 pools --- package-lock.json | 8 ++++---- package.json | 2 +- .../screens/swap/swapConfirmation/functionBlock/index.tsx | 4 +++- src/app/screens/swap/swapConfirmation/index.tsx | 2 +- src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx | 1 + src/app/screens/swap/useSwap.tsx | 1 + 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6511cf152..9947e2828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,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.9", + "alex-sdk": "^0.1.13", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", @@ -4240,9 +4240,9 @@ } }, "node_modules/alex-sdk": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.11.tgz", - "integrity": "sha512-hJvcwWLBcqQuXMwZCGLnQXaBYewInadLajdp/Pwbl/sQ2qFmjt2tHcOvqG6MBGH2YBQadxpRnmTAoLIakX02Og==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.13.tgz", + "integrity": "sha512-J0tlVLTzyxWn1cd8GijFTGRGX7fQBkg68i6w+QVfJzMHwyemkOjf673oSyUma8wURU3xb0wDTwBCk+Du7xXo6A==", "dependencies": { "clarity-codegen": "^0.2.0" }, diff --git a/package.json b/package.json index 65415c735..b3b81fc2a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,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.9", + "alex-sdk": "^0.1.13", "argon2-browser": "^1.18.0", "axios": "^1.1.3", "bignumber.js": "^9.1.0", diff --git a/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx index c84bad1a9..803fd85c5 100644 --- a/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx @@ -2,10 +2,12 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Container, TitleContainer, TitleText } from '@screens/swap/swapConfirmation/stxInfoBlock'; -const FunctionName = styled.div((props) => ({ +const FunctionName = styled.pre((props) => ({ + marginLeft: 10, color: props.theme.colors.white[0], fontSize: 14, fontWeight: 500, + textAlign: 'right' })); interface FunctionBlockProps { diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index d27ab0aeb..e41433f99 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -58,7 +58,7 @@ export default function SwapConfirmation() { {t('TOKEN_SWAP')} - + Date: Fri, 23 Jun 2023 10:15:17 +0800 Subject: [PATCH 21/52] chore: fix lint warnings --- src/app/screens/home/index.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index a8405c33f..1a071155b 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -1,4 +1,6 @@ /* eslint-disable no-await-in-loop */ +import styled from 'styled-components'; +import Swap from '@assets/img/dashboard/swap.svg'; import SIP10Icon from '@assets/img/dashboard/SIP10.svg'; import ArrowDownLeft from '@assets/img/dashboard/arrow_down_left.svg'; import ArrowUpRight from '@assets/img/dashboard/arrow_up_right.svg'; @@ -8,10 +10,11 @@ import CreditCard from '@assets/img/dashboard/credit_card.svg'; import ListDashes from '@assets/img/dashboard/list_dashes.svg'; import OrdinalsIcon from '@assets/img/dashboard/ordinalBRC20.svg'; import IconStacks from '@assets/img/dashboard/stack_icon.svg'; -import Swap from '@assets/img/swap/swap.svg'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ReceiveCardComponent from '@components/receiveCardComponent'; +import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; +import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; import BottomBar from '@components/tabBar'; import TokenTile from '@components/tokenTile'; import useAppConfig from '@hooks/queries/useAppConfig'; @@ -28,12 +31,9 @@ import { CurrencyTypes } from '@utils/constants'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import Theme from 'theme'; import BalanceCard from './balanceCard'; import SquareButton from './squareButton'; -import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; -import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; export const Container = styled.div` display: flex; @@ -101,11 +101,6 @@ const RowButtonContainer = styled.div((props) => ({ marginTop: props.theme.spacing(11), })); -const ButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(11), - columnGap: props.theme.spacing(11), -})); - const TokenListButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', From b06df1da29dfa12d4b4eee20a8d181b1c0dd2f28 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 23 Jun 2023 10:15:39 +0800 Subject: [PATCH 22/52] fix: update swap svg stroke and add missing translation --- src/assets/img/dashboard/swap.svg | 8 ++++---- src/locales/en.json | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/assets/img/dashboard/swap.svg b/src/assets/img/dashboard/swap.svg index 30b8b21c6..8d7c39210 100644 --- a/src/assets/img/dashboard/swap.svg +++ b/src/assets/img/dashboard/swap.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/src/locales/en.json b/src/locales/en.json index a9200cca8..753647ca5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -562,6 +562,7 @@ "SWAP": "Swap", "CONVERT": "Convert", "BALANCE": "Balance", + "FROM": "From", "TO": "To", "SELECT_COIN": "Select asset", "DETAILS": "Details", From 89961fdeeb6baba603615aa5305314e3300fc5e9 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 13:13:53 +0800 Subject: [PATCH 23/52] fix: add key to fix react warnings --- src/app/screens/home/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 1a071155b..3f148bd54 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -304,6 +304,7 @@ function Home() { ft.visible) .map((coin) => ( ( Date: Tue, 27 Jun 2023 13:48:07 +0800 Subject: [PATCH 24/52] chore: fix lint warnings --- src/app/screens/swap/swapTokenBlock/index.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 79346c9be..6c647f93e 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -1,11 +1,6 @@ import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; -import { Coin } from '@secretkeylabs/xverse-core/types/api/xverse/coins'; import ChevronIcon from '@assets/img/swap/chevron.svg'; -import { FungibleToken } from '@secretkeylabs/xverse-core'; -import { ftDecimals } from '@utils/helper'; -import { microstacksToStx } from '@secretkeylabs/xverse-core/currency'; -import BigNumber from 'bignumber.js'; import { SwapToken } from '@screens/swap/useSwap'; const Container = styled.div((props) => ({ @@ -57,10 +52,10 @@ const CoinButtonContainer = styled.button((props) => ({ alignItems: 'center', })); -const CoinButtonArrow = styled.img((props) => ({ +const CoinButtonArrow = styled.img({ width: 12, height: 12, -})); +}); export const AmountTex = styled.input<{ error?: boolean }>((props) => ({ ...props.theme.body_bold_l, @@ -92,34 +87,41 @@ type SwapTokenBlockProps = { error?: boolean; }; -function SwapTokenBlock(props: SwapTokenBlockProps) { +function SwapTokenBlock({ + title, + selectedCoin, + amount, + onAmountChange, + onSelectCoin, + error, +}: SwapTokenBlockProps) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); return ( - {props.title} + {title} {t('BALANCE')}: - {props.selectedCoin?.balance ?? '--'} + {selectedCoin?.balance ?? '--'} - - {props.selectedCoin?.image} - {props.selectedCoin?.name ?? t('SELECT_COIN')} + + {selectedCoin?.image} + {selectedCoin?.name ?? t('SELECT_COIN')} props.onAmountChange?.(e.target.value)} + disabled={onAmountChange == null} + value={amount ?? selectedCoin?.amount?.toString() ?? ''} + onChange={(e) => onAmountChange?.(e.target.value)} /> - {props.selectedCoin?.fiatAmount ? `≈ $ ${props.selectedCoin.fiatAmount} USD` : '--'} + {selectedCoin?.fiatAmount ? `≈ $ ${selectedCoin.fiatAmount} USD` : '--'} From 49b61275074f631abdc61bcfcc87431bb266e4ca Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:20:02 +0800 Subject: [PATCH 25/52] fix: add error 700 border to swap token card --- src/app/screens/swap/swapTokenBlock/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 6c647f93e..2f4ac6e0e 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -31,16 +31,22 @@ const BalanceText = styled.h1((props) => ({ marginRight: props.theme.spacing(2), })); -const CardContainer = styled.div((props) => ({ +const CardContainer = styled.div<{ error?: boolean }>((props) => ({ display: 'flex', flexDirection: 'column', rowGap: props.theme.spacing(3), background: props.theme.colors.background.elevation1, border: `1px solid ${props.theme.colors.background.elevation2}`, + 'border-color': props.error + ? props.theme.colors.feedback.error_700 + : props.theme.colors.background.elevation6, borderRadius: 8, padding: props.theme.spacing(8), ':focus-within': { - border: `1px solid ${props.theme.colors.background.elevation6}`, + border: `1px solid`, + 'border-color': props.error + ? props.theme.colors.feedback.error_700 + : props.theme.colors.background.elevation6, }, })); @@ -104,7 +110,7 @@ function SwapTokenBlock({ {t('BALANCE')}: {selectedCoin?.balance ?? '--'} - + {selectedCoin?.image} From 81c5dbf42a8f9d419a83ff25e3408646232837c3 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:20:57 +0800 Subject: [PATCH 26/52] fix: update swap error message and restrict input to number --- src/app/screens/swap/swapTokenBlock/index.tsx | 15 +++++++++++++-- src/app/screens/swap/useSwap.tsx | 8 ++++---- src/locales/en.json | 6 +++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 2f4ac6e0e..f1a724e1c 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -63,7 +63,17 @@ const CoinButtonArrow = styled.img({ height: 12, }); -export const AmountTex = styled.input<{ error?: boolean }>((props) => ({ +export const NumberInput = styled.input` + ::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + ::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +`; +export const AmountInput = styled(NumberInput)<{ error?: boolean }>((props) => ({ ...props.theme.body_bold_l, flex: 1, color: props.error ? props.theme.colors.feedback.error : props.theme.colors.white['0'], @@ -117,12 +127,13 @@ function SwapTokenBlock({ {selectedCoin?.name ?? t('SELECT_COIN')} - onAmountChange?.(e.target.value)} + type="number" /> diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 2c367fc20..66a1e9617 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -63,7 +63,7 @@ export function useSwap(): UseSwap { const [from, setFrom] = useState(); const [to, setTo] = useState(); - const fromAmount = isNaN(Number(inputAmount)) ? undefined : Number(inputAmount); + const fromAmount = Number.isNaN(Number(inputAmount)) ? undefined : Number(inputAmount); function currencyToToken(currency?: Currency, amount?: number): SwapToken | undefined { if (currency == null) { @@ -116,13 +116,13 @@ export function useSwap(): UseSwap { function onSelectToken(token: 'STX' | FungibleToken, side: 'from' | 'to') { (side === 'from' ? setFrom : setTo)( - token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)! + token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)!, ); } const fromToken = currencyToToken(from, fromAmount); const inputAmountInvalid = - isNaN(Number(inputAmount)) || + Number.isNaN(Number(inputAmount)) || (fromAmount != null && (fromAmount < 0 || (fromToken?.balance != null && fromToken.balance < fromAmount))); @@ -204,7 +204,7 @@ export function useSwap(): UseSwap { ? `${roundForDisplay(info.feeRate * fromAmount)} ${fromToken.name}` : undefined, }, - submitError: inputAmountInvalid ? 'Invalid amount' : undefined, + submitError: inputAmountInvalid ? t('ERRORS.INSUFFICIENT_BALANCE_FEES') : undefined, onSwap: fromAmount != null && toAmount != null && from != null && to != null && info != null ? async () => { diff --git a/src/locales/en.json b/src/locales/en.json index 753647ca5..2b5c67701 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -574,7 +574,11 @@ "POWERED_BY_ALEX": "Powered by ALEX", "CONTINUE": "Continue", "SLIPPAGE_DESC": "Your transaction will reverse if the price changes unfavorably by more of this percentage.", - "APPLY": "Apply" + "APPLY": "Apply", + "ERRORS": { + "INVALID_AMOUNT": "Invalid amount", + "INSUFFICIENT_BALANCE_FEES": "Insufficient balance" + } }, "SWAP_CONFIRM_SCREEN": { "TOKEN_SWAP": "Token swap", From ea886a3a4a868cf0b2b5e73c3f8fd4e09d2c45ec Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:22:28 +0800 Subject: [PATCH 27/52] chore: prettier --- src/app/screens/swap/useSwap.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 66a1e9617..1ba60533e 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -50,7 +50,7 @@ export function useSwap(): UseSwap { stxAvailableBalance, stxBtcRate, btcFiatRate, - fiatCurrency, + // fiatCurrency, stxAddress, stxPublicKey, } = useWalletSelector(); @@ -72,7 +72,7 @@ export function useSwap(): UseSwap { if (currency === Currency.STX) { return { balance: Number(microstacksToStx(BigNumber(stxAvailableBalance) as any)), - image: , + image: , name: 'STX', amount, fiatAmount: @@ -82,7 +82,7 @@ export function useSwap(): UseSwap { }; } const token = acceptableCoinList.find( - (c) => alexSDK.getCurrencyFrom(c.principal) === currency + (c) => alexSDK.getCurrencyFrom(c.principal) === currency, )!; if (token == null) { return undefined; @@ -90,7 +90,7 @@ export function useSwap(): UseSwap { return { amount, image: ( - + ), name: (token.ticker ?? token.name).toUpperCase(), balance: Number(ftDecimals(token.balance, token.decimals ?? 0)), @@ -106,7 +106,7 @@ export function useSwap(): UseSwap { return 'STX'; } const token = acceptableCoinList.find( - (c) => alexSDK.getCurrencyFrom(c.principal) === currency + (c) => alexSDK.getCurrencyFrom(c.principal) === currency, )!; if (token == null) { return currency; @@ -154,7 +154,7 @@ export function useSwap(): UseSwap { const [exchangeRate, setExchangeRate] = useState(); useEffect(() => { - if (from == null || to == null || fromAmount == null || fromAmount == 0 || from === to) { + if (from == null || to == null || fromAmount == null || fromAmount === 0 || from === to) { setExchangeRate(undefined); } else { let cancelled = false; @@ -184,11 +184,11 @@ export function useSwap(): UseSwap { return { coinsList: acceptableCoinList, isLoadingWalletData: false, - inputAmount: inputAmount, + inputAmount, onInputAmountChanged: setInputAmount, selectedFromToken: fromToken, selectedToToken: toToken, - slippage: slippage, + slippage, onSlippageChanged: setSlippage, onSelectToken, inputAmountInvalid, @@ -214,7 +214,7 @@ export function useSwap(): UseSwap { to, BigInt(Math.floor(fromAmount * 1e8)), BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), - info.route + info.route, ); const unsignedTx = await makeUnsignedContractCall({ publicKey: stxPublicKey, From ed6ae7562f6110b50aedf05c8a94823039a2ebb1 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:22:39 +0800 Subject: [PATCH 28/52] style: add a not-allowed cursor to disabled buttons --- src/app/components/button/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/button/index.tsx b/src/app/components/button/index.tsx index 8128c41f5..b7e5e5c98 100644 --- a/src/app/components/button/index.tsx +++ b/src/app/components/button/index.tsx @@ -17,6 +17,7 @@ const Button = styled.button((props) => ({ height: 44, opacity: props.disabled ? 0.6 : 1, transition: 'all 0.2s ease', + cursor: props.disabled ? 'not-allowed' : 'auto', })); const AnimatedButton = styled(Button)` From 8686541a575aa45f8252a053dfc96124152fdb28 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:36:11 +0800 Subject: [PATCH 29/52] fix: update translations --- src/app/screens/swap/index.tsx | 2 +- src/locales/en.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 781b96f35..945848139 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -86,7 +86,7 @@ function SwapScreen() { }} visible={!!selecting} coins={swap.coinsList} - title={selecting === 'from' ? t('FROM') : t('TO')} + title={selecting === 'from' ? t('ASSET_TO_CONVERT_FROM') : t('ASSET_TO_CONVERT_TO')} loadingWalletData={swap.isLoadingWalletData} /> )} diff --git a/src/locales/en.json b/src/locales/en.json index 2b5c67701..ecfd376f3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -564,6 +564,8 @@ "BALANCE": "Balance", "FROM": "From", "TO": "To", + "ASSET_TO_CONVERT_FROM": "Asset to convert from", + "ASSET_TO_CONVERT_TO": "Asset to convert to", "SELECT_COIN": "Select asset", "DETAILS": "Details", "MIN_RECEIVE": "Minimum received", From c9699176cbc228105ce60480903c1b4663b02c8b Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:46:18 +0800 Subject: [PATCH 30/52] fix: use background elevation_1 --- src/app/components/passwordInput/index.tsx | 4 ++-- src/app/components/seedPhraseView/index.tsx | 2 +- src/app/components/sendForm/index.tsx | 6 +++--- src/app/screens/error/index.tsx | 2 +- src/app/screens/sendBrc20/brc20TransferForm.tsx | 4 ++-- src/app/screens/settings/changeNetwork/index.tsx | 4 ++-- src/app/screens/swap/swapTokenBlock/index.tsx | 8 ++++---- src/theme/index.ts | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/app/components/passwordInput/index.tsx b/src/app/components/passwordInput/index.tsx index 7f156b714..2b4332168 100644 --- a/src/app/components/passwordInput/index.tsx +++ b/src/app/components/passwordInput/index.tsx @@ -56,7 +56,7 @@ const PasswordInputContainer = styled.div((props) = border: `1px solid ${ props.hasError ? 'rgba(211, 60, 60, 0.3)' : props.theme.colors.background.elevation3 }`, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, borderRadius: props.theme.radius(1), paddingLeft: props.theme.spacing(4), paddingRight: props.theme.spacing(4), @@ -73,7 +73,7 @@ const PasswordInputLabel = styled.h2((props) => ({ const Input = styled.input((props) => ({ ...props.theme.body_medium_m, height: 44, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, color: props.theme.colors.white['0'], width: '100%', border: 'none', diff --git a/src/app/components/seedPhraseView/index.tsx b/src/app/components/seedPhraseView/index.tsx index 54b83c526..7ecc93940 100644 --- a/src/app/components/seedPhraseView/index.tsx +++ b/src/app/components/seedPhraseView/index.tsx @@ -29,7 +29,7 @@ const SeedContainer = styled.div((props) => ({ })); const OuterSeedContainer = styled.div((props) => ({ - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, border: `1px solid ${props.theme.colors.background.elevation3}`, borderRadius: props.theme.radius(1), })); diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index a67218c6d..a745b19b0 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -108,7 +108,7 @@ const BalanceText = styled.h1((props) => ({ const InputField = styled.input((props) => ({ ...props.theme.body_m, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, color: props.theme.colors.white['0'], width: '100%', border: 'transparent', @@ -121,7 +121,7 @@ const AmountInputContainer = styled.div((props) => ({ marginTop: props.theme.spacing(4), marginBottom: props.theme.spacing(4), border: props.error ? '1px solid rgba(211, 60, 60, 0.3)' : `1px solid ${props.theme.colors.background.elevation3}`, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, borderRadius: 8, paddingLeft: props.theme.spacing(5), paddingRight: props.theme.spacing(5), @@ -137,7 +137,7 @@ const MemoInputContainer = styled.div((props) => ({ marginTop: props.theme.spacing(4), marginBottom: props.theme.spacing(4), border: props.error ? '1px solid rgba(211, 60, 60, 0.3)' : `1px solid ${props.theme.colors.background.elevation3}`, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, borderRadius: 8, padding: props.theme.spacing(7), height: 76, diff --git a/src/app/screens/error/index.tsx b/src/app/screens/error/index.tsx index 2c631ed6f..c4042412e 100644 --- a/src/app/screens/error/index.tsx +++ b/src/app/screens/error/index.tsx @@ -12,7 +12,7 @@ const ScreenContainer = styled.div((props) => ({ width: '100vw', flexDirection: 'column', alignItems: 'center', - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, paddingTop: props.theme.spacing(80), paddingLeft: props.theme.spacing(9), paddingRight: props.theme.spacing(9), diff --git a/src/app/screens/sendBrc20/brc20TransferForm.tsx b/src/app/screens/sendBrc20/brc20TransferForm.tsx index b633bcb96..5f99c232b 100644 --- a/src/app/screens/sendBrc20/brc20TransferForm.tsx +++ b/src/app/screens/sendBrc20/brc20TransferForm.tsx @@ -40,7 +40,7 @@ const AmountInputContainer = styled.div((props) => ({ border: props.error ? '1px solid rgba(211, 60, 60, 0.3)' : `1px solid ${props.theme.colors.background.elevation3}`, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, borderRadius: 8, paddingLeft: props.theme.spacing(5), paddingRight: props.theme.spacing(5), @@ -62,7 +62,7 @@ const InputFieldContainer = styled.div(() => ({ const InputField = styled.input((props) => ({ ...props.theme.body_m, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, color: props.theme.colors.white['0'], width: '100%', border: 'transparent', diff --git a/src/app/screens/settings/changeNetwork/index.tsx b/src/app/screens/settings/changeNetwork/index.tsx index 44ed7989c..d28be51b6 100644 --- a/src/app/screens/settings/changeNetwork/index.tsx +++ b/src/app/screens/settings/changeNetwork/index.tsx @@ -50,7 +50,7 @@ const InputContainer = styled.div((props) => ({ alignItems: 'center', width: '100%', border: `1px solid ${props.theme.colors.background.elevation3}`, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, borderRadius: props.theme.radius(1), paddingLeft: props.theme.spacing(4), paddingRight: props.theme.spacing(4), @@ -75,7 +75,7 @@ const Input = styled.input((props) => ({ height: 44, display: 'flex', flex: 1, - backgroundColor: props.theme.colors.background['elevation-1'], + backgroundColor: props.theme.colors.background.elevation_1, color: props.theme.colors.white['0'], border: 'none', })); diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index f1a724e1c..94c4978fe 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -35,15 +35,15 @@ const CardContainer = styled.div<{ error?: boolean }>((props) => ({ display: 'flex', flexDirection: 'column', rowGap: props.theme.spacing(3), - background: props.theme.colors.background.elevation1, - border: `1px solid ${props.theme.colors.background.elevation2}`, + background: props.theme.colors.background.elevation_1, + border: '1px solid', 'border-color': props.error ? props.theme.colors.feedback.error_700 - : props.theme.colors.background.elevation6, + : props.theme.colors.background.elevation2, borderRadius: 8, padding: props.theme.spacing(8), ':focus-within': { - border: `1px solid`, + border: '1px solid', 'border-color': props.error ? props.theme.colors.feedback.error_700 : props.theme.colors.background.elevation6, diff --git a/src/theme/index.ts b/src/theme/index.ts index 3ff88899d..57c1d5463 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -15,9 +15,9 @@ const Theme = { 900: 'rgba(255, 255, 255, 0.1)', }, background: { - 'elevation-1': '#070A13', elevation0: '#12151E', - elevation1: '#1D2032', + elevation1: '#1D2032', // deprecated? + elevation_1: '#070A13', elevation2: '#272A44', elevation3: '#303354', elevation6: '#4C5187', From e7f56f0c0286b8a1a1a8d07498b38aa56522f043 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 15:50:51 +0800 Subject: [PATCH 31/52] style: update swap details spacing --- src/app/screens/swap/swapInfoBlock/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index c21c1a5a9..7e354f495 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -26,7 +26,7 @@ const DL = styled.dl((props) => ({ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', - rowGap: props.theme.spacing(4), + rowGap: props.theme.spacing(8), })); const DT = styled.dt((props) => ({ From 5a5540ee4547354018b6f38b81b3709b038f8791 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 16:04:12 +0800 Subject: [PATCH 32/52] style: use unicode arrow right for swap route details --- src/app/screens/swap/useSwap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 1ba60533e..5d5dbcfb0 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -198,7 +198,7 @@ export function useSwap(): UseSwap { exchangeRate != null && fromToken != null && toToken != null ? `1 ${fromToken.name} = ${roundForDisplay(exchangeRate)} ${toToken.name}` : undefined, - route: info?.route.map(getCurrencyName).join(' -> '), + route: info?.route.map(getCurrencyName).join(' \u2192 '), lpFee: info?.feeRate != null && fromAmount != null && fromToken != null ? `${roundForDisplay(info.feeRate * fromAmount)} ${fromToken.name}` From 6592b2c09ff8ba7cc9c4a068932932dde51e96ba Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 17:15:25 +0800 Subject: [PATCH 33/52] feat: implement toggle tokens button in swaps --- src/app/screens/swap/index.tsx | 11 +++++++++-- src/app/screens/swap/useSwap.tsx | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 945848139..bc48c9b3c 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -32,10 +32,15 @@ const Container = styled.div((props) => ({ marginTop: props.theme.spacing(16), })); -const DownArrow = styled.img((props) => ({ +const DownArrowButton = styled.button((props) => ({ alignSelf: 'center', + borderRadius: props.theme.radius(3), width: props.theme.spacing(18), height: props.theme.spacing(18), + transition: 'all 0.2s ease', + ':hover': { + opacity: 0.8, + }, })); const SendButtonContainer = styled.div((props) => ({ @@ -66,7 +71,9 @@ function SwapScreen() { onAmountChange={swap.onInputAmountChanged} onSelectCoin={() => setSelecting('from')} /> - + + arrow-down + void; + handleClickDownArrow: (event: React.MouseEvent) => void; submitError?: string; swapInfo?: { exchangeRate?: string; @@ -177,6 +178,12 @@ export function useSwap(): UseSwap { return Math.floor(input * 1000) / 1000; } + const toggleFromToTokens = () => { + const prevFrom = from; + setFrom(to); + setTo(prevFrom); + } + const toAmount = exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; @@ -192,6 +199,7 @@ export function useSwap(): UseSwap { onSlippageChanged: setSlippage, onSelectToken, inputAmountInvalid, + handleClickDownArrow: toggleFromToTokens, minReceived: toAmount != null ? roundForDisplay(toAmount * (1 - slippage)) : undefined, swapInfo: { exchangeRate: From f4b601bd6f0463507c9110cf8564e08212334058 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 27 Jun 2023 17:40:40 +0800 Subject: [PATCH 34/52] feat: add reset slippage button to swaps --- src/app/screens/swap/slippageModal/index.tsx | 38 ++++++++++++++++---- src/locales/en.json | 1 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/app/screens/swap/slippageModal/index.tsx b/src/app/screens/swap/slippageModal/index.tsx index f83137594..ffe227c6d 100644 --- a/src/app/screens/swap/slippageModal/index.tsx +++ b/src/app/screens/swap/slippageModal/index.tsx @@ -1,4 +1,3 @@ -import BottomModal from '@components/bottomModal'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { useState } from 'react'; @@ -11,6 +10,20 @@ const Container = styled.div` row-gap: 16px; `; +const TitleRow = styled.div` + display: flex; + justify-content: space-between; +`; +const ResetButton = styled.button((props) => ({ + display: 'inline', + background: 'transparent', + color: props.theme.colors.orange_main, + ...props.theme.body_medium_m, + ':hover': { + opacity: 0.8, + }, +})); + const Title = styled.div((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white['0'], @@ -37,19 +50,29 @@ const Description = styled.p((props) => ({ color: props.theme.colors.white['400'], })); -export function SlippageModalContent(props: { +const DEFAULT_SLIPPAGE = '4%'; +export function SlippageModalContent({ + slippage, + onChange, +}: { slippage: number; onChange: (slippage: number) => void; }) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const [percentage, setPercentage] = useState((props.slippage * 100).toString() + '%'); + const [percentage, setPercentage] = useState(`${(slippage * 100).toString()}%`); const result = Number(percentage.replace('%', '')); - let invalid = isNaN(result) || result >= 100 || result <= 0; + const invalid = Number.isNaN(result) || result >= 100 || result <= 0; + + const handleClickResetSlippage = () => setPercentage(DEFAULT_SLIPPAGE); + return ( - {t('SLIPPAGE')} + + {t('SLIPPAGE')} + {t('RESET_TO_DEFAULT')} + setPercentage(e.target.value)} onFocus={(e) => { @@ -62,8 +85,9 @@ export function SlippageModalContent(props: { disabled={invalid} warning={invalid} text={t('APPLY')} - onPress={() => props.onChange(result / 100)} + onPress={() => onChange(result / 100)} /> ); } +export default SlippageModalContent; diff --git a/src/locales/en.json b/src/locales/en.json index ecfd376f3..8ba271414 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -577,6 +577,7 @@ "CONTINUE": "Continue", "SLIPPAGE_DESC": "Your transaction will reverse if the price changes unfavorably by more of this percentage.", "APPLY": "Apply", + "RESET_TO_DEFAULT": "Reset to default", "ERRORS": { "INVALID_AMOUNT": "Invalid amount", "INSUFFICIENT_BALANCE_FEES": "Insufficient balance" From 9421f7d589c591597d5708f711bf6939a82432f6 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 28 Jun 2023 18:30:40 +0800 Subject: [PATCH 35/52] fix: refactor select currency logic to use reducer and unit test --- jest.config.js | 10 ++ package.json | 2 +- src/app/screens/swap/swapTokenBlock/index.tsx | 1 + src/app/screens/swap/useSwap.test.ts | 129 ++++++++++++++ src/app/screens/swap/useSwap.tsx | 167 ++++++++++++++---- 5 files changed, 270 insertions(+), 39 deletions(-) create mode 100644 jest.config.js create mode 100644 src/app/screens/swap/useSwap.test.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..a7ccdf396 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + 'node_modules/@secretkeylabs/xverse-core/.+\\.[jt]sx?$': 'babel-jest', + 'src/.+\\.[jt]sx?$': 'ts-jest', + }, + transformIgnorePatterns: ['node_modules/(?!(@secretkeylabs))'], +}; diff --git a/package.json b/package.json index b3b81fc2a..887aa70c7 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "build": "npm run clean && NODE_ENV=production npx node webpack/utils/build.js", "start": "npx node webpack/utils/devServer.js", "clean": "rimraf build", - "test": "npx jest", + "test": "jest ./src", "style": "prettier --write \"src/**/*.{ts,tsx}\"" }, "resolutions": { diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 94c4978fe..b8b7b6ae3 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -56,6 +56,7 @@ const CoinButtonContainer = styled.button((props) => ({ columnGap: props.theme.spacing(2), background: 'transparent', alignItems: 'center', + minHeight: '28px', // same as icon height })); const CoinButtonArrow = styled.img({ diff --git a/src/app/screens/swap/useSwap.test.ts b/src/app/screens/swap/useSwap.test.ts new file mode 100644 index 000000000..0a59c2082 --- /dev/null +++ b/src/app/screens/swap/useSwap.test.ts @@ -0,0 +1,129 @@ +import { Currency } from 'alex-sdk'; + +// TODO tim: fix jest transpiler to be able to import xverse-core +// for now, the function is copied in place. +// +// import { selectedTokenReducer } from './useSwap'; + +type SelectedCurrencyState = { + to?: Currency; + from?: Currency; + prevTo?: Currency; + prevFrom?: Currency; +}; + +type Side = 'from' | 'to'; + +function updateOppositeCurrencyIfSameAsSelected(state, {newCurrency, side}) { + switch (side) { + case 'from': + if (state.to !== newCurrency) { + return state.to; + } + if (state.to === newCurrency && state.prevTo !== newCurrency) { + return state.prevTo; + } + return undefined; + case 'to': + if (state.from !== newCurrency) { + return state.from; + } + if (state.from === newCurrency && state.prevFrom !== newCurrency) { + return state.prevFrom; + } + return undefined; + default: + return state.to; + } +} +const selectedTokenReducer: ( + state: SelectedCurrencyState, + { newCurrency, side }: { newCurrency: Currency; side: Side }, +) => SelectedCurrencyState = (state, { newCurrency, side }) => { + switch (side) { + case 'from': + return { + ...state, + prevFrom: state.from, + from: newCurrency, + to: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + }; + case 'to': + return { + ...state, + prevTo: state.to, + to: newCurrency, + from: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + }; + default: + return state; + } +}; + +describe('useSwap', () => { + describe('selectedTokenReducer', () => { + [ + { + name: 'simple case, no same tokens', + state: { + to: undefined, + from: undefined, + prevTo: undefined, + prevFrom: undefined, + }, + payload: { + newCurrency: Currency.ALEX, + side: 'to' as Side, + }, + expected: { + to: Currency.ALEX, + from: undefined, + prevTo: undefined, + prevFrom: undefined, + }, + }, + { + name: 'same token selected, other token will fall back to previously selected token', + state: { + to: Currency.ALEX, + from: undefined, + prevTo: Currency.STX, + prevFrom: undefined, + }, + payload: { + newCurrency: Currency.ALEX, + side: 'from' as Side, + }, + expected: { + to: Currency.STX, + from: Currency.ALEX, + prevTo: Currency.STX, + prevFrom: undefined, + }, + }, + { + name: 'same token selected, and other token cannot fallback. should be unselected', + state: { + to: Currency.STX, + from: Currency.ALEX, + prevTo: Currency.ALEX, + prevFrom: Currency.ALEX, + }, + payload: { + newCurrency: Currency.ALEX, + side: 'to' as Side, + }, + expected: { + to: Currency.ALEX, + from: undefined, + prevTo: Currency.STX, + prevFrom: Currency.ALEX, + }, + }, + ].forEach(({ name, state, payload, expected }) => { + test(name, () => { + expect(selectedTokenReducer(state, payload)).toEqual(expected); + }); + }); + }); +}); diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index ef2380962..b66d9f855 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -12,6 +12,12 @@ import { useNavigate } from 'react-router-dom'; import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; import { AnchorMode, makeUnsignedContractCall, PostConditionMode } from '@stacks/transactions'; +// const noop = () => null; +const isNotNull = (t: T | null | undefined): t is T => t != null; + +export type STXOrFungibleToken = 'STX' | FungibleToken; +export type Side = 'from' | 'to'; + export type SwapToken = { name: string; image: ReactNode; @@ -25,7 +31,7 @@ export type UseSwap = { isLoadingWalletData: boolean; selectedFromToken?: SwapToken; selectedToToken?: SwapToken; - onSelectToken: (token: 'STX' | FungibleToken, side: 'from' | 'to') => void; + onSelectToken: (token: STXOrFungibleToken, side: Side) => void; inputAmount: string; inputAmountInvalid?: boolean; onInputAmountChanged: (amount: string) => void; @@ -42,6 +48,60 @@ export type UseSwap = { onSwap?: () => void; }; +export type SelectedCurrencyState = { + to?: Currency; + from?: Currency; + prevTo?: Currency; + prevFrom?: Currency; +}; + +function updateOppositeCurrencyIfSameAsSelected(state, {newCurrency, side}) { + switch (side) { + case 'from': + if (state.to !== newCurrency) { + return state.to; + } + if (state.to === newCurrency && state.prevTo !== newCurrency) { + return state.prevTo; + } + return undefined; + case 'to': + if (state.from !== newCurrency) { + return state.from; + } + if (state.from === newCurrency && state.prevFrom !== newCurrency) { + return state.prevFrom; + } + return undefined; + default: + return state.to; + } +} + +export const selectedTokenReducer: ( + state: SelectedCurrencyState, + { newCurrency, side }: { newCurrency: Currency; side: Side }, +) => SelectedCurrencyState = (state, { newCurrency, side }) => { + switch (side) { + case 'from': + return { + ...state, + prevFrom: state.from, + from: newCurrency, + to: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + }; + case 'to': + return { + ...state, + prevTo: state.to, + to: newCurrency, + from: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + }; + default: + return state; + } +}; + export function useSwap(): UseSwap { const navigate = useNavigate(); const alexSDK = useState(() => new AlexSDK())[0]; @@ -61,8 +121,16 @@ export function useSwap(): UseSwap { const [inputAmount, setInputAmount] = useState(''); const [slippage, setSlippage] = useState(0.04); - const [from, setFrom] = useState(); - const [to, setTo] = useState(); + // const [from, setFrom] = useState(); + // const [to, setTo] = useState(); + // const [prevFrom, setPrevFrom] = useState(); + // const [prevTo, setPrevTo] = useState(); + const [selectedCurrency, setSelectedCurrency] = useState({ + to: undefined, + from: undefined, + prevTo: undefined, + prevFrom: undefined, + }); const fromAmount = Number.isNaN(Number(inputAmount)) ? undefined : Number(inputAmount); @@ -73,7 +141,7 @@ export function useSwap(): UseSwap { if (currency === Currency.STX) { return { balance: Number(microstacksToStx(BigNumber(stxAvailableBalance) as any)), - image: , + image: , name: 'STX', amount, fiatAmount: @@ -90,9 +158,7 @@ export function useSwap(): UseSwap { } return { amount, - image: ( - - ), + image: , name: (token.ticker ?? token.name).toUpperCase(), balance: Number(ftDecimals(token.balance, token.decimals ?? 0)), fiatAmount: @@ -115,13 +181,16 @@ export function useSwap(): UseSwap { return (token.ticker ?? token.name).toUpperCase(); } - function onSelectToken(token: 'STX' | FungibleToken, side: 'from' | 'to') { - (side === 'from' ? setFrom : setTo)( - token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)!, - ); + function getCurrencyFromToken(token: 'STX' | FungibleToken) { + return token === 'STX' ? Currency.STX : alexSDK.getCurrencyFrom(token.principal)!; } - const fromToken = currencyToToken(from, fromAmount); + function onSelectToken(token: STXOrFungibleToken, side: Side) { + const newCurrency = getCurrencyFromToken(token); + setSelectedCurrency(selectedTokenReducer(selectedCurrency, { newCurrency, side })); + } + + const fromToken = currencyToToken(selectedCurrency.from, fromAmount); const inputAmountInvalid = Number.isNaN(Number(inputAmount)) || (fromAmount != null && @@ -133,11 +202,18 @@ export function useSwap(): UseSwap { }>(); useEffect(() => { - if (from == null || to == null || from === to) { + if ( + selectedCurrency.from == null || + selectedCurrency.to == null || + selectedCurrency.from === selectedCurrency.to + ) { setInfo(undefined); } else { let cancelled = false; - Promise.all([alexSDK.getFeeRate(from, to), alexSDK.getRouter(from, to)]).then((a) => { + Promise.all([ + alexSDK.getFeeRate(selectedCurrency.from, selectedCurrency.to), + alexSDK.getRouter(selectedCurrency.from, selectedCurrency.to), + ]).then((a) => { if (cancelled) { return; } @@ -150,26 +226,38 @@ export function useSwap(): UseSwap { cancelled = true; }; } - }, [from, to]); + }, [selectedCurrency.from, selectedCurrency.to]); const [exchangeRate, setExchangeRate] = useState(); useEffect(() => { - if (from == null || to == null || fromAmount == null || fromAmount === 0 || from === to) { + if ( + selectedCurrency.from == null || + selectedCurrency.to == null || + fromAmount == null || + fromAmount === 0 || + selectedCurrency.from === selectedCurrency.to + ) { setExchangeRate(undefined); } else { let cancelled = false; - alexSDK.getAmountTo(from, BigInt(Math.floor(fromAmount * 1e8)), to).then((result) => { - if (cancelled) { - return; - } - setExchangeRate(Number(result) / 1e8 / fromAmount); - }); + alexSDK + .getAmountTo( + selectedCurrency.from, + BigInt(Math.floor(fromAmount * 1e8)), + selectedCurrency.to, + ) + .then((result) => { + if (cancelled) { + return; + } + setExchangeRate(Number(result) / 1e8 / fromAmount); + }); return () => { cancelled = true; }; } - }, [from, to, fromAmount]); + }, [selectedCurrency.from, selectedCurrency.to, fromAmount]); function roundForDisplay(input?: number) { if (input == null) { @@ -178,16 +266,17 @@ export function useSwap(): UseSwap { return Math.floor(input * 1000) / 1000; } - const toggleFromToTokens = () => { - const prevFrom = from; - setFrom(to); - setTo(prevFrom); + function toggleFromToTokens() { + setSelectedCurrency((prevState) => ({ + to: prevState.from, + from: prevState.to, + })); } const toAmount = exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; - const toToken = currencyToToken(to, roundForDisplay(toAmount)); + const toToken = currencyToToken(selectedCurrency.to, roundForDisplay(toAmount)); return { coinsList: acceptableCoinList, isLoadingWalletData: false, @@ -214,12 +303,16 @@ export function useSwap(): UseSwap { }, submitError: inputAmountInvalid ? t('ERRORS.INSUFFICIENT_BALANCE_FEES') : undefined, onSwap: - fromAmount != null && toAmount != null && from != null && to != null && info != null + fromAmount != null && + toAmount != null && + selectedCurrency.from != null && + selectedCurrency.to != null && + info != null ? async () => { - const tx = await alexSDK.runSwap( + const tx = alexSDK.runSwap( stxAddress, - from, - to, + selectedCurrency.from!, + selectedCurrency.to!, BigInt(Math.floor(fromAmount * 1e8)), BigInt(Math.floor(toAmount * (1 - slippage) * 1e8)), info.route, @@ -236,15 +329,16 @@ export function useSwap(): UseSwap { }); const state: SwapConfirmationInput = { - from: from!, - to: to!, + from: selectedCurrency.from!, + to: selectedCurrency.to!, fromToken: fromToken!, toToken: toToken!, address: stxAddress, fromAmount: fromAmount!, minToAmount: toAmount! * (1 - slippage), lpFeeAmount: info.feeRate * fromAmount!, - lpFeeFiatAmount: currencyToToken(from!, info.feeRate * fromAmount!)?.fiatAmount, + lpFeeFiatAmount: currencyToToken(selectedCurrency.from!, info.feeRate * fromAmount!) + ?.fiatAmount, routers: info.route.map(currencyToToken).filter(isNotNull), unsignedTx, functionName: `${tx.contractName}\n${tx.functionName}`, @@ -256,6 +350,3 @@ export function useSwap(): UseSwap { : undefined, }; } - -const noop = () => null; -const isNotNull = (t: T | null | undefined): t is T => t != null; From 51b9360b0c94cebf9c5d1fbff39085fdf537e7f6 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 28 Jun 2023 18:54:22 +0800 Subject: [PATCH 36/52] chore: commit package-lock --- package-lock.json | 93 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9947e2828..5ffef2dde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20671,6 +20671,14 @@ "dev": true, "requires": {} }, + "alex-sdk": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.13.tgz", + "integrity": "sha512-J0tlVLTzyxWn1cd8GijFTGRGX7fQBkg68i6w+QVfJzMHwyemkOjf673oSyUma8wURU3xb0wDTwBCk+Du7xXo6A==", + "requires": { + "clarity-codegen": "^0.2.0" + } + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -22503,6 +22511,63 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "clarity-codegen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clarity-codegen/-/clarity-codegen-0.2.0.tgz", + "integrity": "sha512-puhmo5pzCAGI8dBjt87jaM4fTuZHMLLxb9Se+lcAS+scfzkHa3NA91dpX+Vgv1bm3iFl/V8m8DvREKLhflXXVw==", + "requires": { + "@stacks/stacks-blockchain-api-types": "^5.0.1", + "axios": "^0.27.2", + "lodash": "^4.17.21", + "yargs": "^17.6.0", + "yqueue": "^1.0.1" + }, + "dependencies": { + "@stacks/stacks-blockchain-api-types": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-5.0.3.tgz", + "integrity": "sha512-8vL+bPLTK0Sio3aJyIYITmdkwCCj01C5MqOQisC/GwthQvqf53uceAIsSsSQmVpFwvL5rqG0KGAu4rosW7QUwQ==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } + }, "classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -24550,8 +24615,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.2.1", @@ -25270,8 +25334,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -28742,8 +28805,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-from-string": { "version": "2.0.2", @@ -29371,7 +29433,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -29381,8 +29442,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" } } }, @@ -29439,7 +29499,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -30602,7 +30661,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -30613,7 +30671,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -30622,7 +30679,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -30630,8 +30686,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -30674,8 +30729,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "3.1.1", @@ -30714,6 +30768,11 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "yqueue": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/yqueue/-/yqueue-1.0.1.tgz", + "integrity": "sha512-DBxJZBRafFLA/tCc5uO8ZTGFr+sQgn1FRJkZ4cVrIQIk6bv2bInraE3mbpLAJw9z93JGrLkqDoyTLrrZaCNq5w==" + }, "zone-file": { "version": "2.0.0-beta.3", "resolved": "https://registry.npmjs.org/zone-file/-/zone-file-2.0.0-beta.3.tgz", From e8d8c38671415b2d4201025059e68d83a8bfd033 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 28 Jun 2023 19:01:04 +0800 Subject: [PATCH 37/52] fix: disable button when insufficient balance --- src/app/screens/swap/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index bc48c9b3c..66ed06d74 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -99,7 +99,7 @@ function SwapScreen() { )} Date: Wed, 28 Jun 2023 19:27:32 +0800 Subject: [PATCH 38/52] fix: add token name to min received --- src/app/screens/swap/useSwap.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index b66d9f855..d6a578e9e 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -44,7 +44,7 @@ export type UseSwap = { }; slippage: number; onSlippageChanged: (slippage: number) => void; - minReceived?: number; + minReceived?: string; onSwap?: () => void; }; @@ -55,7 +55,7 @@ export type SelectedCurrencyState = { prevFrom?: Currency; }; -function updateOppositeCurrencyIfSameAsSelected(state, {newCurrency, side}) { +function updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) { switch (side) { case 'from': if (state.to !== newCurrency) { @@ -88,14 +88,14 @@ export const selectedTokenReducer: ( ...state, prevFrom: state.from, from: newCurrency, - to: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + to: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }), }; case 'to': return { ...state, prevTo: state.to, to: newCurrency, - from: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) + from: updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }), }; default: return state; @@ -289,7 +289,10 @@ export function useSwap(): UseSwap { onSelectToken, inputAmountInvalid, handleClickDownArrow: toggleFromToTokens, - minReceived: toAmount != null ? roundForDisplay(toAmount * (1 - slippage)) : undefined, + minReceived: + toAmount != null + ? `${roundForDisplay(toAmount * (1 - slippage))} ${toToken?.name}` + : undefined, swapInfo: { exchangeRate: exchangeRate != null && fromToken != null && toToken != null From 51da5e880e4e605bd3a283f9cb9d6668f20badee Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 28 Jun 2023 21:25:45 +0800 Subject: [PATCH 39/52] fix: advanced settings modal on swaps --- .../swap/swapConfirmation/advanceSettings/index.tsx | 7 ++++++- src/app/screens/swap/useSwap.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx index ef8c0f2ac..1851f6574 100644 --- a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx +++ b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx @@ -36,6 +36,8 @@ type Props = { export function AdvanceSettings({ swap }: Props) { const [showModal, setShowModal] = useState(false); + const [showFeeSettings, setShowFeeSettings] = useState(false); + const onApplyClick = useCallback((settingFee: string, nonce?: string) => { const fee = BigInt(stxToMicrostacks(new BigNumber(settingFee) as any).toString()); swap.unsignedTx.setFee(fee); @@ -62,9 +64,12 @@ export function AdvanceSettings({ swap }: Props) { ).toString()} type="STX" nonce={(swap.unsignedTx.auth?.spendingCondition?.nonce ?? BigInt(0)).toString()} + onCrossClick={() => {setShowModal(false); setShowFeeSettings(false)}} onApplyClick={onApplyClick} - onCrossClick={() => setShowModal(false)} + showFeeSettings={showFeeSettings} + setShowFeeSettings={setShowFeeSettings} /> ); } +export default AdvanceSettings; diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index d6a578e9e..80ee4c667 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -55,7 +55,7 @@ export type SelectedCurrencyState = { prevFrom?: Currency; }; -function updateOppositeCurrencyIfSameAsSelected(state, { newCurrency, side }) { +function updateOppositeCurrencyIfSameAsSelected(state: SelectedCurrencyState, { newCurrency, side }) { switch (side) { case 'from': if (state.to !== newCurrency) { From a4bd16b21e52d14f0f83729a47b210721233e14a Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 28 Jun 2023 21:26:32 +0800 Subject: [PATCH 40/52] fix: use 4dp rounding on exchange rate --- src/app/screens/swap/useSwap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 80ee4c667..3b9cf2078 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -263,7 +263,7 @@ export function useSwap(): UseSwap { if (input == null) { return undefined; } - return Math.floor(input * 1000) / 1000; + return input.toFixed(4); } function toggleFromToTokens() { @@ -276,7 +276,7 @@ export function useSwap(): UseSwap { const toAmount = exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; - const toToken = currencyToToken(selectedCurrency.to, roundForDisplay(toAmount)); + const toToken = currencyToToken(selectedCurrency.to, toAmount); return { coinsList: acceptableCoinList, isLoadingWalletData: false, From 844fa7592d892d1b373efa13b2f3272c1c8113b8 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 29 Jun 2023 14:49:56 +0800 Subject: [PATCH 41/52] fix: toggle button should also toggle amounts --- src/app/screens/swap/useSwap.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index 3b9cf2078..3b7748c67 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -266,16 +266,19 @@ export function useSwap(): UseSwap { return input.toFixed(4); } + const toAmount = + exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; + function toggleFromToTokens() { setSelectedCurrency((prevState) => ({ to: prevState.from, from: prevState.to, })); + if (toAmount) { + setInputAmount(String(toAmount)); + } } - const toAmount = - exchangeRate != null && fromAmount != null ? fromAmount * exchangeRate : undefined; - const toToken = currencyToToken(selectedCurrency.to, toAmount); return { coinsList: acceptableCoinList, From 0f5a100cdecb479258d120135274decd110d6092 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 29 Jun 2023 14:54:30 +0800 Subject: [PATCH 42/52] fix: function font style should be body_medium_m --- .../screens/swap/swapConfirmation/functionBlock/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx index 803fd85c5..afa9ce99f 100644 --- a/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/functionBlock/index.tsx @@ -2,11 +2,10 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Container, TitleContainer, TitleText } from '@screens/swap/swapConfirmation/stxInfoBlock'; -const FunctionName = styled.pre((props) => ({ +const FunctionName = styled.div((props) => ({ + ...props.theme.body_medium_m, marginLeft: 10, color: props.theme.colors.white[0], - fontSize: 14, - fontWeight: 500, textAlign: 'right' })); From 44c5e23296d3e2d764c88024207beb89359ff6a2 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 29 Jun 2023 15:08:16 +0800 Subject: [PATCH 43/52] feat: make swap confirmation buttons sticky bottom --- src/app/screens/swap/swapConfirmation/index.tsx | 4 ++++ src/app/screens/swap/swapConfirmation/routeBlock/index.tsx | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index e41433f99..dff086d7e 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -26,6 +26,10 @@ export const ButtonContainer = styled.div((props) => ({ flexDirection: 'row', marginBottom: props.theme.spacing(20), marginTop: props.theme.spacing(16), + position: 'sticky', + bottom: 0, + background: props.theme.colors.background.elevation0, + padding: `${props.theme.spacing(4)}px 0` })); export const ActionButtonWrap = styled.div((props) => ({ diff --git a/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx index a29a932aa..fb852b049 100644 --- a/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/routeBlock/index.tsx @@ -40,7 +40,6 @@ const ProgressItem = styled.div((props) => ({ flexDirection: 'column', alignItems: 'center', position: 'relative', - zIndex: '2', paddingRight: props.theme.spacing(3.5), paddingLeft: props.theme.spacing(3.5), background: props.theme.colors.background.elevation1, From 79b36d44822954f798e79f773b0605594995ed29 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 29 Jun 2023 15:39:34 +0800 Subject: [PATCH 44/52] fix: standardise button hover, active, disabled css ref: https://zeroheight.com/0683c9fa7/p/5270b4-buttons/b/32e1a2 --- src/app/components/button/index.tsx | 76 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/app/components/button/index.tsx b/src/app/components/button/index.tsx index b7e5e5c98..029b08cfb 100644 --- a/src/app/components/button/index.tsx +++ b/src/app/components/button/index.tsx @@ -12,31 +12,40 @@ const Button = styled.button((props) => ({ justifyContent: 'center', alignItems: 'center', borderRadius: props.theme.radius(1), - backgroundColor: props.warning ? props.theme.colors.feedback.error : props.theme.colors.action.classic, + backgroundColor: props.warning + ? props.theme.colors.feedback.error + : props.theme.colors.action.classic, width: '100%', height: 44, - opacity: props.disabled ? 0.6 : 1, - transition: 'all 0.2s ease', - cursor: props.disabled ? 'not-allowed' : 'auto', + transition: 'all 0.1s ease', + ...(props.disabled + ? { + cursor: 'not-allowed', + opacity: 0.4, + } + : { + ':hover': { opacity: 0.8 }, + ':active': { opacity: 0.6 }, + }), })); -const AnimatedButton = styled(Button)` - :hover { - background: ${(props) => (props.warning ? props.theme.colors.feedback.error : props.theme.colors.action.classicLight)}; - opacity: 0.6; - } -`; - -const TransparentButton = styled(Button)` - background-color: transparent; - border: ${(props) => `1px solid ${props.theme.colors.background.elevation6}`} -`; - -const AnimatedTransparentButton = styled(TransparentButton)` -:hover { - background: ${(props) => props.theme.colors.background.elevation6_800}; -} -`; +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, + }, + }), +})); interface TextProps { warning?: boolean; @@ -45,7 +54,9 @@ interface TextProps { const ButtonText = styled.h1((props) => ({ ...props.theme.body_xs, fontWeight: 700, - color: `${props.warning ? props.theme.colors.white[0] : props.theme.colors.background.elevation0}`, + color: `${ + props.warning ? props.theme.colors.white[0] : props.theme.colors.background.elevation0 + }`, textAlign: 'center', })); @@ -82,14 +93,13 @@ function ActionButton({ warning, }: Props) { const handleOnPress = () => { - if (!disabled) { onPress(); } + if (!disabled) { + onPress(); + } }; if (transparent) { return ( - + {processing ? ( ) : ( @@ -98,25 +108,21 @@ function ActionButton({ {text} )} - + ); } return ( - + ); } export default ActionButton; From dcf4f8c6675fa497ae1d34678199884fa8522434 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 29 Jun 2023 16:08:08 +0800 Subject: [PATCH 45/52] feat: add error state to slippage input --- src/app/screens/swap/slippageModal/index.tsx | 44 ++++++++++++++------ src/locales/en.json | 3 +- src/theme/index.ts | 6 +++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/app/screens/swap/slippageModal/index.tsx b/src/app/screens/swap/slippageModal/index.tsx index ffe227c6d..77113a2dc 100644 --- a/src/app/screens/swap/slippageModal/index.tsx +++ b/src/app/screens/swap/slippageModal/index.tsx @@ -29,22 +29,36 @@ const Title = styled.div((props) => ({ color: props.theme.colors.white['0'], })); -const Input = styled.input((props) => ({ +const Input = styled.input<{ error: boolean }>((props) => ({ ...props.theme.body_medium_m, height: 48, backgroundColor: props.theme.colors.background.elevation1, - borderColor: 'transparent', borderStyle: 'solid', borderWidth: 1, borderRadius: 8, color: props.theme.colors.white['0'], padding: '14px 16px', outline: 'none', - ':focus': { - borderColor: props.theme.colors.background.elevation6, + borderColor: props.error ? props.theme.colors.feedback.error_700 : 'transparent', + ':focus-within': { + border: '1px solid', + 'border-color': props.error + ? props.theme.colors.feedback.error_700 + : props.theme.colors.background.elevation6, }, })); +const InputFeedback = styled.span((props) => ({ + ...props.theme.body_s, + color: props.theme.colors.feedback.error, +})); + +const InputRow = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + gap: props.theme.spacing(4), +})); + const Description = styled.p((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white['400'], @@ -71,15 +85,19 @@ export function SlippageModalContent({ {t('SLIPPAGE')} {t('RESET_TO_DEFAULT')} - setPercentage(e.target.value)} - onFocus={(e) => { - const current = e.target.value.replace('%', ''); - e.target.setSelectionRange(0, current.length); - }} - /> + + setPercentage(e.target.value)} + onFocus={(e) => { + const current = e.target.value.replace('%', ''); + e.target.setSelectionRange(0, current.length); + }} + /> + {invalid && {t('ERRORS.SLIPPAGE_TOLERANCE_CANNOT_EXCEED')}} + {t('SLIPPAGE_DESC')} Date: Thu, 29 Jun 2023 17:45:09 +0800 Subject: [PATCH 46/52] fix: use intended params for swap advance settings on apply handler --- .../swapConfirmation/advanceSettings/index.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx index 1851f6574..a0bd18677 100644 --- a/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx +++ b/src/app/screens/swap/swapConfirmation/advanceSettings/index.tsx @@ -38,9 +38,9 @@ export function AdvanceSettings({ swap }: Props) { const [showModal, setShowModal] = useState(false); const [showFeeSettings, setShowFeeSettings] = useState(false); - const onApplyClick = useCallback((settingFee: string, nonce?: string) => { - const fee = BigInt(stxToMicrostacks(new BigNumber(settingFee) as any).toString()); - swap.unsignedTx.setFee(fee); + const onApplyClick = useCallback(({ fee, nonce }: { fee: string; nonce?: string }) => { + const settingFee = BigInt(stxToMicrostacks(new BigNumber(fee) as any).toString()); + swap.unsignedTx.setFee(settingFee); if (nonce != null) { swap.unsignedTx.setNonce(BigInt(nonce)); } @@ -59,12 +59,15 @@ export function AdvanceSettings({ swap }: Props) { visible={showModal} fee={microstacksToStx( new BigNumber( - (swap.unsignedTx.auth?.spendingCondition?.fee ?? BigInt(0)).toString() - ) as any + (swap.unsignedTx.auth?.spendingCondition?.fee ?? BigInt(0)).toString(), + ) as any, ).toString()} type="STX" nonce={(swap.unsignedTx.auth?.spendingCondition?.nonce ?? BigInt(0)).toString()} - onCrossClick={() => {setShowModal(false); setShowFeeSettings(false)}} + onCrossClick={() => { + setShowModal(false); + setShowFeeSettings(false); + }} onApplyClick={onApplyClick} showFeeSettings={showFeeSettings} setShowFeeSettings={setShowFeeSettings} From 4305af5aebcabac6c0aa4eb1147ea7f68da8882d Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 7 Jul 2023 20:00:21 +0800 Subject: [PATCH 47/52] fix: padding on swap confirm buttons --- src/app/screens/swap/swapConfirmation/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index dff086d7e..1884bef9b 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -24,12 +24,12 @@ const TitleText = styled.div((props) => ({ export const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(16), + marginBottom: props.theme.spacing(8), + marginTop: props.theme.spacing(8), position: 'sticky', bottom: 0, background: props.theme.colors.background.elevation0, - padding: `${props.theme.spacing(4)}px 0` + padding: `${props.theme.spacing(12)}px 0` })); export const ActionButtonWrap = styled.div((props) => ({ From 47069e64477e1fa829d64b7c1fa5a0a6072f9b91 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 7 Jul 2023 20:49:29 +0800 Subject: [PATCH 48/52] fix: no exponents in fees block of swap confirmation --- src/app/screens/swap/swapConfirmation/freesBlock/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx index 4c53bc213..b0220d7f1 100644 --- a/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx +++ b/src/app/screens/swap/swapConfirmation/freesBlock/index.tsx @@ -27,7 +27,7 @@ export default function FeesBlock({ lpFee, lpFeeFiatAmount, currency }: FeeTextP {t('FEES')} - {`${lpFee.toString()} ${currency}`} + {`${lpFee.toFixed(6)} ${currency}`} {` ~ $${lpFeeFiatAmount} USD`} From dab6139492a654bff81139b1c16926ef99887b9a Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 12 Jul 2023 12:02:56 +0800 Subject: [PATCH 49/52] feat: add sponsor swap transaction UI and add a hook with placeholder --- src/app/hooks/useSponsoredTransaction.ts | 36 +++++++++++++++++++ .../screens/swap/swapConfirmation/index.tsx | 28 +++++++++++++-- .../swap/swapConfirmation/useConfirmSwap.tsx | 27 ++++++++------ .../img/transactions/CircleWavyCheck.svg | 6 ++++ src/locales/en.json | 25 ++++++++++++- 5 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 src/app/hooks/useSponsoredTransaction.ts create mode 100644 src/assets/img/transactions/CircleWavyCheck.svg diff --git a/src/app/hooks/useSponsoredTransaction.ts b/src/app/hooks/useSponsoredTransaction.ts new file mode 100644 index 000000000..d5487913c --- /dev/null +++ b/src/app/hooks/useSponsoredTransaction.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getSponsorInfo } from '@secretkeylabs/xverse-core/api'; + +export const useSponsorInfoQuery = () => + useQuery({ + queryKey: ['sponsorInfo'], + queryFn: async () => { + try { + return await getSponsorInfo(); + } catch (e: any) { + return Promise.reject(e); + } + }, + }); + +export const useSponsoredTransaction = () => { + // TODO default to false after testing UI + const [isSponsored, setIsSponsored] = useState(true); + + // TODO: fetch from xverse-core sponsor service once it is deployed + // const { error, data: isActive } = useSponsorInfoQuery(); + // useEffect(() => { + // if (!error) { + // setIsSponsored(!!isActive); + // } + // }, [isActive, error]); + + + return { + isSponsored, + setIsSponsored, + }; +}; + +export default useSponsoredTransaction; diff --git a/src/app/screens/swap/swapConfirmation/index.tsx b/src/app/screens/swap/swapConfirmation/index.tsx index 1884bef9b..c4850116f 100644 --- a/src/app/screens/swap/swapConfirmation/index.tsx +++ b/src/app/screens/swap/swapConfirmation/index.tsx @@ -12,6 +12,8 @@ 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'; const TitleText = styled.div((props) => ({ fontSize: 21, @@ -29,7 +31,7 @@ export const ButtonContainer = styled.div((props) => ({ position: 'sticky', bottom: 0, background: props.theme.colors.background.elevation0, - padding: `${props.theme.spacing(12)}px 0` + padding: `${props.theme.spacing(12)}px 0`, })); export const ActionButtonWrap = styled.div((props) => ({ @@ -37,11 +39,26 @@ export const ActionButtonWrap = styled.div((props) => ({ width: '100%', })); +const SponsoredTransactionText = styled.div((props) => ({ + ...props.theme.body_m, + color: props.theme.colors.white[200], + marginTop: props.theme.spacing(10), + display: 'flex', + gap: props.theme.spacing(4), +})); + +const Icon = styled.img((props) => ({ + marginTop: props.theme.spacing(1), + width: 24, + height: 24, +})); + 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(); const onCancel = useCallback(() => { navigate('/swap'); @@ -69,7 +86,14 @@ export default function SwapConfirmation() { lpFeeFiatAmount={swap.lpFeeFiatAmount} currency={swap.fromToken.name} /> - + {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 33625932d..596280660 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -2,16 +2,15 @@ import { SwapToken } from '@screens/swap/useSwap'; import { ReactNode } from 'react'; import { Currency } from 'alex-sdk'; import useWalletSelector from '@hooks/useWalletSelector'; -import { broadcastSignedTransaction, signTransaction } from '@secretkeylabs/xverse-core'; import { - makeUnsignedContractCall, - AnchorMode, - PostConditionMode, - StacksTransaction, -} from '@stacks/transactions'; -import type { TxToBroadCast } from 'alex-sdk/dist/helpers/SwapHelper'; + broadcastSignedTransaction, + signTransaction, + sponsorTransaction, +} from '@secretkeylabs/xverse-core'; +import { StacksTransaction } from '@stacks/transactions'; import useNetworkSelector from '@hooks/useNetwork'; import { useNavigate } from 'react-router-dom'; +import useSponsoredTransaction from '@hooks/useSponsoredTransaction'; export type SwapConfirmationInput = { from: Currency; @@ -29,10 +28,11 @@ export type SwapConfirmationInput = { }; export function useConfirmSwap( - input: SwapConfirmationInput + input: SwapConfirmationInput, ): SwapConfirmationInput & { onConfirm: () => Promise } { - const { selectedAccount, seedPhrase, stxPublicKey } = useWalletSelector(); + const { selectedAccount, seedPhrase } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); + const { isSponsored } = useSponsoredTransaction(); const navigate = useNavigate(); return { ...input, @@ -41,10 +41,15 @@ export function useConfirmSwap( input.unsignedTx, seedPhrase, selectedAccount?.id ?? 0, - selectedNetwork + selectedNetwork, ); try { - const broadcastResult: string = await broadcastSignedTransaction(signed, selectedNetwork); + let broadcastResult: string | null; + if (isSponsored) { + broadcastResult = await sponsorTransaction(signed); + } else { + broadcastResult = await broadcastSignedTransaction(signed, selectedNetwork); + } if (broadcastResult) { navigate('/tx-status', { state: { diff --git a/src/assets/img/transactions/CircleWavyCheck.svg b/src/assets/img/transactions/CircleWavyCheck.svg new file mode 100644 index 000000000..13018548a --- /dev/null +++ b/src/assets/img/transactions/CircleWavyCheck.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 9ba1411d6..745641e8d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -600,6 +600,29 @@ "COPIED": "Copied", "COPY_YOUR_ADDRESS": "copy your address", "ADVANCED_SETTING": "Advanced settings", - "ROUTE_DESC": "For the transaction to proceed, your BTC will be swapped for xBTC and STX. There is no additional cost for you and no STX will be added to your wallet." + "ROUTE_DESC": "For the transaction to proceed, your BTC will be swapped for xBTC and STX. There is no additional cost for you and no STX will be added to your wallet.", + "THIS_IS_A_SPONSORED_TRANSACTION": "This is a sponsored transaction, no transaction fees will be deducted from your account." + }, + "SEND_BRC_20": { + "BRC20_TOKEN": "BRC-20", + "SEND_INFO_TITLE": "Sending a BRC-20 token", + "SEND_INFO_SUBTITLE": "To transfer BRC-20 tokens, you need to make two separate transactions:", + "SEND_STEP_1_TITLE": "Inscribe Transfer", + "SEND_STEP_1": "Create an Ordinal inscription with the token and amount you would like to send.", + "SEND_STEP_2_TITLE": "Send transfer inscription", + "SEND_STEP_2": "Once you have the transfer inscription, you can send it to your recipient to transfer the BRC-20 token.", + "SEND_INFO_START_BUTTON": "Start", + "SEND_NEXT_BUTTON": "Next", + "SEND": "Send", + "AMOUNT": "Amount", + "BALANCE": "Balance" + }, + "CACHE_MIGRATION_SCREEN": { + "TITLE":"Your wallet data store needs to be updated in order to remain secure.", + "WARNING": "Please make sure you have your seed phrase backed up securely before moving forward.", + "SUCCESS_TITLE": "Update Complete", + "SKIP_BUTTON": "Do it later", + "CONFIRM_BUTTON": "Confirm", + "CLOSE_TAB": "Continue" } } From 92fb6490b691490980f99b25fcd5a6afe6f259ab Mon Sep 17 00:00:00 2001 From: Tim Man Date: Mon, 17 Jul 2023 18:52:34 +0800 Subject: [PATCH 50/52] feat: bump xverse-core dep version and use sponsor2 url --- package.json | 4 +-- src/app/hooks/useSponsoredTransaction.ts | 25 ++++++++----------- .../swap/swapConfirmation/useConfirmSwap.tsx | 6 +++-- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 887aa70c7..7b1f87562 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,15 @@ "private": true, "dependencies": { "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "1.1.2", + "@secretkeylabs/xverse-core": "1.3.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "^6.1.1", + "@stacks/transactions": "^4.3.8", "@tanstack/query-sync-storage-persister": "^4.29.1", "@tanstack/react-query": "^4.29.3", "@tanstack/react-query-devtools": "^4.29.3", "@tanstack/react-query-persist-client": "^4.29.3", - "@stacks/transactions": "^4.3.8", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/src/app/hooks/useSponsoredTransaction.ts b/src/app/hooks/useSponsoredTransaction.ts index d5487913c..928eee07d 100644 --- a/src/app/hooks/useSponsoredTransaction.ts +++ b/src/app/hooks/useSponsoredTransaction.ts @@ -1,31 +1,28 @@ import { useEffect, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { getSponsorInfo } from '@secretkeylabs/xverse-core/api'; +import { getSponsorInfo } from '@secretkeylabs/xverse-core'; -export const useSponsorInfoQuery = () => +export const useSponsorInfoQuery = (sponsorUrl?: string) => useQuery({ queryKey: ['sponsorInfo'], queryFn: async () => { try { - return await getSponsorInfo(); + return await getSponsorInfo(sponsorUrl); } catch (e: any) { return Promise.reject(e); } }, }); -export const useSponsoredTransaction = () => { - // TODO default to false after testing UI - const [isSponsored, setIsSponsored] = useState(true); - - // TODO: fetch from xverse-core sponsor service once it is deployed - // const { error, data: isActive } = useSponsorInfoQuery(); - // useEffect(() => { - // if (!error) { - // setIsSponsored(!!isActive); - // } - // }, [isActive, error]); +export const useSponsoredTransaction = (sponsorUrl?: string) => { + const [isSponsored, setIsSponsored] = useState(false); + const { error, data: isActive } = useSponsorInfoQuery(sponsorUrl); + useEffect(() => { + if (!error) { + setIsSponsored(!!isActive); + } + }, [isActive, error]); return { isSponsored, diff --git a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx index 596280660..bfdc4613a 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -27,12 +27,14 @@ export type SwapConfirmationInput = { functionName: string; }; +const XVERSE_SPONSOR_2_URL = "https://sponsor2.xverse.app"; + export function useConfirmSwap( input: SwapConfirmationInput, ): SwapConfirmationInput & { onConfirm: () => Promise } { const { selectedAccount, seedPhrase } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); - const { isSponsored } = useSponsoredTransaction(); + const { isSponsored } = useSponsoredTransaction(XVERSE_SPONSOR_2_URL); const navigate = useNavigate(); return { ...input, @@ -46,7 +48,7 @@ export function useConfirmSwap( try { let broadcastResult: string | null; if (isSponsored) { - broadcastResult = await sponsorTransaction(signed); + broadcastResult = await sponsorTransaction(signed, XVERSE_SPONSOR_2_URL); } else { broadcastResult = await broadcastSignedTransaction(signed, selectedNetwork); } From 3654327d91ac42d0552e9eea0cc19298c490c316 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 18 Jul 2023 16:31:39 +0800 Subject: [PATCH 51/52] fix: bump xverse-core version and handle sponsor transaction error --- package-lock.json | 670 +++++++++++++++++- package.json | 2 +- src/app/hooks/useSponsoredTransaction.ts | 3 +- .../swap/swapConfirmation/useConfirmSwap.tsx | 4 +- 4 files changed, 664 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ffef2dde..9e1c039f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.12.3", "dependencies": { "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "1.1.2", + "@secretkeylabs/xverse-core": "1.4.0-1004b69", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "^6.1.1", @@ -712,6 +712,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bitcoinerlab/secp256k1": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", + "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "dependencies": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1792,6 +1801,67 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, + "node_modules/@ledgerhq/devices": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.0.4.tgz", + "integrity": "sha512-dxOiWZmtEv1tgw70+rW8gviCRZUeGDUnxY6HUPiRqTAc0Ts2AXxiJChgAsPvIywWTGW+S67Nxq1oTZdpRbdt+A==", + "dependencies": { + "@ledgerhq/errors": "^6.12.7", + "@ledgerhq/logs": "^6.10.1", + "rxjs": "6", + "semver": "^7.3.5" + } + }, + "node_modules/@ledgerhq/devices/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/devices/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/devices/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@ledgerhq/errors": { + "version": "6.12.7", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.12.7.tgz", + "integrity": "sha512-1BpjzFErPK7qPFx0oItcX0mNLJMplVAm2Dpl5urZlubewnTyyw5sahIBjU+8LLCWJ2eGEh/0wyvh0jMtR0n2Mg==" + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.28.5", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.28.5.tgz", + "integrity": "sha512-xmw5RhYbqExBBqTvOnOjN/RYNIGMBxFJ+zcYNfkfw/E+uEY3L7xq8Z7sC/n7URTT6xtEctElqduBJnBQE4OQtw==", + "dependencies": { + "@ledgerhq/devices": "^8.0.4", + "@ledgerhq/errors": "^6.12.7", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.10.1.tgz", + "integrity": "sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -2085,33 +2155,47 @@ } }, "node_modules/@secretkeylabs/xverse-core": { +<<<<<<< HEAD "version": "1.1.2", "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/1.1.2/d46450c9efbda848963099bc366d873b516d2ab2", "integrity": "sha512-RI/riO+tH/Nd67XHSLNfO5ru0+1zLFkjc+Aw2mt8XivDOwz5t+8456soaQrSS1b5ZsJ6OrvBYEpnYsGuxD5YyA==", +======= + "version": "1.4.0-1004b69", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/1.4.0-1004b69/99b3ffc3a8e5106caf6aeaa56d0bc340afc3b267", + "integrity": "sha512-OtZL9c4wfCXmEgcj47YWOKG4uxiU1xaAWySrkkxZ4NwV7zF809VXEqeVyPy1MReVUixwi4Sgc3mVCT317wl0Cw==", +>>>>>>> 8482196 (fix: bump xverse-core version and handle sponsor transaction error) "license": "ISC", "dependencies": { + "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", "@scure/btc-signer": "^1.0.0", + "@stacks/auth": "^6.5.1", "@stacks/encryption": "6.1.1", "@stacks/network": "4.3.5", "@stacks/storage": "^6.0.0", "@stacks/transactions": "4.3.5", "@stacks/wallet-sdk": "^5.0.2", + "@zondax/ledger-stacks": "^1.0.4", "axios": "0.27.2", + "base64url": "^3.0.1", "bignumber.js": "9.1.0", - "bip32": "^2.0.6", + "bip32": "^4.0.0", "bip39": "3.0.3", "bitcoin-address-validation": "^2.2.1", - "bitcoinjs-lib": "5.2.0", + "bitcoinjs-lib": "^6.1.3", "bitcoinjs-message": "^2.2.0", "bn.js": "^5.1.3", + "bs58check": "^3.0.1", "buffer": "6.0.3", "c32check": "^2.0.0", + "ecdsa-sig-formatter": "^1.0.11", "ecpair": "^2.1.0", "jsontokens": "^4.0.1", + "ledger-bitcoin": "^0.2.1", "process": "^0.11.10", "util": "^0.12.4", + "uuidv4": "^6.2.13", "varuint-bitcoin": "^1.1.2" } }, @@ -2141,6 +2225,53 @@ "@scure/base": "~1.1.0" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/auth": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.5.4.tgz", + "integrity": "sha512-8Zw+fdKMHdnyDv6EpNnXXTLMWQq31xUYDnFJtFlmr1AnZYPJUbquiqUNTS6Cf2Fk3FSZpsBRDNfWiqss1yGycw==", + "dependencies": { + "@stacks/common": "^6.5.2", + "@stacks/encryption": "^6.5.4", + "@stacks/network": "^6.5.4", + "@stacks/profile": "^6.5.4", + "cross-fetch": "^3.1.5", + "jsontokens": "^4.0.1" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/auth/node_modules/@stacks/common": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.2.tgz", + "integrity": "sha512-tnkyEIA7YgX9GIkqlHocQPPax25uaboJ4aTX5wVs6kAGXY10+XI7VamRG4o+4DqnFVKwvIHR2fGcxdhtxNb/+Q==", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/auth/node_modules/@stacks/encryption": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.5.4.tgz", + "integrity": "sha512-osZGQXuY0BtqwiXZHMPgObg4QlqktDp2OYzbkln7A/7B1hwkjPruJ7yMK44wr0SprjaL9JCA8tKhpXxGd/tO8A==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.5.2", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/auth/node_modules/@stacks/network": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.4.tgz", + "integrity": "sha512-nONCBkTkYyH5yqO80Ith4QHC9PhipObKIGhlZETXd/csdRUkAYzE8/vJCx7LWPh4AiIVMjbEgBrGsjNR3YpGdw==", + "dependencies": { + "@stacks/common": "^6.5.2", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/encryption": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.1.1.tgz", @@ -2166,6 +2297,50 @@ "@types/node": "^18.0.4" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/profile": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.5.4.tgz", + "integrity": "sha512-YEnt2ACOuPFydNKpxYmZ2c/+UrcuusrNHTChePLyXKmU9u3cX3uBNSvdl1H13H6b2CW3G7inxMerRgssXDzTEA==", + "dependencies": { + "@stacks/common": "^6.5.2", + "@stacks/network": "^6.5.4", + "@stacks/transactions": "^6.5.4", + "jsontokens": "^4.0.1", + "schema-inspector": "^2.0.2", + "zone-file": "^2.0.0-beta.3" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/profile/node_modules/@stacks/common": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.2.tgz", + "integrity": "sha512-tnkyEIA7YgX9GIkqlHocQPPax25uaboJ4aTX5wVs6kAGXY10+XI7VamRG4o+4DqnFVKwvIHR2fGcxdhtxNb/+Q==", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/profile/node_modules/@stacks/network": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.4.tgz", + "integrity": "sha512-nONCBkTkYyH5yqO80Ith4QHC9PhipObKIGhlZETXd/csdRUkAYzE8/vJCx7LWPh4AiIVMjbEgBrGsjNR3YpGdw==", + "dependencies": { + "@stacks/common": "^6.5.2", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/profile/node_modules/@stacks/transactions": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.5.4.tgz", + "integrity": "sha512-yQhywPQ5cospYpVPbEMFRTUBZhVvyuI+meJ3fqHpu10IXX4CdURKLtW7N64c71YNXmE1tnsN7hWaPzrwjX0Dyw==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.5.2", + "@stacks/network": "^6.5.4", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/transactions": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-4.3.5.tgz", @@ -2245,6 +2420,31 @@ "node": "*" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bip32/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -2261,6 +2461,61 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bitcoinjs-lib": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz", + "integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.0", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bitcoinjs-lib/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bs58check/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/schema-inspector": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-2.0.3.tgz", + "integrity": "sha512-Q9mpYxrP3w6CpHRfnh3QLOE1urkGTLvnl7xgVH7fsu0HYJUZenUASUr4j/pf7bAxShh+4R3Ta8ZgVKak1b2wyA==", + "dependencies": { + "async": "~2.6.3" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -3665,6 +3920,11 @@ "@types/jest": "*" } }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "node_modules/@types/webextension-polyfill": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.9.2.tgz", @@ -4083,6 +4343,17 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zondax/ledger-stacks": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@zondax/ledger-stacks/-/ledger-stacks-1.0.4.tgz", + "integrity": "sha512-R8CB0CZ2poTzpcG0jhzzXZvXF7axIsmZFhp06aHCUjgz+1df63YbC4tUzyzmseekwqNWnaebWFejQKJ99WiHZA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@ledgerhq/hw-transport": "^6.28.1", + "@stacks/transactions": "^4.1.0", + "varuint-bitcoin": "^1.1.2" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5773,6 +6044,11 @@ "node": ">=6.0.0" } }, + "node_modules/bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==" + }, "node_modules/bip32/node_modules/@types/node": { "version": "10.12.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", @@ -8611,7 +8887,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -12483,6 +12758,44 @@ "shell-quote": "^1.7.3" } }, + "node_modules/ledger-bitcoin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ledger-bitcoin/-/ledger-bitcoin-0.2.2.tgz", + "integrity": "sha512-iZDuY+3tVmtEkJH6NbxCf8RElixKSEA8wt09c7IRvQj3SdJGc9g9oFUe/3bJotog0Ntzq69gDAKYJ4+srLpgHQ==", + "dependencies": { + "@ledgerhq/hw-transport": "^6.20.0", + "bip32-path": "^0.4.2", + "bitcoinjs-lib": "^6.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ledger-bitcoin/node_modules/bitcoinjs-lib": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz", + "integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.0", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ledger-bitcoin/node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14915,6 +15228,22 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -16627,11 +16956,19 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "dependencies": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -17844,6 +18181,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@bitcoinerlab/secp256k1": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", + "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "requires": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -18677,6 +19023,60 @@ } } }, + "@ledgerhq/devices": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.0.4.tgz", + "integrity": "sha512-dxOiWZmtEv1tgw70+rW8gviCRZUeGDUnxY6HUPiRqTAc0Ts2AXxiJChgAsPvIywWTGW+S67Nxq1oTZdpRbdt+A==", + "requires": { + "@ledgerhq/errors": "^6.12.7", + "@ledgerhq/logs": "^6.10.1", + "rxjs": "6", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "@ledgerhq/errors": { + "version": "6.12.7", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.12.7.tgz", + "integrity": "sha512-1BpjzFErPK7qPFx0oItcX0mNLJMplVAm2Dpl5urZlubewnTyyw5sahIBjU+8LLCWJ2eGEh/0wyvh0jMtR0n2Mg==" + }, + "@ledgerhq/hw-transport": { + "version": "6.28.5", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.28.5.tgz", + "integrity": "sha512-xmw5RhYbqExBBqTvOnOjN/RYNIGMBxFJ+zcYNfkfw/E+uEY3L7xq8Z7sC/n7URTT6xtEctElqduBJnBQE4OQtw==", + "requires": { + "@ledgerhq/devices": "^8.0.4", + "@ledgerhq/errors": "^6.12.7", + "events": "^3.3.0" + } + }, + "@ledgerhq/logs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.10.1.tgz", + "integrity": "sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -18860,32 +19260,46 @@ } }, "@secretkeylabs/xverse-core": { +<<<<<<< HEAD "version": "1.1.2", "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/1.1.2/d46450c9efbda848963099bc366d873b516d2ab2", "integrity": "sha512-RI/riO+tH/Nd67XHSLNfO5ru0+1zLFkjc+Aw2mt8XivDOwz5t+8456soaQrSS1b5ZsJ6OrvBYEpnYsGuxD5YyA==", +======= + "version": "1.4.0-1004b69", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/1.4.0-1004b69/99b3ffc3a8e5106caf6aeaa56d0bc340afc3b267", + "integrity": "sha512-OtZL9c4wfCXmEgcj47YWOKG4uxiU1xaAWySrkkxZ4NwV7zF809VXEqeVyPy1MReVUixwi4Sgc3mVCT317wl0Cw==", +>>>>>>> 8482196 (fix: bump xverse-core version and handle sponsor transaction error) "requires": { + "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", "@scure/btc-signer": "^1.0.0", + "@stacks/auth": "^6.5.1", "@stacks/encryption": "6.1.1", "@stacks/network": "4.3.5", "@stacks/storage": "^6.0.0", "@stacks/transactions": "4.3.5", "@stacks/wallet-sdk": "^5.0.2", + "@zondax/ledger-stacks": "^1.0.4", "axios": "0.27.2", + "base64url": "^3.0.1", "bignumber.js": "9.1.0", - "bip32": "^2.0.6", + "bip32": "^4.0.0", "bip39": "3.0.3", "bitcoin-address-validation": "^2.2.1", - "bitcoinjs-lib": "5.2.0", + "bitcoinjs-lib": "^6.1.3", "bitcoinjs-message": "^2.2.0", "bn.js": "^5.1.3", + "bs58check": "^3.0.1", "buffer": "6.0.3", "c32check": "^2.0.0", + "ecdsa-sig-formatter": "^1.0.11", "ecpair": "^2.1.0", "jsontokens": "^4.0.1", + "ledger-bitcoin": "^0.2.1", "process": "^0.11.10", "util": "^0.12.4", + "uuidv4": "^6.2.13", "varuint-bitcoin": "^1.1.2" }, "dependencies": { @@ -18903,6 +19317,55 @@ "@scure/base": "~1.1.0" } }, + "@stacks/auth": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.5.4.tgz", + "integrity": "sha512-8Zw+fdKMHdnyDv6EpNnXXTLMWQq31xUYDnFJtFlmr1AnZYPJUbquiqUNTS6Cf2Fk3FSZpsBRDNfWiqss1yGycw==", + "requires": { + "@stacks/common": "^6.5.2", + "@stacks/encryption": "^6.5.4", + "@stacks/network": "^6.5.4", + "@stacks/profile": "^6.5.4", + "cross-fetch": "^3.1.5", + "jsontokens": "^4.0.1" + }, + "dependencies": { + "@stacks/common": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.2.tgz", + "integrity": "sha512-tnkyEIA7YgX9GIkqlHocQPPax25uaboJ4aTX5wVs6kAGXY10+XI7VamRG4o+4DqnFVKwvIHR2fGcxdhtxNb/+Q==", + "requires": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "@stacks/encryption": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.5.4.tgz", + "integrity": "sha512-osZGQXuY0BtqwiXZHMPgObg4QlqktDp2OYzbkln7A/7B1hwkjPruJ7yMK44wr0SprjaL9JCA8tKhpXxGd/tO8A==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.5.2", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "@stacks/network": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.4.tgz", + "integrity": "sha512-nONCBkTkYyH5yqO80Ith4QHC9PhipObKIGhlZETXd/csdRUkAYzE8/vJCx7LWPh4AiIVMjbEgBrGsjNR3YpGdw==", + "requires": { + "@stacks/common": "^6.5.2", + "cross-fetch": "^3.1.5" + } + } + } + }, "@stacks/encryption": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.1.1.tgz", @@ -18930,6 +19393,52 @@ } } }, + "@stacks/profile": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.5.4.tgz", + "integrity": "sha512-YEnt2ACOuPFydNKpxYmZ2c/+UrcuusrNHTChePLyXKmU9u3cX3uBNSvdl1H13H6b2CW3G7inxMerRgssXDzTEA==", + "requires": { + "@stacks/common": "^6.5.2", + "@stacks/network": "^6.5.4", + "@stacks/transactions": "^6.5.4", + "jsontokens": "^4.0.1", + "schema-inspector": "^2.0.2", + "zone-file": "^2.0.0-beta.3" + }, + "dependencies": { + "@stacks/common": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.2.tgz", + "integrity": "sha512-tnkyEIA7YgX9GIkqlHocQPPax25uaboJ4aTX5wVs6kAGXY10+XI7VamRG4o+4DqnFVKwvIHR2fGcxdhtxNb/+Q==", + "requires": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "@stacks/network": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.4.tgz", + "integrity": "sha512-nONCBkTkYyH5yqO80Ith4QHC9PhipObKIGhlZETXd/csdRUkAYzE8/vJCx7LWPh4AiIVMjbEgBrGsjNR3YpGdw==", + "requires": { + "@stacks/common": "^6.5.2", + "cross-fetch": "^3.1.5" + } + }, + "@stacks/transactions": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.5.4.tgz", + "integrity": "sha512-yQhywPQ5cospYpVPbEMFRTUBZhVvyuI+meJ3fqHpu10IXX4CdURKLtW7N64c71YNXmE1tnsN7hWaPzrwjX0Dyw==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.5.2", + "@stacks/network": "^6.5.4", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + } + } + }, "@stacks/transactions": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-4.3.5.tgz", @@ -18991,6 +19500,24 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" }, + "bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + } + } + }, "bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -19008,6 +19535,50 @@ "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" } } + }, + "bitcoinjs-lib": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz", + "integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==", + "requires": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.0", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + } + } + }, + "bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + } + } + }, + "schema-inspector": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-2.0.3.tgz", + "integrity": "sha512-Q9mpYxrP3w6CpHRfnh3QLOE1urkGTLvnl7xgVH7fsu0HYJUZenUASUr4j/pf7bAxShh+4R3Ta8ZgVKak1b2wyA==", + "requires": { + "async": "~2.6.3" + } } } }, @@ -20238,6 +20809,11 @@ "@types/jest": "*" } }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "@types/webextension-polyfill": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.9.2.tgz", @@ -20554,6 +21130,17 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@zondax/ledger-stacks": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@zondax/ledger-stacks/-/ledger-stacks-1.0.4.tgz", + "integrity": "sha512-R8CB0CZ2poTzpcG0jhzzXZvXF7axIsmZFhp06aHCUjgz+1df63YbC4tUzyzmseekwqNWnaebWFejQKJ99WiHZA==", + "requires": { + "@babel/runtime": "^7.12.5", + "@ledgerhq/hw-transport": "^6.28.1", + "@stacks/transactions": "^4.1.0", + "varuint-bitcoin": "^1.1.2" + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -22018,6 +22605,11 @@ } } }, + "bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==" + }, "bip39": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", @@ -24220,8 +24812,7 @@ "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "evp_bytestokey": { "version": "1.0.3", @@ -27090,6 +27681,40 @@ "shell-quote": "^1.7.3" } }, + "ledger-bitcoin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ledger-bitcoin/-/ledger-bitcoin-0.2.2.tgz", + "integrity": "sha512-iZDuY+3tVmtEkJH6NbxCf8RElixKSEA8wt09c7IRvQj3SdJGc9g9oFUe/3bJotog0Ntzq69gDAKYJ4+srLpgHQ==", + "requires": { + "@ledgerhq/hw-transport": "^6.20.0", + "bip32-path": "^0.4.2", + "bitcoinjs-lib": "^6.0.1" + }, + "dependencies": { + "bitcoinjs-lib": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz", + "integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==", + "requires": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.0", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + } + }, + "bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -28905,6 +29530,21 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -30207,8 +30847,16 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "requires": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } }, "v8-compile-cache": { "version": "2.3.0", diff --git a/package.json b/package.json index 7b1f87562..1ae4e60f6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "dependencies": { "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "1.3.0", + "@secretkeylabs/xverse-core": "1.4.0-1004b69", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "^6.1.1", diff --git a/src/app/hooks/useSponsoredTransaction.ts b/src/app/hooks/useSponsoredTransaction.ts index 928eee07d..1b14a99a1 100644 --- a/src/app/hooks/useSponsoredTransaction.ts +++ b/src/app/hooks/useSponsoredTransaction.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { getSponsorInfo } from '@secretkeylabs/xverse-core'; +import { getSponsorInfo } from '@secretkeylabs/xverse-core/api'; export const useSponsorInfoQuery = (sponsorUrl?: string) => useQuery({ @@ -26,7 +26,6 @@ export const useSponsoredTransaction = (sponsorUrl?: string) => { return { isSponsored, - setIsSponsored, }; }; diff --git a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx index bfdc4613a..9fedffeee 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -11,6 +11,7 @@ import { StacksTransaction } 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'; export type SwapConfirmationInput = { from: Currency; @@ -68,7 +69,8 @@ export function useConfirmSwap( state: { txid: '', currency: 'STX', - error: e.message, + error: e instanceof ApiResponseError ? e.data.message : e.message, + sponsored: isSponsored, browserTx: true, }, }); From 6f9476e620959b97ea0b9126533ef5d5bf4ef98b Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 18 Jul 2023 17:03:41 +0800 Subject: [PATCH 52/52] style: prettier --- src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx index 9fedffeee..6201c7554 100644 --- a/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx +++ b/src/app/screens/swap/swapConfirmation/useConfirmSwap.tsx @@ -11,7 +11,7 @@ import { StacksTransaction } 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 { ApiResponseError } from '@secretkeylabs/xverse-core/types'; export type SwapConfirmationInput = { from: Currency; @@ -28,7 +28,7 @@ export type SwapConfirmationInput = { functionName: string; }; -const XVERSE_SPONSOR_2_URL = "https://sponsor2.xverse.app"; +const XVERSE_SPONSOR_2_URL = 'https://sponsor2.xverse.app'; export function useConfirmSwap( input: SwapConfirmationInput,