From e46657c1ed0aa74557058a7d1816c35ab80702dc Mon Sep 17 00:00:00 2001 From: Mikhala <122326421+imx-mikhala@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:45:58 +0800 Subject: [PATCH] WT-1358 Show need ETH for gas drawer from wallet view NO-CHANGELOG (#577) --- .../checkout/widgets-lib/src/lib/constants.ts | 1 + .../components/BalanceItem/BalanceItem.cy.tsx | 51 +----- .../components/BalanceItem/BalanceItem.tsx | 11 +- .../TokenBalanceList/TokenBalanceList.tsx | 13 +- .../wallet/views/WalletBalances.cy.tsx | 155 +++++++++++++++++- .../widgets/wallet/views/WalletBalances.tsx | 80 ++++++++- 6 files changed, 254 insertions(+), 57 deletions(-) diff --git a/packages/checkout/widgets-lib/src/lib/constants.ts b/packages/checkout/widgets-lib/src/lib/constants.ts index c28ecda87e..99060ead9b 100644 --- a/packages/checkout/widgets-lib/src/lib/constants.ts +++ b/packages/checkout/widgets-lib/src/lib/constants.ts @@ -7,6 +7,7 @@ export const DEFAULT_TOKEN_DECIMALS = 18; export const DEFAULT_TOKEN_FORMATTING_DECIMALS = 6; export const DEFAULT_GT_ONE_TOKEN_FORMATTING_DECIMALS = 2; export const IMX_TOKEN_SYMBOL = 'IMX'; +export const ETH_TOKEN_SYMBOL = 'ETH'; /** * Checkout Widget default env diff --git a/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.cy.tsx b/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.cy.tsx index d15328c00f..836c950f73 100644 --- a/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.cy.tsx +++ b/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.cy.tsx @@ -33,14 +33,13 @@ describe('BalanceItem', () => { beforeEach(() => { cy.stub(orchestrationEvents, 'sendRequestSwapEvent').as('requestSwapEventStub'); - cy.stub(orchestrationEvents, 'sendRequestBridgeEvent').as('requestBridgeEventStub'); cy.stub(orchestrationEvents, 'sendRequestOnrampEvent').as('requestOnrampEventStub'); }); it('should show balance details', () => { mount( - + {}} /> , ); @@ -76,7 +75,7 @@ describe('BalanceItem', () => { mount( - + {}} /> , ); @@ -106,7 +105,7 @@ describe('BalanceItem', () => { mount( - + {}} /> , ); @@ -142,7 +141,7 @@ describe('BalanceItem', () => { mount( - + {}} /> , ); @@ -177,7 +176,7 @@ describe('BalanceItem', () => { mount( - + {}} /> , ); cySmartGet('token-menu').should('exist'); @@ -199,7 +198,7 @@ describe('BalanceItem', () => { }; mount( - + {}} /> , ); @@ -233,7 +232,7 @@ describe('BalanceItem', () => { it('should emit sendRequestSwapEvent when swap menu button is clicked', () => { mount( - + {}} /> , ); @@ -251,7 +250,7 @@ describe('BalanceItem', () => { it('should emit sendRequestOnrampEvent when add menu button is clicked', () => { mount( - + {}} /> , ); @@ -269,39 +268,5 @@ describe('BalanceItem', () => { }, ); }); - - it('should emit sendRequestBridgeEvent when move menu button is clicked', () => { - testWalletState = { - ...testWalletState, - network: { - chainId: ChainId.SEPOLIA, - name: 'Immutable zkEVM Testnet', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - isSupported: true, - }, - }; - mount( - - - , - ); - - cySmartGet('token-menu').should('exist'); - cySmartGet('token-menu').click(); - cySmartGet('balance-item-move-option').click(); - cySmartGet('@requestBridgeEventStub').should('have.been.called'); - cySmartGet('@requestBridgeEventStub').should( - 'have.been.calledWith', - IMTBLWidgetEvents.IMTBL_WALLET_WIDGET_EVENT, - { - tokenAddress: '', - amount: '', - }, - ); - }); }); }); diff --git a/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.tsx b/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.tsx index cb1d0acfd7..a91f4ddb41 100644 --- a/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.tsx +++ b/packages/checkout/widgets-lib/src/widgets/wallet/components/BalanceItem/BalanceItem.tsx @@ -25,10 +25,10 @@ import { formatZeroAmount, tokenValueFormat } from '../../../../lib/utils'; export interface BalanceItemProps { balanceInfo: BalanceInfo; + bridgeToL2OnClick: (address?: string) => void; } -export function BalanceItem(props: BalanceItemProps) { - const { balanceInfo } = props; +export function BalanceItem({ balanceInfo, bridgeToL2OnClick }: BalanceItemProps) { const fiatAmount = `≈ USD $${formatZeroAmount(balanceInfo.fiatAmount)}`; const { walletState } = useContext(WalletContext); const { supportedTopUps, network, checkout } = walletState; @@ -104,12 +104,7 @@ export function BalanceItem(props: BalanceItemProps) { { - orchestrationEvents.sendRequestBridgeEvent(IMTBLWidgetEvents.IMTBL_WALLET_WIDGET_EVENT, { - tokenAddress: balanceInfo.address ?? '', - amount: '', - }); - }} + onClick={() => bridgeToL2OnClick(balanceInfo.address)} > {`Move ${balanceInfo.symbol}`} diff --git a/packages/checkout/widgets-lib/src/widgets/wallet/components/TokenBalanceList/TokenBalanceList.tsx b/packages/checkout/widgets-lib/src/widgets/wallet/components/TokenBalanceList/TokenBalanceList.tsx index cc12946d23..5ca5027094 100644 --- a/packages/checkout/widgets-lib/src/widgets/wallet/components/TokenBalanceList/TokenBalanceList.tsx +++ b/packages/checkout/widgets-lib/src/widgets/wallet/components/TokenBalanceList/TokenBalanceList.tsx @@ -7,16 +7,23 @@ import { WalletWidgetViews } from '../../../../context/view-context/WalletViewCo interface TokenBalanceListProps { balanceInfoItems: BalanceInfo[]; + bridgeToL2OnClick: (address?: string) => void; } -export function TokenBalanceList(props: TokenBalanceListProps) { - const { balanceInfoItems } = props; +export function TokenBalanceList({ + balanceInfoItems, + bridgeToL2OnClick, +}: TokenBalanceListProps) { const { noTokensFound } = text.views[WalletWidgetViews.WALLET_BALANCES].tokenBalancesList; return ( {balanceInfoItems.length === 0 && {noTokensFound}} {balanceInfoItems.map((balance) => ( - + ))} ); diff --git a/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.cy.tsx b/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.cy.tsx index 8284fb0f9e..04d3a83144 100644 --- a/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.cy.tsx +++ b/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.cy.tsx @@ -1,5 +1,5 @@ import { - Checkout, WalletProviderName, TokenInfo, ChainId, ChainName, + Checkout, WalletProviderName, TokenInfo, ChainId, ChainName, GasEstimateType, } from '@imtbl/checkout-sdk'; import { describe, it, cy } from 'local-cypress'; import { mount } from 'cypress/react18'; @@ -7,10 +7,12 @@ import { BiomeCombinedProviders } from '@biom3/react'; import { Web3Provider } from '@ethersproject/providers'; import { Environment } from '@imtbl/config'; import { BigNumber } from 'ethers'; +import { IMTBLWidgetEvents } from '@imtbl/checkout-widgets'; import { WalletBalances } from './WalletBalances'; import { WalletContext, WalletState } from '../context/WalletContext'; import { cyIntercept, cySmartGet } from '../../../lib/testUtils'; import { WalletWidgetTestComponent } from '../test-components/WalletWidgetTestComponent'; +import { orchestrationEvents } from '../../../lib/orchestrationEvents'; describe('WalletBalances', () => { beforeEach(() => { @@ -100,6 +102,157 @@ describe('WalletBalances', () => { }); }); + describe('move coins gas check', () => { + it('should show not enough gas drawer when trying to bridge to L2 with 0 eth balance', () => { + const walletState: WalletState = { + checkout, + network: { + chainId: ChainId.SEPOLIA, + name: 'Sepolia', + nativeCurrency: {} as unknown as TokenInfo, + isSupported: true, + }, + provider, + walletProvider: WalletProviderName.METAMASK, + tokenBalances: [ + { + id: 'eth', + balance: '0.0', + symbol: 'ETH', + fiatAmount: '0', + }, + ], + supportedTopUps: { + isBridgeEnabled: true, + }, + }; + mount( + + {} }} + > + + + , + ); + cySmartGet('token-menu').click(); + cySmartGet('balance-item-move-option').click(); + cySmartGet('not-enough-gas-bottom-sheet').should('be.visible'); + cySmartGet('not-enough-gas-copy-address-button').should('be.visible'); + cySmartGet('not-enough-gas-cancel-button').should('be.visible'); + cySmartGet('not-enough-gas-cancel-button').click(); + cySmartGet('not-enough-gas-bottom-sheet').should('not.exist'); + }); + + it('should show not enough gas drawer when trying to bridge to L2 with eth balance less than gas', () => { + cy.stub(Checkout.prototype, 'gasEstimate') + .as('gasEstimateStub') + .resolves({ + gasEstimateType: GasEstimateType.BRIDGE_TO_L2, + gasFee: { + estimatedAmount: BigNumber.from('10000000000000000'), + }, + }); + + const walletState: WalletState = { + checkout, + network: { + chainId: ChainId.SEPOLIA, + name: 'Sepolia', + nativeCurrency: {} as unknown as TokenInfo, + isSupported: true, + }, + provider, + walletProvider: WalletProviderName.METAMASK, + tokenBalances: [ + { + id: 'eth', + balance: '0.001', + symbol: 'ETH', + fiatAmount: '0', + }, + ], + supportedTopUps: { + isBridgeEnabled: true, + }, + }; + mount( + + {} }} + > + + + , + ); + cySmartGet('token-menu').click(); + cySmartGet('balance-item-move-option').click(); + cySmartGet('not-enough-gas-bottom-sheet').should('be.visible'); + cySmartGet('not-enough-gas-copy-address-button').should('be.visible'); + cySmartGet('not-enough-gas-cancel-button').should('be.visible'); + cySmartGet('not-enough-gas-cancel-button').click(); + cySmartGet('not-enough-gas-bottom-sheet').should('not.exist'); + }); + + it('should not show not enough gas drawer when enough eth to cover gas and call request bridge event', () => { + cy.stub(orchestrationEvents, 'sendRequestBridgeEvent').as('requestBridgeEventStub'); + cy.stub(Checkout.prototype, 'gasEstimate') + .as('gasEstimateStub') + .resolves({ + gasEstimateType: GasEstimateType.BRIDGE_TO_L2, + gasFee: { + estimatedAmount: BigNumber.from('10000000000000000'), + }, + }); + + const walletState: WalletState = { + checkout, + network: { + chainId: ChainId.SEPOLIA, + name: 'Sepolia', + nativeCurrency: {} as unknown as TokenInfo, + isSupported: true, + }, + provider, + walletProvider: WalletProviderName.METAMASK, + tokenBalances: [ + { + id: 'eth', + balance: '100', + symbol: 'ETH', + fiatAmount: '0', + address: 'NATIVE', + }, + ], + supportedTopUps: { + isBridgeEnabled: true, + }, + }; + mount( + + {} }} + > + + + , + ); + cySmartGet('token-menu').click(); + cySmartGet('balance-item-move-option').click(); + cySmartGet('not-enough-gas-bottom-sheet').should('not.exist'); + + cySmartGet('@requestBridgeEventStub').should('have.been.called'); + cySmartGet('@requestBridgeEventStub').should( + 'have.been.calledWith', + IMTBLWidgetEvents.IMTBL_WALLET_WIDGET_EVENT, + { + tokenAddress: 'NATIVE', + amount: '', + }, + ); + }); + }); + describe('add coins button', () => { it('should show add coins button on zkEVM when topUps are supported', () => { const topUpFeatureTestCases = [ diff --git a/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.tsx b/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.tsx index c8e88463ed..747f8b0cd2 100644 --- a/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.tsx +++ b/packages/checkout/widgets-lib/src/widgets/wallet/views/WalletBalances.tsx @@ -2,6 +2,9 @@ import { Box, Icon, MenuItem } from '@biom3/react'; import { useContext, useEffect, useMemo, useState, } from 'react'; +import { GasEstimateType } from '@imtbl/checkout-sdk'; +import { utils } from 'ethers'; +import { IMTBLWidgetEvents } from '@imtbl/checkout-widgets'; import { FooterLogo } from '../../../components/Footer/FooterLogo'; import { HeaderNavigation } from '../../../components/Header/HeaderNavigation'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; @@ -15,7 +18,7 @@ import { WALLET_BALANCE_CONTAINER_STYLE, WalletBalanceItemStyle, } from './WalletBalancesStyles'; -import { getL2ChainId } from '../../../lib/networkUtils'; +import { getL1ChainId, getL2ChainId } from '../../../lib/networkUtils'; import { CryptoFiatActions, CryptoFiatContext, @@ -28,6 +31,10 @@ import { ViewContext, } from '../../../context/view-context/ViewContext'; import { fetchTokenSymbols } from '../../../lib/fetchTokenSymbols'; +import { NotEnoughGas } from '../../../components/NotEnoughGas/NotEnoughGas'; +import { isNativeToken } from '../../../lib/utils'; +import { DEFAULT_TOKEN_DECIMALS, ETH_TOKEN_SYMBOL } from '../../../lib'; +import { orchestrationEvents } from '../../../lib/orchestrationEvents'; export function WalletBalances() { const { cryptoFiatState, cryptoFiatDispatch } = useContext(CryptoFiatContext); @@ -44,6 +51,9 @@ export function WalletBalances() { } = walletState; const { conversions } = cryptoFiatState; const [balancesLoading, setBalancesLoading] = useState(true); + const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false); + const [walletAddress, setWalletAddress] = useState(''); + const [insufficientFundsForBridgeToL2Gas, setInsufficientFundsForBridgeToL2Gas] = useState(false); useEffect(() => { (async () => { @@ -62,6 +72,14 @@ export function WalletBalances() { })(); }, [checkout, cryptoFiatDispatch, network]); + useEffect(() => { + const setWalletAddressFromProvider = async () => { + if (!provider) return; + setWalletAddress(await provider.getSigner().getAddress()); + }; + setWalletAddressFromProvider(); + }, [provider]); + useEffect(() => { let totalAmount = 0.0; @@ -73,6 +91,41 @@ export function WalletBalances() { setTotalFiatAmount(totalAmount); }, [tokenBalances]); + // Silently runs a gas check for bridge to L2 + // This is to prevent the user having to wait for the gas estimate to complete to use the UI + // As a trade-off there is a slight delay between when the gas estimate is fetched and checked against the user balance, so 'move' can be selected before the gas estimate is completed + useEffect(() => { + const bridgeToL2GasCheck = async () => { + if (!checkout) return; + if (!network) return; + if (network.chainId !== getL1ChainId(checkout.config)) return; + + const ethBalance = tokenBalances + .find((balance) => isNativeToken(balance.address) && balance.symbol === ETH_TOKEN_SYMBOL); + if (!ethBalance) return; + + if (ethBalance.balance === '0.0') { + setInsufficientFundsForBridgeToL2Gas(true); + return; + } + + const { gasFee } = await checkout.gasEstimate({ + gasEstimateType: GasEstimateType.BRIDGE_TO_L2, + isSpendingCapApprovalRequired: false, + }); + + if (!gasFee.estimatedAmount) { + setInsufficientFundsForBridgeToL2Gas(false); + return; + } + + setInsufficientFundsForBridgeToL2Gas( + gasFee.estimatedAmount.gt(utils.parseUnits(ethBalance.balance, DEFAULT_TOKEN_DECIMALS)), + ); + }; + bridgeToL2GasCheck(); + }, [tokenBalances, checkout, network]); + useEffect(() => { if (!checkout || !provider || !network) return; (async () => { @@ -114,6 +167,17 @@ export function WalletBalances() { }); }; + const handleBridgeToL2OnClick = (address?: string) => { + if (insufficientFundsForBridgeToL2Gas) { + setShowNotEnoughGasDrawer(true); + return; + } + orchestrationEvents.sendRequestBridgeEvent(IMTBLWidgetEvents.IMTBL_WALLET_WIDGET_EVENT, { + tokenAddress: address ?? '', + amount: '', + }); + }; + return ( )} - {!balancesLoading && ()} + {!balancesLoading && ( + + )} {showAddCoins && ( @@ -180,6 +249,13 @@ export function WalletBalances() { Add coins )} + setShowNotEnoughGasDrawer(false)} + walletAddress={walletAddress} + showAdjustAmount={false} + /> );