From 8bb064f34bcfc2982b0b35b4aaed4464f143655c Mon Sep 17 00:00:00 2001 From: Charlie McKenzie Date: Thu, 30 Nov 2023 12:49:18 +1100 Subject: [PATCH] WT-1933 Bridge form & available balances (#1214) Co-authored-by: Mikhala Co-authored-by: Mikhala <122326421+imx-mikhala@users.noreply.github.com> --- .../components/CoinSelector/CoinSelector.tsx | 3 +- .../TextInputForm/TextInputForm.tsx | 2 +- .../CryptoFiatProvider.tsx | 2 +- .../view-context/XBridgeViewContextTypes.ts | 16 +- .../src/resources/text/textConfig.ts | 9 +- .../src/widgets/x-bridge/XBridgeWidget.tsx | 10 +- .../x-bridge/components/BridgeForm.tsx | 293 +++++++----------- .../components/TokenSelectShimmer.tsx | 75 +++++ .../x-bridge/context/XBridgeContext.ts | 32 +- .../src/widgets/x-bridge/views/Bridge.tsx | 35 ++- 10 files changed, 284 insertions(+), 193 deletions(-) create mode 100644 packages/checkout/widgets-lib/src/widgets/x-bridge/components/TokenSelectShimmer.tsx diff --git a/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelector.tsx b/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelector.tsx index 44b6f4a259..e19b943444 100644 --- a/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelector.tsx +++ b/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelector.tsx @@ -1,6 +1,7 @@ import { Body, - BottomSheet, Box, + BottomSheet, + Box, } from '@biom3/react'; import { CoinSelectorOption, CoinSelectorOptionProps } from './CoinSelectorOption'; import { selectOptionsContainerStyles } from './CoinSelectorStyles'; diff --git a/packages/checkout/widgets-lib/src/components/FormComponents/TextInputForm/TextInputForm.tsx b/packages/checkout/widgets-lib/src/components/FormComponents/TextInputForm/TextInputForm.tsx index 7f54129b28..6d3886bc06 100644 --- a/packages/checkout/widgets-lib/src/components/FormComponents/TextInputForm/TextInputForm.tsx +++ b/packages/checkout/widgets-lib/src/components/FormComponents/TextInputForm/TextInputForm.tsx @@ -73,7 +73,7 @@ export function TextInputForm({ onFocus={handleOnFocus} disabled={disabled} hideClearValueButton - sx={{ minWidth: 'base.spacing.x46' }} + sx={{ minWidth: '100%' }} > {maxButtonClick && ( { - if (!cryptoFiat || tokenSymbols.length === 0 || !fiatSymbol) return; + if (!cryptoFiat || !fiatSymbol) return; (async () => { const conversions = await getCryptoToFiatConversion( diff --git a/packages/checkout/widgets-lib/src/context/view-context/XBridgeViewContextTypes.ts b/packages/checkout/widgets-lib/src/context/view-context/XBridgeViewContextTypes.ts index 10d614f971..19139a6c91 100644 --- a/packages/checkout/widgets-lib/src/context/view-context/XBridgeViewContextTypes.ts +++ b/packages/checkout/widgets-lib/src/context/view-context/XBridgeViewContextTypes.ts @@ -2,12 +2,26 @@ import { ViewType } from './ViewType'; export enum XBridgeWidgetViews { BRIDGE_WALLET_SELECTION = 'BRIDGE_WALLET_SELECTION', + BRIDGE_FORM = 'BRIDGE_FORM', + BRIDGE_REVIEW = 'BRIDGE_REVIEW', } export type XBridgeWidgetView = - | XBridgeCrossWalletSelection; + | XBridgeCrossWalletSelection + | XBridgeForm + | XBridgeReview; interface XBridgeCrossWalletSelection extends ViewType { type: XBridgeWidgetViews.BRIDGE_WALLET_SELECTION, data?: {} } + +interface XBridgeForm extends ViewType { + type: XBridgeWidgetViews.BRIDGE_FORM, + data?: {} +} + +interface XBridgeReview extends ViewType { + type: XBridgeWidgetViews.BRIDGE_REVIEW, + data?: {} +} diff --git a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts index e15724266c..a4c7299801 100644 --- a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts +++ b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts @@ -203,6 +203,13 @@ export const text = { header: { title: 'Move coins', }, + xBridgeContent: { + title: 'How much would you like to move?', + }, + xBridgeFees: { + title: 'Gas Fee', + fiatPricePrefix: '~ USD', + }, content: { title: 'What would you like to move from Ethereum to Immutable zkEVM?', fiatPricePrefix: 'Approx USD', @@ -213,7 +220,7 @@ export const text = { inputPlaceholder: '0', selectorTitle: 'What would you like to move?', }, - buttonText: 'Move', + buttonText: 'Review', }, fees: { title: 'Fees subtotal', diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx index bf6c32d2f9..5b4d62db58 100644 --- a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx @@ -15,9 +15,14 @@ import { initialViewState, viewReducer, } from '../../context/view-context/ViewContext'; -import { XBridgeContext, xBridgeReducer, initialXBridgeState } from './context/XBridgeContext'; +import { + XBridgeContext, + xBridgeReducer, + initialXBridgeState, +} from './context/XBridgeContext'; import { widgetTheme } from '../../lib/theme'; import { BridgeWalletSelection } from './views/BridgeWalletSelection'; +import { Bridge } from './views/Bridge'; export type BridgeWidgetInputs = BridgeWidgetParams & { config: StrongCheckoutWidgetsConfig, @@ -57,6 +62,9 @@ export function XBridgeWidget({ {viewState.view.type === XBridgeWidgetViews.BRIDGE_WALLET_SELECTION && ( )} + {viewState.view.type === XBridgeWidgetViews.BRIDGE_FORM && ( + + )} diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx index 65c931f7eb..5742db0571 100644 --- a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx +++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx @@ -1,17 +1,20 @@ import { - Box, Button, Heading, OptionKey, + Box, + Button, + Heading, + MenuItem, + OptionKey, } from '@biom3/react'; import { - CheckoutErrorType, GasEstimateBridgeToL2Result, GasEstimateType, GetBalanceResult, + GasEstimateBridgeToL2Result, GetBalanceResult, } from '@imtbl/checkout-sdk'; import { useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; -import { ApproveDepositBridgeResponse, BridgeDepositResponse } from '@imtbl/bridge-sdk'; import { BigNumber, utils } from 'ethers'; import { amountInputValidation } from '../../../lib/validations/amountInputValidations'; -import { XBridgeContext } from '../context/XBridgeContext'; -import { SharedViews, ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; +import { BridgeActions, XBridgeContext } from '../context/XBridgeContext'; +import { ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; import { BridgeWidgetViews } from '../../../context/view-context/BridgeViewContextTypes'; import { CryptoFiatActions, CryptoFiatContext } from '../../../context/crypto-fiat-context/CryptoFiatContext'; import { text } from '../../../resources/text/textConfig'; @@ -21,7 +24,6 @@ import { } from '../../../lib/utils'; import { SelectForm } from '../../../components/FormComponents/SelectForm/SelectForm'; import { validateAmount, validateToken } from '../functions/BridgeFormValidator'; -import { Fees } from '../../../components/Fees/Fees'; import { bridgeFormButtonContainerStyles, bridgeFormWrapperStyles, @@ -29,33 +31,49 @@ import { } from './BridgeFormStyles'; import { CoinSelectorOptionProps } from '../../../components/CoinSelector/CoinSelectorOption'; import { useInterval } from '../../../lib/hooks/useInterval'; -import { DEFAULT_TOKEN_DECIMALS, DEFAULT_QUOTE_REFRESH_INTERVAL, NATIVE } from '../../../lib'; +import { + DEFAULT_TOKEN_DECIMALS, + DEFAULT_QUOTE_REFRESH_INTERVAL, + NATIVE, +} from '../../../lib'; import { swapButtonIconLoadingStyle } from '../../swap/components/SwapButtonStyles'; import { TransactionRejected } from '../../../components/TransactionRejected/TransactionRejected'; import { NotEnoughGas } from '../../../components/NotEnoughGas/NotEnoughGas'; -import { ConnectLoaderContext } from '../../../context/connect-loader-context/ConnectLoaderContext'; +import { XBridgeWidgetViews } from '../../../context/view-context/XBridgeViewContextTypes'; +import { TokenSelectShimmer } from './TokenSelectShimmer'; interface BridgeFormProps { testId?: string; defaultAmount?: string; defaultFromContractAddress?: string; + isTokenBalancesLoading?: boolean; } export function BridgeForm(props: BridgeFormProps) { const { + bridgeDispatch, bridgeState: { - tokenBridge, tokenBalances, allowedTokens, + checkout, + web3Provider, }, } = useContext(XBridgeContext); - const { connectLoaderState } = useContext(ConnectLoaderContext); - const { checkout, provider } = connectLoaderState; const { cryptoFiatState, cryptoFiatDispatch } = useContext(CryptoFiatContext); const { viewDispatch } = useContext(ViewContext); - const { testId, defaultAmount, defaultFromContractAddress } = props; - const { content, bridgeForm, fees } = text.views[BridgeWidgetViews.BRIDGE]; + const { + testId, + defaultAmount, + defaultFromContractAddress, + isTokenBalancesLoading, + } = props; + const { + xBridgeContent, + xBridgeFees, + content, + bridgeForm, + } = text.views[BridgeWidgetViews.BRIDGE]; // Form state const [amount, setAmount] = useState(defaultAmount || ''); @@ -66,15 +84,15 @@ export function BridgeForm(props: BridgeFormProps) { const [editing, setEditing] = useState(false); const [loading, setLoading] = useState(false); const hasSetDefaultState = useRef(false); + const tokenBalanceSubtext = token + ? `${content.availableBalancePrefix} ${tokenValueFormat(token?.formattedBalance)}` + : ''; // Fee estimates & transactions const [isFetching, setIsFetching] = useState(false); const [estimates, setEstimates] = useState(undefined); const [gasFee, setGasFee] = useState(''); const [gasFeeFiatValue, setGasFeeFiatValue] = useState(''); - const [approvalTransaction, setApprovalTransaction] = useState(undefined); - const [unsignedBridgeTransaction, - setUnsignedBridgeTransaction] = useState(undefined); const [tokensOptions, setTokensOptions] = useState([]); // Not enough ETH to cover gas @@ -151,30 +169,6 @@ export function BridgeForm(props: BridgeFormProps) { return true; }; - const getUnsignedTransactions = async () - : Promise<{ approveRes: ApproveDepositBridgeResponse, bridgeTxn:BridgeDepositResponse } | undefined> => { - if (!checkout || !provider || !tokenBridge || !token || !token.token) return; - - const depositorAddress = await provider.getSigner().getAddress(); - const depositAmount = utils.parseUnits(amount, token.token.decimals); - - const approveRes: ApproveDepositBridgeResponse = await tokenBridge.getUnsignedApproveDepositBridgeTx({ - depositorAddress, - token: isNativeToken(token.token.address) ? NATIVE : token.token.address, - depositAmount, - }); - - const bridgeTxn: BridgeDepositResponse = await tokenBridge.getUnsignedDepositTx({ - depositorAddress, - recipientAddress: depositorAddress, - token: isNativeToken(token.token.address) ? NATIVE : token.token.address, - depositAmount, - }); - - // eslint-disable-next-line consistent-return - return { approveRes, bridgeTxn }; - }; - const fetchEstimates = async (silently: boolean = false) => { if (!canFetchEstimates()) return; @@ -186,20 +180,28 @@ export function BridgeForm(props: BridgeFormProps) { setIsFetching(true); } - // get approval txn and bridge txn - const transactions = await getUnsignedTransactions(); - setApprovalTransaction(transactions?.approveRes); - setUnsignedBridgeTransaction(transactions?.bridgeTxn); - // Prevent silently fetching and set a new fee estimate // if the user has updated and the widget is already // fetching or the user is updating the inputs. - if ((silently && (loading || editing)) || !transactions?.bridgeTxn || !checkout) return; - - const gasEstimateResult = await checkout.gasEstimate({ - gasEstimateType: GasEstimateType.BRIDGE_TO_L2, - isSpendingCapApprovalRequired: !!transactions?.approveRes?.unsignedTx, - }) as GasEstimateBridgeToL2Result; + // if ((silently && (loading || editing)) || !checkout) return; + + // TODO: Implement the gas estimate + // const gasEstimateResult = await checkout.gasEstimate({ + // gasEstimateType: GasEstimateType.BRIDGE_TO_L2, + // isSpendingCapApprovalRequired: !!transactions?.approveRes?.unsignedTx, + // }) as GasEstimateBridgeToL2Result; + + const gasEstimateResult = { + gasFee: { + estimatedAmount: BigNumber.from(100), + token: { + address: 'native', + decimals: 18, + name: 'IMX', + symbol: 'IMX', + }, + }, + } as GasEstimateBridgeToL2Result; setEstimates(gasEstimateResult); const estimatedAmount = utils.formatUnits( @@ -220,6 +222,7 @@ export function BridgeForm(props: BridgeFormProps) { } }; + // TODO: rename uses of ETH to native token const insufficientFundsForGas = useMemo(() => { const ethBalance = tokenBalances .find((balance) => isNativeToken(balance.token.address)); @@ -309,8 +312,8 @@ export function BridgeForm(props: BridgeFormProps) { useEffect(() => { (async () => { - if (!provider) return; - const address = await provider.getSigner().getAddress(); + if (!web3Provider) return; + const address = await web3Provider.getSigner().getAddress(); setWalletAddress((previous) => { if (previous !== '' && previous !== address) { setToken(undefined); @@ -318,7 +321,7 @@ export function BridgeForm(props: BridgeFormProps) { return address; }); })(); - }, [provider, tokenBalances]); + }, [web3Provider, tokenBalances]); const bridgeFormValidator = useCallback((): boolean => { const validateTokenError = validateToken(token); @@ -331,101 +334,33 @@ export function BridgeForm(props: BridgeFormProps) { const submitBridge = useCallback(async () => { if (!bridgeFormValidator()) return; - if (!checkout || !provider || !token || !unsignedBridgeTransaction) return; + if (!checkout || !web3Provider || !token) return; if (insufficientFundsForGas) { setShowNotEnoughGasDrawer(true); return; } - try { - setLoading(true); - if (approvalTransaction && approvalTransaction.unsignedTx) { - // move to new Approve ERC20 view - // pass in approvalTransaction and unsignedBridgeTransaction - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: BridgeWidgetViews.APPROVE_ERC20, - data: { - approveTransaction: approvalTransaction, - transaction: unsignedBridgeTransaction, - bridgeFormInfo: { - fromContractAddress: token.token?.address ?? '', - fromAmount: amount, - }, - }, - }, - currentViewData: { - tokenAddress: token.token?.address ?? '', - amount, - }, - }, - }); - return; - } - - const { transactionResponse } = await checkout.sendTransaction({ - provider, - transaction: unsignedBridgeTransaction.unsignedTx, - }); - - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: BridgeWidgetViews.IN_PROGRESS, - data: { - token: token?.token!, - transactionResponse, - bridgeForm: { - fromContractAddress: token?.token.address ?? '', - fromAmount: amount, - }, - }, - }, - }, - }); - } catch (err: any) { - setLoading(false); + bridgeDispatch({ + payload: { + type: BridgeActions.SET_TOKEN_AND_AMOUNT, + token: token.token, + amount, + }, + }); - if (err.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { - setShowTxnRejectedState(true); - return; - } - if (err.type === CheckoutErrorType.UNPREDICTABLE_GAS_LIMIT - || err.type === CheckoutErrorType.TRANSACTION_FAILED - || err.type === CheckoutErrorType.INSUFFICIENT_FUNDS - || (err.receipt && err.receipt.status === 0)) { - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: BridgeWidgetViews.FAIL, - reason: 'Transaction failed', - data: { - fromContractAddress: token?.token.address ?? '', - fromAmount: amount, - }, - }, - }, - }); - return; - } - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { type: SharedViews.ERROR_VIEW, error: err }, + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: XBridgeWidgetViews.BRIDGE_REVIEW, }, - }); - } + }, + }); }, [ checkout, - provider, + web3Provider, bridgeFormValidator, - approvalTransaction, - unsignedBridgeTransaction, insufficientFundsForGas, token]); @@ -446,44 +381,52 @@ export function BridgeForm(props: BridgeFormProps) { weight="regular" sx={{ paddingBottom: 'base.spacing.x4' }} > - {content.title} + {xBridgeContent.title} - - handleSelectTokenChange(option)} - disabled={isFetching} - /> - handleBridgeAmountChange(value)} - onTextInputBlur={(value) => handleAmountInputBlur(value)} - textAlign="right" - errorMessage={amountError} - disabled={isFetching} - /> - - {/** TODO: update here when we have the correct gas values from the estimator */} - + {isTokenBalancesLoading && ( + + )} + {!isTokenBalancesLoading && ( + + handleSelectTokenChange(option)} + disabled={isFetching} + /> + handleBridgeAmountChange(value)} + onTextInputBlur={(value) => handleAmountInputBlur(value)} + textAlign="right" + errorMessage={amountError} + disabled={isFetching} + /> + + )} + {gasFee && ( + + + + {xBridgeFees.title} + + + + + )}