From 9982d4b07bd90edd030884fbc50f4dca62d7e7a7 Mon Sep 17 00:00:00 2001
From: port <108868128+portdeveloper@users.noreply.github.com>
Date: Wed, 19 Jun 2024 17:28:35 +0300
Subject: [PATCH] Feat/more chains (#109)
---
.../nextjs/components/NetworksDropdown.tsx | 232 +++++++++++++++---
.../components/scaffold-eth/Address.tsx | 7 +-
.../scaffold-eth/Contract/ContractUI.tsx | 4 +-
.../RainbowKitCustomConnectButton/index.tsx | 2 +-
packages/nextjs/pages/index.tsx | 9 +-
packages/nextjs/scaffold.config.ts | 1 +
.../nextjs/utils/scaffold-eth/networks.ts | 10 +
7 files changed, 212 insertions(+), 53 deletions(-)
diff --git a/packages/nextjs/components/NetworksDropdown.tsx b/packages/nextjs/components/NetworksDropdown.tsx
index 13eadfb3..ded58dd8 100644
--- a/packages/nextjs/components/NetworksDropdown.tsx
+++ b/packages/nextjs/components/NetworksDropdown.tsx
@@ -1,31 +1,46 @@
-import { useEffect, useState } from "react";
+import { ReactNode, useEffect, useRef, useState } from "react";
import Image from "next/image";
+import * as chains from "@wagmi/core/chains";
import { useTheme } from "next-themes";
-import Select, { OptionProps, components } from "react-select";
-import { getTargetNetworks } from "~~/utils/scaffold-eth";
+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;
+ value: number | string;
label: string;
- icon?: string;
+ icon?: string | ReactNode;
+ isTestnet?: boolean;
};
+
type GroupedOptions = Record<
- "mainnet" | "testnet" | "localhost",
+ "mainnet" | "testnet" | "localhost" | "other",
{
label: string;
options: Options[];
}
>;
-const networks = getTargetNetworks();
+const getIconComponent = (iconName: string | undefined) => {
+ switch (iconName) {
+ case "EyeIcon":
+ return ;
+ case "localhost":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const networks = getPopularTargetNetworks();
const groupedOptions = networks.reduce(
(groups, network) => {
- // Handle the case for localhost
if (network.id === 31337) {
groups.localhost.options.push({
value: network.id,
- label: network.name,
- icon: network.icon,
+ label: "31337 - Localhost",
+ icon: getIconComponent("localhost"),
});
return groups;
}
@@ -36,6 +51,7 @@ const groupedOptions = networks.reduce(
value: network.id,
label: network.name,
icon: network.icon,
+ isTestnet: network.testnet,
});
return groups;
@@ -44,14 +60,51 @@ const groupedOptions = networks.reduce(
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 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 getStoredChains = (): Options[] => {
+ if (typeof window !== "undefined") {
+ const storedChains = localStorage.getItem("customChains");
+ return storedChains ? JSON.parse(storedChains) : [];
+ }
+ return [];
+};
+
+const networkIds = new Set(networks.map(network => network.id));
+
const { Option } = components;
const IconOption = (props: OptionProps) => (
@@ -60,6 +113,11 @@ const IconOption = (props: OptionProps) => (
export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any }) => {
const [isMobile, setIsMobile] = useState(false);
const { resolvedTheme } = useTheme();
+ const [selectedOption, setSelectedOption] = useState>(groupedOptions.mainnet.options[0]);
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const searchInputRef = useRef(null);
+ const seeAllModalRef = useRef(null);
const isDarkMode = resolvedTheme === "dark";
@@ -67,6 +125,13 @@ export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any
useEffect(() => {
setMounted(true);
+ const customChains = getStoredChains();
+ customChains.forEach((chain: Options) => {
+ const groupName = chain.isTestnet ? "testnet" : "mainnet";
+ if (!groupedOptions[groupName].options.some(option => option.value === chain.value)) {
+ groupedOptions[groupName].options.push(chain);
+ }
+ });
}, []);
useEffect(() => {
@@ -80,36 +145,123 @@ export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any
}
}, []);
+ 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("customChains", JSON.stringify(customChains));
+ 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 filteredChains = filterChains(chains, networkIds, existingChainIds);
+
+ const modalChains = mapChainsToOptions(filteredChains).filter(chain =>
+ `${chain.label} ${chain.value}`.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
+
if (!mounted) return ;
+
return (
-
)}
diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
index bc9dc93a..ed991aad 100644
--- a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
+++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
@@ -46,7 +46,7 @@ export const RainbowKitCustomConnectButton = () => {
- {chain.name}
+ {chain.id === 31337 ? "Localhost" : chain.name}
{
const [activeTab, setActiveTab] = useState(TabName.verifiedContract);
- const [network, setNetwork] = useState(networks[0].id.toString());
+ const [network, setNetwork] = useState(mainnet.id.toString());
const [verifiedContractAddress, setVerifiedContractAddress] = useState("");
const [localAbiContractAddress, setLocalAbiContractAddress] = useState("");
const [localContractAbi, setLocalContractAbi] = useState("");
@@ -95,6 +93,7 @@ const Home: NextPage = () => {
if (isAddress(verifiedContractAddress)) {
if (network === "31337") {
setActiveTab(TabName.addressAbi);
+ setLocalAbiContractAddress(verifiedContractAddress);
return;
}
fetchContractAbi();
diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts
index bcc92962..1216506a 100644
--- a/packages/nextjs/scaffold.config.ts
+++ b/packages/nextjs/scaffold.config.ts
@@ -25,6 +25,7 @@ const scaffoldConfig = {
chains.zkSyncTestnet,
chains.scroll,
chains.scrollSepolia,
+ chains.hardhat,
],
// The interval at which your front-end polls the RPC servers for new data
diff --git a/packages/nextjs/utils/scaffold-eth/networks.ts b/packages/nextjs/utils/scaffold-eth/networks.ts
index 6783aeb2..e31658ec 100644
--- a/packages/nextjs/utils/scaffold-eth/networks.ts
+++ b/packages/nextjs/utils/scaffold-eth/networks.ts
@@ -157,6 +157,16 @@ export function getBlockExplorerAddressLink(network: chains.Chain, address: stri
* @returns targetNetworks array containing networks configured in scaffold.config including extra network metadata
*/
export function getTargetNetworks(): ChainWithAttributes[] {
+ // Get all chains from viem/chains
+ const allChains: ChainWithAttributes[] = Object.values(chains).map(chain => ({
+ ...chain,
+ ...NETWORKS_EXTRA_DATA[chain.id],
+ }));
+
+ return allChains;
+}
+
+export function getPopularTargetNetworks(): ChainWithAttributes[] {
return scaffoldConfig.targetNetworks.map(targetNetwork => ({
...targetNetwork,
...NETWORKS_EXTRA_DATA[targetNetwork.id],