Remove all chain override contexts and move to global store (#4743)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor chain-related functionality and imports, update TypeScript types, and improve store management.

### Detailed summary
- Refactored chain-related functionality and imports
- Updated TypeScript types for chain mapping
- Improved store management for chains
- Updated API calls for chain data
- Removed unused hooks and contexts

> The following files were skipped due to too many changes: `apps/dashboard/src/components/configure-networks/ConfigureNetworkModal.tsx`, `apps/dashboard/src/contract-ui/tabs/account-permissions/components/account-signer.tsx`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`, `apps/dashboard/src/stores/SyncStoreToStorage.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/NetworkDropdown.tsx`, `apps/dashboard/src/components/configure-networks/Form/NetworkIdInput.tsx`, `apps/dashboard/src/contract-ui/tabs/embed/components/embed-setup.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/components/engine/contract-subscription/contract-subscriptions-table.tsx`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/components/selects/NetworkSelectDropdown.tsx`, `apps/dashboard/src/components/selects/CustomChainRenderer.tsx`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/components/contract-components/published-contract/addresses-modal.tsx`, `apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx`, `apps/dashboard/src/lib/v5-adapter.ts`, `apps/dashboard/src/components/engine/overview/transactions-table.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts`, `apps/dashboard/src/stores/chainStores.tsx`, `apps/dashboard/src/components/engine/relayer/relayers-table.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`, `apps/dashboard/src/hooks/chains/allChains.ts`, `apps/dashboard/src/pages/[chain_id]/[...paths].tsx`, `apps/dashboard/src/components/selects/NetworkSelectorButton.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
MananTank
2024-09-21 21:10:49 +00:00
parent f2307fde02
commit 0a4e44575d
45 changed files with 609 additions and 902 deletions
+1
View File
@@ -10,6 +10,7 @@
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
"fix": "biome check ./src --fix && eslint ./src --fix",
"typecheck": "tsc --noEmit",
"gen:theme-typings": "chakra-cli tokens src/theme/index.ts",
"postinstall": "pnpm run gen:theme-typings",
"postbuild": "next-sitemap",
+7 -1
View File
@@ -1,4 +1,6 @@
type Store<T> = {
import { useSyncExternalStore } from "react";
export type Store<T> = {
getValue(): T;
setValue(newValue: T): void;
subscribe(listener: () => void): () => void;
@@ -45,3 +47,7 @@ export function createStore<T>(initialValue: T): Store<T> {
},
};
}
export function useStore<T>(store: Store<T>): T {
return useSyncExternalStore(store.subscribe, store.getValue, store.getValue);
}
@@ -3,28 +3,31 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useStore } from "@/lib/reactive";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { useTrack } from "hooks/analytics/useTrack";
import {
useSupportedChains,
useSupportedChainsRecord,
} from "hooks/chains/configureChains";
import {
useAddRecentlyUsedChainId,
useRecentlyUsedChains,
} from "hooks/chains/recentlyUsedChains";
import { useTheme } from "next-themes";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import type { Chain } from "thirdweb";
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
import {
AutoConnect,
ConnectButton,
type NetworkSelectorProps,
useConnectModal,
} from "thirdweb/react";
import { useFavoriteChainIds } from "../../../../app/(dashboard)/(chain)/components/client/star-button";
import { LazyConfigureNetworkModal } from "../../../../components/configure-networks/LazyConfigureNetworkModal";
import type { StoredChain } from "../../../../contexts/configured-chains";
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
import {
type StoredChain,
addRecentlyUsedChainId,
recentlyUsedChainIdsStore,
} from "../../../../stores/chainStores";
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
import { popularChains } from "../popularChains";
@@ -36,43 +39,81 @@ export const CustomConnectWallet = (props: {
const thirdwebClient = useThirdwebClient();
const loginRequired =
props.loginRequired === undefined ? true : props.loginRequired;
const { theme } = useTheme();
const recentChainsv4 = useRecentlyUsedChains();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
const t = theme === "light" ? "light" : "dark";
const allv4Chains = useSupportedChains();
const chainsRecord = useSupportedChainsRecord();
const favChainsQuery = useFavoriteChains();
const allChains = useMemo(() => {
return allv4Chains.map(mapV4ChainToV5Chain);
}, [allv4Chains]);
const popularChainsWithMeta = useMemo(() => {
return popularChains.map((x) => chainsRecord[x.id] || x);
}, [chainsRecord]);
// chains
const favChainIdsQuery = useFavoriteChainIds();
const recentChainIds = useStore(recentlyUsedChainIdsStore);
const { idToChain, allChains } = useAllChainsData();
const allChainsWithMetadata = useMemo(
() => allChains.map(mapV4ChainToV5Chain),
[allChains],
);
const recentlyUsedChainsWithMetadata = useMemo(
() =>
recentChainIds
.map((id) => {
const c = idToChain.get(id);
// eslint-disable-next-line no-restricted-syntax
return c ? mapV4ChainToV5Chain(c) : undefined;
})
.filter((x) => !!x),
[recentChainIds, idToChain],
);
const favoriteChainsWithMetadata = useMemo(() => {
return (favChainIdsQuery.data || [])
.map((id) => {
const c = idToChain.get(Number(id));
// eslint-disable-next-line no-restricted-syntax
return c ? mapV4ChainToV5Chain(c) : undefined;
})
.filter((x) => !!x);
}, [idToChain, favChainIdsQuery.data]);
const popularChainsWithMetadata = useMemo(() => {
// eslint-disable-next-line no-restricted-syntax
return popularChains.map((x) =>
// eslint-disable-next-line no-restricted-syntax
{
const v4Chain = idToChain.get(x.id);
// eslint-disable-next-line no-restricted-syntax
return v4Chain ? mapV4ChainToV5Chain(v4Chain) : x;
},
);
}, [idToChain]);
// Network Config Modal
const [isNetworkConfigModalOpen, setIsNetworkConfigModalOpen] =
useState(false);
const [editChain, setEditChain] = useState<StoredChain | undefined>(
undefined,
);
const chainSections = useMemo(() => {
const chainSections: NetworkSelectorProps["sections"] = useMemo(() => {
return [
{
label: "Favorites",
chains: favChainsQuery.data.map(mapV4ChainToV5Chain),
},
{
label: "Popular",
chains: popularChainsWithMeta.map(mapV4ChainToV5Chain),
chains: favoriteChainsWithMetadata,
},
{
label: "Recent",
chains: recentChainsv4.map(mapV4ChainToV5Chain),
chains: recentlyUsedChainsWithMetadata,
},
{
label: "Popular",
chains: popularChainsWithMetadata,
},
];
}, [recentChainsv4, favChainsQuery.data, popularChainsWithMeta]);
}, [
recentlyUsedChainsWithMetadata,
favoriteChainsWithMetadata,
popularChainsWithMetadata,
]);
// ensures login status on pages that need it
const { isPending, isLoggedIn } = useLoggedInUser();
@@ -84,7 +125,7 @@ export const CustomConnectWallet = (props: {
<div className="w-[144px] h-[48px] bg-muted border border-border rounded-lg flex items-center justify-center">
<Spinner className="size-4" />
</div>
{/* need autoconnect here so that we actually connect */}
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
@@ -100,7 +141,7 @@ export const CustomConnectWallet = (props: {
Sign In
</Link>
</Button>
{/* need autoconnect here so that we actually connect */}
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
@@ -138,7 +179,7 @@ export const CustomConnectWallet = (props: {
detailsButton={{
className: props.detailsButtonClassName,
}}
chains={allChains}
chains={allChainsWithMetadata}
detailsModal={{
networkSelector: {
sections: chainSections,
@@ -5,7 +5,6 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
import type { ChainMetadata } from "thirdweb/chains";
import invariant from "tiny-invariant";
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
@@ -947,28 +946,3 @@ export function useAuthorizedWallets() {
gcTime: 0,
});
}
/**
*
*/
async function fetchChainsFromApi() {
// always fetch from prod for chains for now
// TODO: re-visit this
const res = await fetch("https://api.thirdweb.com/v1/chains");
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.data as ChainMetadata[];
}
export function useApiChains() {
return useQuery({
queryKey: ["all-chains"],
queryFn: () => {
return fetchChainsFromApi();
},
});
}
@@ -1,31 +0,0 @@
import { useFavouriteChainIds } from "app/(dashboard)/(chain)/components/client/star-button";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useMemo } from "react";
import type { ChainMetadata } from "thirdweb/chains";
export function useFavoriteChains() {
const allChains = useSupportedChains();
const favChainsQuery = useFavouriteChainIds();
const data = useMemo(() => {
if (favChainsQuery.data) {
const _chains: ChainMetadata[] = [];
// biome-ignore lint/complexity/noForEach: FIXME
favChainsQuery.data.forEach((chainId) => {
const chain = allChains.find((c) => String(c.chainId) === chainId);
if (chain) {
_chains.push(chain);
}
});
return _chains;
}
return [] as ChainMetadata[];
}, [favChainsQuery.data, allChains]);
return {
isPending: favChainsQuery.isPending,
data,
};
}
@@ -8,7 +8,6 @@ import {
import type { BasicContract } from "contract-ui/types/types";
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
import { useAllChainsData } from "hooks/chains/allChains";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { sendAndConfirmTransaction } from "thirdweb";
import { remove } from "thirdweb/extensions/thirdweb";
@@ -42,7 +41,8 @@ export const useAllContractList = (
) => {
const multiChainQuery = useMultiChainRegContractList(walletAddress);
const configuredChainsRecord = useSupportedChainsRecord();
// TODO - instead of using ALL chains, fetch only the ones used here
const { idToChain } = useAllChainsData();
const contractList = useMemo(() => {
const data = multiChainQuery.data || [];
@@ -51,14 +51,12 @@ export const useAllContractList = (
// biome-ignore lint/complexity/noForEach: FIXME
data.forEach((net) => {
if (net.chainId in configuredChainsRecord) {
const chainRecord = configuredChainsRecord[net.chainId];
if (chainRecord.status !== "deprecated") {
if (chainRecord.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
const chn = idToChain.get(net.chainId);
if (chn && chn.status !== "deprecated") {
if (chn.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
}
});
@@ -71,7 +69,7 @@ export const useAllContractList = (
testnets.sort((a, b) => a.chainId - b.chainId);
return mainnets.concat(testnets);
}, [multiChainQuery.data, onlyMainnet, configuredChainsRecord]);
}, [multiChainQuery.data, onlyMainnet, idToChain]);
return {
...multiChainQuery,
@@ -85,12 +83,10 @@ type RemoveContractParams = {
};
export function useRemoveContractMutation() {
const { chainIdToChainRecord } = useAllChainsData();
const queryClient = useQueryClient();
const account = useActiveAccount();
return useMutation({
mutationFn: async (data: RemoveContractParams) => {
invariant(chainIdToChainRecord, "chains not initialzed yet");
invariant(data.chainId, "chainId not provided");
invariant(account, "No wallet connected");
const { contractAddress, chainId } = data;
@@ -42,7 +42,7 @@ async function removeChainFromFavorites(chainId: number) {
return result?.data?.favorite;
}
export function useFavouriteChainIds() {
export function useFavoriteChainIds() {
const loggedInUser = useLoggedInUser();
return useQuery({
queryKey: ["favoriteChains", loggedInUser.user?.address],
@@ -59,7 +59,7 @@ export function StarButton(props: {
}) {
const loggedInUser = useLoggedInUser();
const queryClient = useQueryClient();
const favChainsQuery = useFavouriteChainIds();
const favChainsQuery = useFavoriteChainIds();
const mutation = useMutation({
mutationFn: (preferred: boolean) => {
+7 -11
View File
@@ -1,25 +1,21 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ThemeProvider } from "next-themes";
import { ThirdwebProvider } from "thirdweb/react";
import { SyncChainStores } from "../stores/chainStores";
const queryClient = new QueryClient();
export function AppRouterProviders(props: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AllChainsProvider>
<ChainsProvider>
<ThirdwebProvider>
<ThemeProvider attribute="class" defaultTheme="dark">
{props.children}
</ThemeProvider>
</ThirdwebProvider>
</ChainsProvider>
</AllChainsProvider>
<SyncChainStores />
<ThirdwebProvider>
<ThemeProvider attribute="class" defaultTheme="dark">
{props.children}
</ThemeProvider>
</ThirdwebProvider>
</QueryClientProvider>
);
}
@@ -15,14 +15,13 @@ import { AppShell, type AppShellProps } from "components/layout/app-shell";
import { Onboarding as OnboardingModal } from "components/onboarding";
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
import { PosthogIdentifier } from "components/wallets/PosthogIdentifier";
import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ErrorProvider } from "contexts/error-handler";
import { isSanctionedAddress } from "data/eth-sanctioned-addresses";
import { useEffect, useMemo, useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { Heading } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
import { SyncChainStores } from "../../stores/chainStores";
import { DashboardThirdwebProvider } from "./providers";
interface AppLayoutProps extends AppShellProps {
@@ -50,22 +49,19 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={props.dehydratedState}>
<ErrorProvider>
<AllChainsProvider>
<ChainsProvider>
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<SyncChainStores />
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ChainsProvider>
</AllChainsProvider>
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ErrorProvider>
</HydrationBoundary>
</QueryClientProvider>
@@ -37,7 +37,6 @@ import type {
import { getSDKTheme } from "app/components/sdk-component-theme";
import { LOCAL_NODE_PKEY } from "constants/misc";
import { useTrack } from "hooks/analytics/useTrack";
import { useSupportedChain } from "hooks/chains/configureChains";
import { ExternalLinkIcon } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
@@ -58,6 +57,7 @@ import {
import { privateKeyToAccount } from "thirdweb/wallets";
import { Button, type ButtonProps, Card, Heading, Text } from "tw-components";
import { THIRDWEB_API_HOST } from "../../constants/urls";
import { useAllChainsData } from "../../hooks/chains/allChains";
import { useV5DashboardChain } from "../../lib/v5-adapter";
const GAS_FREE_CHAINS = [
@@ -408,10 +408,12 @@ const MismatchNotice: React.FC<{
const activeWallet = useActiveWallet();
const actuallyCanAttemptSwitch =
activeWallet && activeWallet.id !== "global.safe";
const walletConnectedNetworkInfo = useSupportedChain(connectedChainId || -1);
const { idToChain } = useAllChainsData();
const walletConnectedNetworkInfo = connectedChainId
? idToChain.get(connectedChainId)
: undefined;
const isMobile = useBreakpointValue({ base: true, md: false });
const chain = useSupportedChain(desiredChainId || -1);
const chain = desiredChainId ? idToChain.get(desiredChainId) : undefined;
const chainV5 = useV5DashboardChain(desiredChainId);
const onSwitchWallet = useCallback(async () => {
@@ -8,13 +8,14 @@ import {
RadioGroup,
} from "@chakra-ui/react";
import { ChainIcon } from "components/icons/ChainIcon";
import type { StoredChain } from "contexts/configured-chains";
import { useAllChainsData } from "hooks/chains/allChains";
import { useSupportedChainsNameRecord } from "hooks/chains/configureChains";
import { useRemoveChainModification } from "hooks/chains/useModifyChain";
import { getDashboardChainRpc } from "lib/rpc";
import { useForm } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { useAllChainsData } from "../../hooks/chains/allChains";
import {
type StoredChain,
removeChainOverrides,
} from "../../stores/chainStores";
import { ChainIdInput } from "./Form/ChainIdInput";
import { ConfirmationPopover } from "./Form/ConfirmationPopover";
import { IconUpload } from "./Form/IconUpload";
@@ -55,9 +56,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
prefillSlug,
prefillChainId,
}) => {
const configuredChainNameRecord = useSupportedChainsNameRecord();
const { chainIdToChainRecord } = useAllChainsData();
const removeChainModification = useRemoveChainModification();
const { idToChain, nameToChain } = useAllChainsData();
const form = useForm<NetworkConfigFormData>({
values: {
@@ -86,11 +85,11 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
const chainId = Number(form.watch("chainId"));
const isChainIdDirty = form.formState.dirtyFields.chainId;
const overwritingChain = isChainIdDirty && chainIdToChainRecord[chainId];
const overwritingChain = isChainIdDirty && idToChain.get(chainId);
const editedFrom =
editingChain?.isModified || editingChain?.isOverwritten
? chainIdToChainRecord[editingChain.chainId]
? idToChain.get(editingChain.chainId)
: undefined;
const { ref } = form.register("name", {
@@ -104,7 +103,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
return true;
}
const chain = configuredChainNameRecord[value];
const chain = nameToChain.get(value);
// valid if chain is not found
if (!chain) {
@@ -173,7 +172,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
}
if (editingChain && editingChain.chainId !== configuredNetwork.chainId) {
removeChainModification(editingChain.chainId);
removeChainOverrides(editingChain.chainId);
}
if (overwritingChain) {
@@ -394,7 +393,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
variant="outline"
onClick={() => {
onSubmit(editedFrom);
removeChainModification(editedFrom.chainId);
removeChainOverrides(editedFrom.chainId);
}}
>
Reset
@@ -1,7 +1,9 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import type { StoredChain } from "contexts/configured-chains";
import { useAddRecentlyUsedChainId } from "../../hooks/chains/recentlyUsedChains";
import {
type StoredChain,
addRecentlyUsedChainId,
} from "../../stores/chainStores";
import { ConfigureNetworks } from "./ConfigureNetworks";
export type ConfigureNetworkModalProps = {
@@ -14,8 +16,6 @@ export type ConfigureNetworkModalProps = {
export const ConfigureNetworkModal: React.FC<ConfigureNetworkModalProps> = (
props,
) => {
const addRecentlyUsedChains = useAddRecentlyUsedChainId();
const onModalClose = () => {
props.onOpenChange(false);
};
@@ -30,7 +30,7 @@ export const ConfigureNetworkModal: React.FC<ConfigureNetworkModalProps> = (
<ChakraProviderSetup>
<ConfigureNetworks
onNetworkAdded={(chain) => {
addRecentlyUsedChains(chain.chainId);
addRecentlyUsedChainId(chain.chainId);
props.onNetworkAdded?.(chain);
onModalClose();
}}
@@ -1,8 +1,10 @@
import type { StoredChain } from "contexts/configured-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import { useModifyChain } from "hooks/chains/useModifyChain";
import { toast } from "sonner";
import {
type StoredChain,
addChainOverrides,
addRecentlyUsedChainId,
} from "../../stores/chainStores";
import { ConfigureNetworkForm } from "./ConfigureNetworkForm";
function useChainConfigTrack() {
@@ -26,12 +28,10 @@ interface ConfigureNetworksProps {
export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
const trackChainConfig = useChainConfigTrack();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
const modifyChain = useModifyChain();
const { editChain } = props;
const handleSubmit = (chain: StoredChain) => {
modifyChain(chain);
addChainOverrides(chain);
addRecentlyUsedChainId(chain.chainId);
if (chain.isCustom) {
@@ -1,8 +1,8 @@
import { Input } from "@/components/ui/input";
import { FormControl } from "@chakra-ui/react";
import { useAllChainsData } from "hooks/chains/allChains";
import type { UseFormReturn } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
import { TooltipBox } from "./TooltipBox";
@@ -11,7 +11,8 @@ export const NetworkIDInput: React.FC<{
disabled?: boolean;
}> = ({ form, disabled }) => {
const slug = form.watch("slug");
const { slugToChainRecord } = useAllChainsData();
const { slugToChain } = useAllChainsData();
const existingChain = slugToChain.get(slug);
return (
<FormControl
@@ -52,18 +53,18 @@ export const NetworkIDInput: React.FC<{
return true;
}
return !(_slug in slugToChainRecord);
return !slugToChain.has(_slug);
},
},
})}
/>
<FormErrorMessage>
Can not use Network ID {`"${slug}"`}.
{slug && slug in slugToChainRecord && (
{slug && existingChain && (
<>
{" "}
It is being used by {`"`}
{slugToChainRecord[slug].name}
{existingChain.name}
{`"`}
</>
)}
@@ -1,8 +1,8 @@
import { Select } from "chakra-react-select";
import type { SizeProp } from "chakra-react-select";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { useAllChainsData } from "../../../hooks/chains/allChains";
interface NetworkDropdownProps {
useCleanChainName?: boolean;
@@ -26,10 +26,10 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
size = "md",
}) => {
const form = useFormContext();
const supportedChains = useSupportedChains();
const { allChains } = useAllChainsData();
const options = useMemo(() => {
return supportedChains.map((chain) => {
return allChains.map((chain) => {
return {
label: useCleanChainName
? cleanChainName(chain.name)
@@ -37,7 +37,7 @@ export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
value: chain.chainId,
};
});
}, [supportedChains, useCleanChainName]);
}, [allChains, useCleanChainName]);
const defaultValues = useMemo(() => {
const networksEnabled = form?.watch(
@@ -14,7 +14,7 @@ export const AddressesModal: React.FC<AddressesModalProps> = ({
buttonTitle,
modalTitle,
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
@@ -35,47 +35,45 @@ export const AddressesModal: React.FC<AddressesModalProps> = ({
</Link>
</Text>
{chainIdToChainRecord && (
<Drawer
size="lg"
allowPinchZoom
isOpen={isOpen}
onClose={onClose}
header={{
children: (
<Heading as="h3" size="title.sm">
{modalTitle}
</Heading>
),
}}
>
<Flex flexDir="column" gap={{ base: 2, md: 1 }}>
{Object.entries(chainAddressRecord).map(([chainId, address]) =>
!address ? null : (
<Flex
key={chainId}
flexDir={{ base: "column", md: "row" }}
alignItems={{ base: "auto", md: "center" }}
justifyContent="space-between"
<Drawer
size="lg"
allowPinchZoom
isOpen={isOpen}
onClose={onClose}
header={{
children: (
<Heading as="h3" size="title.sm">
{modalTitle}
</Heading>
),
}}
>
<Flex flexDir="column" gap={{ base: 2, md: 1 }}>
{Object.entries(chainAddressRecord).map(([chainId, address]) =>
!address ? null : (
<Flex
key={chainId}
flexDir={{ base: "column", md: "row" }}
alignItems={{ base: "auto", md: "center" }}
justifyContent="space-between"
>
<Text size="body.md">
{idToChain.get(Number.parseInt(chainId))?.name}
</Text>
<Link
href={`${THIRDWEB_DOMAIN}/${chainId}/${address}`}
isExternal
color="blue.500"
>
<Text size="body.md">
{chainIdToChainRecord[Number.parseInt(chainId)]?.name}
<Text color="inherit" fontFamily="mono">
{address}
</Text>
<Link
href={`${THIRDWEB_DOMAIN}/${chainId}/${address}`}
isExternal
color="blue.500"
>
<Text color="inherit" fontFamily="mono">
{address}
</Text>
</Link>
</Flex>
),
)}
</Flex>
</Drawer>
)}
</Link>
</Flex>
),
)}
</Flex>
</Drawer>
</>
);
};
@@ -26,9 +26,6 @@ import {
import { ChainIcon } from "components/icons/ChainIcon";
import { NetworkSelectDropdown } from "components/selects/NetworkSelectDropdown";
import type { BasicContract } from "contract-ui/types/types";
import { useAllChainsData } from "hooks/chains/allChains";
import { useChainSlug } from "hooks/chains/chainSlug";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import {
DownloadIcon,
EllipsisVerticalIcon,
@@ -45,6 +42,8 @@ import {
usePagination,
useTable,
} from "react-table";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { useChainSlug } from "../../../hooks/chains/chainSlug";
import { ImportModal } from "../import-contract/modal";
import { AsyncContractNameCell, AsyncContractTypeCell } from "./cells";
import { ShowMoreButton } from "./show-more-button";
@@ -198,8 +197,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
chainIdsWithDeployments,
loading,
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const configuredChains = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
const columns: Column<(typeof combinedList)[number]>[] = useMemo(
() => [
@@ -231,9 +229,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
filter: "equals",
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => {
const data =
configuredChains[cell.row.original.chainId] ||
chainIdToChainRecord[cell.row.original.chainId];
const data = idToChain.get(cell.row.original.chainId);
const cleanedChainName =
data?.name?.replace("Mainnet", "").trim() ||
`Unknown Network (#${cell.row.original.chainId})`;
@@ -296,7 +292,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
},
},
],
[configuredChains, chainIdsWithDeployments, chainIdToChainRecord],
[chainIdsWithDeployments, idToChain],
);
const defaultColumn = useMemo(
@@ -71,13 +71,13 @@ export const ContractSubscriptionTable: React.FC<
const removeDisclosure = useDisclosure();
const [selectedContractSub, setSelectedContractSub] =
useState<EngineContractSubscription>();
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const columns = [
columnHelper.accessor("chainId", {
header: "Chain",
cell: (cell) => {
const chain = chainIdToChainRecord[cell.getValue()];
const chain = idToChain.get(cell.getValue());
return (
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
@@ -90,7 +90,7 @@ export const ContractSubscriptionTable: React.FC<
header: "Contract Address",
cell: (cell) => {
const { chainId } = cell.row.original;
const chain = chainIdToChainRecord[chainId];
const chain = idToChain.get(chainId);
const explorer = chain?.explorers?.[0];
if (!explorer) {
return (
@@ -338,8 +338,8 @@ const RemoveModal = ({
"Successfully removed contract subscription.",
"Failed to remove contract subscription.",
);
const { chainIdToChainRecord } = useAllChainsData();
const chain = chainIdToChainRecord[contractSubscription.chainId];
const { idToChain } = useAllChainsData();
const chain = idToChain.get(contractSubscription.chainId);
const onClick = () => {
removeContractSubscription(
@@ -22,7 +22,6 @@ import { useAllChainsData } from "hooks/chains/allChains";
import { useState } from "react";
import { FiArrowLeft, FiArrowRight, FiInfo } from "react-icons/fi";
import { toTokens } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import {
Button,
Card,
@@ -101,7 +100,7 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
isFetched,
instanceUrl,
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const transactionDisclosure = useDisclosure();
const [selectedTransaction, setSelectedTransaction] =
useState<Transaction | null>(null);
@@ -128,7 +127,7 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
return;
}
const chain = chainIdToChainRecord[Number.parseInt(chainId)];
const chain = idToChain.get(Number.parseInt(chainId));
if (chain) {
return (
<Flex align="center" gap={2} className="py-2">
@@ -200,7 +199,7 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
return;
}
const chain = chainIdToChainRecord[Number.parseInt(chainId)];
const chain = idToChain.get(Number.parseInt(chainId));
if (chain) {
const explorer = chain.explorers?.[0];
if (!explorer) {
@@ -306,7 +305,7 @@ const TransactionDetailsDrawer = ({
onClickPrevious?: () => void;
onClickNext?: () => void;
}) => {
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const errorMessageDisclosure = useDisclosure();
const advancedTxDetailsDisclosure = useDisclosure();
@@ -314,10 +313,9 @@ const TransactionDetailsDrawer = ({
return null;
}
const chain: ChainMetadata | undefined =
chainIdToChainRecord[Number.parseInt(transaction.chainId)];
const decimals = chain.nativeCurrency.decimals || 18;
const symbol = chain.nativeCurrency.symbol || "ETH";
const chain = idToChain.get(Number.parseInt(transaction.chainId));
const decimals = chain?.nativeCurrency.decimals || 18;
const symbol = chain?.nativeCurrency.symbol || "ETH";
const explorer = chain?.explorers?.[0];
const status = statusDetails[transaction.status as EngineStatus];
@@ -75,7 +75,7 @@ const AddModal = ({
}) => {
const { mutate: createRelayer } = useEngineCreateRelayer(instanceUrl);
const { data: backendWallets } = useEngineBackendWallets(instanceUrl);
const { slugToChainRecord, chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const trackEvent = useTrack();
const { onSuccess, onError } = useTxNotifications(
"Relayer created successfully.",
@@ -84,13 +84,13 @@ const AddModal = ({
const form = useForm<AddModalInput>({
defaultValues: {
chainId: slugToChainRecord.sepolia.chainId,
chainId: 11155111, // sepolia chain id
},
});
const onSubmit = (data: AddModalInput) => {
const createRelayerData: CreateRelayerInput = {
chain: chainIdToChainRecord[data.chainId]?.slug ?? "unknown",
chain: idToChain.get(data.chainId)?.slug ?? "unknown",
backendWalletAddress: data.backendWalletAddress,
name: data.name,
allowedContracts: parseAddressListRaw(data.allowedContractsRaw),
@@ -62,13 +62,13 @@ export const RelayersTable: React.FC<RelayersTableProps> = ({
const editDisclosure = useDisclosure();
const removeDisclosure = useDisclosure();
const [selectedRelayer, setSelectedRelayer] = useState<EngineRelayer>();
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const columns = [
columnHelper.accessor("chainId", {
header: "Chain",
cell: (cell) => {
const chain = chainIdToChainRecord[Number.parseInt(cell.getValue())];
const chain = idToChain.get(Number.parseInt(cell.getValue()));
return (
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
@@ -81,7 +81,7 @@ export const RelayersTable: React.FC<RelayersTableProps> = ({
header: "Backend Wallet",
cell: (cell) => {
const { chainId, backendWalletAddress } = cell.row.original;
const chain = chainIdToChainRecord[Number.parseInt(chainId)];
const chain = idToChain.get(Number.parseInt(chainId));
const explorer = chain?.explorers?.[0];
if (!explorer) {
@@ -208,7 +208,7 @@ const EditModal = ({
}) => {
const { mutate: updateRelayer } = useEngineUpdateRelayer(instanceUrl);
const { data: backendWallets } = useEngineBackendWallets(instanceUrl);
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const trackEvent = useTrack();
const { onSuccess, onError } = useTxNotifications(
"Successfully updated relayer",
@@ -227,7 +227,7 @@ const EditModal = ({
const onSubmit = (data: AddModalInput) => {
const updateRelayerData: UpdateRelayerInput = {
id: relayer.id,
chain: chainIdToChainRecord[data.chainId]?.slug ?? "unknown",
chain: idToChain.get(data.chainId)?.slug ?? "unknown",
backendWalletAddress: data.backendWalletAddress,
name: data.name,
allowedContracts: parseAddressListRaw(data.allowedContractsRaw),
@@ -347,7 +347,7 @@ const RemoveModal = ({
"Successfully removed relayer",
"Failed to remove relayer",
);
const { chainIdToChainRecord } = useAllChainsData();
const { idToChain } = useAllChainsData();
const onClick = () => {
revokeRelayer(
@@ -392,12 +392,11 @@ const RemoveModal = ({
<ChainIcon
size={12}
ipfsSrc={
chainIdToChainRecord[Number.parseInt(relayer.chainId)]?.icon
?.url
idToChain.get(Number.parseInt(relayer.chainId))?.icon?.url
}
/>
<Text>
{chainIdToChainRecord[Number.parseInt(relayer.chainId)]?.name}
{idToChain.get(Number.parseInt(relayer.chainId))?.name}
</Text>
</Flex>
</FormControl>
@@ -3,12 +3,13 @@ import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ChainIcon } from "components/icons/ChainIcon";
import { OPSponsoredChains } from "constants/chains";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import { SettingsIcon } from "lucide-react";
import { useMemo } from "react";
import type { UseNetworkSwitcherModalOptions } from "thirdweb/react";
import type { StoredChain } from "../../contexts/configured-chains";
import { useAllChainsData } from "../../hooks/chains/allChains";
import {
type StoredChain,
addRecentlyUsedChainId,
} from "../../stores/chainStores";
type ChainRenderProps = React.ComponentProps<
NonNullable<UseNetworkSwitcherModalOptions["renderChain"]>
@@ -28,15 +29,8 @@ export const CustomChainRenderer = ({
disableChainConfig,
openEditChainModal,
}: CustomChainRendererProps) => {
const addRecentlyUsedChain = useAddRecentlyUsedChainId();
const supportedChainsRecord = useSupportedChainsRecord();
const storedChain = useMemo(() => {
return chain.id in supportedChainsRecord
? supportedChainsRecord[chain.id]
: undefined;
}, [chain, supportedChainsRecord]);
const { idToChain } = useAllChainsData();
const storedChain = idToChain.get(chain.id);
const isDeprecated = storedChain?.status === "deprecated";
const isSponsored = OPSponsoredChains.includes(chain.id);
@@ -104,7 +98,7 @@ export const CustomChainRenderer = ({
aria-label="Configure Network"
onClick={() => {
openEditChainModal(storedChain);
addRecentlyUsedChain(chain.id);
addRecentlyUsedChainId(chain.id);
if (close) {
close();
} else {
@@ -6,8 +6,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ChainIcon } from "components/icons/ChainIcon";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { useAllChainsData } from "../../hooks/chains/allChains";
interface NetworkSelectDropdownProps {
enabledChainIds?: number[];
@@ -26,26 +26,24 @@ export const NetworkSelectDropdown: React.FC<NetworkSelectDropdownProps> = ({
selectedChain,
onSelect,
}) => {
const supportedChains = useSupportedChains();
const { allChains } = useAllChainsData();
const chains = useMemo(() => {
// return only enabled chains if enabled chains are specified
if (enabledChainIds && enabledChainIds.length > 0) {
const enabledChainIdsSet = new Set(enabledChainIds);
return supportedChains.filter((chain) =>
enabledChainIdsSet.has(chain.chainId),
);
return allChains.filter((chain) => enabledChainIdsSet.has(chain.chainId));
}
// return supported chains that are not disabled if disabled chains are specified
if (disabledChainIds && disabledChainIds.length > 0) {
const disabledChainIdsSet = new Set(disabledChainIds);
return supportedChains.filter(
return allChains.filter(
(chain) => !disabledChainIdsSet.has(chain.chainId),
);
}
// if no enabled or disabled chains are specified, return all supported chains
return supportedChains;
}, [supportedChains, enabledChainIds, disabledChainIds]);
return allChains;
}, [allChains, enabledChainIds, disabledChainIds]);
const cleanChainName = (chainName: string) => {
return chainName.replace("Mainnet", "");
@@ -2,26 +2,24 @@
import { Button } from "@/components/ui/button";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useStore } from "@/lib/reactive";
import { popularChains } from "@3rdweb-sdk/react/components/popularChains";
import { useFavoriteChains } from "@3rdweb-sdk/react/hooks/useFavoriteChains";
import { ChainIcon } from "components/icons/ChainIcon";
import type { StoredChain } from "contexts/configured-chains";
import {
useSupportedChains,
useSupportedChainsRecord,
} from "hooks/chains/configureChains";
import {
useAddRecentlyUsedChainId,
useRecentlyUsedChains,
} from "hooks/chains/recentlyUsedChains";
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
import { useTheme } from "next-themes";
import { useEffect, useMemo, useRef, useState } from "react";
import { BiChevronDown } from "react-icons/bi";
import { useActiveWallet } from "thirdweb/react";
import { useNetworkSwitcherModal } from "thirdweb/react";
import { useFavoriteChainIds } from "../../app/(dashboard)/(chain)/components/client/star-button";
import { getSDKTheme } from "../../app/components/sdk-component-theme";
import { mapV4ChainToV5Chain } from "../../contexts/map-chains";
import { useAllChainsData } from "../../hooks/chains/allChains";
import {
type StoredChain,
addRecentlyUsedChainId,
recentlyUsedChainIdsStore,
} from "../../stores/chainStores";
import { LazyConfigureNetworkModal } from "../configure-networks/LazyConfigureNetworkModal";
import { CustomChainRenderer } from "./CustomChainRenderer";
@@ -39,37 +37,66 @@ export const NetworkSelectorButton: React.FC<NetworkSelectorButtonProps> = ({
onSwitchChain,
}) => {
const client = useThirdwebClient();
const recentlyUsedChains = useRecentlyUsedChains();
const addRecentlyUsedChains = useAddRecentlyUsedChainId();
const { idToChain, allChains } = useAllChainsData();
// recently used chains
const recentlyUsedChainIds = useStore(recentlyUsedChainIdsStore);
const recentlyUsedChains = useMemo(() => {
return recentlyUsedChainIds
.map((id) => idToChain.get(id))
.filter((v) => !!v);
}, [recentlyUsedChainIds, idToChain]);
// configure network modal
const [isNetworkConfigModalOpen, setIsNetworkConfigModalOpen] =
useState(false);
const [editChain, setEditChain] = useState<StoredChain | undefined>(
undefined,
);
const { theme } = useTheme();
const supportedChains = useSupportedChains();
const supportedChainsRecord = useSupportedChainsRecord();
const favoriteChainsQuery = useFavoriteChains();
const favoriteChainIdsQuery = useFavoriteChainIds();
const popularChainsWithMetadata = useMemo(() => {
// eslint-disable-next-line no-restricted-syntax
return popularChains.map((x) =>
// eslint-disable-next-line no-restricted-syntax
{
const v4Chain = idToChain.get(x.id);
// eslint-disable-next-line no-restricted-syntax
return v4Chain ? mapV4ChainToV5Chain(v4Chain) : x;
},
);
}, [idToChain]);
const favoriteChainsWithMetadata = useMemo(() => {
return (favoriteChainIdsQuery.data || [])
.map((id) => {
const c = idToChain.get(Number(id));
// eslint-disable-next-line no-restricted-syntax
return c ? mapV4ChainToV5Chain(c) : undefined;
})
.filter((x) => !!x);
}, [idToChain, favoriteChainIdsQuery.data]);
const networkSwitcherModal = useNetworkSwitcherModal();
const chains = useMemo(() => {
if (disabledChainIds && disabledChainIds.length > 0) {
const disabledChainIdsSet = new Set(disabledChainIds);
return supportedChains.filter(
return allChains.filter(
(chain) => !disabledChainIdsSet.has(chain.chainId),
);
}
if (networksEnabled && networksEnabled.length > 0) {
const networksEnabledSet = new Set(networksEnabled);
return supportedChains.filter((chain) =>
networksEnabledSet.has(chain.chainId),
);
return allChains.filter((chain) => networksEnabledSet.has(chain.chainId));
}
// if no restrictions, show all supported chains
return supportedChains;
}, [disabledChainIds, networksEnabled, supportedChains]);
return allChains;
}, [disabledChainIds, networksEnabled, allChains]);
const filteredRecentlyUsedChains = useMemo(() => {
if (recentlyUsedChains && recentlyUsedChains.length > 0) {
@@ -103,10 +130,10 @@ export const NetworkSelectorButton: React.FC<NetworkSelectorButtonProps> = ({
if (onSwitchChain) {
onSwitchChain(chain);
}
addRecentlyUsedChains(chain.chainId);
addRecentlyUsedChainId(chain.chainId);
prevChain.current = chain;
}
}, [chain, onSwitchChain, addRecentlyUsedChains]);
}, [chain, onSwitchChain]);
const wallet = useActiveWallet();
@@ -128,13 +155,11 @@ export const NetworkSelectorButton: React.FC<NetworkSelectorButtonProps> = ({
},
{
label: "Favorites",
chains: (favoriteChainsQuery.data ?? []).map(
mapV4ChainToV5Chain,
),
chains: favoriteChainsWithMetadata,
},
{
label: "Popular",
chains: networksEnabled ? [] : popularChains,
chains: networksEnabled ? [] : popularChainsWithMetadata,
},
{
label: "All Networks",
@@ -159,10 +184,11 @@ export const NetworkSelectorButton: React.FC<NetworkSelectorButtonProps> = ({
setIsNetworkConfigModalOpen(true);
},
async onSwitch(chain) {
addRecentlyUsedChains(chain.id);
addRecentlyUsedChainId(chain.id);
if (onSwitchChain) {
if (supportedChainsRecord[chain.id]) {
onSwitchChain(supportedChainsRecord[chain.id]);
const storedChain = idToChain.get(chain.id);
if (storedChain) {
onSwitchChain(storedChain);
}
}
},
@@ -1,10 +1,10 @@
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { Flex, Input, Select, type SelectProps } from "@chakra-ui/react";
import { CURRENCIES, type CurrencyMetadata } from "constants/currencies";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo, useState } from "react";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS, isAddress } from "thirdweb";
import { Button } from "tw-components";
import { useAllChainsData } from "../../hooks/chains/allChains";
interface CurrencySelectorProps extends SelectProps {
value: string;
@@ -26,8 +26,8 @@ export const CurrencySelector: React.FC<CurrencySelectorProps> = ({
...props
}) => {
const chainId = useDashboardEVMChainId();
const configuredChainsRecord = useSupportedChainsRecord();
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
const { idToChain } = useAllChainsData();
const chain = chainId ? idToChain.get(chainId) : undefined;
const helperCurrencies =
defaultCurrencies.length > 0
@@ -1,49 +0,0 @@
import { useApiChains } from "@3rdweb-sdk/react/hooks/useApi";
import { createContext, useMemo } from "react";
import type { ChainMetadata } from "thirdweb/chains";
type AllChainsData = {
allChains: ChainMetadata[];
chainIdToChainRecord: Record<number, ChainMetadata>;
chainIdToIndexRecord: Record<number, number>;
slugToChainRecord: Record<string, ChainMetadata>;
};
// Legacy: we need to find a way to remove this
// eslint-disable-next-line no-restricted-syntax
export const AllChainsContext = createContext<AllChainsData | undefined>(
undefined,
);
/**
* if no networks are configured by the user, return the defaultChains
*/
export function AllChainsProvider(props: { children: React.ReactNode }) {
const { data } = useApiChains();
const allChainsData: AllChainsData = useMemo(() => {
const slugToChainRecord: Record<string, ChainMetadata> = {};
const chainIdToChainRecord: Record<number, ChainMetadata> = {};
const chainIdToIndexRecord: Record<number, number> = {};
if (data?.length) {
for (let i = 0; i < data.length; i++) {
const chain = data[i];
slugToChainRecord[chain.slug] = chain;
chainIdToChainRecord[chain.chainId] = chain;
chainIdToIndexRecord[chain.chainId] = i;
}
}
return {
allChains: data || [],
chainIdToChainRecord,
slugToChainRecord,
chainIdToIndexRecord,
};
}, [data]);
return (
<AllChainsContext.Provider value={allChainsData}>
{props.children}
</AllChainsContext.Provider>
);
}
@@ -1,336 +0,0 @@
import { isProd } from "@/constants/env";
import { useAllChainsData } from "hooks/chains/allChains";
import { createContext, useCallback, useEffect, useState } from "react";
import type { ChainMetadata } from "thirdweb/chains";
// extra information on top of Chain interface
// all keys added here must be optional
export interface StoredChain extends ChainMetadata {
isModified?: boolean;
isCustom?: boolean;
isOverwritten?: boolean;
}
const MODIFIED_CHAINS_KEY = "tw-modified-chains";
const RECENTLY_USED_CHAIN_IDS_KEY = "tw-recently-used-chains";
/**
* LEGACY
*
* holds the "supported chains" array
* initially it is set to the defaultChains, then it is updated to the "allChains" with "modified chains" overrides
*/
// eslint-disable-next-line no-restricted-syntax
export const SupportedChainsContext = createContext<StoredChain[] | undefined>(
undefined,
);
/**
* LEGACY
*
* holds the "modified chains" array
*/
// eslint-disable-next-line no-restricted-syntax
const ModifiedChainsContext = createContext<StoredChain[] | undefined>(
undefined,
);
/**
* LEGACY
*
* holds the "modified chains" array
*/
// eslint-disable-next-line no-restricted-syntax
export const RemoveChainModification = createContext<
((chainId: number) => void) | undefined
>(undefined);
/**
* LEGACY
*
* holds the "recently used chain ids" array
*/
// eslint-disable-next-line no-restricted-syntax
export const RecentlyUsedChainIdsContext = createContext<number[] | undefined>(
undefined,
);
/**
* LEGACY
*
* holds the function that takes a chainId and adds it to the "recently used chains" and handles its storage
*/
// eslint-disable-next-line no-restricted-syntax
export const AddRecentlyUsedChainIdsContext = createContext<
((chainId: number) => void) | undefined
>(undefined);
/**
* LEGACY
*
* holds the function that takes the "modified chain" object
* and handles the logic of updating the "supported chains" and "modified chains" and "recently used chains"
*/
// eslint-disable-next-line no-restricted-syntax
export const ModifyChainContext = createContext<
((chain: ChainMetadata, remove?: boolean) => void) | undefined
>(undefined);
/**
* LEGACY
*
* flag indicating if the supported chains is having the final value or not
* app starts with the defaultChains as supported chains
* then allChains is dynamically imported or fetched from indexedDB, user modified chains are fetched from localStorage
* and then the final supported chains is calculated by overriding the modifiedChains on allChains
*/
// eslint-disable-next-line no-restricted-syntax
export const SupportedChainsReadyContext = createContext(false);
const replaceRpcsWithDevUrl = (chains: ChainMetadata[]) => {
if (isProd) {
return chains;
}
return chains.map((chn) => {
return {
...chn,
rpc: chn.rpc.map((rpc) =>
rpc.replace("rpc.thirdweb.com", "rpc.thirdweb-dev.com"),
),
};
});
};
/**
* if no networks are configured by the user, return the defaultChains
*/
export function ChainsProvider(props: { children: React.ReactNode }) {
const [supportedChains, setSupportedChains] = useState<StoredChain[]>([]);
const [modifiedChains, setModifiedChains] = useState<StoredChain[]>([]);
const [isSupportedChainsReady, setIsSupportedChainsReady] = useState(false);
const [recentlyUsedChainIds, setRecentlyUsedChainIds] = useState<number[]>(
[],
);
const addRecentlyUsedChainId = useCallback((chainId: number) => {
setRecentlyUsedChainIds((_recentlyUsedChainIds) => {
const newRecentlyUsedChains = _recentlyUsedChainIds.filter(
(c) => c !== chainId,
);
// add to the front of the array
newRecentlyUsedChains.unshift(chainId);
// only keep the first 5
newRecentlyUsedChains.splice(5);
return newRecentlyUsedChains;
});
}, []);
// save recently used chains to storage
// FIXME: probably want to move this to backend (similar to favorites)
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
try {
localStorage.setItem(
RECENTLY_USED_CHAIN_IDS_KEY,
JSON.stringify(recentlyUsedChainIds),
);
} catch {
localStorage.clear();
localStorage.setItem(
RECENTLY_USED_CHAIN_IDS_KEY,
JSON.stringify(recentlyUsedChainIds),
);
}
}, [recentlyUsedChainIds]);
const { allChains, chainIdToIndexRecord } = useAllChainsData();
// get recently used chains from stroage
// FIXME: probably want to move this to backend (similar to favorites)
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!isSupportedChainsReady) {
return;
}
const _recentlyUsedChainsStr = localStorage.getItem(
RECENTLY_USED_CHAIN_IDS_KEY,
);
if (!_recentlyUsedChainsStr) {
return;
}
try {
const _recentlyUsedChainIds = JSON.parse(
_recentlyUsedChainsStr,
) as number[];
setRecentlyUsedChainIds(_recentlyUsedChainIds);
} catch {
localStorage.removeItem(RECENTLY_USED_CHAIN_IDS_KEY);
}
}, [isSupportedChainsReady]);
const applyOverrides = useCallback(
(target: StoredChain[], overrides: StoredChain[]) => {
const result = [...target];
// biome-ignore lint/complexity/noForEach: FIXME
overrides.forEach((modifiedChain) => {
// if this chain is already in the supported chains, update it
if (modifiedChain.chainId in chainIdToIndexRecord) {
const i = chainIdToIndexRecord[modifiedChain.chainId];
if (!result[i]) {
throw new Error("invalid attempt to overide");
}
result[i] = modifiedChain;
} else {
result.push(modifiedChain);
}
});
return result;
},
[chainIdToIndexRecord],
);
const applyModificationsToSupportedChains = useCallback(
(newModifiedChains: ChainMetadata[]) => {
setSupportedChains((_supportedChains) => {
return applyOverrides(_supportedChains, newModifiedChains);
});
},
[applyOverrides],
);
// create supported chains and modified chains on mount
// FIXME: this should be computed not via setState
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (allChains.length === 0) {
return;
}
if (isSupportedChainsReady) {
return;
}
const allChainsReplaced = replaceRpcsWithDevUrl(allChains);
const _modifiedChains = chainStorage.get(MODIFIED_CHAINS_KEY);
if (_modifiedChains.length === 0) {
setSupportedChains(allChainsReplaced);
setIsSupportedChainsReady(true);
return;
}
const newSupportedChains = applyOverrides(
allChainsReplaced,
_modifiedChains,
);
setSupportedChains(newSupportedChains);
setModifiedChains(_modifiedChains);
setIsSupportedChainsReady(true);
}, [allChains, isSupportedChainsReady, applyOverrides]);
const modifyChain = useCallback(
(chain: StoredChain) => {
setModifiedChains((_modifiedChains) => {
const i = _modifiedChains.findIndex((c) => c.chainId === chain.chainId);
let newModifiedChains: StoredChain[];
if (i !== -1) {
// if this chain is already in the modified chains, update it
newModifiedChains = [..._modifiedChains];
newModifiedChains[i] = chain as StoredChain;
} else {
// else add it to the modified chains
newModifiedChains = [..._modifiedChains, chain as StoredChain];
}
applyModificationsToSupportedChains(newModifiedChains);
chainStorage.set(MODIFIED_CHAINS_KEY, newModifiedChains);
return newModifiedChains;
});
},
[applyModificationsToSupportedChains],
);
const removeChainModification = useCallback(
(chainId: number) => {
setModifiedChains((_modifiedChains) => {
const newModifiedChains = _modifiedChains.filter(
(c) => c.chainId !== chainId,
);
applyModificationsToSupportedChains(newModifiedChains);
chainStorage.set(MODIFIED_CHAINS_KEY, newModifiedChains);
// also remove from recently used chains
setRecentlyUsedChainIds((prevIds) => {
return prevIds.filter((id) => id !== chainId);
});
return newModifiedChains;
});
},
[applyModificationsToSupportedChains],
);
return (
<SupportedChainsContext.Provider
// always run this again
value={replaceRpcsWithDevUrl(supportedChains)}
>
<SupportedChainsReadyContext.Provider value={isSupportedChainsReady}>
<ModifiedChainsContext.Provider value={modifiedChains}>
<ModifyChainContext.Provider value={modifyChain}>
<RemoveChainModification.Provider value={removeChainModification}>
<RecentlyUsedChainIdsContext.Provider
value={recentlyUsedChainIds}
>
<AddRecentlyUsedChainIdsContext.Provider
value={addRecentlyUsedChainId}
>
{props.children}
</AddRecentlyUsedChainIdsContext.Provider>
</RecentlyUsedChainIdsContext.Provider>
</RemoveChainModification.Provider>
</ModifyChainContext.Provider>
</ModifiedChainsContext.Provider>
</SupportedChainsReadyContext.Provider>
</SupportedChainsContext.Provider>
);
}
// storage --------------------------------------------
const chainStorage = {
get(key: string): ChainMetadata[] {
try {
const networkListStr = localStorage.getItem(key);
return networkListStr ? JSON.parse(networkListStr) : [];
} catch {
// if parsing error, clear dirty storage
localStorage.removeItem(key);
}
return [];
},
set(key: string, networkList: ChainMetadata[]) {
const value = JSON.stringify(networkList);
try {
localStorage.setItem(key, value);
} catch {
// if storage limit exceed
// clear entire local storage and then try again
localStorage.clear();
localStorage.setItem(key, value);
}
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Chain } from "thirdweb";
import type { StoredChain } from "./configured-chains";
import type { StoredChain } from "../stores/chainStores";
export function mapV4ChainToV5Chain(v4Chain: StoredChain) {
const chain: Chain = {
@@ -2,9 +2,9 @@ import { WalletAddress } from "@/components/blocks/wallet-address";
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { Flex, SimpleGrid, useBreakpointValue } from "@chakra-ui/react";
import { formatDistance } from "date-fns/formatDistance";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useActiveAccount } from "thirdweb/react";
import { Badge, Card, Heading, Text } from "tw-components";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
export type AccountSignerType = {
signer: string;
@@ -20,8 +20,8 @@ interface AccountSignerProps {
export const AccountSigner: React.FC<AccountSignerProps> = ({ item }) => {
const address = useActiveAccount()?.address;
const chainId = useDashboardEVMChainId();
const configuredChainsRecord = useSupportedChainsRecord();
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
const { idToChain } = useAllChainsData();
const chain = chainId ? idToChain.get(chainId) : undefined;
const isMobile = useBreakpointValue({ base: true, md: false });
const {
isAdmin,
@@ -7,8 +7,8 @@ import { type ChangeEvent, useState } from "react";
import { prepareTransaction, toWei } from "thirdweb";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { Card } from "tw-components";
import type { StoredChain } from "../../../../contexts/configured-chains";
import { useV5DashboardChain } from "../../../../lib/v5-adapter";
import type { StoredChain } from "../../../../stores/chainStores";
interface DepositNativeProps {
address: string;
@@ -1,7 +1,7 @@
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import type { ThirdwebContract } from "thirdweb";
import { Heading } from "tw-components";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { AccountBalance } from "./components/account-balance";
import { DepositNative } from "./components/deposit-native";
import { NftsOwned } from "./components/nfts-owned";
@@ -11,9 +11,9 @@ interface AccountPageProps {
}
export const AccountPage: React.FC<AccountPageProps> = ({ contract }) => {
const configuredChainsRecord = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
const chainId = useDashboardEVMChainId();
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
const chain = chainId ? idToChain.get(chainId) : undefined;
const symbol = chain?.nativeCurrency.symbol || "Native Token";
return (
@@ -1,9 +1,9 @@
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { CURRENCIES } from "constants/currencies";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { Text } from "tw-components";
import { shortenIfAddress } from "utils/usedapp-external";
import { isAddressZero } from "utils/zeroAddress";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
interface PricePreviewProps {
price: string | number | undefined;
@@ -15,8 +15,8 @@ export const PricePreview: React.FC<PricePreviewProps> = ({
currencyAddress,
}) => {
const chainId = useDashboardEVMChainId();
const configuredChainsRecord = useSupportedChainsRecord();
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
const { idToChain } = useAllChainsData();
const chain = chainId ? idToChain.get(chainId) : undefined;
const helperCurrencies = chainId ? CURRENCIES[chainId] || [] : [];
@@ -28,7 +28,6 @@ import {
} from "components/contract-components/hooks";
import { CodeSegment } from "components/contract-tabs/code/CodeSegment";
import type { CodeEnvironment } from "components/contract-tabs/code/types";
import { useSupportedChain } from "hooks/chains/configureChains";
import { useSearchParams } from "next/navigation";
import { useMemo, useState } from "react";
import * as ERC20Ext from "thirdweb/extensions/erc20";
@@ -38,6 +37,7 @@ import * as ERC4337Ext from "thirdweb/extensions/erc4337";
import { useActiveAccount } from "thirdweb/react";
import { toFunctionSelector } from "thirdweb/utils";
import { Button, Card, Heading, Link, Text, TrackedLink } from "tw-components";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
interface CodeOverviewProps {
abi?: Abi;
@@ -592,7 +592,8 @@ export const CodeOverview: React.FC<CodeOverviewProps> = ({
}, [isERC20, isERC721, isERC1155]);
const chainId = useDashboardEVMChainId() || chainIdProp || 1;
const chainInfo = useSupportedChain(chainId || -1);
const { idToChain } = useAllChainsData();
const chainInfo = chainId ? idToChain.get(chainId) : undefined;
const functions = useContractFunctions(abi || []);
const events = useContractEvents(abi as Abi);
@@ -11,7 +11,6 @@ import {
} from "@chakra-ui/react";
import { IoMdCheckmark } from "@react-icons/all-files/io/IoMdCheckmark";
import { useTrack } from "hooks/analytics/useTrack";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useClipboard } from "hooks/useClipboard";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useMemo } from "react";
@@ -28,7 +27,8 @@ import {
Heading,
Text,
} from "tw-components";
import type { StoredChain } from "../../../../contexts/configured-chains";
import { useAllChainsData } from "../../../../hooks/chains/allChains";
import type { StoredChain } from "../../../../stores/chainStores";
interface EmbedSetupProps {
contract: ThirdwebContract;
@@ -236,11 +236,9 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
);
const chainId = useDashboardEVMChainId();
const configuredChains = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
const chain = (configuredChains[chainId as number] as
| StoredChain
| undefined) || {
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
name: "Unknown Chain",
chainId: chainId || -1,
rpc: [],
@@ -13,7 +13,6 @@ import {
useBreakpointValue,
} from "@chakra-ui/react";
import { useNFTDrawerTabs } from "core-ui/nft-drawer/useNftDrawerTabs";
import { useChainSlug } from "hooks/chains/chainSlug";
import { useRouter } from "next/router";
import { useState } from "react";
import { IoChevronBack } from "react-icons/io5";
@@ -24,6 +23,7 @@ import { useReadContract } from "thirdweb/react";
import { Badge, Button, Card, CodeBlock, Heading, Text } from "tw-components";
import { AddressCopyButton } from "tw-components/AddressCopyButton";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { useChainSlug } from "../../../../hooks/chains/chainSlug";
import { NftProperty } from "./nft-property";
function isValidUrl(possibleUrl?: string | null) {
@@ -9,7 +9,6 @@ import {
StatLabel,
StatNumber,
} from "@chakra-ui/react";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo } from "react";
import {
type ThirdwebContract,
@@ -25,6 +24,7 @@ import {
} from "thirdweb/react";
import { Card, Heading, Text } from "tw-components";
import { shortenIfAddress } from "utils/usedapp-external";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { DistributeButton } from "./components/distribute-button";
export type Balance = {
@@ -41,9 +41,9 @@ interface SplitPageProps {
export const ContractSplitPage: React.FC<SplitPageProps> = ({ contract }) => {
const address = useActiveAccount()?.address;
const configuredChainsRecord = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
const chainId = contract.chain.id;
const v4Chain = configuredChainsRecord[chainId];
const v4Chain = idToChain.get(chainId);
const contractAddress = contract.address;
const nativeBalanceQuery = useWalletBalance({
address: contractAddress,
+131 -6
View File
@@ -1,9 +1,134 @@
import { AllChainsContext } from "contexts/all-chains";
import { useContext } from "react";
import invariant from "tiny-invariant";
import { isProd } from "@/constants/env";
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import type { ChainMetadata } from "thirdweb/chains";
import { createStore, useStore } from "../../@/lib/reactive";
import {
type StoredChain,
chainOverridesStore,
} from "../../stores/chainStores";
type AllChainsStore = {
allChains: StoredChain[];
idToChain: Map<number, StoredChain | undefined>;
nameToChain: Map<string, StoredChain | undefined>;
slugToChain: Map<string, StoredChain | undefined>;
};
function createAllChainsStore() {
const store = createStore<AllChainsStore>({
allChains: [],
idToChain: new Map(),
nameToChain: new Map(),
slugToChain: new Map(),
});
const dependencies = [chainOverridesStore, originalAllChainsStore];
for (const dep of dependencies) {
dep.subscribe(() => {
updateAllChainsStore(
originalAllChainsStore.getValue(),
chainOverridesStore.getValue(),
);
});
}
function updateAllChainsStore(
originalAllChains: ChainMetadata[],
chainOverrides: StoredChain[],
) {
// if original chains are not loaded yet - ignore
if (originalAllChains.length === 0) {
return;
}
// but don't ignore if chainOverrides is empty!
const allChains: StoredChain[] = [];
const idToChain: Map<number, StoredChain | undefined> = new Map();
const nameToChain: Map<string, StoredChain | undefined> = new Map();
const slugToChain: Map<string, StoredChain | undefined> = new Map();
for (let i = 0; i < originalAllChains.length; i++) {
let _chain = originalAllChains[i];
// for dev env, replace the rpc urls to dev domain
if (!isProd) {
_chain = {
..._chain,
rpc: _chain.rpc.map((rpc) =>
rpc.replace("rpc.thirdweb.com", "rpc.thirdweb-dev.com"),
),
};
}
const chain: StoredChain =
chainOverrides.find((x) => x.chainId === _chain.chainId) || _chain;
allChains.push(chain);
idToChain.set(chain.chainId, chain);
nameToChain.set(chain.name, chain);
slugToChain.set(chain.slug, chain);
}
// add custom chains
for (const c of chainOverrides) {
if (c.isCustom) {
allChains.push(c);
idToChain.set(c.chainId, c);
nameToChain.set(c.name, c);
slugToChain.set(c.slug, c);
}
}
store.setValue({
allChains,
idToChain: idToChain,
nameToChain: nameToChain,
slugToChain: slugToChain,
});
}
return store;
}
const originalAllChainsStore = /* @__PURE__ */ createStore<ChainMetadata[]>([]);
const allChainsStore = /* @__PURE__ */ createAllChainsStore();
async function fetchChainsFromApi() {
// always fetch from prod for chains for now
// TODO: re-visit this
const res = await fetch("https://api.thirdweb.com/v1/chains");
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.data as ChainMetadata[];
}
export function useAllChainsData() {
const data = useContext(AllChainsContext);
invariant(data, "useAllChains must be used within AllChainsContext");
return data;
// trigger fetching all chains if this hook is used instead of putting this on root
// so we can avoid fetching all chains if it's not required
const allChainsQuery = useQuery({
queryKey: ["all-chains"],
queryFn: () => fetchChainsFromApi(),
});
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// already set
if (originalAllChainsStore.getValue().length > 0) {
return;
}
if (!allChainsQuery.data) {
return;
}
originalAllChainsStore.setValue(allChainsQuery.data);
}, [allChainsQuery.data]);
return useStore(allChainsStore);
}
+13 -6
View File
@@ -1,15 +1,22 @@
import { useSupportedChainsRecord } from "./configureChains";
import { useAllChainsData } from "./allChains";
/**
*
* @returns the slug for the given evm chainId
*/
export function useChainSlug(chainId: string | number) {
const configuredChainsRecord = useSupportedChainsRecord();
export function useChainSlug(chainId: string | number | undefined) {
const { idToChain } = useAllChainsData();
// EVM
if (chainId in configuredChainsRecord) {
return configuredChainsRecord[chainId as number].slug;
if (!chainId) {
return "";
}
const _chain = idToChain.get(
typeof chainId === "string" ? Number.parseInt(chainId) : chainId,
);
if (_chain) {
return _chain.slug;
}
// if can not find a network slug - use chainId as slug
@@ -1,81 +0,0 @@
import {
type StoredChain,
SupportedChainsContext,
} from "contexts/configured-chains";
import { useContext, useMemo } from "react";
import type { ChainMetadata } from "thirdweb/chains";
import invariant from "tiny-invariant";
/**
* @returns a list of all the chains that are configured
*/
export function useSupportedChains() {
const chains = useContext(SupportedChainsContext);
invariant(
chains,
"useSupportedChains must be used within a SupportedChainsContext",
);
return chains;
}
// maps chainId to Chain
type ConfiguredChainRecord = Record<number, StoredChain>;
/**
* @returns a list of record that maps configured chainId to `Chain` object
*/
export function useSupportedChainsRecord() {
const chains = useSupportedChains();
return useMemo(() => {
const record: ConfiguredChainRecord = {};
// biome-ignore lint/complexity/noForEach: FIXME
chains.forEach((network) => {
record[network.chainId] = network;
});
return record;
}, [chains]);
}
/**
* @returns a list of record that maps configured chainId to `Chain` object
*/
export function useSupportedChainsNameRecord() {
const chains = useSupportedChains();
return useMemo(() => {
const record: Record<string, StoredChain | undefined> = {};
// biome-ignore lint/complexity/noForEach: FIXME
chains.forEach((network) => {
record[network.name] = network;
});
return record;
}, [chains]);
}
/**
* @returns a list of record that maps configured chainSlug to `Chain` object
*/
export function useSupportedChainsSlugRecord() {
const chains = useSupportedChains();
return useMemo(() => {
const record: Record<string, ChainMetadata> = {};
// biome-ignore lint/complexity/noForEach: FIXME
chains.forEach((network) => {
record[network.slug] = network;
});
return record;
}, [chains]);
}
/**
* @returns `Chain` object for the given chainId if it is configured, otherwise `undefined`
*/
export function useSupportedChain(chainId: number) {
const record = useSupportedChainsRecord();
if (chainId in record) {
return record[chainId];
}
}
@@ -1,40 +0,0 @@
import {
AddRecentlyUsedChainIdsContext,
RecentlyUsedChainIdsContext,
SupportedChainsReadyContext,
} from "contexts/configured-chains";
import { useContext, useMemo } from "react";
import invariant from "tiny-invariant";
import { useSupportedChainsRecord } from "./configureChains";
export function useRecentlyUsedChains() {
const recentlyUsedChainIds = useContext(RecentlyUsedChainIdsContext);
const supportedChainsRecord = useSupportedChainsRecord();
const isSupportedChainsReady = useContext(SupportedChainsReadyContext);
const recentlyUsedChains = useMemo(() => {
if (!recentlyUsedChainIds || !isSupportedChainsReady) {
return [];
}
return recentlyUsedChainIds.map(
(chainId) => supportedChainsRecord[chainId],
);
}, [supportedChainsRecord, recentlyUsedChainIds, isSupportedChainsReady]);
invariant(
recentlyUsedChainIds,
"useRecentlyUsedChains must be used within RecentlyUsedChainIdsContext",
);
return recentlyUsedChains;
}
export function useAddRecentlyUsedChainId() {
const context = useContext(AddRecentlyUsedChainIdsContext);
invariant(
context,
"useAddRecentlyUsedChainId must be used within AddRecentlyUsedChainIdsContext",
);
return context;
}
@@ -1,21 +0,0 @@
import {
ModifyChainContext,
RemoveChainModification,
} from "contexts/configured-chains";
import { useContext } from "react";
import invariant from "tiny-invariant";
export function useModifyChain() {
const context = useContext(ModifyChainContext);
invariant(context, "useModifyChain must be used within ModifyChainContext");
return context;
}
export function useRemoveChainModification() {
const context = useContext(RemoveChainModification);
invariant(
context,
"useRemoveChainModification must be used within ModifyChainContext",
);
return context;
}
+8 -17
View File
@@ -2,7 +2,7 @@ import { defineDashboardChain } from "lib/defineDashboardChain";
import { useMemo } from "react";
import type { Chain, ChainMetadata } from "thirdweb/chains";
import { useActiveWalletChain } from "thirdweb/react";
import { useSupportedChainsRecord } from "../hooks/chains/configureChains";
import { useAllChainsData } from "../hooks/chains/allChains";
export function useV5DashboardChain(chainId: undefined): undefined;
export function useV5DashboardChain(chainId: number): Chain;
@@ -12,23 +12,17 @@ export function useV5DashboardChain(
export function useV5DashboardChain(
chainId: number | undefined,
): Chain | undefined {
const configuredChainsRecord = useSupportedChainsRecord();
const { idToChain } = useAllChainsData();
// memo is very very important!
return useMemo(() => {
let configuedChain = undefined;
if (chainId === undefined) {
return undefined;
}
if (chainId in configuredChainsRecord) {
configuedChain = configuredChainsRecord[chainId as number];
}
// eslint-disable-next-line no-restricted-syntax
return defineDashboardChain(chainId, configuedChain);
}, [chainId, configuredChainsRecord]);
return defineDashboardChain(chainId, idToChain.get(chainId));
}, [chainId, idToChain]);
}
/**
@@ -36,14 +30,11 @@ export function useV5DashboardChain(
*/
export function useActiveChainAsDashboardChain(): ChainMetadata | undefined {
// eslint-disable-next-line no-restricted-syntax
const activeChain = useActiveWalletChain()?.id;
const configuredChainsRecord = useSupportedChainsRecord();
const activeChainId = useActiveWalletChain()?.id;
const { idToChain } = useAllChainsData();
// memo is very very important!
return useMemo(() => {
if (activeChain && activeChain in configuredChainsRecord) {
return configuredChainsRecord[activeChain as number];
}
return undefined;
}, [activeChain, configuredChainsRecord]);
return activeChainId ? idToChain.get(activeChainId) : undefined;
}, [activeChainId, idToChain]);
}
@@ -2,6 +2,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert } from "@/components/ui/alert";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { useStore } from "@/lib/reactive";
import {
type EVMContractInfo,
useEVMContractInfo,
@@ -20,15 +21,10 @@ import { ensQuery } from "components/contract-components/hooks";
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import { THIRDWEB_DOMAIN } from "constants/urls";
import { SupportedChainsReadyContext } from "contexts/configured-chains";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { PrimaryDashboardButton } from "contract-ui/components/primary-dashboard-button";
import { useContractRouteConfig } from "contract-ui/hooks/useRouteConfig";
import { ContractSidebar } from "core-ui/sidebar/detail-page";
import {
useSupportedChainsRecord,
useSupportedChainsSlugRecord,
} from "hooks/chains/configureChains";
import { getDashboardChainRpc } from "lib/rpc";
import { resolveFunctionSelectors } from "lib/selectors";
import { useV5DashboardChain } from "lib/v5-adapter";
@@ -37,7 +33,7 @@ import { NextSeo } from "next-seo";
import { useRouter } from "next/router";
import { ContractOG } from "og-lib/url-utils";
import { PageId } from "page-id";
import { useContext, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
type ThirdwebContract,
getAddress,
@@ -52,6 +48,8 @@ import { stringify } from "thirdweb/utils";
import { fetchChain } from "utils/fetchChain";
import type { ThirdwebNextPage } from "utils/types";
import { shortenIfAddress } from "utils/usedapp-external";
import { useAllChainsData } from "../../hooks/chains/allChains";
import { isChainOverridesLoadedStore } from "../../stores/chainStores";
type EVMContractProps = {
contractInfo?: EVMContractInfo;
@@ -68,7 +66,7 @@ type EVMContractProps = {
const ContractPage: ThirdwebNextPage = () => {
// show optimistic UI first - assume chain is configured until proven otherwise
const [chainNotFound, setChainNotFound] = useState(false);
const isSupportedChainsReady = useContext(SupportedChainsReadyContext);
const isChainOverridesLoaded = useStore(isChainOverridesLoadedStore);
const contractInfo = useEVMContractInfo();
@@ -77,13 +75,12 @@ const ContractPage: ThirdwebNextPage = () => {
const contractAddress = contractInfo?.contractAddress || "";
const setContractInfo = useSetEVMContractInfo();
const supportedChainsSlugRecord = useSupportedChainsSlugRecord();
const configuredChainsRecord = useSupportedChainsRecord();
const { idToChain, slugToChain } = useAllChainsData();
// this will go away as part of the RSC rewrite!
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!isSupportedChainsReady || !chainSlug) {
if (!isChainOverridesLoaded || !chainSlug) {
return;
}
@@ -95,7 +92,7 @@ const ContractPage: ThirdwebNextPage = () => {
// currently user can only update RPC - so check if it is updated or not
// if updated, update the contractInfo.chain
const configuredChain = supportedChainsSlugRecord[chainSlug];
const configuredChain = slugToChain.get(chainSlug);
if (
configuredChain &&
getDashboardChainRpc(configuredChain.chainId, configuredChain) !==
@@ -116,32 +113,32 @@ const ContractPage: ThirdwebNextPage = () => {
// if server could not resolve the chain using allChains
else {
const chainFoundBySlug = slugToChain.get(chainSlug);
const chainFoundById = idToChain.get(Number(chainSlug));
// if it is configured on client storage, use that
if (chainSlug in supportedChainsSlugRecord) {
if (chainFoundBySlug) {
setContractInfo({
chainSlug,
contractAddress,
chain: supportedChainsSlugRecord[chainSlug],
chain: chainFoundBySlug,
});
} else if (chainSlug in configuredChainsRecord) {
} else if (chainFoundById) {
// this is for thirdweb internal tools
// it allows us to use chainId as slug for a custom network as well
const chainId = Number(chainSlug);
const _chain = configuredChainsRecord[chainId];
// replace the chainId with slug in URL without reloading the page
// If we don't do this, tanstack router creates issues
window.history.replaceState(
null,
document.title,
`/${_chain.slug}/${contractAddress}`,
`/${chainSlug}/${contractAddress}`,
);
setContractInfo({
chainSlug: _chain.slug,
chainSlug,
contractAddress,
chain: _chain,
chain: chainFoundById,
});
}
@@ -154,11 +151,11 @@ const ContractPage: ThirdwebNextPage = () => {
}, [
chain,
chainSlug,
supportedChainsSlugRecord,
configuredChainsRecord,
idToChain,
slugToChain,
contractAddress,
setContractInfo,
isSupportedChainsReady,
isChainOverridesLoaded,
]);
const isSlugNumber = !Number.isNaN(Number(chainSlug));
@@ -216,7 +213,7 @@ const ContractPage: ThirdwebNextPage = () => {
);
}
if (!isSupportedChainsReady) {
if (!isChainOverridesLoaded) {
return (
<div className="h-full flex items-center justify-center">
<Spinner className="size-10" />
@@ -0,0 +1,45 @@
import { type Store, useStore } from "@/lib/reactive";
import { useEffect } from "react";
const loadedStoreKeys = new Set<string>();
export function SyncStoreToStorage<T>(props: {
store: Store<T>;
storageKey: string;
onLoaded?: () => void;
}) {
const storeValue = useStore(props.store);
// load from storage
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (loadedStoreKeys.has(props.storageKey)) {
return;
}
try {
const storedValueStr = localStorage.getItem(props.storageKey);
if (storedValueStr) {
const parsedValue = JSON.parse(storedValueStr);
props.store.setValue(parsedValue);
}
} catch {
// ignore errors
}
loadedStoreKeys.add(props.storageKey);
props.onLoaded?.();
}, [props.store, props.storageKey, props.onLoaded]);
// save changes to storage
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
try {
localStorage.setItem(props.storageKey, JSON.stringify(storeValue));
} catch {
// ignore
}
}, [storeValue, props.storageKey]);
return null;
}
+80
View File
@@ -0,0 +1,80 @@
"use client";
import type { ChainMetadata } from "thirdweb/chains";
import { createStore } from "../@/lib/reactive";
import { SyncStoreToStorage } from "./SyncStoreToStorage";
export interface StoredChain extends ChainMetadata {
// any of the name, logo, rpc etc is modified, testnet or not
// user can not change chainId, symbol, slug
isModified?: boolean;
// if this chain (id) is not in our db, user added it
isCustom?: boolean;
// TODO - remove this, we can not allow this anymore
isOverwritten?: boolean;
}
export const isChainOverridesLoadedStore = createStore(false);
export const chainOverridesStore = createStore<StoredChain[]>([]);
export const recentlyUsedChainIdsStore = createStore<number[]>([]);
export function SyncChainStores() {
return (
<>
<SyncStoreToStorage
storageKey="TW_LOCAL_CHAIN_STORE"
store={chainOverridesStore}
onLoaded={() => {
console.log("chain overrides loaded");
isChainOverridesLoadedStore.setValue(true);
}}
/>
<SyncStoreToStorage
storageKey="TW_RECENTLY_USED_CHAIN_IDS"
store={recentlyUsedChainIdsStore}
/>
</>
);
}
export function addRecentlyUsedChainId(chainId: number) {
const newRecentlyUsedChains = recentlyUsedChainIdsStore
.getValue()
.filter((c) => c !== chainId);
// add to the front of the array
newRecentlyUsedChains.unshift(chainId);
// only keep the first 5
newRecentlyUsedChains.splice(5);
recentlyUsedChainIdsStore.setValue(newRecentlyUsedChains);
}
export function addChainOverrides(newChain: StoredChain) {
const currentChains = chainOverridesStore.getValue();
const index = currentChains.findIndex((c) => c.chainId === newChain.chainId);
let newChains: StoredChain[];
// if this chain is already in the chains, update it in array
if (index !== -1) {
newChains = [...currentChains];
newChains[index] = newChain;
}
// else append it
else {
newChains = [...currentChains, newChain];
}
chainOverridesStore.setValue(newChains);
}
export function removeChainOverrides(id: number) {
chainOverridesStore.setValue(
chainOverridesStore.getValue().filter((c) => c.chainId !== id),
);
}