|
|
|
@ -19,6 +19,7 @@ import { IconButton } from '../../components/buttons/IconButton'; |
|
|
|
|
import { SolidButton } from '../../components/buttons/SolidButton'; |
|
|
|
|
import { ChevronIcon } from '../../components/icons/Chevron'; |
|
|
|
|
import { TextField } from '../../components/input/TextField'; |
|
|
|
|
import { toastIgpDetails } from '../../components/toast/IgpDetailsToast'; |
|
|
|
|
import { config } from '../../consts/config'; |
|
|
|
|
import SwapIcon from '../../images/icons/swap.svg'; |
|
|
|
|
import { Color } from '../../styles/Color'; |
|
|
|
@ -27,17 +28,20 @@ import { getProtocolType } from '../caip/chains'; |
|
|
|
|
import { getTokenAddress, isNonFungibleToken, parseCaip19Id } from '../caip/tokens'; |
|
|
|
|
import { ChainSelectField } from '../chains/ChainSelectField'; |
|
|
|
|
import { getChainDisplayName } from '../chains/utils'; |
|
|
|
|
import { getChainMetadata } from '../multiProvider'; |
|
|
|
|
import { AppState, useStore } from '../store'; |
|
|
|
|
import { SelectOrInputTokenIds } from '../tokens/SelectOrInputTokenIds'; |
|
|
|
|
import { TokenSelectField } from '../tokens/TokenSelectField'; |
|
|
|
|
import { useIsApproveRequired } from '../tokens/approval'; |
|
|
|
|
import { useDestinationBalance, useOriginBalance } from '../tokens/balances'; |
|
|
|
|
import { getToken } from '../tokens/metadata'; |
|
|
|
|
import { useRouteChains } from '../tokens/routes/hooks'; |
|
|
|
|
import { RoutesMap } from '../tokens/routes/types'; |
|
|
|
|
import { getTokenRoute } from '../tokens/routes/utils'; |
|
|
|
|
import { getTokenRoute, isRouteFromNative } from '../tokens/routes/utils'; |
|
|
|
|
import { useAccountForChain } from '../wallet/hooks'; |
|
|
|
|
|
|
|
|
|
import { TransferFormValues } from './types'; |
|
|
|
|
import { useIgpQuote } from './useIgpQuote'; |
|
|
|
|
import { useTokenTransfer } from './useTokenTransfer'; |
|
|
|
|
|
|
|
|
|
export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { |
|
|
|
@ -49,10 +53,13 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { |
|
|
|
|
// Flag for check current type of token
|
|
|
|
|
const [isNft, setIsNft] = useState(false); |
|
|
|
|
|
|
|
|
|
const balances = useStore((state) => state.balances); |
|
|
|
|
const { balances, igpQuote } = useStore((state) => ({ |
|
|
|
|
balances: state.balances, |
|
|
|
|
igpQuote: state.igpQuote, |
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
const validate = (values: TransferFormValues) => |
|
|
|
|
validateFormValues(values, tokenRoutes, balances); |
|
|
|
|
validateFormValues(values, tokenRoutes, balances, igpQuote); |
|
|
|
|
|
|
|
|
|
const onSubmitForm = (values: TransferFormValues) => { |
|
|
|
|
logger.debug('Reviewing transfer form values:', JSON.stringify(values)); |
|
|
|
@ -190,7 +197,7 @@ function AmountSection({ |
|
|
|
|
isReview: boolean; |
|
|
|
|
}) { |
|
|
|
|
const { values } = useFormikContext<TransferFormValues>(); |
|
|
|
|
const { balance, decimals } = useOriginBalance(values, tokenRoutes); |
|
|
|
|
const { tokenBalance, tokenDecimals } = useOriginBalance(values, tokenRoutes); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div className="flex-1"> |
|
|
|
@ -198,7 +205,7 @@ function AmountSection({ |
|
|
|
|
<label htmlFor="amount" className="block uppercase text-sm text-gray-500 pl-0.5"> |
|
|
|
|
Amount |
|
|
|
|
</label> |
|
|
|
|
<TokenBalance label="My balance" balance={balance} decimals={decimals} /> |
|
|
|
|
<TokenBalance label="My balance" balance={tokenBalance} decimals={tokenDecimals} /> |
|
|
|
|
</div> |
|
|
|
|
{isNft ? ( |
|
|
|
|
<SelectOrInputTokenIds disabled={isReview} tokenRoutes={tokenRoutes} /> |
|
|
|
@ -212,7 +219,7 @@ function AmountSection({ |
|
|
|
|
step="any" |
|
|
|
|
disabled={isReview} |
|
|
|
|
/> |
|
|
|
|
<MaxButton disabled={isReview} balance={balance} decimals={decimals} /> |
|
|
|
|
<MaxButton disabled={isReview} balance={tokenBalance} decimals={tokenDecimals} /> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
@ -395,15 +402,25 @@ function SelfButton({ disabled }: { disabled?: boolean }) { |
|
|
|
|
|
|
|
|
|
function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes: RoutesMap }) { |
|
|
|
|
const { |
|
|
|
|
values: { amount, originCaip2Id, destinationCaip2Id, tokenCaip19Id: token }, |
|
|
|
|
values: { amount, originCaip2Id, destinationCaip2Id, tokenCaip19Id }, |
|
|
|
|
} = useFormikContext<TransferFormValues>(); |
|
|
|
|
|
|
|
|
|
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, token, tokenRoutes); |
|
|
|
|
const isNft = token && isNonFungibleToken(token); |
|
|
|
|
const sendValue = isNft ? amount.toString() : toWei(amount, route?.originDecimals).toString(); |
|
|
|
|
const { isLoading, isApproveRequired } = useIsApproveRequired(route!, token, sendValue, visible); |
|
|
|
|
const originProtocol = getProtocolType(originCaip2Id); |
|
|
|
|
const originUnitName = ProtocolSmallestUnit[originProtocol]; |
|
|
|
|
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); |
|
|
|
|
const isNft = tokenCaip19Id && isNonFungibleToken(tokenCaip19Id); |
|
|
|
|
const sendValue = isNft ? amount.toString() : toWei(amount, route?.originDecimals).toFixed(0); |
|
|
|
|
const originUnitName = ProtocolSmallestUnit[getProtocolType(originCaip2Id)]; |
|
|
|
|
const originTokenSymbol = getToken(tokenCaip19Id)?.symbol || ''; |
|
|
|
|
const originNativeTokenSymbol = getChainMetadata(originCaip2Id)?.nativeToken?.symbol || ''; |
|
|
|
|
|
|
|
|
|
const { isLoading: isApproveLoading, isApproveRequired } = useIsApproveRequired( |
|
|
|
|
tokenCaip19Id, |
|
|
|
|
sendValue, |
|
|
|
|
route, |
|
|
|
|
visible, |
|
|
|
|
); |
|
|
|
|
const { isLoading: isQuoteLoading, igpQuote } = useIgpQuote(route); |
|
|
|
|
|
|
|
|
|
const isLoading = isApproveLoading || isQuoteLoading; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div |
|
|
|
@ -422,7 +439,7 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes |
|
|
|
|
<div> |
|
|
|
|
<h4>Transaction 1: Approve Transfer</h4> |
|
|
|
|
<div className="mt-1.5 ml-1.5 pl-2 border-l border-gray-300 space-y-1.5 text-xs"> |
|
|
|
|
<p>{`Token Address: ${getTokenAddress(token)}`}</p> |
|
|
|
|
<p>{`Token Address: ${getTokenAddress(tokenCaip19Id)}`}</p> |
|
|
|
|
<p>{`Collateral Address: ${route?.baseRouterAddress}`}</p> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
@ -430,11 +447,26 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes |
|
|
|
|
<div> |
|
|
|
|
<h4>{`Transaction${isApproveRequired ? ' 2' : ''}: Transfer Remote`}</h4> |
|
|
|
|
<div className="mt-1.5 ml-1.5 pl-2 border-l border-gray-300 space-y-1.5 text-xs"> |
|
|
|
|
<p>{`Remote Token: ${route?.destRouterAddress}`}</p> |
|
|
|
|
<p className="flex"> |
|
|
|
|
<span className="min-w-[7rem]">Remote Token</span> |
|
|
|
|
<span>{route?.destRouterAddress}</span> |
|
|
|
|
</p> |
|
|
|
|
{isNft ? ( |
|
|
|
|
<p>{`Token ID: ${sendValue}`}</p> |
|
|
|
|
<p className="flex"> |
|
|
|
|
<span className="min-w-[7rem]">Token ID</span> |
|
|
|
|
<span>{sendValue}</span> |
|
|
|
|
</p> |
|
|
|
|
) : ( |
|
|
|
|
<p>{`Amount (${originUnitName}): ${sendValue}`}</p> |
|
|
|
|
<> |
|
|
|
|
<p className="flex"> |
|
|
|
|
<span className="min-w-[7rem]">{`Amount (${originUnitName})`}</span> |
|
|
|
|
<span>{`${sendValue} ${originTokenSymbol}`}</span> |
|
|
|
|
</p> |
|
|
|
|
<p className="flex"> |
|
|
|
|
<span className="min-w-[7rem]">{`Interchain Gas (${originUnitName})`}</span> |
|
|
|
|
<span>{`${igpQuote?.weiAmount || '0'} ${originNativeTokenSymbol}`}</span> |
|
|
|
|
</p> |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
@ -448,6 +480,7 @@ function validateFormValues( |
|
|
|
|
values: TransferFormValues, |
|
|
|
|
tokenRoutes: RoutesMap, |
|
|
|
|
balances: AppState['balances'], |
|
|
|
|
igpQuote: AppState['igpQuote'], |
|
|
|
|
) { |
|
|
|
|
const { originCaip2Id, destinationCaip2Id, amount, tokenCaip19Id, recipientAddress } = values; |
|
|
|
|
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); |
|
|
|
@ -472,7 +505,16 @@ function validateFormValues( |
|
|
|
|
|
|
|
|
|
if (!isNft) { |
|
|
|
|
// Validate balances for ERC20-like tokens
|
|
|
|
|
if (sendValue.gt(balances.senderBalance)) return { amount: 'Insufficient balance' }; |
|
|
|
|
if (sendValue.gt(balances.senderTokenBalance)) return { amount: 'Insufficient balance' }; |
|
|
|
|
// Ensure balances can cover IGP fees
|
|
|
|
|
const igpWeiAmount = new BigNumber(igpQuote?.weiAmount || 0); |
|
|
|
|
const requiredNativeBalance = isRouteFromNative(route) |
|
|
|
|
? sendValue.plus(igpWeiAmount) |
|
|
|
|
: igpWeiAmount; |
|
|
|
|
if (requiredNativeBalance.gt(balances.senderNativeBalance)) { |
|
|
|
|
toastIgpDetails(); |
|
|
|
|
return { amount: 'Insufficient native token for gas' }; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// Validate balances for ERC721-like tokens
|
|
|
|
|
const { isSenderNftOwner, senderNftIds } = balances; |
|
|
|
|