diff --git a/apps/next/pages/deposit/index.tsx b/apps/next/pages/deposit/index.tsx index 946d72f3b..04e8cbc63 100644 --- a/apps/next/pages/deposit/index.tsx +++ b/apps/next/pages/deposit/index.tsx @@ -3,7 +3,7 @@ import { HomeLayout } from 'app/features/home/layout.web' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' import type { NextPageWithLayout } from '../_app' -import { TopNav } from 'app/components/TopNav' +import { ButtonOption, TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( @@ -19,7 +19,9 @@ export const Page: NextPageWithLayout = () => { export const getServerSideProps = userProtectedGetSSP() Page.getLayout = (children) => ( - }>{children} + }> + {children} + ) export default Page diff --git a/apps/next/pages/deposit/web3.tsx b/apps/next/pages/deposit/web3.tsx deleted file mode 100644 index f27580f5b..000000000 --- a/apps/next/pages/deposit/web3.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { DepositWeb3Screen } from 'app/features/deposit/web3/screen' -import { HomeLayout } from 'app/features/home/layout.web' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import type { NextPageWithLayout } from '../_app' -import { TopNav } from 'app/components/TopNav' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Send | Deposit - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() - -Page.getLayout = (children) => ( - }>{children} -) - -export default Page diff --git a/packages/app/components/DepositAddress.tsx b/packages/app/components/DepositAddress.tsx index 2ffc8e342..f3aaee190 100644 --- a/packages/app/components/DepositAddress.tsx +++ b/packages/app/components/DepositAddress.tsx @@ -37,7 +37,16 @@ function CopyAddressDialog({ isOpen, onClose, onConfirm }) { Please confirm you agree to the following before copying your address: - 1. The tokens I am depositing are on Base Network. + + 1. The external address is on the Base Network and{' '} + + can receive transfers from Smart Contracts + + 2. I have double checked that the tokens are USDC, SEND, or ETH on Base Network. diff --git a/packages/app/features/deposit/DepositPopover.tsx b/packages/app/features/deposit/DepositPopover.tsx deleted file mode 100644 index 8995f0e1d..000000000 --- a/packages/app/features/deposit/DepositPopover.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Button, XStack, YStack, Adapt, Popover, type PopoverProps, useMedia, Theme } from '@my/ui' -import { IconClose, IconDeposit } from 'app/components/icons' -import { DepositWelcome } from './screen' - -export function DepositPopover(props: PopoverProps) { - const media = useMedia() - - return ( - - - - - - - - - - - - - - - - - - - - - - - - + +

Deposit on Base

