diff --git a/.changeset/lucky-tigers-unite.md b/.changeset/lucky-tigers-unite.md new file mode 100644 index 000000000..18100406f --- /dev/null +++ b/.changeset/lucky-tigers-unite.md @@ -0,0 +1,10 @@ +--- +"create-eth": patch +--- + +1. basic example to show connected address (#721) +2. Standardize displaying of address and follow ERC-55 (#734) +3. fix contract balance hot reload balance issue (#739) +4. Fix cursor stealing & display loading for AddressInput (#738) +5. Fix blockexplorer code tab (#741) +6. Match link name with actual tab name for Debug Contracts (#743) diff --git a/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs b/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs index fb1d35111..f12b3f5c2 100644 --- a/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs +++ b/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs @@ -63,7 +63,7 @@ const getContractData = async (address: string) => { const deployedContractsOnChain = contracts ? contracts[chainId] : {}; for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) { - if (contractInfo.address.toLowerCase() === address) { + if (contractInfo.address.toLowerCase() === address.toLowerCase()) { contractPath = \`contracts/\${contractName}.sol\`; break; } diff --git a/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx index 49f0f2ee1..31fcc7faa 100644 --- a/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx +++ b/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx @@ -1,5 +1,6 @@ "use client"; +// @refresh reset import { useReducer } from "react"; import { ContractReadMethods } from "./ContractReadMethods"; import { ContractVariables } from "./ContractVariables"; diff --git a/templates/base/packages/nextjs/app/page.tsx b/templates/base/packages/nextjs/app/page.tsx index 035b3314c..fcca994c5 100644 --- a/templates/base/packages/nextjs/app/page.tsx +++ b/templates/base/packages/nextjs/app/page.tsx @@ -1,16 +1,26 @@ +"use client"; + import Link from "next/link"; import type { NextPage } from "next"; +import { useAccount } from "wagmi"; import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { Address } from "~~/components/scaffold-eth"; const Home: NextPage = () => { + const { address: connectedAddress } = useAccount(); + return ( <>
-

+

Welcome to Scaffold-ETH 2

+
+

Connected Address:

+
+

Get started by editing{" "} @@ -36,7 +46,7 @@ const Home: NextPage = () => {

Tinker with your smart contract using the{" "} - Debug Contract + Debug Contracts {" "} tab.

diff --git a/templates/base/packages/nextjs/components/scaffold-eth/Address.tsx b/templates/base/packages/nextjs/components/scaffold-eth/Address.tsx index bc8bfffa2..1c1ad600a 100644 --- a/templates/base/packages/nextjs/components/scaffold-eth/Address.tsx +++ b/templates/base/packages/nextjs/components/scaffold-eth/Address.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { CopyToClipboard } from "react-copy-to-clipboard"; -import { Address as AddressType, isAddress } from "viem"; +import { Address as AddressType, getAddress, isAddress } from "viem"; import { hardhat } from "viem/chains"; import { useEnsAvatar, useEnsName } from "wagmi"; import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; @@ -35,10 +35,15 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: const [ens, setEns] = useState(); const [ensAvatar, setEnsAvatar] = useState(); const [addressCopied, setAddressCopied] = useState(false); + const checkSumAddress = address ? getAddress(address) : undefined; const { targetNetwork } = useTargetNetwork(); - const { data: fetchedEns } = useEnsName({ address, enabled: isAddress(address ?? ""), chainId: 1 }); + const { data: fetchedEns } = useEnsName({ + address: checkSumAddress, + enabled: isAddress(checkSumAddress ?? ""), + chainId: 1, + }); const { data: fetchedEnsAvatar } = useEnsAvatar({ name: fetchedEns, enabled: Boolean(fetchedEns), @@ -56,7 +61,7 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: }, [fetchedEnsAvatar]); // Skeleton UI - if (!address) { + if (!checkSumAddress) { return (
@@ -67,24 +72,24 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: ); } - if (!isAddress(address)) { + if (!isAddress(checkSumAddress)) { return Wrong address; } - const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, address); - let displayAddress = address?.slice(0, 5) + "..." + address?.slice(-4); + const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, checkSumAddress); + let displayAddress = checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4); if (ens) { displayAddress = ens; } else if (format === "long") { - displayAddress = address; + displayAddress = checkSumAddress; } return (
@@ -112,7 +117,7 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: /> ) : ( { setAddressCopied(true); setTimeout(() => { diff --git a/templates/base/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx b/templates/base/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx index 4f057015a..164664466 100644 --- a/templates/base/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx +++ b/templates/base/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { blo } from "blo"; -import { useDebounce } from "usehooks-ts"; +import { useDebounceValue } from "usehooks-ts"; import { Address, isAddress } from "viem"; import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi"; import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth"; @@ -11,29 +11,39 @@ import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth"; export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps
) => { // Debounce the input to keep clean RPC calls when resolving ENS names // If the input is an address, we don't need to debounce it - const _debouncedValue = useDebounce(value, 500); + const [_debouncedValue] = useDebounceValue(value, 500); const debouncedValue = isAddress(value) ? value : _debouncedValue; const isDebouncedValueLive = debouncedValue === value; // If the user changes the input after an ENS name is already resolved, we want to remove the stale result const settledValue = isDebouncedValueLive ? debouncedValue : undefined; - const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({ + const { + data: ensAddress, + isLoading: isEnsAddressLoading, + isError: isEnsAddressError, + isSuccess: isEnsAddressSuccess, + } = useEnsAddress({ name: settledValue, - enabled: isENS(debouncedValue), + enabled: isDebouncedValueLive && isENS(debouncedValue), chainId: 1, cacheTime: 30_000, }); const [enteredEnsName, setEnteredEnsName] = useState(); - const { data: ensName, isLoading: isEnsNameLoading } = useEnsName({ + const { + data: ensName, + isLoading: isEnsNameLoading, + isError: isEnsNameError, + isSuccess: isEnsNameSuccess, + } = useEnsName({ address: settledValue as Address, enabled: isAddress(debouncedValue), chainId: 1, cacheTime: 30_000, }); - const { data: ensAvatar } = useEnsAvatar({ + const { data: ensAvatar, isLoading: isEnsAvtarLoading } = useEnsAvatar({ name: ensName, enabled: Boolean(ensName), chainId: 1, @@ -57,6 +67,14 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C [onChange], ); + const reFocus = + isEnsAddressError || + isEnsNameError || + isEnsNameSuccess || + isEnsAddressSuccess || + ensName === null || + ensAddress === null; + return ( name={name} @@ -65,9 +83,11 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C value={value as Address} onChange={handleChange} disabled={isEnsAddressLoading || isEnsNameLoading || disabled} + reFocus={reFocus} prefix={ - ensName && ( + ensName ? (
+ {isEnsAvtarLoading &&
} {ensAvatar ? ( { @@ -78,6 +98,13 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C ) : null} {enteredEnsName ?? ensName}
+ ) : ( + (isEnsNameLoading || isEnsAddressLoading) && ( +
+
+
+
+ ) ) } suffix={ diff --git a/templates/base/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx b/templates/base/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx index 73d5a4f8a..f38bca217 100644 --- a/templates/base/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx +++ b/templates/base/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx @@ -1,10 +1,11 @@ -import { ChangeEvent, ReactNode, useCallback } from "react"; +import { ChangeEvent, FocusEvent, ReactNode, useCallback, useEffect, useRef } from "react"; import { CommonInputProps } from "~~/components/scaffold-eth"; type InputBaseProps = CommonInputProps & { error?: boolean; prefix?: ReactNode; suffix?: ReactNode; + reFocus?: boolean; }; export const InputBase = string } | undefined = string>({ @@ -16,7 +17,10 @@ export const InputBase = string } | undefined = str disabled, prefix, suffix, + reFocus, }: InputBaseProps) => { + const inputReft = useRef(null); + let modifier = ""; if (error) { modifier = "border-error"; @@ -31,6 +35,17 @@ export const InputBase = string } | undefined = str [onChange], ); + // Runs only when reFocus prop is passed, usefull for setting the cursor + // at the end of the input. Example AddressInput + const onFocus = (e: FocusEvent) => { + if (reFocus !== undefined) { + e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length); + } + }; + useEffect(() => { + if (reFocus !== undefined && reFocus === true) inputReft.current?.focus(); + }, [reFocus]); + return (
{prefix} @@ -42,6 +57,8 @@ export const InputBase = string } | undefined = str onChange={handleChange} disabled={disabled} autoComplete="off" + ref={inputReft} + onFocus={onFocus} /> {suffix}
diff --git a/templates/base/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx b/templates/base/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx index 9112d93e3..b86128c9b 100644 --- a/templates/base/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx +++ b/templates/base/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx @@ -1,6 +1,7 @@ import { useRef, useState } from "react"; import { NetworkOptions } from "./NetworkOptions"; import CopyToClipboard from "react-copy-to-clipboard"; +import { getAddress } from "viem"; import { Address, useDisconnect } from "wagmi"; import { ArrowLeftOnRectangleIcon, @@ -11,7 +12,7 @@ import { DocumentDuplicateIcon, QrCodeIcon, } from "@heroicons/react/24/outline"; -import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { BlockieAvatar, isENS } from "~~/components/scaffold-eth"; import { useOutsideClick } from "~~/hooks/scaffold-eth"; import { getTargetNetworks } from "~~/utils/scaffold-eth"; @@ -31,6 +32,7 @@ export const AddressInfoDropdown = ({ blockExplorerAddressLink, }: AddressInfoDropdownProps) => { const { disconnect } = useDisconnect(); + const checkSumAddress = getAddress(address); const [addressCopied, setAddressCopied] = useState(false); @@ -46,8 +48,10 @@ export const AddressInfoDropdown = ({ <>
- - {displayName} + + + {isENS(displayName) ? displayName : checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4)} +
    ) : ( { setAddressCopied(true); setTimeout(() => {