From 88a93eb5a349768ec2fa54eded8c39ee801b1d73 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Fri, 13 Sep 2024 17:05:34 -0300 Subject: [PATCH] fix(website): interact --- .../website/src/features/Packages/Abi.tsx | 15 +- .../src/features/Packages/Function.tsx | 38 ++--- .../src/features/Packages/Interact.tsx | 15 +- .../features/Packages/Tabs/InteractTab.tsx | 155 ++++++++++-------- .../usePackageNameTagVariantUrlParams.ts | 25 +-- .../src/hooks/routing/usePackageUrlParams.ts | 10 +- .../routing/usePackageVersionUrlParams.ts | 2 +- .../routing/useTransactionDetailsParams.ts | 16 +- .../[name]/[tag]/[variant]/_layout.tsx | 2 +- .../[contractAddress]/index.tsx | 27 +-- .../[tag]/[variant]/interact/_layout.tsx | 20 +-- .../[name]/[tag]/[variant]/interact/index.tsx | 4 +- 12 files changed, 181 insertions(+), 148 deletions(-) diff --git a/packages/website/src/features/Packages/Abi.tsx b/packages/website/src/features/Packages/Abi.tsx index 5c7cc3af9..c603f1f6c 100644 --- a/packages/website/src/features/Packages/Abi.tsx +++ b/packages/website/src/features/Packages/Abi.tsx @@ -85,7 +85,6 @@ export const Abi: FC<{ const hasSubnav = useContext(SubnavContext); const containerRef = useRef(null); const [selectedSelector, setSelectedSelector] = useState(null); - const [isUpdatingRoute, setIsUpdatingRoute] = useState(false); const allContractMethods = useMemo( () => @@ -129,18 +128,10 @@ export const Abi: FC<{ offset: adjust * -1, }); - // update the url in shallow mode - setIsUpdatingRoute(true); - await router.replace( - `${router.asPath.split('#')[0]}#${selectedSelector}`, - undefined, - { shallow: true } - ); - setIsUpdatingRoute(false); + await router.push(`${router.asPath.split('#')[0]}#${newSelector}`); }; const handleMethodClick = async (functionSelector: AbiFunction) => { - if (isUpdatingRoute) return; const newSelector = getSelectorSlug(functionSelector); if (newSelector === selectedSelector) { return; @@ -160,7 +151,7 @@ export const Abi: FC<{ if (urlSelectorFromPath || !selectedSelector) { setSelectedSelector(urlSelectorFromPath); } - }, [router.asPath]); + }, [router.asPath, selectedSelector]); return ( @@ -209,7 +200,6 @@ export const Abi: FC<{ handleMethodClick(f)} > {f.name}( @@ -240,7 +230,6 @@ export const Abi: FC<{ .map((f, index) => ( handleMethodClick(f)} > diff --git a/packages/website/src/features/Packages/Function.tsx b/packages/website/src/features/Packages/Function.tsx index 172cc3f97..9203de93e 100644 --- a/packages/website/src/features/Packages/Function.tsx +++ b/packages/website/src/features/Packages/Function.tsx @@ -3,6 +3,7 @@ import { FunctionInput } from '@/features/Packages/FunctionInput'; import { FunctionOutput } from '@/features/Packages/FunctionOutput'; import { useQueueTxsStore, useStore } from '@/helpers/store'; import { useContractCall, useContractTransaction } from '@/hooks/ethereum'; +import { useCannonChains } from '@/providers/CannonProvidersProvider'; import { CheckCircleIcon, ChevronDownIcon, @@ -34,6 +35,7 @@ import React, { FC, useEffect, useMemo, useRef, useState } from 'react'; import { FaCode } from 'react-icons/fa6'; import { Address, + createPublicClient, encodeFunctionData, parseEther, toFunctionSelector, @@ -41,12 +43,7 @@ import { TransactionRequestBase, zeroAddress, } from 'viem'; -import { - useAccount, - usePublicClient, - useSwitchChain, - useWalletClient, -} from 'wagmi'; +import { useAccount, useSwitchChain, useWalletClient } from 'wagmi'; export const Function: FC<{ selected?: boolean; @@ -78,12 +75,20 @@ export const Function: FC<{ const [loading, setLoading] = useState(false); const [simulated, setSimulated] = useState(false); const [error, setError] = useState(null); + const [hasExpandedSelected, setHasExpandedSelected] = useState(false); // TODO: don't know why, had to use a ref instead of an array to be able to // keep the correct reference. const sadParams = useRef(new Array(f.inputs.length).fill(undefined)); const [params, setParams] = useState([...sadParams.current]); + const { getChainById, transports } = useCannonChains(); + const chain = getChainById(chainId); + const publicClient = createPublicClient({ + chain, + transport: transports[chainId], + }); + const setParam = (index: number, value: any) => { sadParams.current[index] = value; setParams([...sadParams.current]); @@ -114,9 +119,7 @@ export const Function: FC<{ const { isConnected, address: from, chain: connectedChain } = useAccount(); const { openConnectModal } = useConnectModal(); - const publicClient = usePublicClient({ - chainId: chainId as number, - })!; + const { switchChain } = useSwitchChain(); const { data: walletClient } = useWalletClient({ chainId: chainId as number, @@ -127,7 +130,7 @@ export const Function: FC<{ f.name, [...params], abi, - publicClient as any // TODO: fix type + publicClient ); const [writeContractResult, fetchWriteContractResult] = @@ -168,7 +171,7 @@ export const Function: FC<{ try { if (readOnly) { - await fetchReadContractResult(zeroAddress); + await fetchReadContractResult(from ?? zeroAddress); } else { if (!isConnected) { if (openConnectModal) openConnectModal(); @@ -457,9 +460,7 @@ export const Function: FC<{ size="xs" mr={3} mb={3} - onClick={() => { - void submit(false, true); - }} + onClick={async () => await submit(false, true)} > Simulate transaction @@ -473,9 +474,7 @@ export const Function: FC<{ size="xs" mr={3} mb={3} - onClick={() => { - void submit(false); - }} + onClick={async () => await submit(false)} > Submit using wallet {!simulated && statusIcon} @@ -569,10 +568,11 @@ export const Function: FC<{ ); useEffect(() => { - if (selected && !isOpen) { + if (!hasExpandedSelected && selected && !isOpen) { onToggle(); + setHasExpandedSelected(true); } - }, [selected]); + }, [selected, isOpen, onToggle, hasExpandedSelected]); return ( <> diff --git a/packages/website/src/features/Packages/Interact.tsx b/packages/website/src/features/Packages/Interact.tsx index c966e507a..cfea7dd90 100644 --- a/packages/website/src/features/Packages/Interact.tsx +++ b/packages/website/src/features/Packages/Interact.tsx @@ -1,8 +1,10 @@ +'use client'; + import QueueDrawer from '@/features/Deploy/QueueDrawer'; import { Abi } from '@/features/Packages/Abi'; import { SubnavContext } from '@/features/Packages/Tabs/InteractTab'; import { useQueryIpfsDataParsed } from '@/hooks/ipfs'; -import { usePackageVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; +import { usePackageNameTagVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; import { getOutput } from '@/lib/builder'; import { Box, @@ -29,7 +31,7 @@ import { usePackageByRef } from '@/hooks/api/usePackage'; const Interact: FC = () => { const { variant, tag, name, moduleName, contractName, contractAddress } = - usePackageVersionUrlParams(); + usePackageNameTagVersionUrlParams(); const { isOpen, onOpen, onClose } = useDisclosure(); const { getExplorerUrl } = useCannonChains(); @@ -88,7 +90,14 @@ const Interact: FC = () => { } }; findContract(cannonOutputs.contracts, name, cannonOutputs.imports); - }, [deploymentData.data, contractName]); + }, [ + contractName, + deploymentData.data, + deploymentData.isPending, + name, + moduleName, + contractAddress, + ]); const deployUrl = `${ externalLinks.IPFS_CANNON diff --git a/packages/website/src/features/Packages/Tabs/InteractTab.tsx b/packages/website/src/features/Packages/Tabs/InteractTab.tsx index 6f0266b32..e28f26596 100644 --- a/packages/website/src/features/Packages/Tabs/InteractTab.tsx +++ b/packages/website/src/features/Packages/Tabs/InteractTab.tsx @@ -1,6 +1,13 @@ 'use client'; -import { FC, ReactNode, useEffect, useState, createContext } from 'react'; +import { + FC, + ReactNode, + useEffect, + useState, + createContext, + useMemo, +} from 'react'; import { useQueryIpfsDataParsed } from '@/hooks/ipfs'; import { Box, @@ -18,10 +25,11 @@ import { import { ChainArtifacts, DeploymentInfo } from '@usecannon/builder'; import { getOutput } from '@/lib/builder'; import { useRouter } from 'next/router'; -import { usePackageVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; +import { usePackageNameTagVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; import { CustomSpinner } from '@/components/CustomSpinner'; import { usePackageByRef } from '@/hooks/api/usePackage'; import SearchInput from '@/components/SearchInput'; +import { Address } from 'viem'; type Option = { moduleName: string; @@ -35,36 +43,81 @@ export const SubnavContext = createContext<{ hasSubnav: boolean }>({ function useActiveContract() { const pathName = useRouter().asPath; - // first remove the hash and selected method - // then split the path by the interact keyword - const activeContractPath = pathName.split('#')[0].split('interact/')[1]; - if (activeContractPath) { - const [moduleName, contractName, contractAddress] = - activeContractPath.split('/'); + return useMemo(() => { + // first remove the hash and selected method + // then split the path by the interact keyword + const activeContractPath = pathName.split('#')[0].split('interact/')[1]; - return { - moduleName, - contractName, - contractAddress, - }; - } + if (activeContractPath) { + const [moduleName, contractName, contractAddress] = + activeContractPath.split('/'); - return undefined; + return { + moduleName, + contractName, + contractAddress, + }; + } + }, [pathName]); } +type AllContracts = { + moduleName: string; + contractName: string; + contractAddress: Address; + highlight: boolean; +}; + +const processContracts = ( + allContractsRef: AllContracts[], // array passed by reference + contracts: ChainArtifacts['contracts'], + moduleName: string +) => { + if (!contracts) return allContractsRef; + + const processedContracts = Object.entries(contracts).map( + ([contractName, contractInfo]) => ({ + moduleName: moduleName, + contractName, + contractAddress: contractInfo.address, + highlight: Boolean(contractInfo.highlight), + }) + ); + + // Add to the existing array (modifying the reference) + allContractsRef.push(...processedContracts); +}; + +const processImports = ( + allContractsRef: AllContracts[], // array passed by reference + imports: ChainArtifacts['imports'], + parentModuleName = '' +) => { + if (imports) { + Object.entries(imports).forEach(([_moduleName, bundle]) => { + // Concatenate module names + const moduleName = `${parentModuleName}.${_moduleName}`; + processContracts(allContractsRef, bundle.contracts, moduleName); + // recursively process imports + processImports(allContractsRef, bundle.imports, moduleName); + }); + } +}; + export const InteractTab: FC<{ children?: ReactNode; }> = ({ children }) => { const router = useRouter(); - const { name, tag, preset, chainId, variant } = usePackageVersionUrlParams(); + const { name, tag, preset, chainId, variant } = + usePackageNameTagVersionUrlParams(); const packagesQuery = usePackageByRef({ name, tag, preset, chainId }); const activeContractOption = useActiveContract(); const [highlightedOptions, setHighlightedOptions] = useState([]); const [otherOptions, setOtherOptions] = useState([]); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [routing, setRouting] = useState(false); + // const [routing, setRouting] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const deploymentData = useQueryIpfsDataParsed( packagesQuery?.data?.deployUrl, @@ -73,16 +126,6 @@ export const InteractTab: FC<{ const hasSubnav = otherOptions.length > 0 || highlightedOptions.length > 1; - const selectContract = async (contract: Option) => { - setRouting(true); - await router.replace( - `/packages/${name}/${tag}/${variant}/interact/${contract.moduleName}/${contract.contractName}/${contract.contractAddress}`, - undefined, - { shallow: true } - ); - setRouting(false); - }; - const isActiveContract = (contract: Option) => { if (!activeContractOption) return false; return ( @@ -97,37 +140,10 @@ export const InteractTab: FC<{ return; } - let allContracts: any[] = []; - - const processContracts = (contracts: any, moduleName: string) => { - if (contracts) { - const processedContracts = Object.entries(contracts).map(([k, v]) => ({ - moduleName: moduleName, - contractName: k, - contractAddress: (v as any).address, - highlight: (v as any).highlight, - })); - allContracts = allContracts.concat(processedContracts); - } - }; - - const cannonOutputs: ChainArtifacts = getOutput(deploymentData.data); - - if (cannonOutputs.contracts) { - processContracts(cannonOutputs.contracts, name); - } - - const processImports = (imports: any, parentModuleName: string) => { - if (imports) { - Object.entries(imports).forEach(([k, v]) => { - const moduleName = parentModuleName ? `${parentModuleName}.${k}` : k; - processContracts((v as any).contracts, moduleName); - processImports((v as any).imports, moduleName); - }); - } - }; - - processImports(cannonOutputs.imports, ''); + const allContracts: AllContracts[] = []; + const cannonOutputs = getOutput(deploymentData.data); + processContracts(allContracts, cannonOutputs.contracts, name); + processImports(allContracts, cannonOutputs.imports); const highlightedContracts = allContracts.filter( (contract) => contract.highlight @@ -189,13 +205,14 @@ export const InteractTab: FC<{ ); if (!activeContractOption) { - if (highlightedData.length > 0) { - void selectContract(highlightedData[0]); - } else if (otherData.length > 0) { - void selectContract(otherData[0]); + const _contract = highlightedData[0] || otherData[0]; + if (_contract) { + void router.push( + `/packages/${name}/${tag}/${variant}/interact/${_contract.moduleName}/${_contract.contractName}/${_contract.contractAddress}` + ); } } - }, [deploymentData.data]); + }, [activeContractOption, deploymentData.data, name, router, tag, variant]); return ( @@ -246,7 +263,11 @@ export const InteractTab: FC<{ mr={4} height="48px" px={2} - onClick={() => selectContract(option)} + onClick={async () => + await router.push( + `/packages/${name}/${tag}/${variant}/interact/${option.moduleName}/${option.contractName}/${option.contractAddress}` + ) + } > { setIsPopoverOpen(false); - await selectContract(option); + await router.push( + `/packages/${name}/${tag}/${variant}/interact/${option.moduleName}/${option.contractName}/${option.contractAddress}` + ); }} > )} - {deploymentData.isLoading || packagesQuery.isLoading || routing ? ( + {deploymentData.isLoading || packagesQuery.isLoading ? ( { + const _variant = decodeURIComponent(variant); + const [chainId, preset] = PackageReference.parseVariant(decodeURIComponent(_variant)); - return { - name: decodeURIComponent(name), - tag: decodeURIComponent(tag), - variant: _variant, - preset, - chainId, - }; + return { + name: decodeURIComponent(name), + tag: decodeURIComponent(tag), + variant: _variant, + preset, + chainId, + }; + }, [name, tag, variant]); + + return memoizedParams; } diff --git a/packages/website/src/hooks/routing/usePackageUrlParams.ts b/packages/website/src/hooks/routing/usePackageUrlParams.ts index 368cf7322..42b930f37 100644 --- a/packages/website/src/hooks/routing/usePackageUrlParams.ts +++ b/packages/website/src/hooks/routing/usePackageUrlParams.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router'; +import { useMemo } from 'react'; export function usePackageUrlParams() { const { query: params } = useRouter(); @@ -6,7 +7,10 @@ export function usePackageUrlParams() { if (typeof name !== 'string') throw new Error('Missing name param'); - return { - name: decodeURIComponent(name), - }; + return useMemo( + () => ({ + name: decodeURIComponent(name), + }), + [name] + ); } diff --git a/packages/website/src/hooks/routing/usePackageVersionUrlParams.ts b/packages/website/src/hooks/routing/usePackageVersionUrlParams.ts index 7d28a9e5f..fcc4ea369 100644 --- a/packages/website/src/hooks/routing/usePackageVersionUrlParams.ts +++ b/packages/website/src/hooks/routing/usePackageVersionUrlParams.ts @@ -2,7 +2,7 @@ import { PackageReference } from '@usecannon/builder'; import { useRouter } from 'next/router'; import { Address, isAddress } from 'viem'; -export function usePackageVersionUrlParams() { +export function usePackageNameTagVersionUrlParams() { const { query: params } = useRouter(); const { name, tag, variant, moduleName, contractName, contractAddress } = params; diff --git a/packages/website/src/hooks/routing/useTransactionDetailsParams.ts b/packages/website/src/hooks/routing/useTransactionDetailsParams.ts index dbf0761a7..7c99e9d37 100644 --- a/packages/website/src/hooks/routing/useTransactionDetailsParams.ts +++ b/packages/website/src/hooks/routing/useTransactionDetailsParams.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router'; +import { useMemo } from 'react'; import { Address, isAddress, isHash } from 'viem'; export function useTransactionDetailsParams() { @@ -30,10 +31,13 @@ export function useTransactionDetailsParams() { throw new Error('Invalid signHash'); } - return { - safeAddress: safeAddress as Address, - chainId: parseInt(chainId), - nonce: parseInt(nonce), - sigHash, - }; + return useMemo( + () => ({ + safeAddress: safeAddress as Address, + chainId: parseInt(chainId), + nonce: parseInt(nonce), + sigHash, + }), + [safeAddress, chainId, nonce, sigHash] + ); } diff --git a/packages/website/src/pages/packages/[name]/[tag]/[variant]/_layout.tsx b/packages/website/src/pages/packages/[name]/[tag]/[variant]/_layout.tsx index 6bae3957d..3524bad92 100644 --- a/packages/website/src/pages/packages/[name]/[tag]/[variant]/_layout.tsx +++ b/packages/website/src/pages/packages/[name]/[tag]/[variant]/_layout.tsx @@ -195,7 +195,7 @@ function TagVariantLayout({ children }: { children: ReactNode }) { ); } -export default function WrapperTagVariantLayout({ +export default function PackageNameTagVariantLayout({ children, }: { children: ReactNode; diff --git a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/[moduleName]/[contractName]/[contractAddress]/index.tsx b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/[moduleName]/[contractName]/[contractAddress]/index.tsx index a69c1dd08..f9505f769 100644 --- a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/[moduleName]/[contractName]/[contractAddress]/index.tsx +++ b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/[moduleName]/[contractName]/[contractAddress]/index.tsx @@ -1,16 +1,16 @@ -import dynamic from 'next/dynamic'; import { ReactElement } from 'react'; - -import InteractLayout from '../../../../_layout'; -import ModuleLayout from '../../../_layout'; import { NextSeo } from 'next-seo'; +import { useParams } from 'next/navigation'; + import defaultSEO from '@/constants/defaultSeo'; +import PageLoading from '@/components/PageLoading'; +import Interact from '@/features/Packages/Interact'; +import PackageNameTagVariantLayout from '@/pages/packages/[name]/[tag]/[variant]/_layout'; +import PackageInteractModuleLayout from '@/pages/packages/[name]/[tag]/[variant]/interact/_layout'; -const NoSSRInteract = dynamic(() => import('@/features/Packages/Interact'), { - ssr: false, -}); +export default function InteractPage() { + const params = useParams(); -export default function Interact() { return ( <> - + {params == null ? : } ); } -Interact.getLayout = function getLayout(page: ReactElement) { + +InteractPage.getLayout = function getLayout(page: ReactElement) { return ( - - {page} - + + {page} + ); }; diff --git a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/_layout.tsx b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/_layout.tsx index cc40b6bd4..84f069042 100644 --- a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/_layout.tsx +++ b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/_layout.tsx @@ -1,19 +1,17 @@ 'use client'; -import { ReactNode } from 'react'; +import { PropsWithChildren } from 'react'; import InteractTab from '@/features/Packages/Tabs/InteractTab'; -import { useRouter } from 'next/router'; import PageLoading from '@/components/PageLoading'; +import { useParams } from 'next/navigation'; -function WrapperInteractLayout({ children }: { children: ReactNode }) { - return {children}; -} - -export default function InteractLayout({ children }: { children: ReactNode }) { - const router = useRouter(); - return router.isReady ? ( - {children} - ) : ( +export default function PackageInteractModuleLayout({ + children, +}: PropsWithChildren) { + const params = useParams(); + return params == null ? ( + ) : ( + {children} ); } diff --git a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/index.tsx b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/index.tsx index 2e92e161f..3a164ced8 100644 --- a/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/index.tsx +++ b/packages/website/src/pages/packages/[name]/[tag]/[variant]/interact/index.tsx @@ -3,7 +3,7 @@ import TagVariantLayout from '../_layout'; import InteractLayout from './_layout'; import { NextSeo } from 'next-seo'; import defaultSEO from '@/constants/defaultSeo'; -import { usePackageVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; +import { usePackageNameTagVersionUrlParams } from '@/hooks/routing/usePackageVersionUrlParams'; import { useCannonChains } from '@/providers/CannonProvidersProvider'; function generateMetadata({ @@ -41,7 +41,7 @@ function generateMetadata({ } export default function Interact() { - const { name, tag, chainId, preset } = usePackageVersionUrlParams(); + const { name, tag, chainId, preset } = usePackageNameTagVersionUrlParams(); const { getChainById } = useCannonChains(); const metadata = generateMetadata({ name,