diff --git a/CHANGELOG.md b/CHANGELOG.md index c4de2d32d..c2f134007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/components/CustomTab.tsx b/src/lib/components/CustomTab.tsx index 8dc396164..570172c96 100644 --- a/src/lib/components/CustomTab.tsx +++ b/src/lib/components/CustomTab.tsx @@ -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) => { @@ -33,13 +33,15 @@ export const CustomTab = ({ count, ...restProps }: CustomTabProps) => { > {tabProps.children} - - {count} - + {count && ( + + {count} + + )} ); }; diff --git a/src/lib/components/modal/CodeSnippet.tsx b/src/lib/components/modal/CodeSnippet.tsx new file mode 100644 index 000000000..380e331bc --- /dev/null +++ b/src/lib/components/modal/CodeSnippet.tsx @@ -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, chainId: Option) => { + 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 = ''; +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 ( + <> + + + + + + + + + Code Snippet + + + + + + + {codeSnippets[type].map((item) => ( + {item.name} + ))} + + + {codeSnippets[type].map((item) => ( + + + + + + + + + ))} + + + + + + + ); +}; + +export default CodeSnippet; diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index 87f2bf55f..f7893cb42 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -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"; @@ -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; setValue: UseFormSetValue; @@ -246,8 +251,15 @@ export const ExecuteArea = ({ control, setValue, cmds }: ExecuteAreaProps) => { - - + + + + + Transaction Fee:{" "} diff --git a/src/lib/pages/query/components/QueryArea.tsx b/src/lib/pages/query/components/QueryArea.tsx index c359537af..b2cd24705 100644 --- a/src/lib/pages/query/components/QueryArea.tsx +++ b/src/lib/pages/query/components/QueryArea.tsx @@ -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"; @@ -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; @@ -119,7 +124,14 @@ export const QueryArea = ({ height="240px" /> - + + + +