diff --git a/package.json b/package.json index 8b30922f..32b64ae0 100644 --- a/package.json +++ b/package.json @@ -117,9 +117,9 @@ "@commitlint/cli": "17.0.3", "@commitlint/config-angular": "17.0.3", "@commitlint/config-conventional": "17.0.3", - "@eslint/compat": "1.2.4", - "@eslint/eslintrc": "3.2.0", - "@eslint/js": "9.16.0", + "@eslint/compat": "1.2.4", + "@eslint/eslintrc": "3.2.0", + "@eslint/js": "9.16.0", "@google/semantic-release-replace-plugin": "1.1.0", "@lavamoat/allow-scripts": "2.0.0", "@lavamoat/preinstall-always-fail": "1.0.0", @@ -167,7 +167,7 @@ "eslint-plugin-react-hooks": "5.1.0", "file-loader": "6.2.0", "fs": "0.0.1-security", - "globals": "15.13.0", + "globals": "15.13.0", "husky": "7.0.4", "i18next-scanner": "4.1.1", "i18next-scanner-typescript": "1.1.1", diff --git a/src/components/common/BNInput.tsx b/src/components/common/BNInput.tsx index ab65310c..5c671109 100644 --- a/src/components/common/BNInput.tsx +++ b/src/components/common/BNInput.tsx @@ -60,7 +60,10 @@ export function BNInput({ return; } - if (value === 0n && Number(valStr) === 0) { + if (value === 0n) { + if (Number(valStr) !== 0) { + setValStr(''); + } return; } @@ -119,7 +122,7 @@ export function BNInput({ onValueChanged(e.target.value)} type="number" onKeyDown={(e) => { diff --git a/src/components/common/TokenSelect.tsx b/src/components/common/TokenSelect.tsx index b050c8dc..d591b236 100644 --- a/src/components/common/TokenSelect.tsx +++ b/src/components/common/TokenSelect.tsx @@ -86,6 +86,7 @@ interface TokenSelectProps { skipHandleMaxAmount?: boolean; containerRef?: MutableRefObject; withMaxButton?: boolean; + withOnlyTokenPreselect?: boolean; } export function TokenSelect({ @@ -107,6 +108,7 @@ export function TokenSelect({ setIsOpen, containerRef, withMaxButton = true, + withOnlyTokenPreselect = true, }: TokenSelectProps) { const { t } = useTranslation(); const { currencyFormatter, currency } = useSettingsContext(); @@ -184,7 +186,7 @@ export function TokenSelect({ const isOnlyTokenNotSelected = theOnlyToken && theOnlyToken?.symbol !== selectedToken?.symbol; - if (isOnlyTokenNotSelected) { + if (withOnlyTokenPreselect && isOnlyTokenNotSelected) { onTokenChange(theOnlyToken); return; } @@ -198,7 +200,7 @@ export function TokenSelect({ ) { onTokenChange(tokensList[0]); } - }, [tokensList, onTokenChange, selectedToken]); + }, [withOnlyTokenPreselect, tokensList, onTokenChange, selectedToken]); const rowRenderer = useCallback( ({ key, index, style }) => { diff --git a/src/contexts/SwapProvider/SwapProvider.test.tsx b/src/contexts/SwapProvider/SwapProvider.test.tsx index d15547f2..05b0277a 100644 --- a/src/contexts/SwapProvider/SwapProvider.test.tsx +++ b/src/contexts/SwapProvider/SwapProvider.test.tsx @@ -33,7 +33,14 @@ const getSwapProvider = (): SwapContextAPI => { return ref.current ?? ({} as SwapContextAPI); }; -const TestConsumerComponent = forwardRef((_props, ref) => { +const waitForRetries = async (retries: number) => { + for (let i = 0; i < retries; i++) { + jest.runAllTimers(); + await new Promise(jest.requireActual('timers').setImmediate); + } +}; + +const TestConsumerComponent = forwardRef((_props: unknown, ref) => { const { getRate, swap } = useSwapContext(); useImperativeHandle(ref, () => ({ @@ -105,6 +112,7 @@ describe.only('contexts/SwapProvider', () => { beforeEach(() => { jest.resetAllMocks(); + jest.useRealTimers(); jest.spyOn(global, 'fetch').mockResolvedValue({ json: async () => ({}), @@ -643,19 +651,29 @@ describe.only('contexts/SwapProvider', () => { slippage, } = getSwapParams(); - await expect( - swap({ - srcToken, - srcDecimals, - srcAmount, - destToken, - destDecimals, - destAmount, - gasLimit, - priceRoute, - slippage, - }), - ).rejects.toThrow('Data Error: Error: Invalid transaction params'); + jest.useFakeTimers(); + + swap({ + srcToken, + srcDecimals, + srcAmount, + destToken, + destDecimals, + destAmount, + gasLimit, + priceRoute, + slippage, + }) + .then(() => { + fail('Expected to throw'); + }) + .catch((err) => { + expect(err.message).toEqual( + 'Data Error: Error: Invalid transaction params', + ); + }); + + await waitForRetries(10); }); it('handles Paraswap API error responses', async () => { @@ -691,19 +709,29 @@ describe.only('contexts/SwapProvider', () => { slippage, } = getSwapParams(); - await expect( - swap({ - srcToken, - srcDecimals, - srcAmount, - destToken, - destDecimals, - destAmount, - gasLimit, - priceRoute, - slippage, - }), - ).rejects.toThrow('Data Error: Error: Some API error happened'); + jest.useFakeTimers(); + + swap({ + srcToken, + srcDecimals, + srcAmount, + destToken, + destDecimals, + destAmount, + gasLimit, + priceRoute, + slippage, + }) + .then(() => { + fail('Expected to throw'); + }) + .catch((err) => { + expect(err.message).toEqual( + 'Data Error: Error: Some API error happened', + ); + }); + + await waitForRetries(10); }); it('handles API HTTP errors', async () => { @@ -743,19 +771,29 @@ describe.only('contexts/SwapProvider', () => { slippage, } = getSwapParams(); - await expect( - swap({ - srcToken, - srcDecimals, - srcAmount, - destToken, - destDecimals, - destAmount, - gasLimit, - priceRoute, - slippage, - }), - ).rejects.toThrow('Data Error: Error: Invalid transaction params'); + jest.useFakeTimers(); + + swap({ + srcToken, + srcDecimals, + srcAmount, + destToken, + destDecimals, + destAmount, + gasLimit, + priceRoute, + slippage, + }) + .then(() => { + fail('Expected to throw'); + }) + .catch((err) => { + expect(err.message).toEqual( + 'Data Error: Error: Invalid transaction params', + ); + }); + + await waitForRetries(10); }); describe('when everything goes right', () => { diff --git a/src/contexts/SwapProvider/SwapProvider.tsx b/src/contexts/SwapProvider/SwapProvider.tsx index aff7921f..c7aa1ddd 100644 --- a/src/contexts/SwapProvider/SwapProvider.tsx +++ b/src/contexts/SwapProvider/SwapProvider.tsx @@ -411,7 +411,8 @@ export function SwapContextProvider({ children }: { children: any }) { (result as APIError).message === 'Server too busy' || // paraswap returns responses like this: {error: 'Not enough 0x4f60a160d8c2dddaafe16fcc57566db84d674…} // when they are too slow to detect the approval - (result as any).error + (result as any).error || + result instanceof Error ); } diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 623de131..0403336c 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -318,6 +318,7 @@ "Error:": "Error:", "Estimated": "Estimated", "Estimated gas units needed to complete the transaction. Includes a small buffer. Not editable for this transaction.": "Estimated gas units needed to complete the transaction. Includes a small buffer. Not editable for this transaction.", + "Estimated loss greater than impact. Try lowering the amount.": "Estimated loss greater than impact. Try lowering the amount.", "Ethereum Address": "Ethereum Address", "Euro": "Euro", "Explore Ecosystem": "Explore Ecosystem", diff --git a/src/pages/Swap/Swap.tsx b/src/pages/Swap/Swap.tsx index 01170d72..286bbf56 100644 --- a/src/pages/Swap/Swap.tsx +++ b/src/pages/Swap/Swap.tsx @@ -35,10 +35,7 @@ import { useAccountsContext } from '@src/contexts/AccountsProvider'; import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitcoinNetwork'; import { isUserRejectionError } from '@src/utils/errors'; import { DISALLOWED_SWAP_ASSETS } from '@src/contexts/SwapProvider/models'; -import { - NetworkTokenWithBalance, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; +import { SwappableToken } from './models'; const ReviewOrderButtonContainer = styled('div')<{ isTransactionDetailsOpen: boolean; @@ -82,13 +79,6 @@ export function Swap() { useState(false); const [isConfirming, setIsConfirming] = useState(false); - const AVAX_TOKEN = tokensWBalances.find( - (token) => token.symbol === 'AVAX', - ) as NetworkTokenWithBalance | TokenWithBalanceERC20; - const USDC_TOKEN = allTokensOnNetwork.find( - (token) => token.symbol === 'USDC', - ) as TokenWithBalanceERC20; - const { calculateTokenValueToInput, reverseTokens, @@ -107,13 +97,9 @@ export function Swap() { swapWarning, isReversed, toTokenValue, - maxFromValue, optimalRate, destAmount, - } = useSwapStateFunctions({ - defaultFromToken: AVAX_TOKEN, - defaultToToken: USDC_TOKEN, - }); + } = useSwapStateFunctions(); const activeAddress = useMemo( () => @@ -134,18 +120,13 @@ export function Swap() { const toAmount = useMemo(() => { const result = - destinationInputField === 'to' && destAmount + destinationInputField === 'to' ? BigInt(destAmount) : toTokenValue?.bigint; return result; }, [destAmount, destinationInputField, toTokenValue]); - const maxFromAmount = useMemo(() => { - if (isLoading || destinationInputField === 'to') return undefined; - if (destinationInputField === 'from') return maxFromValue ?? 0n; - }, [destinationInputField, isLoading, maxFromValue]); - async function performSwap() { const { amount, @@ -261,14 +242,9 @@ export function Swap() { > { + onTokenChange={(token: SwappableToken) => { onTokenChange({ - token, - destination: 'to', - toToken: selectedToToken, - fromValue: fromTokenValue, + fromToken: token, }); }} onSelectToggle={() => { @@ -276,7 +252,6 @@ export function Swap() { setIsToTokenSelectOpen(false); }} tokensList={tokensWBalances} - maxAmount={maxFromAmount} skipHandleMaxAmount isOpen={isFromTokenSelectOpen} selectedToken={selectedFromToken} @@ -287,6 +262,7 @@ export function Swap() { onFromInputAmountChange(value); }} setIsOpen={setIsFromTokenSelectOpen} + withOnlyTokenPreselect={false} /> { - reverseTokens( - isReversed, - selectedFromToken, - selectedToToken, - undefined, - ); - }} - disabled={!selectedFromToken || !selectedToToken} sx={{ transition: 'all 0.2s', transform: isReversed ? 'rotate(0deg)' : 'rotate(180deg)', }} > - + { + reverseTokens(selectedFromToken, selectedToToken); + }} + disabled={ + !selectedFromToken || + !selectedToToken || + isLoading || + isConfirming + } + sx={{ + '&.Mui-disabled': { + backgroundColor: '#FFFFFF10', + }, + }} + > { + onTokenChange={(token: SwappableToken) => { onTokenChange({ - token, - fromToken: selectedFromToken, - destination: 'from', - fromValue: fromTokenValue, + toToken: token, }); }} onSelectToggle={() => { @@ -372,6 +351,7 @@ export function Swap() { }} setIsOpen={setIsToTokenSelectOpen} withMaxButton={false} + withOnlyTokenPreselect={false} /> {isDetailsAvailable && ( diff --git a/src/pages/Swap/hooks/useSwap.tsx b/src/pages/Swap/hooks/useSwap.tsx index 95879312..c84d5897 100644 --- a/src/pages/Swap/hooks/useSwap.tsx +++ b/src/pages/Swap/hooks/useSwap.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { BehaviorSubject, debounceTime } from 'rxjs'; import { OptimalRate, SwapSide } from 'paraswap-core'; import { useSwapContext } from '@src/contexts/SwapProvider/SwapProvider'; -import { Amount, DestinationInput, isAPIError } from '../utils'; +import { DestinationInput, isAPIError } from '../utils'; import { useTranslation } from 'react-i18next'; export interface SwapError { @@ -25,7 +25,7 @@ export function useSwap() { const setValuesDebouncedSubject = useMemo(() => { return new BehaviorSubject<{ - amount?: Amount; + amount?: bigint; toTokenAddress?: string; fromTokenAddress?: string; toTokenDecimals?: number; @@ -55,7 +55,7 @@ export function useSwap() { fromTokenDecimals && toTokenDecimals ) { - const amountString = amount.bigint.toString(); + const amountString = amount.toString(); if (amountString === '0') { setSwapError({ message: t('Please enter an amount') }); @@ -105,7 +105,7 @@ export function useSwap() { if ( fromTokenBalance && destinationInputField === 'to' && - amount.bigint > fromTokenBalance + amount > fromTokenBalance ) { setSwapError({ message: t('Insufficient balance.') }); return; @@ -114,11 +114,24 @@ export function useSwap() { }) .catch((error) => { setOptimalRate(undefined); - setSwapError({ - message: t('Something went wrong, '), - hasTryAgain: true, - errorInfo: error, - }); + setDestAmount(''); + if ( + error?.message === 'ESTIMATED_LOSS_GREATER_THAN_MAX_IMPACT' + ) { + setSwapError({ + message: t( + 'Estimated loss greater than impact. Try lowering the amount.', + ), + hasTryAgain: false, + errorInfo: error, + }); + } else { + setSwapError({ + message: t('Something went wrong, '), + hasTryAgain: true, + errorInfo: error, + }); + } }) .finally(() => { if (!isCalculateAvaxMax) { @@ -150,9 +163,12 @@ export function useSwap() { return { setValuesDebouncedSubject, swapError, + setSwapError, + setIsSwapLoading, isSwapLoading, optimalRate, swapGasLimit, destAmount, + setDestAmount, }; } diff --git a/src/pages/Swap/hooks/useSwapStateFunctions.ts b/src/pages/Swap/hooks/useSwapStateFunctions.ts index 76fa5a6e..9c2f2b1a 100644 --- a/src/pages/Swap/hooks/useSwapStateFunctions.ts +++ b/src/pages/Swap/hooks/useSwapStateFunctions.ts @@ -1,74 +1,63 @@ import { t } from 'i18next'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { bigIntToString } from '@avalabs/core-utils-sdk'; -import { Amount, DestinationInput, getTokenAddress } from '../utils'; +import { USDC_ADDRESS_C_CHAIN } from '@src/utils/constants'; import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; import { usePageHistory } from '@src/hooks/usePageHistory'; import { useSendAnalyticsData } from '@src/hooks/useSendAnalyticsData'; import { useSwap } from './useSwap'; import { DISALLOWED_SWAP_ASSETS } from '@src/contexts/SwapProvider/models'; -import { - NetworkTokenWithBalance, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; import { stringToBigint } from '@src/utils/stringToBigint'; -import { TokenUnit } from '@avalabs/core-utils-sdk'; -export function useSwapStateFunctions({ - defaultFromToken, - defaultToToken, -}: { - defaultFromToken: NetworkTokenWithBalance | TokenWithBalanceERC20; - defaultToToken: NetworkTokenWithBalance | TokenWithBalanceERC20; -}) { +import { Amount, DestinationInput, getTokenAddress } from '../utils'; +import { useTokensBySymbols } from './useTokensBySymbols'; +import { SwappableToken } from '../models'; + +export function useSwapStateFunctions() { const tokensWBalances = useTokensWithBalances({ disallowedAssets: DISALLOWED_SWAP_ASSETS, }); - const { setNavigationHistoryData } = usePageHistory(); + const { getPageHistoryData, setNavigationHistoryData } = usePageHistory(); const { sendTokenSelectedAnalytics, sendAmountEnteredAnalytics } = useSendAnalyticsData(); - const { getPageHistoryData } = usePageHistory(); const pageHistory: { - selectedFromToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; - selectedToToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; + selectedFromToken?: SwappableToken; + selectedToToken?: SwappableToken; destinationInputField?: DestinationInput; - tokenValue?: Amount; + tokenValue?: bigint; isLoading: boolean; } = getPageHistoryData(); const [destinationInputField, setDestinationInputField] = useState(''); - const [selectedFromToken, setSelectedFromToken] = useState< - NetworkTokenWithBalance | TokenWithBalanceERC20 - >(); - - const [selectedToToken, setSelectedToToken] = useState< - NetworkTokenWithBalance | TokenWithBalanceERC20 - >(); + const [selectedFromToken, setSelectedFromToken] = useState(); + const [selectedToToken, setSelectedToToken] = useState(); const [isReversed, setIsReversed] = useState(false); const [swapWarning, setSwapWarning] = useState(''); const [defaultFromValue, setFromDefaultValue] = useState(); const [fromTokenValue, setFromTokenValue] = useState(); const [toTokenValue, setToTokenValue] = useState(); - const [maxFromValue, setMaxFromValue] = useState(); - const isHistoryLoaded = useRef(false); const { setValuesDebouncedSubject, swapError, isSwapLoading, + setIsSwapLoading, optimalRate, swapGasLimit, destAmount, + setDestAmount, + setSwapError, } = useSwap(); const calculateTokenValueToInput = useCallback( ( - amount: Amount, + amount: bigint, destinationInput: DestinationInput, - sourceToken?: NetworkTokenWithBalance | TokenWithBalanceERC20, - destinationToken?: NetworkTokenWithBalance | TokenWithBalanceERC20, + sourceToken?: SwappableToken, + destinationToken?: SwappableToken, ) => { if (!sourceToken || !destinationToken) { return; @@ -84,84 +73,65 @@ export function useSwapStateFunctions({ toTokenDecimals: destinationToken.decimals, amount, destinationInputField: destinationInput, - fromTokenBalance: selectedFromToken?.balance, + fromTokenBalance: sourceToken?.balance, }); }, - [selectedFromToken?.balance, setValuesDebouncedSubject], + [setValuesDebouncedSubject], ); + const { AVAX, USDC } = useTokensBySymbols({ + AVAX: true, + USDC: USDC_ADDRESS_C_CHAIN, + }); + // reload and recalculate the data from the history useEffect(() => { if ( - Object.keys(pageHistory).length > 1 && + pageHistory && !pageHistory.isLoading && - !isHistoryLoaded.current + !selectedFromToken && + !selectedToToken ) { const historyFromToken = pageHistory.selectedFromToken ? { ...pageHistory.selectedFromToken, balance: pageHistory.selectedFromToken.balance, } - : undefined; + : AVAX; setSelectedFromToken(historyFromToken); - setMaxFromValue(historyFromToken?.balance); const historyToToken = pageHistory.selectedToToken ? { ...pageHistory.selectedToToken, balance: pageHistory.selectedToToken.balance, } - : undefined; + : USDC; setSelectedToToken(historyToToken); - const tokenValueBigint = - pageHistory.tokenValue && pageHistory.tokenValue.bigint - ? pageHistory.tokenValue.bigint - : 0n; + const tokenValueBigint = pageHistory.tokenValue ?? 0n; if (pageHistory.destinationInputField === 'from') { setToTokenValue({ bigint: tokenValueBigint, - amount: new TokenUnit( + amount: bigIntToString( tokenValueBigint, - pageHistory.selectedToToken?.decimals ?? 18, - '', - ).toDisplay(), + historyToToken?.decimals ?? 18, + ), }); } else { setFromDefaultValue(tokenValueBigint); } calculateTokenValueToInput( - { - bigint: tokenValueBigint, - amount: new TokenUnit( - tokenValueBigint, - pageHistory.selectedToToken?.decimals ?? 18, - '', - ).toDisplay(), - }, + tokenValueBigint, pageHistory.destinationInputField || 'to', historyFromToken, historyToToken, ); - - isHistoryLoaded.current = true; - } - - if ( - !pageHistory.selectedFromToken && - !pageHistory.selectedToToken && - !pageHistory.isLoading && - !isHistoryLoaded.current && - defaultFromToken && - defaultFromToken - ) { - setSelectedFromToken(defaultFromToken); - setSelectedToToken(defaultToToken); - isHistoryLoaded.current = true; } }, [ calculateTokenValueToInput, - defaultFromToken, - defaultToToken, pageHistory, + AVAX, + USDC, + selectedFromToken, + selectedToToken, ]); const resetValues = () => { @@ -182,35 +152,28 @@ export function useSwapStateFunctions({ fromToken, toToken, fromValue, + toValue, }: { - fromToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; - toToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; + fromToken?: SwappableToken; + toToken?: SwappableToken; fromValue?: Amount; + toValue?: Amount; }) => { if (!fromToken || !toToken) { return; } - const amount = fromValue - ? ({ - amount: fromValue.amount || '0', - bigint: stringToBigint( - fromValue.amount || '0', - fromToken.decimals || 18, - ), - } as Amount) - : undefined; - if (amount) { - calculateTokenValueToInput(amount, 'to', fromToken, toToken); + if (fromValue) { + calculateTokenValueToInput(fromValue.bigint, 'to', fromToken, toToken); + } else if (toValue) { + calculateTokenValueToInput(toValue.bigint, 'from', fromToken, toToken); } else { resetValues(); } }; const reverseTokens = ( - reversed: boolean, - fromToken?: NetworkTokenWithBalance | TokenWithBalanceERC20, - toToken?: NetworkTokenWithBalance | TokenWithBalanceERC20, - fromValue?: Amount, + fromToken?: SwappableToken, + toToken?: SwappableToken, ) => { if ( !tokensWBalances.some( @@ -225,69 +188,137 @@ export function useSwapStateFunctions({ ); return; } + setSwapError({ message: '' }); setSelectedFromToken(toToken); setSelectedToToken(fromToken); - setIsReversed(!reversed); - calculateSwapValue({ - fromToken: toToken, - toToken: fromToken, - fromValue, - }); + setIsReversed((reversed) => reversed); + + if (!toToken || !fromToken || !destAmount) { + return; + } + + const fromValue = + destinationInputField === 'to' + ? { + amount: bigIntToString(BigInt(destAmount), fromToken.decimals), + bigint: BigInt(destAmount), + } + : toTokenValue; + setDestAmount(''); + setFromTokenValue(fromValue); + setToTokenValue(undefined); + + if (fromValue) { + setFromDefaultValue(fromValue.bigint); + setIsSwapLoading(true); + calculateTokenValueToInput(fromValue.bigint, 'to', toToken, fromToken); + } }; const onTokenChange = ({ - token, - destination, - toToken, fromToken, - fromValue, - }: { - token: NetworkTokenWithBalance | TokenWithBalanceERC20; - destination: 'from' | 'to'; - toToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; - fromToken?: NetworkTokenWithBalance | TokenWithBalanceERC20; - fromValue?: Amount; - }) => { - setSwapWarning(''); - if (destination === 'to') { - setSelectedFromToken(token); - setMaxFromValue(token?.balance); - if (!toToken) { - return; + toToken, + }: + | { + fromToken: SwappableToken; + toToken?: never; } + | { + toToken: SwappableToken; + fromToken?: never; + }) => { + sendTokenSelectedAnalytics('Swap'); + setSwapWarning(''); + + if (fromToken) { + setSelectedFromToken(fromToken); + } else if (toToken) { setSelectedToToken(toToken); + } + + const data = { + toToken: toToken ?? selectedToToken, + fromToken: fromToken ?? selectedFromToken, + fromValue: destinationInputField === 'to' ? fromTokenValue : undefined, + toValue: destinationInputField === 'to' ? undefined : toTokenValue, + }; + + const newTokenDecimals = (toToken ?? fromToken).decimals; + const prevTokenDecimals = (toToken ? selectedToToken : selectedFromToken) + ?.decimals; + const decimalsDiff = + typeof prevTokenDecimals === 'number' + ? newTokenDecimals - prevTokenDecimals + : 0; + + // If previous and new token have different denominations, + // we need to recalculate the amount. + const currentAmount = + destinationInputField === 'to' && fromToken + ? fromTokenValue + : destinationInputField === 'from' && toToken + ? toTokenValue + : undefined; + + if (decimalsDiff && currentAmount) { + const amount = { + amount: currentAmount.amount, + bigint: stringToBigint(currentAmount.amount, newTokenDecimals), + }; + + if (fromToken) { + setFromDefaultValue(amount.bigint); + setFromTokenValue(amount); + calculateTokenValueToInput( + amount.bigint, + destinationInputField, + fromToken, + selectedToToken, + ); + } else if (toToken) { + setToTokenValue(amount); + calculateTokenValueToInput( + amount.bigint, + destinationInputField, + selectedToToken, + toToken, + ); + } } else { - setSelectedToToken(token); + calculateSwapValue(data); } - const data = - destination === 'to' - ? { selectedFromToken: token, selectedToToken: toToken, fromValue } - : { selectedFromToken: fromToken, selectedToToken: token, fromValue }; - calculateSwapValue(data); setNavigationHistoryData({ - ...data, - destination, + selectedFromToken: data.fromToken, + selectedToToken: data.toToken, + tokenValue: (data.fromValue ?? data.toValue)?.bigint, + destinationInputField, }); - sendTokenSelectedAnalytics('Swap'); }; const onFromInputAmountChange = (value: Amount) => { + setDestAmount(''); setFromDefaultValue(value.bigint); - setFromTokenValue(value as any); - calculateTokenValueToInput(value, 'to', selectedFromToken, selectedToToken); + setFromTokenValue(value); + calculateTokenValueToInput( + value.bigint, + 'to', + selectedFromToken, + selectedToToken, + ); setNavigationHistoryData({ selectedFromToken, selectedToToken, - tokenValue: value, + tokenValue: value.bigint, destinationInputField: 'to', }); sendAmountEnteredAnalytics('Swap'); }; const onToInputAmountChange = (value: Amount) => { - setToTokenValue(value as any); + setDestAmount(''); + setToTokenValue(value); calculateTokenValueToInput( - value as any, + value.bigint, 'from', selectedFromToken, selectedToToken, @@ -295,7 +326,7 @@ export function useSwapStateFunctions({ setNavigationHistoryData({ selectedFromToken, selectedToToken, - tokenValue: value, + tokenValue: value.bigint, destinationInputField: 'from', }); sendAmountEnteredAnalytics('Swap'); @@ -321,7 +352,6 @@ export function useSwapStateFunctions({ swapWarning, isReversed, toTokenValue, - maxFromValue, optimalRate, destAmount, getSwapValues, diff --git a/src/pages/Swap/hooks/useTokensBySymbols.ts b/src/pages/Swap/hooks/useTokensBySymbols.ts new file mode 100644 index 00000000..4ca34af0 --- /dev/null +++ b/src/pages/Swap/hooks/useTokensBySymbols.ts @@ -0,0 +1,50 @@ +import { + NftTokenWithBalance, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; + +import { DISALLOWED_SWAP_ASSETS } from '@src/contexts/SwapProvider/models'; +import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; + +type RequestedTokens = { + // Provide `true` for native tokens and the address for ERC-* + [symbol: string]: string | boolean; +}; + +type Result = Record< + keyof T, + Exclude | undefined +>; + +export function useTokensBySymbols( + requestedTokens: T, +): Result { + const balances = useTokensWithBalances({ + disallowedAssets: DISALLOWED_SWAP_ASSETS, + forceShowTokensWithoutBalances: true, + }); + + return Object.entries(requestedTokens).reduce( + (dict, [symbol, identifier]) => { + if (!identifier) { + return dict; + } + + return { + ...dict, + [symbol]: balances.find((token) => { + if (typeof identifier === 'boolean') { + return token.type === TokenType.NATIVE && token.symbol === symbol; + } else if (typeof identifier === 'string') { + return ( + token.type !== TokenType.NATIVE && + token.address.toLowerCase() === identifier.toLowerCase() + ); + } + }), + }; + }, + {} as Result, + ); +} diff --git a/src/pages/Swap/models.ts b/src/pages/Swap/models.ts new file mode 100644 index 00000000..4438a074 --- /dev/null +++ b/src/pages/Swap/models.ts @@ -0,0 +1,6 @@ +import { + NetworkTokenWithBalance, + TokenWithBalanceERC20, +} from '@avalabs/vm-module-types'; + +export type SwappableToken = NetworkTokenWithBalance | TokenWithBalanceERC20; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 00000000..268e7444 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,2 @@ +export const USDC_ADDRESS_C_CHAIN = + '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E' as const;