From 8b76e86d4773ece092ca9782913b4a34c84a003c Mon Sep 17 00:00:00 2001 From: Richard Watts <108257153+rrw-zilliqa@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:17:31 +0100 Subject: [PATCH] Display init parameters (#52) * (fix) When the user gives you a long block number, don't express it in mantissa/exponent notation, get confused and error out. (feat) When the user gives you a solidity 0x00.. prefixed constant that looks like an address, treat it as an address. (feat) Display Scilla contract state and init parameters. * (fix) Make the type system happy * (fix) Formatting --- src/components/SwitchTab.tsx | 29 +++++++ src/execution/address/Contracts.tsx | 4 +- src/execution/address/ScillaContract.tsx | 53 ++++++++++--- src/execution/address/ScillaInitParams.tsx | 69 ++++++++++++++++ src/execution/address/ScillaState.tsx | 91 ++++++++++++++++++++++ src/search/search.ts | 27 ++++++- src/useZilliqaHooks.ts | 60 ++++++++++++++ 7 files changed, 319 insertions(+), 14 deletions(-) create mode 100644 src/components/SwitchTab.tsx create mode 100644 src/execution/address/ScillaInitParams.tsx create mode 100644 src/execution/address/ScillaState.tsx diff --git a/src/components/SwitchTab.tsx b/src/components/SwitchTab.tsx new file mode 100644 index 00000000..6c80d9d8 --- /dev/null +++ b/src/components/SwitchTab.tsx @@ -0,0 +1,29 @@ +import { Tab } from "@headlessui/react"; +import React, { PropsWithChildren } from "react"; + +type SwitchTabProps = { + disabled?: boolean | undefined; + children: any; +}; + +const SwitchTab: React.FC> = ({ + disabled, + children, +}) => ( + + `${ + disabled + ? "cursor-default border-gray-100 text-gray-300" + : selected + ? "border-link-blue text-link-blue" + : "border-transparent text-gray-500" + } border-b-2 px-3 py-3 text-sm font-bold hover:text-link-blue` + } + disabled={disabled} + > + {children} + +); + +export default SwitchTab; diff --git a/src/execution/address/Contracts.tsx b/src/execution/address/Contracts.tsx index c4c7bcf5..81edda35 100644 --- a/src/execution/address/Contracts.tsx +++ b/src/execution/address/Contracts.tsx @@ -170,7 +170,9 @@ const Contracts: React.FC = ({ checksummedAddress, match }) => { )}
{code === undefined && Getting contract bytecode...} - {scillaCode && } + {scillaCode && ( + + )} {!scillaCode && code && ( <>
Contract Bytecode
diff --git a/src/execution/address/ScillaContract.tsx b/src/execution/address/ScillaContract.tsx index 8acce70a..4791d747 100644 --- a/src/execution/address/ScillaContract.tsx +++ b/src/execution/address/ScillaContract.tsx @@ -1,19 +1,52 @@ +import { Tab } from "@headlessui/react"; import React from "react"; +import SwitchTab from "../../components/SwitchTab"; import { SyntaxHighlighter, docco } from "../../highlight-init"; +import { ScillaInitParams } from "./ScillaInitParams"; +import { ScillaState } from "./ScillaState"; type ContractProps = { + address: string; content: any; }; -const ScillaContract: React.FC = ({ content }) => ( - - {content ?? ""} - -); +const ScillaContract: React.FC = ({ address, content }) => { + let [loadContractState, setLoadContractState] = React.useState(); + + return ( +
+ + + Code + Init Params + State + + + + + {content ?? ""} + + + + {" "} + + + + + + + +
+ ); +}; export default React.memo(ScillaContract); diff --git a/src/execution/address/ScillaInitParams.tsx b/src/execution/address/ScillaInitParams.tsx new file mode 100644 index 00000000..4bee0843 --- /dev/null +++ b/src/execution/address/ScillaInitParams.tsx @@ -0,0 +1,69 @@ +import { FC, useContext } from "react"; +import { RuntimeContext } from "../../useRuntime"; +import { useSmartContractInit } from "../../useZilliqaHooks"; + +type ScillaInitParamsProps = { + address: string; +}; + +type ScillaInitParamRowProps = { + name: string; + valueType: string; + value: string; +}; + +const ScillaInitParamRow: FC = ({ + name, + valueType, + value, +}) => { + return ( + <> + + + {name} + + {valueType} + {value} + + + ); +}; + +export const ScillaInitParams: FC = ({ address }) => { + const { zilliqa } = useContext(RuntimeContext); + const { data, isLoading } = useSmartContractInit(zilliqa, address); + if (isLoading) { + return ( +
+ Loading (or cannot retrieve) contract init parameters +
+ ); + } else { + return ( +
+ + + + + + + + + + {data + ? data.map((val) => ( + + )) + : undefined} + +
nametypevalue
+
+ ); + } +}; diff --git a/src/execution/address/ScillaState.tsx b/src/execution/address/ScillaState.tsx new file mode 100644 index 00000000..cd843049 --- /dev/null +++ b/src/execution/address/ScillaState.tsx @@ -0,0 +1,91 @@ +import { FC, useContext, useState } from "react"; +import { RuntimeContext } from "../../useRuntime"; +import { ContractState, useSmartContractState } from "../../useZilliqaHooks"; + +type ScillaStateProps = { + address: string; + loadContractState: boolean | undefined; + setLoadContractState: (arg0: boolean) => void; +}; + +type ScillaStateRowProps = { + name: string; + value: string; +}; + +const ScillaStateParamRow: FC = ({ name, value }) => { + return ( + <> + + + {name} + + {value} + + + ); +}; + +const formatJsonValue = (value: any): string => { + return JSON.stringify(value, null, 2); +}; + +export const ScillaState: FC = ({ + address, + loadContractState, + setLoadContractState, +}) => { + const { zilliqa } = useContext(RuntimeContext); + const [contractState, setContractState] = useState( + null, + ); + + const { data, isLoading } = useSmartContractState( + loadContractState ? zilliqa : undefined, + address, + ); + if (data && contractState == null) { + setContractState(data); + } + + if (!loadContractState && !contractState) { + return ( +
+ +
+ ); + } + + if (!contractState) { + return
Loading contract state
; + } + + return ( +
+ + + + + + + + + {contractState + ? Object.keys(contractState).map((val) => ( + + )) + : undefined} + +
namevalue
+
+ ); +}; diff --git a/src/search/search.ts b/src/search/search.ts index 6fab2c54..7ed82130 100644 --- a/src/search/search.ts +++ b/src/search/search.ts @@ -247,6 +247,23 @@ const doSearch = async (q: string, navigate: NavigateFunction) => { maybeIndex = q.substring(sepIndex + 1); } + // The type checker is convinced that ethers:isAddress() will never say that a string > 40 characters + // long is not an address. I'm not sure why... + if (!isAddress(maybeAddress)) { + let typeCheckerIsWrong = maybeAddress as string; + if (typeCheckerIsWrong.length > 40) { + try { + maybeAddress = + "0x" + + typeCheckerIsWrong + .substr(typeCheckerIsWrong.length - 40) + .toLowerCase(); + } catch (e) { + // Obviously not. + } + } + } + // Plain address? if (isAddress(maybeAddress)) { navigate( @@ -267,10 +284,14 @@ const doSearch = async (q: string, navigate: NavigateFunction) => { } // Block number? - const blockNumber = parseInt(q); - if (!isNaN(blockNumber)) { - navigate(`/block/${blockNumber}`); + // If the number here is very large, parseInt() will return an fp number which + // will cause errors, so .. + try { + const blockNumber = BigInt(q); + navigate(`/block/${blockNumber.toString()}`); return; + } catch (e) { + // Obviously not! } // DS Block number? diff --git a/src/useZilliqaHooks.ts b/src/useZilliqaHooks.ts index ac843687..2b601d12 100644 --- a/src/useZilliqaHooks.ts +++ b/src/useZilliqaHooks.ts @@ -11,6 +11,18 @@ import { Fetcher } from "swr"; import useSWRImmutable from "swr/immutable"; import useSWRInfinite from "swr/infinite"; +export type InitValue = { + vname: string; + type: string; + value: any; +}; + +export type ContractInitData = Array; + +export type ContractState = { + [key: string]: object; +}; + const dsBlockDataFetcher: Fetcher< DsBlockObj | null, [Zilliqa, number] @@ -102,3 +114,51 @@ export const useBlockChainInfo = ( } return { data, isLoading }; }; + +export const smartContractInitFetcher: Fetcher< + ContractInitData, + [Zilliqa, string, string] +> = async ([zilliqa, methodName, address]) => { + const contract = zilliqa.contracts.at(address); + const initParams = await contract.getInit(); + return initParams as ContractInitData; +}; + +export const useSmartContractInit = ( + zilliqa: Zilliqa | undefined, + address: string, +): { data: ContractInitData | undefined; isLoading: boolean } => { + const { data, error, isLoading } = useSWRImmutable( + zilliqa !== undefined ? [zilliqa, "useSmartContractInit", address] : null, + smartContractInitFetcher, + { keepPreviousData: true }, + ); + if (error) { + return { data: undefined, isLoading: false }; + } + return { data, isLoading }; +}; + +export const smartContractStateFetcher: Fetcher< + any, + [Zilliqa, string, string] +> = async ([zilliqa, methodName, address]) => { + const contract = zilliqa.contracts.at(address); + const initParams = await contract.getState(); + return initParams as any; +}; + +export const useSmartContractState = ( + zilliqa: Zilliqa | undefined, + address: string, +): { data: ContractState | undefined; isLoading: boolean } => { + const { data, error, isLoading } = useSWRImmutable( + zilliqa !== undefined ? [zilliqa, "useSmartContractState", address] : null, + smartContractStateFetcher, + { keepPreviousData: true }, + ); + if (error) { + return { data: undefined, isLoading: false }; + } + return { data, isLoading }; +};