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/code snippet #88

Merged
merged 15 commits into from
Jan 20, 2023
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

- [#88](https://github.com/alleslabs/celatone-frontend/pull/88) Add code snippet for query and execute
- [#107](https://github.com/alleslabs/celatone-frontend/pull/107) Remove osmosis mainnet from chain list
- [#99](https://github.com/alleslabs/celatone-frontend/pull/99) Validate label and codeId field in instantiate page
- [#103](https://github.com/alleslabs/celatone-frontend/pull/103) Add check mark to selected network
Expand Down
18 changes: 10 additions & 8 deletions src/lib/components/CustomTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { TabProps } from "@chakra-ui/react";
import { Button, useTab, Badge, useMultiStyleConfig } from "@chakra-ui/react";

interface CustomTabProps extends TabProps {
count: number;
count?: number;
}

export const CustomTab = ({ count, ...restProps }: CustomTabProps) => {
Expand Down Expand Up @@ -33,13 +33,15 @@ export const CustomTab = ({ count, ...restProps }: CustomTabProps) => {
>
{tabProps.children}

<Badge
variant={isSelected ? "primary" : "gray"}
ml="6px"
color="text.main"
>
{count}
</Badge>
{count && (
<Badge
variant={isSelected ? "primary" : "gray"}
ml="6px"
color="text.main"
>
{count}
</Badge>
)}
</Button>
);
};
256 changes: 256 additions & 0 deletions src/lib/components/modal/CodeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
Button,
useDisclosure,
ModalCloseButton,
TabList,
Tabs,
TabPanels,
TabPanel,
Heading,
Icon,
Box,
} from "@chakra-ui/react";
import { useWallet } from "@cosmos-kit/react";
import AceEditor from "react-ace";
import { MdCode } from "react-icons/md";

import { CopyButton } from "../CopyButton";
import { CustomTab } from "lib/components/CustomTab";
import { useEndpoint } from "lib/hooks";
import type { ContractAddr, HumanAddr, Option } from "lib/types";

import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-monokai";

interface CodeSnippetProps {
contractAddress: HumanAddr | ContractAddr;
message: string;
type: "query" | "execute";
}

/**
*
* @todo: This is a temporary solution to get the full RPC URL for Osmosis.
*/
const getFullRpcUrl = (rpcUrl: Option<string>, chainId: Option<string>) => {
const baseUrl = rpcUrl?.slice(0, rpcUrl.length - 1);
switch (chainId) {
case "osmosis-1":
case "osmo-test-4":
return `${baseUrl}:443`;
default:
return `${baseUrl}:26657`;
}
};

const CodeSnippet = ({
contractAddress,
message,
type = "query",
}: CodeSnippetProps) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const { currentChainRecord, currentChainName } = useWallet();
const isDisabled = !contractAddress || !message.length;

const endpoint = useEndpoint();
const client = currentChainRecord?.chain.daemon_name;
const rpcUrl = currentChainRecord?.preferredEndpoints?.rpc?.[0];
const chainId = currentChainRecord?.chain.chain_id;
const denom = currentChainRecord?.assetList.assets[0].base;
const codeSnippets: Record<
string,
{ name: string; mode: string; snippet: string }[]
> = {
query: [
{
name: "CLI",
mode: "sh",
snippet: `export CHAIN_ID='${chainId}'\n
export CONTRACT_ADDRESS='${contractAddress}'\n
export QUERY_MSG='${message}'\n
export RPC_URL='${getFullRpcUrl(rpcUrl, chainId)}'\n
${client} query wasm contract-state smart $CONTRACT_ADDRESS $QUERY_MSG \\
--chain-id $CHAIN_ID \\
--node $RPC_URL`,
},
{
name: "Python",
mode: "python",
snippet: `import base64
import requests\n
CONTRACT_ADDRESS = "${contractAddress}"
LCD_URL = "${endpoint}"
QUERY_MSG = b'''${message}'''\n
query_b64encoded = base64.b64encode(QUERY_MSG).decode("ascii")
res = requests.get(
f"{LCD_URL}/cosmwasm/wasm/v1/contract/{CONTRACT_ADDRESS}/smart/{query_b64encoded}"
).json()\n
print(res)`,
},
{
name: "CosmJS",
mode: "javascript",
snippet: `const { SigningCosmWasmClient } = require("@cosmjs/cosmwasm-stargate");
const rpcURL = "${rpcUrl}";
const contractAddress =
"${contractAddress}";
const queryMsg = \`${message}\`;\n
const queryContract = async (rpcURL, contractAddress, queryMsg) => {
const client = await SigningCosmWasmClient.connect(rpcURL);
const queryResult = await client.queryContractSmart(
contractAddress,
JSON.parse(queryMsg)
);
console.log(queryResult);
};\n
queryContract(rpcURL, contractAddress, queryMsg);`,
},
{
name: "Axios",
mode: "javascript",
snippet: `const axios = require('axios');\n
const lcdURL = '${endpoint}';
const contractAddress =
"${contractAddress}";
const queryMsg = \`${message}\`;\n
const queryContract = async () => {
const queryB64Encoded = Buffer.from(JSON.stringify(queryMsg)).toString('base64');
console.log(res.data);
};\n
queryContract();`,
},
],
execute: [
{
name: "CLI",
mode: "sh",
snippet: `${client} keys add --recover celatone\n
export CHAIN_ID='${chainId}'\n
export RPC_URL='${getFullRpcUrl(rpcUrl, chainId)}'\n
export CONTRACT_ADDRESS='${contractAddress}'\n
export EXECUTE_MSG='${message}'\n
${client} tx wasm execute $CONTRACT_ADDRESS $EXECUTE_MSG \\
--from celatone \\
--chain-id $CHAIN_ID \\
--node $RPC_URL`,
},
{
name: "CosmJs",
mode: "javascript",
snippet: `const { getOfflineSignerAmino, cosmwasm } = require('osmojs');
const { SigningCosmWasmClient } = require('@cosmjs/cosmwasm-stargate');
const { Dec, IntPretty } = require('@keplr-wallet/unit');
const { coins } = require('@cosmjs/amino');
const { toUtf8 } = require('@cosmjs/encoding');
const { chains } = require('chain-registry');
const { executeContract } = cosmwasm.wasm.v1.MessageComposer.withTypeUrl;\n
const chain = chains.find(({ chain_name }) => chain_name === '${currentChainName}');
const mnemonic = '<Mnemonic>';
const contractAddress = '${contractAddress}'\n
const execute = async () => {
const signer = await getOfflineSignerAmino({ mnemonic, chain });
const rpcEndpoint = '${rpcUrl}';
const client = await SigningCosmWasmClient.connectWithSigner(rpcEndpoint, signer);
const [sender] = await signer.getAccounts();\n
const msg = executeContract({
sender: sender.address,
contract: contractAddress,
msg: toUtf8(JSON.stringify(JSON.parse(\`${message}\`))),
funds: [],
});\n
const gasEstimated = await client.simulate(sender.address, [msg]);
const fee = {
amount: coins(0, '${denom}'),
gas: new IntPretty(new Dec(gasEstimated).mul(new Dec(1.3)))
.maxDecimals(0)
.locale(false)
.toString(),
};\n
const tx = await client.signAndBroadcast(sender.address, [msg], fee);
console.log(tx);
};\n
execute();`,
},
],
};

return (
<>
<Button
isDisabled={isDisabled}
variant="outline-info"
size="sm"
ml="auto"
onClick={onOpen}
>
<Icon as={MdCode} boxSize={5} mr={1} />
Code Snippet
</Button>

<Modal isOpen={isOpen} onClose={onClose} isCentered size="4xl">
<ModalOverlay />
<ModalContent w="840px">
<ModalHeader>
<Icon as={MdCode} color="text.dark" fontSize="24px" />
<Heading as="h5" variant="h5">
Code Snippet
</Heading>
</ModalHeader>
<ModalCloseButton color="text.dark" />
<ModalBody px={4} maxH="640px" overflow="scroll">
<Tabs>
<TabList borderBottom="1px solid" borderColor="divider.main">
{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="gray.900"
p={4}
borderRadius={4}
position="relative"
>
<AceEditor
readOnly
mode={item.mode}
theme="monokai"
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} />
</Box>
</Box>
</TabPanel>
))}
</TabPanels>
</Tabs>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

export default CodeSnippet;
16 changes: 14 additions & 2 deletions src/lib/pages/execute/components/ExecuteArea.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, Flex, Button, ButtonGroup, Icon, Text } from "@chakra-ui/react";
import type { StdFee } from "@cosmjs/stargate";
import { useWallet } from "@cosmos-kit/react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useFieldArray, useFormState, useWatch } from "react-hook-form";
import type { Control, UseFormSetValue } from "react-hook-form";
Expand All @@ -23,6 +24,10 @@ import type { ComposedMsg, ContractAddr, HumanAddr, Token } from "lib/types";
import { MsgType } from "lib/types";
import { composeMsg, jsonPrettify, jsonValidate, microfy } from "lib/utils";

