Skip to content

Commit

Permalink
Feat: Ether deposit validation on change and display balance (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunomenezes authored Jan 14, 2025
1 parent 07636a8 commit 1713cd5
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 283 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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[];
Expand All @@ -54,22 +56,27 @@ export const EtherDepositForm: FC<EtherDepositFormProps> = (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",
},
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 {
Expand All @@ -79,22 +86,25 @@ export const EtherDepositForm: FC<EtherDepositFormProps> = (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],
Expand All @@ -118,16 +128,27 @@ export const EtherDepositForm: FC<EtherDepositFormProps> = (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 (
<form data-testid="ether-deposit-form">
<Stack>
<Autocomplete
label="Application"
description="The application smart contract address"
data-testid="application-input"
placeholder="0x"
data={applications}
withAsterisk
Expand Down Expand Up @@ -157,21 +178,44 @@ export const EtherDepositForm: FC<EtherDepositFormProps> = (props) => {
</Alert>
)}

<TextInput
type="number"
step="1"
min={0}
label="Amount"
description="Amount of ether to deposit"
placeholder="0"
rightSectionWidth={60}
rightSection={<Text>ETH</Text>}
withAsterisk
{...form.getInputProps("amount")}
/>
<Stack gap="xs">
<TextInput
type="number"
step="1"
min={0}
label="Amount"
description="Amount of ether to deposit"
data-testid="amount-input"
placeholder="0"
rightSectionWidth={60}
rightSection={<Text>ETH</Text>}
withAsterisk
{...form.getInputProps("amount")}
/>

<Flex c={"dark.2"} gap="3">
<Text fz="xs">Balance: {accountBalance.formatted}</Text>
{accountBalance.value > 0 && (
<UnstyledButton
fz={"xs"}
c={"cyan"}
onClick={() => {
form.setFieldValue(
"amount",
accountBalance.formatted,
);
}}
data-testid="max-button"
>
Max
</UnstyledButton>
)}
</Flex>
</Stack>

<Collapse in={advanced}>
<Textarea
data-testid="eth-extra-data-input"
label="Extra data"
description="Extra execution layer data handled by the application"
{...form.getInputProps("execLayerData")}
Expand Down
42 changes: 42 additions & 0 deletions packages/ui/src/hooks/useAccountBalance.ts
Original file line number Diff line number Diff line change
@@ -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<bigint, DataType, bigint>(0n, "value", data);
const decimals = propOr<number, DataType, number>(18, "decimals", data);
const symbol = propOr<string, DataType, string>("ETH", "symbol", data);
const formatted = isNotNil(data) ? formatUnits(value, decimals) : "0";

return {
value,
decimals,
symbol,
formatted,
refetch,
};
}, [data, refetch]);

return result;
};
15 changes: 0 additions & 15 deletions packages/ui/src/hooks/useFormattedBalance.ts

This file was deleted.

8 changes: 4 additions & 4 deletions packages/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export { ERC1155DepositForm } from "./ERC1155DepositForm";
export { ERC20DepositForm } from "./ERC20DepositForm";
export { ERC721DepositForm } from "./ERC721DepositForm";
export { EtherDepositForm } from "./EtherDepositForm";
export {
GenericInputForm,
type GenericInputFormSpecification,
} from "./GenericInputForm";
export {
InputContent,
InputDetails,
Expand All @@ -16,7 +20,3 @@ export { Summary } from "./Summary";
export { SummaryCard } from "./SummaryCard";
export { TransactionProgress } from "./TransactionProgress";
export { default as useWatchQueryOnBlockChange } from "./hooks/useWatchQueryOnBlockChange";
export {
GenericInputForm,
type GenericInputFormSpecification,
} from "./GenericInputForm";
Loading

0 comments on commit 1713cd5

Please sign in to comment.