From 11bd9ecf0b1d169a3403f16702bb9e39c9b03b28 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Wed, 8 Jan 2025 14:52:43 +1300 Subject: [PATCH 1/5] feat: Add hook to provide account-balance data. tests included. --- packages/ui/src/hooks/useAccountBalance.ts | 42 +++++++++++++ .../ui/test/hooks/useAccountBalance.test.ts | 60 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/ui/src/hooks/useAccountBalance.ts create mode 100644 packages/ui/test/hooks/useAccountBalance.test.ts diff --git a/packages/ui/src/hooks/useAccountBalance.ts b/packages/ui/src/hooks/useAccountBalance.ts new file mode 100644 index 00000000..e9f525c3 --- /dev/null +++ b/packages/ui/src/hooks/useAccountBalance.ts @@ -0,0 +1,42 @@ +import { isNotNil, propOr } from "ramda"; +import { useMemo } from "react"; +import { formatUnits } from "viem"; +import { useAccount, useBalance } from "wagmi"; + +type DataType = + | { + decimals: number; + formatted: string; + symbol: string; + value: bigint; + } + | undefined; + +/** + * Check the ETH balance of the connected account. + * returns the value, decimals, symbol, formatted value and fn to refresh the information. + * @returns + */ +export const useAccountBalance = () => { + const { address } = useAccount(); + const { data, refetch } = useBalance({ + address, + }); + + const result = useMemo(() => { + const value = propOr(0n, "value", data); + const decimals = propOr(18, "decimals", data); + const symbol = propOr("ETH", "symbol", data); + const formatted = isNotNil(data) ? formatUnits(value, decimals) : "0"; + + return { + value, + decimals, + symbol, + formatted, + refetch, + }; + }, [data, refetch]); + + return result; +}; diff --git a/packages/ui/test/hooks/useAccountBalance.test.ts b/packages/ui/test/hooks/useAccountBalance.test.ts new file mode 100644 index 00000000..3115f6f8 --- /dev/null +++ b/packages/ui/test/hooks/useAccountBalance.test.ts @@ -0,0 +1,60 @@ +import { renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, vi } from "vitest"; +import { useAccount, useBalance } from "wagmi"; +import { useAccountBalance } from "../../src/hooks/useAccountBalance"; + +vi.mock("wagmi"); + +const useAccountMock = vi.mocked(useAccount, { partial: true }); +const useBalanceMock = vi.mocked(useBalance, { partial: true }); +const balanceRefetchMock = vi.fn(); + +const ethBalance = 456632268985698099n; +const ethDecimals = 18; +const ethSymbol = "ETH"; + +describe("UseAccountBalance Hook", () => { + beforeEach(() => { + useAccountMock.mockReturnValue({ + address: "0xCF6bd2F07c24C50BDDf5Ed3c1858f9aDECB727cE", + isConnected: true, + }); + useBalanceMock.mockReturnValue({ + data: { + value: ethBalance, + decimals: ethDecimals, + symbol: ethSymbol, + }, + refetch: balanceRefetchMock, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should return information about the connected account", () => { + const { result } = renderHook(() => useAccountBalance()); + + expect(result.current.symbol).toEqual("ETH"); + expect(result.current.value).toEqual(ethBalance); + expect(result.current.decimals).toEqual(18); + expect(result.current.formatted).toEqual("0.456632268985698099"); + expect(result.current.refetch).toEqual(expect.any(Function)); + }); + + it("should return defaults in case the data is undefined", () => { + useBalanceMock.mockReturnValue({ + data: undefined, + refetch: balanceRefetchMock, + }); + + const { result } = renderHook(() => useAccountBalance()); + + expect(result.current.symbol).toEqual("ETH"); + expect(result.current.value).toEqual(0n); + expect(result.current.decimals).toEqual(18); + expect(result.current.formatted).toEqual("0"); + expect(result.current.refetch).toEqual(expect.any(Function)); + }); +}); From 0fc9c5f5346ef79d2cb29bb0a352ccdf127de154 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Wed, 8 Jan 2025 14:55:37 +1300 Subject: [PATCH 2/5] feat: Display balance at all times, validation run on change. small refactoring on how ether-form is exported. --- .../index.tsx} | 122 ++++++++++++------ packages/ui/src/index.tsx | 8 +- 2 files changed, 87 insertions(+), 43 deletions(-) rename packages/ui/src/{EtherDepositForm.tsx => EtherDepositForm/index.tsx} (64%) diff --git a/packages/ui/src/EtherDepositForm.tsx b/packages/ui/src/EtherDepositForm/index.tsx similarity index 64% rename from packages/ui/src/EtherDepositForm.tsx rename to packages/ui/src/EtherDepositForm/index.tsx index ec279348..6564dec3 100644 --- a/packages/ui/src/EtherDepositForm.tsx +++ b/packages/ui/src/EtherDepositForm/index.tsx @@ -7,12 +7,14 @@ import { Autocomplete, Button, Collapse, + Flex, Group, Loader, Stack, Text, - Textarea, TextInput, + Textarea, + UnstyledButton, } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useDisclosure } from "@mantine/hooks"; @@ -25,18 +27,18 @@ import { } from "react-icons/tb"; import { BaseError, - getAddress, Hex, + getAddress, isAddress, isHex, parseUnits, zeroAddress, } from "viem"; import { useAccount, useWaitForTransactionReceipt } from "wagmi"; -import { TransactionProgress } from "./TransactionProgress"; -import useUndeployedApplication from "./hooks/useUndeployedApplication"; -import { TransactionFormSuccessData } from "./DepositFormTypes"; -import { useFormattedBalance } from "./hooks/useFormattedBalance"; +import { TransactionFormSuccessData } from "../DepositFormTypes"; +import { TransactionProgress } from "../TransactionProgress"; +import { useAccountBalance } from "../hooks/useAccountBalance"; +import useUndeployedApplication from "../hooks/useUndeployedApplication"; export interface EtherDepositFormProps { applications: string[]; @@ -54,11 +56,12 @@ export const EtherDepositForm: FC = (props) => { } = props; const [advanced, { toggle: toggleAdvanced }] = useDisclosure(false); const { chain } = useAccount(); - const balance = useFormattedBalance(); + const accountBalance = useAccountBalance(); const form = useForm({ - validateInputOnBlur: true, + validateInputOnChange: true, initialValues: { + accountBalance: accountBalance, application: "", amount: "", execLayerData: "0x", @@ -66,10 +69,14 @@ export const EtherDepositForm: FC = (props) => { validate: { application: (value) => value !== "" && isAddress(value) ? null : "Invalid application", - amount: (value) => { + amount: (value, values) => { if (value !== "" && Number(value) > 0) { - if (Number(value) > Number(balance)) { - return `The amount ${value} exceeds your current balance of ${balance} ETH`; + const val = parseUnits( + value, + values.accountBalance.decimals, + ); + if (val > values.accountBalance.value) { + return `The amount ${value} exceeds your current balance of ${values.accountBalance.formatted} ETH`; } return null; } else { @@ -79,22 +86,25 @@ export const EtherDepositForm: FC = (props) => { execLayerData: (value) => isHex(value) ? null : "Invalid hex string", }, - transformValues: (values) => ({ - address: isAddress(values.application) - ? getAddress(values.application) - : zeroAddress, - amount: - values.amount !== "" - ? parseUnits( - values.amount, - chain?.nativeCurrency.decimals ?? 18, - ) - : undefined, - execLayerData: values.execLayerData - ? (values.execLayerData as Hex) - : "0x", - }), + transformValues: (values) => { + return { + address: isAddress(values.application) + ? getAddress(values.application) + : zeroAddress, + amount: + values.amount !== "" + ? parseUnits( + values.amount, + chain?.nativeCurrency.decimals ?? 18, + ) + : undefined, + execLayerData: values.execLayerData + ? (values.execLayerData as Hex) + : "0x", + }; + }, }); + const { address, amount, execLayerData } = form.getTransformedValues(); const prepare = useSimulateEtherPortalDepositEther({ args: [address, execLayerData], @@ -118,9 +128,19 @@ export const EtherDepositForm: FC = (props) => { form.reset(); execute.reset(); onSearchApplications(""); + accountBalance.refetch(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [wait, onSearchApplications, onSuccess]); + }, [wait, onSearchApplications, onSuccess, accountBalance]); + + useEffect(() => { + form.setValues({ accountBalance: accountBalance }); + + if (form.isDirty("amount")) { + form.validateField("amount"); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountBalance]); return (
@@ -128,6 +148,7 @@ export const EtherDepositForm: FC = (props) => { = (props) => { )} - ETH} - withAsterisk - {...form.getInputProps("amount")} - /> + + ETH} + withAsterisk + {...form.getInputProps("amount")} + /> + + + Balance: {accountBalance.formatted} + {accountBalance.value > 0 && ( + { + form.setFieldValue( + "amount", + accountBalance.formatted, + ); + }} + data-testid="max-button" + > + Max + + )} + +