From ae8c3bf929ef95fa516dde61aa5ac522bdd643f8 Mon Sep 17 00:00:00 2001 From: Neven Dyulgerov Date: Wed, 20 Dec 2023 06:21:10 +0200 Subject: [PATCH] #21 Create form for sending raw input to application (#36) --- apps/web/package.json | 10 +- apps/web/src/components/sendTransaction.tsx | 105 + apps/web/src/components/shell.tsx | 104 +- .../src/providers/connectionConfig/reducer.ts | 4 +- .../test/components/sendTransaction.test.tsx | 209 + apps/web/test/components/shell.test.tsx | 9 +- .../src/stories/SendTransaction.stories.tsx | 142 + packages/ui/package.json | 4 +- packages/ui/src/ERC20DepositForm.tsx | 20 +- packages/ui/src/EtherDepositForm.tsx | 19 +- packages/ui/src/RawInputForm.tsx | 150 + packages/ui/src/index.tsx | 3 +- packages/ui/test/ERC20DepositForm.test.tsx | 2 + packages/ui/test/EtherDepositForm.test.tsx | 3 +- packages/ui/test/RawInputForm.test.tsx | 174 + yarn.lock | 4406 ++++++++--------- 16 files changed, 3013 insertions(+), 2351 deletions(-) create mode 100644 apps/web/src/components/sendTransaction.tsx create mode 100644 apps/web/test/components/sendTransaction.test.tsx create mode 100644 apps/workshop/src/stories/SendTransaction.stories.tsx create mode 100644 packages/ui/src/RawInputForm.tsx create mode 100644 packages/ui/test/RawInputForm.test.tsx diff --git a/apps/web/package.json b/apps/web/package.json index b9054574..76667e7d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,11 +16,11 @@ "dependencies": { "@cartesi/rollups-explorer-ui": "*", "@cartesi/rollups-wagmi": "*", - "@mantine/core": "^7.2.2", - "@mantine/form": "^7.2.2", - "@mantine/hooks": "^7.2.2", - "@mantine/notifications": "^7.2.2", - "@rainbow-me/rainbowkit": "1.3.1", + "@mantine/core": "^7.3.0", + "@mantine/form": "^7.3.0", + "@mantine/hooks": "^7.3.0", + "@mantine/notifications": "^7.3.0", + "@rainbow-me/rainbowkit": "1.2.0", "@react-spring/web": "^9.7.3", "abitype": "^0.9", "encoding": "^0.1", diff --git a/apps/web/src/components/sendTransaction.tsx b/apps/web/src/components/sendTransaction.tsx new file mode 100644 index 00000000..4106db5e --- /dev/null +++ b/apps/web/src/components/sendTransaction.tsx @@ -0,0 +1,105 @@ +"use client"; +import { + ERC20DepositForm, + EtherDepositForm, + RawInputForm, +} from "@cartesi/rollups-explorer-ui"; +import { FC, useMemo, useState } from "react"; +import { Select } from "@mantine/core"; +import { useApplicationsQuery, useTokensQuery } from "../graphql"; +import { useDebouncedValue } from "@mantine/hooks"; + +export type DepositType = + | "ether" + | "erc20" + | "erc721" + | "erc1155" + | "relay" + | "input"; + +interface DepositProps { + initialDepositType?: DepositType; +} + +const SendTransaction: FC = ({ + initialDepositType = "ether", +}) => { + const [depositType, setDepositType] = + useState(initialDepositType); + const [applicationId, setApplicationId] = useState(""); + const [debouncedApplicationId] = useDebouncedValue(applicationId, 400); + const [{ data: applicationData, fetching }] = useApplicationsQuery({ + variables: { + limit: 10, + where: { + id_containsInsensitive: debouncedApplicationId ?? "", + }, + }, + }); + const applications = useMemo( + () => (applicationData?.applications ?? []).map((a) => a.id), + [applicationData], + ); + const [{ data: tokenData }] = useTokensQuery({ + variables: { + limit: 100, + }, + }); + const tokens = useMemo( + () => + (tokenData?.tokens ?? []).map( + (a) => `${a.symbol} - ${a.name} - ${a.id}`, + ), + [tokenData], + ); + + return ( + <> + { + setDepositType(nextValue); + }} + /> + +
+ {items.find((d) => d.value === depositType)?.label} +
+ + ); +}; + +export const FormWithSegmentedControl: Story = { + render: () => , +}; + +export const FormWithSelect: Story = { + render: () => , +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 172cbbaa..75e7a3bb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,8 +26,8 @@ }, "dependencies": { "@cartesi/rollups-wagmi": "*", - "@mantine/core": "^7.2.2", - "@mantine/hooks": "^7.2.2", + "@mantine/core": "^7.3.0", + "@mantine/hooks": "^7.3.0", "@react-spring/web": "^9.7.3", "ramda": "^0.29.0", "react-icons": "^4", diff --git a/packages/ui/src/ERC20DepositForm.tsx b/packages/ui/src/ERC20DepositForm.tsx index c3c2c20d..d3bb6f83 100644 --- a/packages/ui/src/ERC20DepositForm.tsx +++ b/packages/ui/src/ERC20DepositForm.tsx @@ -66,13 +66,18 @@ export const transactionButtonState = ( export interface ERC20DepositFormProps { applications: string[]; + isLoadingApplications: boolean; + onSearchApplications: (applicationId: string) => void; tokens: string[]; } -export const ERC20DepositForm: FC = ({ - applications, - tokens, -}) => { +export const ERC20DepositForm: FC = (props) => { + const { + applications, + isLoadingApplications, + onSearchApplications, + tokens, + } = props; const tokenAddresses = useMemo( () => tokens.map((token) => { @@ -232,7 +237,7 @@ export const ERC20DepositForm: FC = ({ ); return ( -
+ = ({ data={applications} withAsterisk data-testid="application" + rightSection={isLoadingApplications && } {...form.getInputProps("application")} + onChange={(nextValue) => { + form.setFieldValue("application", nextValue); + onSearchApplications(nextValue); + }} /> {!form.errors.application && diff --git a/packages/ui/src/EtherDepositForm.tsx b/packages/ui/src/EtherDepositForm.tsx index 63f069de..3c32e237 100644 --- a/packages/ui/src/EtherDepositForm.tsx +++ b/packages/ui/src/EtherDepositForm.tsx @@ -37,11 +37,12 @@ import { TransactionProgress } from "./TransactionProgress"; export interface EtherDepositFormProps { applications: string[]; + isLoadingApplications: boolean; + onSearchApplications: (applicationId: string) => void; } -export const EtherDepositForm: FC = ({ - applications, -}) => { +export const EtherDepositForm: FC = (props) => { + const { applications, isLoadingApplications, onSearchApplications } = props; const [advanced, { toggle: toggleAdvanced }] = useDisclosure(false); const { chain } = useNetwork(); const form = useForm({ @@ -93,7 +94,7 @@ export const EtherDepositForm: FC = ({ }, [wait.status]); return ( - + = ({ placeholder="0x" data={applications} withAsterisk - rightSection={prepare.isLoading && } + rightSection={ + (prepare.isLoading || isLoadingApplications) && ( + + ) + } {...form.getInputProps("application")} error={ form.errors?.application || (prepare.error as BaseError)?.shortMessage } + onChange={(nextValue) => { + form.setFieldValue("application", nextValue); + onSearchApplications(nextValue); + }} /> {!form.errors.application && diff --git a/packages/ui/src/RawInputForm.tsx b/packages/ui/src/RawInputForm.tsx new file mode 100644 index 00000000..82dff704 --- /dev/null +++ b/packages/ui/src/RawInputForm.tsx @@ -0,0 +1,150 @@ +import { FC, useEffect, useMemo } from "react"; +import { + useInputBoxAddInput, + usePrepareInputBoxAddInput, +} from "@cartesi/rollups-wagmi"; +import { + Button, + Collapse, + Group, + Stack, + Textarea, + Autocomplete, + Alert, + Loader, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { TbCheck, TbAlertCircle } from "react-icons/tb"; +import { + getAddress, + isAddress, + isHex, + toHex, + zeroAddress, + BaseError, +} from "viem"; +import { useWaitForTransaction } from "wagmi"; +import { TransactionProgress } from "./TransactionProgress"; + +export interface RawInputFormProps { + applications: string[]; + isLoadingApplications: boolean; + onSearchApplications: (applicationId: string) => void; +} + +export const RawInputForm: FC = (props) => { + const { applications, isLoadingApplications, onSearchApplications } = props; + const addresses = useMemo( + () => applications.map(getAddress), + [applications], + ); + const form = useForm({ + validateInputOnBlur: true, + initialValues: { + application: "", + rawInput: "0x", + }, + validate: { + application: (value) => + value !== "" && isAddress(value) ? null : "Invalid application", + rawInput: (value) => (isHex(value) ? null : "Invalid hex string"), + }, + transformValues: (values) => ({ + address: isAddress(values.application) + ? getAddress(values.application) + : zeroAddress, + rawInput: toHex(values.rawInput), + }), + }); + const { address, rawInput } = form.getTransformedValues(); + const prepare = usePrepareInputBoxAddInput({ + args: [address, rawInput], + enabled: form.isValid(), + }); + const execute = useInputBoxAddInput(prepare.config); + const wait = useWaitForTransaction(execute.data); + const loading = execute.status === "loading" || wait.status === "loading"; + const canSubmit = form.isValid() && prepare.error === null; + + useEffect(() => { + if (wait.status === "success") { + form.reset(); + } + }, [wait.status]); + + return ( + + + + ) + } + {...form.getInputProps("application")} + error={ + form.errors.application || + (prepare.error as BaseError)?.shortMessage + } + onChange={(nextValue) => { + form.setFieldValue("application", nextValue); + onSearchApplications(nextValue); + }} + /> + + {!form.errors.application && + address !== zeroAddress && + !addresses.includes(address) && ( + } + > + This is an undeployed application. + + )} + +