Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add evm code snippet, fix fields bugs #1250

Merged
merged 2 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- [#1250](https://github.com/alleslabs/celatone-frontend/pull/1250) Add evm code snippet modal and fix input fields
- [#1247](https://github.com/alleslabs/celatone-frontend/pull/1247) Support multi address creation txs
- [#1245](https://github.com/alleslabs/celatone-frontend/pull/1245) Support assets from assetlist in chain config
- [#1244](https://github.com/alleslabs/celatone-frontend/pull/1244) Add EVM contract verify alert info to both Solidity and Vyper upload file(s) and contract code
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/evm-abi/fields/BoolField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import type { SelectInputOption } from "lib/components/forms";
import { SelectInput } from "lib/components/forms";
import type { FieldProps } from "./types";

const BOOL_FIELD_OPTIONS: SelectInputOption<string>[] = [
const BOOL_FIELD_OPTIONS: SelectInputOption<boolean>[] = [
{
label: "True",
value: "1",
value: true,
},
{
label: "False",
value: "0",
value: false,
},
];

Expand Down
6 changes: 4 additions & 2 deletions src/lib/components/evm-abi/fields/FieldTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Flex, Text } from "@chakra-ui/react";
import type { FieldPath, FieldValues } from "react-hook-form";
import { useController } from "react-hook-form";
import { useController, useWatch } from "react-hook-form";
import { CustomIcon } from "lib/components/icon";
import type { Option } from "lib/types";
import { Field } from "./Field";
Expand All @@ -19,12 +19,14 @@ export const FieldTemplate = <T extends FieldValues>({
...rest
}: FieldTemplateProps<T>) => {
const {
field: { value, onChange },
field: { onChange },
} = useController<T>({
control,
name,
});

const value = useWatch({ control, name });

if (dimensions.length === 0)
return (
<Field control={control} name={name} components={components} {...rest} />
Expand Down
232 changes: 176 additions & 56 deletions src/lib/components/modal/EvmCodeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { ButtonProps } from "@chakra-ui/react";
import {
Box,
Button,
Heading,
Modal,
Expand All @@ -9,76 +8,197 @@ import {
ModalContent,
ModalHeader,
ModalOverlay,
Text,
TabList,
TabPanel,
TabPanels,
Tabs,
useDisclosure,
} from "@chakra-ui/react";
import type { JsonFragment } from "ethers";
import { useMemo } from "react";
import AceEditor from "react-ace";

import { AmpEvent, track } from "lib/amplitude";
import { CustomIcon } from "../icon";
import { useCelatoneApp } from "lib/app-provider/contexts";
import { useEvmConfig } from "lib/app-provider/hooks";
import { CustomTab } from "lib/components/CustomTab";
import type { HexAddr20, JsonDataType, Nullable } from "lib/types";

import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/theme-one_dark";
import "ace-builds/src-noconflict/theme-pastel_on_dark";
import { formatEvmFunctionInputsArgs } from "lib/utils";
import { CopyButton } from "../copy";
import { CustomIcon } from "../icon";

type CodeSnippetType = "read" | "write";

interface CodeSnippet {
name: string;
mode: string;
snippet: string;
}

interface EvmCodeSnippetProps {
ml?: ButtonProps["ml"];
type: CodeSnippetType;
contractAddress: HexAddr20;
abiSection: JsonFragment;
inputs?: JsonDataType[];
}

// TODO: Implement this modal
const EvmCodeSnippet = ({ ml }: EvmCodeSnippetProps) => {
const EvmCodeSnippet = ({
contractAddress,
abiSection,
type,
inputs,
}: EvmCodeSnippetProps) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const { theme } = useCelatoneApp();
const evm = useEvmConfig({ shouldRedirect: false });

const codeSnippets: Nullable<Record<CodeSnippetType, CodeSnippet[]>> =
useMemo(() => {
if (!evm.enabled || !evm.jsonRpc || !abiSection.name) return null;

const functionName = `"${abiSection.name}"`;
const inputsString = formatEvmFunctionInputsArgs(inputs);

return {
read: [
{
name: "Ethers",
mode: "javascript",
snippet: `import { ethers, Interface } from "ethers";

const provider = new ethers.JsonRpcProvider("${evm.jsonRpc}");
const iface = new Interface([${JSON.stringify(abiSection)}]);

const main = async () => {
const encodedData = iface.encodeFunctionData(${functionName}, ${inputsString});

const rawResult = await provider.call({
to: "${contractAddress}",
data: encodedData,
});

const decodedResult = iface.decodeFunctionResult(${functionName}, rawResult);
console.log(decodedResult);
};

main();`,
},
],
write: [
{
name: "Ethers",
mode: "javascript",
snippet: `import { ethers, Interface } from "ethers";

const provider = new ethers.JsonRpcProvider("${evm.jsonRpc}");
const privateKey = "your-private-key";
const wallet = new ethers.Wallet(privateKey, provider);

const ABI = [${JSON.stringify(abiSection)}];
const iface = new Interface(ABI);
const encodedData = iface.encodeFunctionData(${functionName}, ${inputsString});

const main = async () => {
const tx = await wallet.sendTransaction({
to: "${contractAddress}",
data: encodedData,
});

console.log(tx);
};

main();`,
},
],
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(evm), JSON.stringify(inputs), abiSection]);

if (!codeSnippets) return null;

return (
<>
<Button
variant="outline-secondary"
minW="128px"
size="sm"
gap={1}
onClick={() => {
track(AmpEvent.USE_CONTRACT_SNIPPET, {});
onOpen();
}}
>
<CustomIcon name="code" />
Code Snippet
</Button>

return null;

// return (
// <>
// <Button
// variant="outline-secondary"
// minW="128px"
// size="sm"
// ml={ml}
// gap={1}
// onClick={() => {
// track(AmpEvent.USE_CONTRACT_SNIPPET, {});
// onOpen();
// }}
// >
// <CustomIcon name="code" />
// Code Snippet
// </Button>

// <Modal isOpen={isOpen} onClose={onClose} isCentered size="4xl">
// <ModalOverlay />
// <ModalContent w="840px">
// <ModalHeader>
// <CustomIcon name="code" boxSize={6} color="gray.600" />
// <Heading as="h5" variant="h5">
// Code Snippet
// </Heading>
// </ModalHeader>
// <ModalCloseButton color="gray.600" />
// <ModalBody px={4} maxH="640px" overflow="scroll">
// <Text wordBreak="break-word">
// Lorem Ipsum is simply dummy text of the printing and typesetting
// industry. Lorem Ipsum has been the industry's standard dummy text
// ever since the 1500s, when an unknown printer took a galley of
// type and scrambled it to make a type specimen book. It has
// survived not only five centuries, but also the leap into
// electronic typesetting, remaining essentially unchanged. It was
// popularised in the 1960s with the release of Letraset sheets
// containing Lorem Ipsum passages, and more recently with desktop
// publishing software like Aldus PageMaker including versions of
// Lorem Ipsum.
// </Text>
// </ModalBody>
// </ModalContent>
// </Modal>
// </>
// );
<Modal isOpen={isOpen} onClose={onClose} isCentered size="4xl">
<ModalOverlay />
<ModalContent w="840px">
<ModalHeader>
<CustomIcon name="code" boxSize={6} color="gray.600" />
<Heading as="h5" variant="h5">
Code Snippet
</Heading>
</ModalHeader>
<ModalCloseButton color="gray.600" />
<ModalBody px={4} maxH="640px" overflow="scroll">
<Tabs>
<TabList borderBottom="1px solid" borderColor="gray.700">
{codeSnippets[type].map((item) => (
<CustomTab key={`menu-${item.name}`}>{item.name}</CustomTab>
))}
</TabList>
<TabPanels>
{codeSnippets[type].map((item) => (
<TabPanel key={item.name} px={2} py={4}>
<Box
bgColor="background.main"
p={4}
borderRadius="8px"
position="relative"
>
<AceEditor
readOnly
mode={item.mode}
theme={theme.jsonTheme}
fontSize="14px"
style={{
width: "100%",
background: "transparent",
}}
value={item.snippet}
setOptions={{
showGutter: false,
useWorker: false,
printMargin: false,
wrap: true,
}}
/>
<Box position="absolute" top={4} right={4}>
<CopyButton
value={item.snippet}
amptrackSection="code_snippet"
amptrackSubSection={item.name}
amptrackInfo={type}
/>
</Box>
</Box>
</TabPanel>
))}
</TabPanels>
</Tabs>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

export default EvmCodeSnippet;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Flex,
Grid,
GridItem,
HStack,
Text,
} from "@chakra-ui/react";
import type { JsonFragment } from "ethers";
Expand Down Expand Up @@ -81,7 +82,7 @@ export const ReadBox = ({
[abiSection, inputs]
);
const { refetch, isFetching } = useEthCall(contractAddress, data ?? "", {
enabled: !isUndefined(data) && opened,
enabled: opened && !isUndefined(data) && !inputRequired,
retry: false,
cacheTime: 0,
onSuccess: (data) => {
Expand Down Expand Up @@ -143,15 +144,22 @@ export const ReadBox = ({
gap={2}
mt={6}
>
<CopyButton
variant="outline-secondary"
isDisable={isUndefined(data)}
value={data ?? ""}
amptrackSection="read_inputs"
buttonText="Copy Encoded Inputs"
w="100%"
/>
<EvmCodeSnippet />
<HStack gap={2}>
<CopyButton
variant="outline-secondary"
isDisable={isUndefined(data)}
value={data ?? ""}
amptrackSection="read_inputs"
buttonText="Copy Encoded Inputs"
w="100%"
/>
<EvmCodeSnippet
abiSection={abiSection}
contractAddress={contractAddress}
type="read"
inputs={inputs}
/>
</HStack>
<Button
variant="primary"
size="sm"
Expand Down Expand Up @@ -195,14 +203,21 @@ export const ReadBox = ({
justifyContent="space-between"
>
<Grid gridTemplateColumns="1fr 1fr" gap={2}>
<CopyButton
variant="outline-secondary"
isDisable={isUndefined(data)}
value={data ?? ""}
amptrackSection="read_inputs"
buttonText="Copy Encoded Inputs"
w="100%"
/>
<HStack gap={2}>
<CopyButton
variant="outline-secondary"
isDisable={isUndefined(data)}
value={data ?? ""}
amptrackSection="read_inputs"
buttonText="Copy Encoded Inputs"
w="100%"
/>
<EvmCodeSnippet
abiSection={abiSection}
contractAddress={contractAddress}
type="read"
/>
</HStack>
<CopyButton
variant="outline-secondary"
isDisable={res === "" || Boolean(queryError)}
Expand All @@ -213,7 +228,6 @@ export const ReadBox = ({
w="100%"
/>
</Grid>
<EvmCodeSnippet />
<Flex
gap={{
md: 2,
Expand Down
Loading