diff --git a/packages/nextjs/components/NetworksDropdown.tsx b/packages/nextjs/components/NetworksDropdown.tsx deleted file mode 100644 index 5f5c177c..00000000 --- a/packages/nextjs/components/NetworksDropdown.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { ReactNode, useEffect, useRef, useState } from "react"; -import Image from "next/image"; -import * as wagmiChains from "@wagmi/core/chains"; -import { useTheme } from "next-themes"; -import Select, { MultiValue, OptionProps, SingleValue, components } from "react-select"; -import { Chain } from "viem"; -import { EyeIcon, WrenchScrewdriverIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { getPopularTargetNetworks } from "~~/utils/scaffold-eth"; - -type Options = { - value: number | string; - label: string; - icon: string | ReactNode; - isTestnet?: boolean; -}; - -type GroupedOptions = Record< - "mainnet" | "testnet" | "localhost" | "other", - { - label: string; - options: Options[]; - } ->; - -type Chains = Record; - -const getIconComponent = (iconName: string | undefined) => { - switch (iconName) { - case "EyeIcon": - return ; - case "localhost": - return ; - default: - return default icon; - } -}; - -const networks = getPopularTargetNetworks(); -const initialGroupedOptions = networks.reduce( - (groups, network) => { - if (network.id === 31337) { - groups.localhost.options.push({ - value: network.id, - label: "31337 - Localhost", - icon: getIconComponent("localhost"), - }); - return groups; - } - - const groupName = network.testnet ? "testnet" : "mainnet"; - - groups[groupName].options.push({ - value: network.id, - label: network.name, - icon: network.icon, - isTestnet: network.testnet, - }); - - return groups; - }, - { - mainnet: { label: "mainnet", options: [] }, - testnet: { label: "testnet", options: [] }, - localhost: { label: "localhost", options: [] }, - other: { - label: "other", - options: [ - { - value: "other-chains", - label: "Other chains", - icon: "EyeIcon", - }, - ], - }, - }, -); - -const excludeChainKeys = ["lineaTestnet", "x1Testnet"]; // duplicate chains in viem chains - -const unfilteredChains: Chains = wagmiChains as Chains; - -const filteredChains = Object.keys(unfilteredChains) - .filter(key => !excludeChainKeys.includes(key)) - .reduce((obj: Chains, key) => { - obj[key] = unfilteredChains[key]; - return obj; - }, {} as Chains); - -const filterChains = ( - chains: Record, - networkIds: Set, - existingChainIds: Set, -): Chain[] => { - return Object.values(chains).filter(chain => !networkIds.has(chain.id) && !existingChainIds.has(chain.id)); -}; - -const mapChainsToOptions = (chains: Chain[]): Options[] => { - return chains.map(chain => ({ - value: chain.id, - label: chain.name, - icon: "", - isTestnet: (chain as any).testnet || false, - })); -}; - -const CUSTOM_CHAINS_LOCAL_STORAGE_KEY = "customChains"; -const getStoredChains = (): Options[] => { - if (typeof window !== "undefined") { - const storedChains = localStorage.getItem(CUSTOM_CHAINS_LOCAL_STORAGE_KEY); - return storedChains ? JSON.parse(storedChains) : []; - } - return []; -}; - -const isChainStored = (chain: Options): boolean => { - const storedChains = getStoredChains(); - return storedChains.some(storedChain => storedChain.value === chain.value); -}; - -const networkIds = new Set(networks.map(network => network.id)); - -const { Option } = components; - -type CustomOptionProps = OptionProps & { - onDelete: (chain: Options) => void; -}; -const CustomOption = (props: CustomOptionProps) => { - const { data } = props; - const handleDelete = (e: React.MouseEvent) => { - e.stopPropagation(); - props.onDelete(data); - }; - - return ( - - ); -}; - -export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any }) => { - const [isMobile, setIsMobile] = useState(false); - const { resolvedTheme } = useTheme(); - const [selectedOption, setSelectedOption] = useState>(initialGroupedOptions.mainnet.options[0]); - const [searchTerm, setSearchTerm] = useState(""); - const [groupedOptions, setGroupedOptions] = useState(initialGroupedOptions); - - const searchInputRef = useRef(null); - const seeAllModalRef = useRef(null); - - const isDarkMode = resolvedTheme === "dark"; - - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - const customChains = getStoredChains(); - customChains.forEach((chain: Options) => { - const groupName = chain.isTestnet ? "testnet" : "mainnet"; - if (!initialGroupedOptions[groupName].options.some(option => option.value === chain.value)) { - initialGroupedOptions[groupName].options.push(chain); - } - }); - setGroupedOptions({ ...initialGroupedOptions }); - }, []); - - useEffect(() => { - if (typeof window !== "undefined") { - const mediaQuery = window.matchMedia("(max-width: 640px)"); - setIsMobile(mediaQuery.matches); - - const handleResize = () => setIsMobile(mediaQuery.matches); - mediaQuery.addEventListener("change", handleResize); - return () => mediaQuery.removeEventListener("change", handleResize); - } - }, []); - - const handleSelectChange = (newValue: SingleValue | MultiValue) => { - const selected = newValue as SingleValue; - if (selected?.value === "other-chains") { - if (!seeAllModalRef.current || !searchInputRef.current) return; - seeAllModalRef.current.showModal(); - searchInputRef.current.focus(); - } else { - setSelectedOption(selected); - onChange(selected); - } - }; - - const handleChainSelect = (option: Options) => { - const groupName = option.isTestnet ? "testnet" : "mainnet"; - if (!groupedOptions[groupName].options.some(chain => chain.value === option.value)) { - groupedOptions[groupName].options.push(option); - } - const customChains = [...getStoredChains(), option]; - localStorage.setItem(CUSTOM_CHAINS_LOCAL_STORAGE_KEY, JSON.stringify(customChains)); - setGroupedOptions({ ...groupedOptions }); - setSelectedOption(option); - onChange(option); - if (seeAllModalRef.current) { - seeAllModalRef.current.close(); - } - }; - - const handleModalClose = () => { - if (searchInputRef.current) { - searchInputRef.current.value = ""; - setSearchTerm(""); - } - if (seeAllModalRef.current) { - seeAllModalRef.current.close(); - } - }; - - const existingChainIds = new Set( - Object.values(groupedOptions) - .flatMap(group => group.options.map(option => option.value)) - .filter(value => typeof value === "number") as number[], - ); - - const filteredChainsForModal = filterChains(filteredChains, networkIds, existingChainIds); - - const modalChains = mapChainsToOptions(filteredChainsForModal).filter(chain => - `${chain.label} ${chain.value}`.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - const handleDeleteCustomChain = (chain: Options) => { - const updatedChains = getStoredChains().filter((c: Options) => c.value !== chain.value); - - if (typeof window !== "undefined") { - localStorage.setItem(CUSTOM_CHAINS_LOCAL_STORAGE_KEY, JSON.stringify(updatedChains)); - } - - const updatedGroupedOptions = { ...groupedOptions }; - Object.keys(updatedGroupedOptions).forEach(groupName => { - updatedGroupedOptions[groupName as keyof GroupedOptions].options = updatedGroupedOptions[ - groupName as keyof GroupedOptions - ].options.filter(option => option.value !== chain.value); - }); - - setGroupedOptions(updatedGroupedOptions); - - if (selectedOption?.value === chain.value) { - setSelectedOption(updatedGroupedOptions.mainnet.options[0]); - onChange(updatedGroupedOptions.mainnet.options[0]); - } - }; - - if (!mounted) return
; - - return ( - <> - setSearchTerm(e.target.value)} - ref={searchInputRef} - /> - -
- {modalChains.map(option => ( -
handleChainSelect(option)} - > -
- Chain Id: {option.value} - {option.label} -
-
- ))} -
-
- - - ); -}; diff --git a/packages/nextjs/components/NetworksDropdown/AddCustomChainModal.tsx b/packages/nextjs/components/NetworksDropdown/AddCustomChainModal.tsx new file mode 100644 index 00000000..35ca5224 --- /dev/null +++ b/packages/nextjs/components/NetworksDropdown/AddCustomChainModal.tsx @@ -0,0 +1,129 @@ +import { forwardRef } from "react"; +import { + GroupedOptions, + chainToOption, + formDataToChain, + getStoredChainsFromLocalStorage, + storeChainInLocalStorage, +} from "./utils"; +import { Options } from "./utils"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { useGlobalState } from "~~/services/store/store"; +import { notification } from "~~/utils/scaffold-eth"; + +type AddCustomChainModalProps = { + groupedOptionsState: GroupedOptions; + setGroupedOptionsState: React.Dispatch>; + setSelectedOption: React.Dispatch>; + onChange: (option: Options | null) => void; +}; + +export const AddCustomChainModal = forwardRef( + ({ groupedOptionsState, setGroupedOptionsState, setSelectedOption, onChange }, ref) => { + const { addCustomChain } = useGlobalState(state => ({ + addCustomChain: state.addChain, + })); + + const handleSubmitCustomChain = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const chain = formDataToChain(formData); + + const storedChains = getStoredChainsFromLocalStorage(); + + if (storedChains.find(c => c.id === chain.id)) { + handleCloseModal(); + e.currentTarget.reset(); + notification.error("This chain is already added!"); + return; + } + + storeChainInLocalStorage(chain); + addCustomChain(chain); + + const newGroupedOptions = { ...groupedOptionsState }; + const groupName = chain.testnet ? "testnet" : "mainnet"; + const newOption = chainToOption(chain); + newGroupedOptions[groupName].options.push(newOption); + + setGroupedOptionsState(newGroupedOptions); + + e.currentTarget.reset(); + + setSelectedOption(newOption); + onChange(newOption); + + handleCloseModal(); + }; + + const handleCloseModal = () => { + if (ref && "current" in ref && ref.current) { + ref.current.close(); + } + }; + + return ( + +
+
+

Add Custom Chain

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ ); + }, +); + +AddCustomChainModal.displayName = "AddCustomChainModal"; diff --git a/packages/nextjs/components/NetworksDropdown/CustomOption.tsx b/packages/nextjs/components/NetworksDropdown/CustomOption.tsx new file mode 100644 index 00000000..3b231ce5 --- /dev/null +++ b/packages/nextjs/components/NetworksDropdown/CustomOption.tsx @@ -0,0 +1,50 @@ +import Image from "next/image"; +import { Options, isChainStored } from "./utils"; +import { OptionProps, components } from "react-select"; +import { EyeIcon, PlusIcon, WrenchScrewdriverIcon } from "@heroicons/react/24/outline"; + +const { Option } = components; + +export const getIconComponent = (iconName: string | undefined) => { + switch (iconName) { + case "EyeIcon": + return ; + case "localhost": + return ; + case "PlusIcon": + return ; + default: + return default icon; + } +}; + +type CustomOptionProps = OptionProps & { + onDelete: (chain: Options) => void; +}; +export const CustomOption = (props: CustomOptionProps) => { + const { data } = props; + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + props.onDelete(data); + }; + + return ( + + ); +}; diff --git a/packages/nextjs/components/NetworksDropdown/NetworksDropdown.tsx b/packages/nextjs/components/NetworksDropdown/NetworksDropdown.tsx new file mode 100644 index 00000000..dd527832 --- /dev/null +++ b/packages/nextjs/components/NetworksDropdown/NetworksDropdown.tsx @@ -0,0 +1,185 @@ +import { useEffect, useRef, useState } from "react"; +import { + Options, + chainToOption, + filterChains, + filteredChains, + getStoredChainsFromLocalStorage, + initialGroupedOptions, + mapChainsToOptions, + networkIds, + removeChainFromLocalStorage, + storeChainInLocalStorage, +} from "./utils"; +import { useTheme } from "next-themes"; +import Select, { MultiValue, SingleValue } from "react-select"; +import { Chain } from "viem"; +import { AddCustomChainModal, CustomOption, OtherChainsModal } from "~~/components/NetworksDropdown"; +import { useGlobalState } from "~~/services/store/store"; + +export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any }) => { + const [isMobile, setIsMobile] = useState(false); + const { resolvedTheme } = useTheme(); + const [groupedOptionsState, setGroupedOptionsState] = useState(initialGroupedOptions); + const [selectedOption, setSelectedOption] = useState>(initialGroupedOptions.mainnet.options[0]); + + const { addCustomChain, removeChain } = useGlobalState(state => ({ + addCustomChain: state.addChain, + removeChain: state.removeChain, + })); + + const seeOtherChainsModalRef = useRef(null); + const customChainModalRef = useRef(null); + + const isDarkMode = resolvedTheme === "dark"; + + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + + const updateGroupedOptions = () => { + const storedChains = getStoredChainsFromLocalStorage(); + const newGroupedOptions = { ...groupedOptionsState }; + + storedChains.forEach(chain => { + const groupName = chain.testnet ? "testnet" : "mainnet"; + if (!newGroupedOptions[groupName].options.some(option => option.value === chain.id)) { + const option = chainToOption(chain); + newGroupedOptions[groupName].options.push(option); + } + }); + + setGroupedOptionsState(newGroupedOptions); + }; + + updateGroupedOptions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [addCustomChain]); + + useEffect(() => { + if (typeof window !== "undefined") { + const mediaQuery = window.matchMedia("(max-width: 640px)"); + setIsMobile(mediaQuery.matches); + + const handleResize = () => setIsMobile(mediaQuery.matches); + mediaQuery.addEventListener("change", handleResize); + return () => mediaQuery.removeEventListener("change", handleResize); + } + }, []); + + const handleSelectChange = (newValue: SingleValue | MultiValue) => { + const selected = newValue as SingleValue; + if (selected?.value === "other-chains") { + if (seeOtherChainsModalRef.current) { + seeOtherChainsModalRef.current.showModal(); + } + } else if (selected?.value === "custom-chains") { + if (customChainModalRef.current) { + customChainModalRef.current.showModal(); + } + } else { + setSelectedOption(selected); + onChange(selected); + } + }; + + const handleSelectOtherChainInModal = (option: Options) => { + const groupName = option.testnet ? "testnet" : "mainnet"; + if (!groupedOptionsState[groupName].options.some(chain => chain.value === option.value)) { + const newGroupedOptions = { ...groupedOptionsState }; + newGroupedOptions[groupName].options.push(option); + setGroupedOptionsState(newGroupedOptions); + } + + const chain = Object.values(filteredChains).find(chain => chain.id === option.value); + + storeChainInLocalStorage(chain as Chain); + + setSelectedOption(option); + onChange(option); + if (seeOtherChainsModalRef.current) { + seeOtherChainsModalRef.current.close(); + } + }; + + const handleDeleteCustomChain = (option: Options) => { + const chainId = +option.value; + + removeChain(chainId); + removeChainFromLocalStorage(chainId); + + const newGroupedOptions = { ...groupedOptionsState }; + const groupName = option.testnet ? "testnet" : "mainnet"; + newGroupedOptions[groupName].options = newGroupedOptions[groupName].options.filter( + chain => chain.value !== option.value, + ); + + setGroupedOptionsState(newGroupedOptions); + + if (selectedOption?.value === option.value) { + const mainnet = newGroupedOptions.mainnet.options[0]; + setSelectedOption(mainnet); + onChange(mainnet); + } + }; + + const existingChainIds = new Set( + Object.values(groupedOptionsState) + .flatMap(group => group.options.map(option => option.value)) + .filter(value => typeof value === "number") as number[], + ); + + const filteredChainsForModal = filterChains(filteredChains, networkIds, existingChainIds); + + const modalChains = mapChainsToOptions(filteredChainsForModal); + + if (!mounted) return
; + + return ( + <> + setSearchTerm(e.target.value)} + ref={searchInputRef} + /> +
+ {filteredChains.map(option => ( +
onSelect(option)} + > +
+ Chain Id: {option.value} + {option.label} +
+
+ ))} +
+
+ + ); + }, +); + +OtherChainsModal.displayName = "OtherChainsModal"; diff --git a/packages/nextjs/components/NetworksDropdown/index.ts b/packages/nextjs/components/NetworksDropdown/index.ts new file mode 100644 index 00000000..7ebd10a7 --- /dev/null +++ b/packages/nextjs/components/NetworksDropdown/index.ts @@ -0,0 +1,3 @@ +export * from "./AddCustomChainModal"; +export * from "./CustomOption"; +export * from "./OtherChainsModal"; diff --git a/packages/nextjs/components/NetworksDropdown/utils.ts b/packages/nextjs/components/NetworksDropdown/utils.ts new file mode 100644 index 00000000..bd8779bc --- /dev/null +++ b/packages/nextjs/components/NetworksDropdown/utils.ts @@ -0,0 +1,157 @@ +import { ReactNode } from "react"; +import * as wagmiChains from "@wagmi/core/chains"; +import { Chain } from "viem"; +import { getPopularTargetNetworks } from "~~/utils/scaffold-eth"; + +export type Options = { + value: number | string; + label: string; + icon?: string | ReactNode; + testnet?: boolean; +}; + +export type GroupedOptions = Record< + "mainnet" | "testnet" | "localhost" | "other" | "custom", + { + label: string; + options: Options[]; + } +>; + +export const networks = getPopularTargetNetworks(); + +export const initialGroupedOptions = networks.reduce( + (groups, network) => { + if (network.id === 31337) { + groups.localhost.options.push({ + value: network.id, + label: "31337 - Localhost", + icon: "localhost", + }); + return groups; + } + + const groupName = network.testnet ? "testnet" : "mainnet"; + + groups[groupName].options.push({ + value: network.id, + label: network.name, + icon: network.icon, + testnet: network.testnet, + }); + + return groups; + }, + { + mainnet: { label: "mainnet", options: [] }, + testnet: { label: "testnet", options: [] }, + localhost: { label: "localhost", options: [] }, + other: { + label: "other", + options: [ + { + value: "other-chains", + label: "Other chains", + icon: "EyeIcon", + }, + ], + }, + custom: { + label: "custom", + options: [ + { + value: "custom-chains", + label: "Add custom chain", + icon: "PlusIcon", + }, + ], + }, + }, +); + +export const networkIds = new Set(networks.map(network => network.id)); + +export const filterChains = ( + chains: Record, + networkIds: Set, + existingChainIds: Set, +): Chain[] => { + return Object.values(chains).filter(chain => !networkIds.has(chain.id) && !existingChainIds.has(chain.id)); +}; + +const excludeChainKeys = ["lineaTestnet", "x1Testnet"]; // duplicate chains in viem chains + +type Chains = Record; + +const unfilteredChains: Chains = wagmiChains as Chains; + +export const filteredChains = Object.keys(unfilteredChains) + .filter(key => !excludeChainKeys.includes(key)) + .reduce((obj: Chains, key) => { + obj[key] = unfilteredChains[key]; + return obj; + }, {} as Chains); + +export const mapChainsToOptions = (chains: Chain[]): Options[] => { + return chains.map(chain => ({ + value: chain.id, + label: chain.name, + icon: "", + testnet: (chain as any).testnet || false, + })); +}; + +export const chainToOption = (chain: Chain): Options => ({ + value: chain.id, + label: chain.name, + testnet: chain.testnet, + icon: "", +}); + +const STORED_CHAINS_STORAGE_KEY = "storedChains"; + +export const getStoredChainsFromLocalStorage = (): Chain[] => { + if (typeof window !== "undefined") { + const storedChains = localStorage.getItem(STORED_CHAINS_STORAGE_KEY); + return storedChains ? JSON.parse(storedChains) : []; + } + return []; +}; + +export const storeChainInLocalStorage = (chain: Chain) => { + if (typeof window !== "undefined") { + const chains = [...getStoredChainsFromLocalStorage(), chain]; + localStorage.setItem(STORED_CHAINS_STORAGE_KEY, JSON.stringify(chains)); + } +}; + +export const removeChainFromLocalStorage = (chainId: number) => { + if (typeof window !== "undefined") { + const chains = getStoredChainsFromLocalStorage().filter(chain => chain.id !== chainId); + localStorage.setItem(STORED_CHAINS_STORAGE_KEY, JSON.stringify(chains)); + } +}; + +export const formDataToChain = (formData: FormData): Chain => { + const chain = { + id: Number(formData.get("id")), + name: formData.get("name") as string, + nativeCurrency: { + name: formData.get("nativeCurrencyName") as string, + symbol: formData.get("nativeCurrencySymbol") as string, + decimals: Number(formData.get("nativeCurrencyDecimals")), + }, + rpcUrls: { + public: { http: [formData.get("rpcUrl") as string] }, + default: { http: [formData.get("rpcUrl") as string] }, + }, + testnet: formData.get("testnet") === "on", + } as const satisfies Chain; + + return chain; +}; + +export const isChainStored = (option: Options): boolean => { + const storedChains = getStoredChainsFromLocalStorage(); + return storedChains.some(storedChain => storedChain.id === option.value); +}; diff --git a/packages/nextjs/components/scaffold-eth/Address.tsx b/packages/nextjs/components/scaffold-eth/Address.tsx index 3e2ddf32..ee992493 100644 --- a/packages/nextjs/components/scaffold-eth/Address.tsx +++ b/packages/nextjs/components/scaffold-eth/Address.tsx @@ -95,7 +95,7 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: size={(blockieSizeMap[size] * 24) / blockieSizeMap["base"]} /> - {disableAddressLink || targetNetwork.id === hardhat.id ? ( + {disableAddressLink || targetNetwork.id === hardhat.id || !Boolean(blockExplorerAddressLink) ? ( {displayAddress} ) : ( void; removeMethod: (uid: string) => void; -} +}; export const MethodSelector = ({ readMethodsWithInputsAndWriteMethods, diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx index 431d420a..716bb841 100644 --- a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx @@ -89,19 +89,21 @@ export const AddressInfoDropdown = ({ View QR Code -
  • - -
  • + {blockExplorerAddressLink && ( +
  • + +
  • + )}
  • + + + + ) : ( +
    +
    +

    Add Custom Chain and ABI

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + )} + + + diff --git a/packages/nextjs/pages/_app.tsx b/packages/nextjs/pages/_app.tsx index a772687e..6a8c5f42 100644 --- a/packages/nextjs/pages/_app.tsx +++ b/packages/nextjs/pages/_app.tsx @@ -8,10 +8,10 @@ import { ThemeProvider, useTheme } from "next-themes"; import NextNProgress from "nextjs-progressbar"; import { Toaster } from "react-hot-toast"; import { WagmiProvider } from "wagmi"; +import { getStoredChainsFromLocalStorage } from "~~/components/NetworksDropdown/utils"; import { BlockieAvatar } from "~~/components/scaffold-eth"; import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth"; import { useGlobalState } from "~~/services/store/store"; -import { wagmiConfig } from "~~/services/web3/wagmiConfig"; import "~~/styles/globals.css"; export const queryClient = new QueryClient({ @@ -24,11 +24,22 @@ export const queryClient = new QueryClient({ const ScaffoldEthApp = ({ Component, pageProps }: AppProps) => { const price = useNativeCurrencyPrice(); - const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice); + const { addChain, setNativeCurrencyPrice } = useGlobalState(state => ({ + addChain: state.addChain, + setNativeCurrencyPrice: state.setNativeCurrencyPrice, + })); const { resolvedTheme } = useTheme(); const isDarkMode = resolvedTheme === "dark"; const [mounted, setMounted] = useState(false); + useEffect(() => { + const storedCustomChains = getStoredChainsFromLocalStorage(); + + storedCustomChains.forEach(chain => { + addChain(chain); + }); + }, [addChain]); + useEffect(() => { setMounted(true); }, []); @@ -55,6 +66,8 @@ const ScaffoldEthApp = ({ Component, pageProps }: AppProps) => { }; const ScaffoldEthAppWithProviders = (props: AppProps) => { + const wagmiConfig = useGlobalState(state => state.wagmiConfig); + return ( diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index c3638386..9783dde1 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -9,7 +9,7 @@ import { usePublicClient } from "wagmi"; import { ChevronLeftIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { MetaHeader } from "~~/components/MetaHeader"; import { MiniFooter } from "~~/components/MiniFooter"; -import { NetworksDropdown } from "~~/components/NetworksDropdown"; +import { NetworksDropdown } from "~~/components/NetworksDropdown/NetworksDropdown"; import { SwitchTheme } from "~~/components/SwitchTheme"; import { AddressInput } from "~~/components/scaffold-eth"; import { useAbiNinjaState } from "~~/services/store/store"; @@ -27,7 +27,7 @@ const tabValues = Object.values(TabName) as TabName[]; const Home: NextPage = () => { const [activeTab, setActiveTab] = useState(TabName.verifiedContract); const [network, setNetwork] = useState(mainnet.id.toString()); - const [verifiedContractAddress, setVerifiedContractAddress] = useState
    (""); + const [verifiedContractAddress, setVerifiedContractAddress] = useState(""); const [localAbiContractAddress, setLocalAbiContractAddress] = useState(""); const [localContractAbi, setLocalContractAbi] = useState(""); const [isFetchingAbi, setIsFetchingAbi] = useState(false); @@ -50,7 +50,7 @@ const Home: NextPage = () => { const fetchContractAbi = async () => { setIsFetchingAbi(true); try { - const implementationAddress = await detectProxyTarget(verifiedContractAddress, publicClient); + const implementationAddress = await detectProxyTarget(verifiedContractAddress as Address, publicClient); if (implementationAddress) { setImplementationAddress(implementationAddress); @@ -75,7 +75,7 @@ const Home: NextPage = () => { console.error("Error fetching ABI from Etherscan: ", etherscanError); const bytecode = await publicClient?.getBytecode({ - address: verifiedContractAddress, + address: verifiedContractAddress as Address, }); const isContract = Boolean(bytecode) && bytecode !== "0x"; @@ -131,7 +131,7 @@ const Home: NextPage = () => { } }; - const fetchAbiFromHeimdall = async (contractAddress: string) => { + const fetchAbiFromHeimdall = async (contractAddress: Address) => { setIsFetchingAbi(true); try { const response = await fetch(`${process.env.NEXT_PUBLIC_HEIMDALL_URL}/${network}/${contractAddress}`); @@ -249,7 +249,7 @@ const Home: NextPage = () => { @@ -263,7 +263,7 @@ const Home: NextPage = () => { onChange={e => setLocalContractAbi(e.target.value)} >