+ +
) } diff --git a/packages/app/features/deposit/web3/__snapshots__/screen.test.tsx.snap b/packages/app/features/deposit/web3/__snapshots__/screen.test.tsx.snap deleted file mode 100644 index 6bcb940fd..000000000 --- a/packages/app/features/deposit/web3/__snapshots__/screen.test.tsx.snap +++ /dev/null @@ -1,1343 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DepositWeb3Screen renders the connect to deposit when wallet is not connected 1`] = ` -[ - - - Connect to Deposit - - - You need to connect to a wallet to deposit funds. - - - - - Or direct deposit on base - - - - 0xb0b...0000 - - - - - - - - - - , - - - - - - , -] -`; - -exports[`DepositWeb3Screen renders the deposit web3 form when wallet is connected 1`] = ` -[ - - - - - - - - Or direct deposit on base - - - - 0xb0b...0000 - - - - - - - - - - - - - - - - - - - - - - - - - Send recommends depositing USDC first. This will ensure a smooth sending experience throughout the app - - - - - - Token - - - - USDC - - - - - - - Amount - - - - - - - - - - - - - - - - - - - - - - Deposit USDC - - - - - - View - 0x123 - on - - Basescan - - - - - - - - - , - - - - - - , -] -`; - -exports[`DepositWeb3Screen renders the switch network screen when base network is not selected 1`] = ` - - - Switch to - Base - - - You are currently on - Ethereum - . Switch to - Base - to deposit funds. - - - -`; diff --git a/packages/app/features/deposit/web3/screen.test.tsx b/packages/app/features/deposit/web3/screen.test.tsx deleted file mode 100644 index af9e1b15d..000000000 --- a/packages/app/features/deposit/web3/screen.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import '@jest/globals' -import { act, render, screen, userEvent, waitFor } from '@testing-library/react-native' -import { Provider } from 'app/__mocks__/app/provider' -import * as wagmi from 'wagmi' -import * as myWagmi from '@my/wagmi' -import { DepositWeb3Screen } from './screen' -import * as web3modal from '@web3modal/wagmi/react' - -jest.mock('wagmi') -jest.mock('@my/wagmi') -jest.mock('@web3modal/wagmi/react') - -jest.mock('app/utils/useSendAccountBalances', () => ({ - useSendAccountBalances: jest.fn().mockReturnValue({ - balances: { - USDC: 0n, - SEND: 1n, - }, - totalBalance: () => 1n, - }), -})) - -const { useAccount, useSendTransaction, useBalance, useSwitchAccount } = - wagmi as unknown as typeof import('app/__mocks__/wagmi') -const { useWriteErc20Transfer } = myWagmi as unknown as typeof import('app/__mocks__/@my/wagmi') -const { useWeb3Modal } = - web3modal as unknown as typeof import('app/__mocks__/@web3modal/wagmi/react') - -describe('DepositWeb3Screen', () => { - afterEach(() => { - useAccount.mockClear() - useWriteErc20Transfer.mockClear() - useSendTransaction.mockClear() - useBalance.mockClear() - useWeb3Modal.mockClear() - useSwitchAccount.mockClear() - }) - it('renders the deposit web3 form when wallet is connected', async () => { - const mockAccount = { - isConnected: true, - address: '0x123', - chainId: 845337, - chain: { - id: 845337, - name: 'Ethereum', - nativeCurrency: { - decimals: 18, - name: 'Ethereum', - symbol: 'ETH', - }, - }, - } - useAccount.mockReturnValue(mockAccount) - const mockSendTransaction = { - sendTransactionAsync: jest.fn(), - isLoading: false, - isFetching: false, - isFetched: true, - isSubmitting: false, - isSubmitted: false, - error: null, - } - useSendTransaction.mockReturnValue(mockSendTransaction) - const mockBalance = { - data: { - value: 123n, - }, - isLoading: false, - isSuccess: true, - isFetching: false, - isFetched: true, - error: null, - refetch: jest.fn(), - } - useBalance.mockImplementation((/*{address, token, query }*/) => { - return mockBalance - }) - const mockWriteErc20Transfer = { - data: '0x123', - writeContractAsync: jest.fn(), - isPending: false, - error: null, - } - useWriteErc20Transfer.mockReturnValue(mockWriteErc20Transfer) - render( - - - - ) - await waitFor(() => expect(screen.getByTestId('DepositWeb3ScreenBefore')).toBeVisible()) - const usdcLabel = screen.getByTestId('noUsdc') - await waitFor(() => expect(usdcLabel).toBeVisible()) - - expect(screen).toMatchSnapshot() - const user = userEvent.setup() - await user.type(screen.getByTestId('amountInput'), '0.01') - const depositButton = screen.getByRole('button', { name: 'Deposit USDC' }) - await act(async () => { - // await user.press(depositButton) // something about tamagui button is causing this to fail - await depositButton.props.onPress() - }) - expect(mockWriteErc20Transfer.writeContractAsync).toHaveBeenCalled() - }) - it('renders the connect to deposit when wallet is not connected', async () => { - useAccount.mockReturnValue({ - isConnected: false, - }) - useWeb3Modal.mockReturnValue({ - open: jest.fn(), - }) - - render( - - - - ) - await waitFor(() => - expect(screen.getByRole('header', { name: 'Connect to Deposit' })).toBeVisible() - ) - expect(screen).toMatchSnapshot() - }) - it('renders the switch network screen when base network is not selected', async () => { - useAccount.mockReturnValue({ - isConnected: true, - address: '0x123', - chainId: 1337, - chain: { - id: 1337, - name: 'Ethereum', - nativeCurrency: { - decimals: 18, - name: 'Ethereum', - symbol: 'ETH', - }, - }, - }) - useWeb3Modal.mockReturnValue({ - open: jest.fn(), - }) - render( - - - - ) - expect(screen).toMatchSnapshot() - }) -}) diff --git a/packages/app/features/deposit/web3/screen.tsx b/packages/app/features/deposit/web3/screen.tsx deleted file mode 100644 index 3c03af4d8..000000000 --- a/packages/app/features/deposit/web3/screen.tsx +++ /dev/null @@ -1,476 +0,0 @@ -import { - Button, - ButtonText, - Fade, - FormWrapper, - H2, - Paragraph, - Shake, - Spinner, - Stack, - SubmitButton, - XStack, - YStack, - isWeb, -} from '@my/ui' -import { baseMainnet, useWriteErc20Transfer } from '@my/wagmi' -import { useWeb3Modal } from '@web3modal/wagmi/react' -import { DepositAddress } from 'app/components/DepositAddress' -import { IconInfoCircle, IconRefresh } from 'app/components/icons' -import { IconChainBase } from 'app/components/icons/IconChainBase' -import { coins } from 'app/data/coins' -import { SchemaForm, formFields } from 'app/utils/SchemaForm' -import { assert } from 'app/utils/assert' -import formatAmount from 'app/utils/formatAmount' -import { useSendAccount } from 'app/utils/send-accounts' -import { shorten } from 'app/utils/strings' -import { useSendAccountBalances } from 'app/utils/useSendAccountBalances' -import { useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' -import { Link } from 'solito/link' -import { formatUnits, parseUnits } from 'viem' -import { - useAccount, - useBalance, - useSendTransaction, - useSwitchAccount, - useWaitForTransactionReceipt, -} from 'wagmi' -import { z } from 'zod' - -/** - * Deposit from web3 wallet - * 1. Connect to web3 wallet - * 2. Select token USDC, ETH, or SEND and enter amount - * 3. Sign transaction - */ -export function DepositWeb3Screen() { - const { isConnected, chainId, chain } = useAccount() - - if (!isConnected) { - return ( - -

- Connect to Deposit -

- - You need to connect to a wallet to deposit funds. - - - - -
- ) - } - - if (chainId !== baseMainnet.id) { - return ( - -

- Switch to {baseMainnet.name} -

- - You are currently on {chain?.name}. Switch to {baseMainnet.name} to deposit funds. - - -
- ) - } - - return ( - - - - ) -} - -function DepositAddressWrapper() { - const { data: sendAccount } = useSendAccount() - - return ( - - Or direct deposit on base - - - ) -} - -const schema = z.object({ - token: formFields.select.describe('Token'), - amount: formFields.text.describe('Amount'), -}) - -type DepositSchema = z.infer - -function DepositForm() { - const form = useForm() - const options = coins.map((coin) => ({ name: coin.symbol, value: coin.token })) - const first = options[0] - assert(!!first, 'first coin not found') - const { balances } = useSendAccountBalances() - const sendUSDCBalance = balances?.USDC - - const coin = coins.find((coin) => coin.token === form.watch('token')) - const isCoinSelected = !!coin - const amount = form.watch('amount') - let value = 0n - try { - value = parseUnits(amount ?? '0', coin?.decimals ?? 0) - } catch (e) { - // ignore - } - const { address: account } = useAccount() - const { connectors, switchAccount } = useSwitchAccount() - const { data: sendAccount } = useSendAccount() - - const { - data: depositorBalance, - isLoading: isLoadingDepositorBalance, - error: balanceDepositorError, - refetch: refetchDepositorBalance, - } = useBalance({ - address: account, - token: coin?.token === 'eth' ? undefined : coin?.token, - query: { enabled: !!account && isCoinSelected }, - chainId: baseMainnet.id, - }) - const isDepositorEmpty = depositorBalance?.value === 0n - const { - // helpful but not required for submitting - data: sendAccountBalance, - refetch: refetchSendAccountBalance, - } = useBalance({ - address: sendAccount?.address, - token: coin?.token === 'eth' ? undefined : coin?.token, - query: { enabled: !!sendAccount && isCoinSelected }, - chainId: baseMainnet.id, - }) - const { writeContractAsync } = useWriteErc20Transfer() - const { sendTransactionAsync } = useSendTransaction() - const [depositHash, setDepositHash] = useState<`0x${string}`>() - const { - data: receipt, - isLoading: isLoadingReceipt, - error: receiptError, - } = useWaitForTransactionReceipt({ - hash: depositHash, - }) - const canSubmit = !isLoadingReceipt - - // handle depositor balance error - useEffect(() => { - if (isLoadingDepositorBalance) return - if (balanceDepositorError) { - if (balanceDepositorError.name !== 'Error') { - form.setError('root', { - type: 'custom', - message: balanceDepositorError.details?.split('.').at(0), - }) - } else { - form.setError('root', { - type: 'custom', - message: balanceDepositorError.message.split('.').at(0), - }) - } - } - // check if balance is enough - if (isDepositorEmpty || (depositorBalance?.value ?? 0n) < value) { - form.setError('amount', { - type: 'custom', - message: 'Insufficient balance', - }) - - form.setError('root', { - type: 'custom', - message: 'Switch to an account with more funds', - }) - } else { - form.clearErrors('amount') - form.clearErrors('root') - } - }, [ - depositorBalance, - value, - form, - isLoadingDepositorBalance, - isDepositorEmpty, - balanceDepositorError, - ]) - - // handle tx receipt error - useEffect(() => { - if (receiptError) { - form.setError('root', { - type: 'custom', - message: receiptError.message.split('.').at(0), - }) - } - if (receipt?.status === 'reverted') { - form.setError('root', { - type: 'custom', - message: 'Transaction failed', - }) - } - if (receipt) { - refetchDepositorBalance() - refetchSendAccountBalance() - } - }, [receiptError, form, receipt, refetchDepositorBalance, refetchSendAccountBalance]) - - const onSubmit = async () => { - if (!canSubmit) return - try { - assert(!!coin?.token, 'No coin selected') - assert(!!sendAccount, 'No send account found') - assert(value > 0n, 'Amount must be greater than 0') - if (coin?.token === 'eth') { - const hash = await sendTransactionAsync({ - to: sendAccount.address, - value: value, - }) - setDepositHash(hash) - } else { - const hash = await writeContractAsync({ - args: [sendAccount.address, value], - address: coin.token, - }) - setDepositHash(hash) - } - form.clearErrors('root') - form.setValue('amount', '') - } catch (e) { - if (e.name !== 'Error') { - const message = e?.details?.split('.').at(0) ?? e?.message.split('.').at(0) - form.setError('root', { - type: 'custom', - message, - }) - } - const message = e.message.split('.').at(0) - form.setError('root', { - type: 'custom', - message, - }) - } - } - - return ( - ( - - - - - )} - renderAfter={({ submit }) => ( - - - {form.formState.errors?.root?.message ? ( - - - {form.formState.errors?.root?.message} - - - ) : null} - {value > 0n ? ( - - - After depositing, your Send Account balance will be {(() => { - const amount = (sendAccountBalance?.value ?? 0n) + value - return formatAmount(formatUnits(amount, coin?.decimals ?? 0)) - })()} {coin?.symbol}. - - - ) : null} - - : undefined} - onPress={() => { - if (isDepositorEmpty) { - try { - assert(!!connectors[0], 'No connector found') - switchAccount({ connector: connectors[0] }) - } catch (e) { - form.setError('root', { - type: 'custom', - message: `Failed to switch account. ${e?.message.split('.').at(0)}`, - }) - } - return - } - submit() - }} - $gtSm={{ miw: 200 }} - br={12} - > - - {isLoadingReceipt ? 'Depositing...' : `Deposit ${coin?.symbol}`} - - - - {receipt?.transactionHash && ( - - - View {shorten(receipt?.transactionHash)} on{' '} - {baseMainnet.blockExplorers.default.name} - - - )} - - - )} - > - {({ token, amount }) => ( - - {!sendUSDCBalance && ( - - - - Send recommends depositing USDC first. This will ensure a smooth sending experience - throughout the app - - - )} - - {sendUSDCBalance ? ( - token - ) : ( - - - Token - - - USDC - - - )} - - {amount} - - - - )} - - ) -} - -function Wrapper({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -/** - * This component is used to fail safely when the user is on the wrong network. - * This happens when the window ethereum provider gets out of sync with the wagmi chainId. - * @see https://discord.com/channels/1156791276818157609/1156791580938739822/1249030557489299558 - */ -function FailsafeChainId({ children }: { children: React.ReactNode }) { - const { chainId } = useAccount() - const [failsafeChainId, setFailsafeChainId] = useState() - const [error, setError] = useState() - const [ignoreError, setIgnoreError] = useState(false) - const { open: openWeb3Modal } = useWeb3Modal() - const canCheckChainId = isWeb && window.ethereum - - // biome-ignore lint/correctness/useExhaustiveDependencies: hack - useEffect(() => { - if (canCheckChainId) { - window.ethereum - .request({ method: 'eth_chainId' }) - .then((cid) => setFailsafeChainId(Number(cid))) - .catch((e) => setError(e.message?.split('.').at(0) ?? e.toString())) - } - }, [chainId, openWeb3Modal]) - - if (!canCheckChainId) return children // we don't need to do anything on non-web or wallet connect somtimes does not add ethereum provider - - if (failsafeChainId === undefined) { - return ( - - - - - - ) - } - - if (!ignoreError && error) { - return ( - -

- Error -

- - {error} - - -
- ) - } - - if (chainId !== failsafeChainId) { - return ( - -

- Switch to {baseMainnet.name} -

- - You are on the wrong network. Switch to {baseMainnet.name} to deposit funds. - - -
- ) - } - - return children -} diff --git a/packages/app/features/home/__snapshots__/screen.test.tsx.snap b/packages/app/features/home/__snapshots__/screen.test.tsx.snap index 61f4ca993..dd48e1262 100644 --- a/packages/app/features/home/__snapshots__/screen.test.tsx.snap +++ b/packages/app/features/home/__snapshots__/screen.test.tsx.snap @@ -1,43 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HomeScreen 1`] = ` -[ + - - - Total Balance - - - + - - 0 - - - USD - - + Total Balance + - - - - - - - Deposit - - + - - - - - - - - - - - - - - + } + suppressHighlighting={true} + > + USD + + + + + + + Deposit + - - Send - - + + + + + + - - - - + + Send + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + + + 0 + + + + + + + + + + + + + + + + - - + + + + + + + + - + - - - - - - - - - - - USDC - - - + + + + - - 0 - - - - - - - - - - - - - - + + + + 0 + - - - - - - - - - - - - - - - Ethereum - - + + + + + + + - - 0 - - - + + + - + + + - - - - - - - - + + + + - - + Send + + + + + 0 + - - - - - - - - - - - - - - Send - - - - - 0 - - - - - - - - + /> + + - - + + - , - - - - - - , -] + + `; diff --git a/packages/app/features/home/screen.tsx b/packages/app/features/home/screen.tsx index b1dbb0434..f789dfb75 100644 --- a/packages/app/features/home/screen.tsx +++ b/packages/app/features/home/screen.tsx @@ -16,11 +16,10 @@ import { type XStackProps, H5, } from '@my/ui' -import { IconArrowRight, IconError, IconPlus } from 'app/components/icons' +import { IconArrowRight, IconDeposit, IconError, IconPlus } from 'app/components/icons' import { coins, coinsDict } from 'app/data/coins' import { useSendAccount } from 'app/utils/send-accounts' import { useCoinFromTokenParam } from 'app/utils/useCoinFromTokenParam' -import { DepositPopover } from '../deposit/DepositPopover' import { TokenBalanceCard } from './TokenBalanceCard' import { TokenBalanceList } from './TokenBalanceList' import { TokenDetails } from './TokenDetails' @@ -88,7 +87,27 @@ function HomeBody(props: XStackProps) { w={'100%'} > - + + + + Deposit + + + + + + @@ -139,7 +158,27 @@ function HomeBody(props: XStackProps) { - + + + + Deposit + + + + + +