const CodeSnippet = dynamic(() => import("lib/components/modal/CodeSnippet"), {
ssr: false,
});

interface ExecuteAreaProps {
control: Control<ExecutePageState>;
setValue: UseFormSetValue<ExecutePageState>;
Expand Down Expand Up @@ -246,8 +251,15 @@ export const ExecuteArea = ({ control, setValue, cmds }: ExecuteAreaProps) => {
</Button>
</Box>
</Flex>
<Flex alignItems="center" justify="space-between" mt={{ md: 8, xl: 0 }}>
<CopyButton isDisable={msg.length === 0} value={msg} />
<Flex alignItems="center" justify="space-between">
<Flex gap={2}>
<CopyButton isDisable={!msg.length} value={msg} />
<CodeSnippet
type="execute"
contractAddress={contractAddress}
message={msg}
/>
</Flex>
<Flex direction="row" align="center" gap={2}>
<Flex fontSize="14px" color="text.dark" alignItems="center">
Transaction Fee:{" "}
Expand Down
14 changes: 13 additions & 1 deletion src/lib/pages/query/components/QueryArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Box, Flex, Spacer, Button, ButtonGroup, Text } from "@chakra-ui/react";
import { useWallet } from "@cosmos-kit/react";
import { useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";

import { ContractCmdButton } from "lib/components/ContractCmdButton";
Expand All @@ -15,6 +16,10 @@ import { queryData } from "lib/services/contract";
import type { ContractAddr, RpcQueryError } from "lib/types";
import { encode, jsonPrettify, jsonValidate } from "lib/utils";

const CodeSnippet = dynamic(() => import("lib/components/modal/CodeSnippet"), {
ssr: false,
});

interface QueryAreaProps {
contractAddress: ContractAddr;
initialMsg: string;
Expand Down Expand Up @@ -119,7 +124,14 @@ export const QueryArea = ({
height="240px"
/>
<Flex align="center" justify="space-between">
<CopyButton isDisable={msg.length === 0} value={msg} />
<Flex gap={2}>
<CopyButton isDisable={!msg.length} value={msg} />
<CodeSnippet
type="query"
contractAddress={contractAddress}
message={msg}
/>
</Flex>
<Button
variant="primary"
fontSize="14px"
Expand Down