Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/components/UnsupportedChain.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { SUPPORTED_CHAINS } from '@/config';
import { AlertOctagon } from 'lucide-react';
import useUserStore from '@/stores/useUser.store';
import { useAccount } from 'wagmi';
import { Alert, AlertDescription, AlertTitle } from './ui/alert';

const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map((chain) => chain.id);

export function UnsupportedChain() {
const { isConnected, chainId } = useUserStore();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chainId from store may be different from account's chainId


const { isConnected, chainId } = useAccount();
const isChainSupported =
chainId !== undefined && SUPPORTED_CHAIN_IDS.includes(chainId);

if (isChainSupported || !isConnected) {
if (!isConnected || isChainSupported) {
return null;
}

Expand Down
22 changes: 8 additions & 14 deletions src/components/navbar/ChainSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SUPPORTED_CHAINS } from '@/config.ts';
import { useRouter } from '@tanstack/react-router';
import { useChainSwitch } from '@/hooks/useChainSwitch.ts';
import useUserStore from '@/stores/useUser.store.ts';
import {
Select,
Expand All @@ -11,23 +11,17 @@ import {

export function ChainSelector() {
const { chainId } = useUserStore();
const { navigate } = useRouter();

const { requestChainChange } = useChainSwitch();
const handleChainChange = async (value: string) => {
const newChainSlug = SUPPORTED_CHAINS.find(
(chain) => chain.id === Number(value)
)?.slug;
const pathParts = location.pathname.split('/').filter(Boolean);
const newPath =
pathParts.length > 1
? `/${newChainSlug}/${pathParts.slice(1).join('/')}`
: `/${newChainSlug}`;

navigate({ to: newPath });
Comment on lines -17 to -26
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed side effect in the component implementation, chain navigation is handled by a single component

requestChainChange(Number(value));
};

return (
<Select value={chainId.toString()} onValueChange={handleChainChange}>
<Select
value={chainId?.toString()}
onValueChange={handleChainChange}
defaultValue="-1"
>
<SelectTrigger>
<SelectValue placeholder="Select Chain" />
</SelectTrigger>
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import arbitrumSepoliaIcon from './assets/chain-icons/arbitrum-sepolia.svg';
import iexecLogo from './assets/iexec-logo.svg';
import { bellecour, arbitrumSepolia } from './utils/wagmiNetworks';

export const PREVIEW_TABLE_LENGTH = 5;
export const TABLE_LENGTH = 16;
Expand All @@ -14,6 +15,7 @@ export const SUPPORTED_CHAINS = [
icon: iexecLogo,
blockExplorerUrl: 'https://blockscout-bellecour.iex.ec',
subgraphUrl: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5',
wagmiNetwork: bellecour,
},
{
id: 421614,
Expand All @@ -24,5 +26,6 @@ export const SUPPORTED_CHAINS = [
blockExplorerUrl: 'https://sepolia.arbiscan.io/',
subgraphUrl:
'http://localhost:8080/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz',
wagmiNetwork: arbitrumSepolia,
},
];
5 changes: 4 additions & 1 deletion src/graphql/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import type { TypedDocumentString } from './graphql'

export async function execute<TResult, TVariables>(
query: TypedDocumentString<TResult, TVariables>,
chainId: number,
chainId?: number,
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
) {
if (!chainId) {
throw Error('Missing chainId')
}
Comment on lines +9 to +11
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chainId initial state is undefined

const subgraphUrl = getSubgraphUrl(chainId);
const response = await fetch(subgraphUrl, {
method: 'POST',
Expand Down
100 changes: 100 additions & 0 deletions src/hooks/ChainSyncManger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useParams, useRouter } from '@tanstack/react-router';
import { switchChain } from '@wagmi/core';
import { useEffect, useRef } from 'react';
import { useAccount } from 'wagmi';
import useUserStore from '@/stores/useUser.store';
import { getChainFromId, INITIAL_CHAIN } from '@/utils/chain.utils';
import { wagmiAdapter } from '@/utils/wagmiConfig';

/**
* Synchronize URL, account and app state
*
* - keep the global store up to date with user's account state
* - keep the URL up to date with the chain
* - request chain changes when user's account connects to the wrong chain
*/
export function ChainSyncManager() {
const { chainSlug } = useParams({ from: '/$chainSlug' });
const { navigate, history } = useRouter();
const { pathname } = history.location;
const {
chain: accountChain,
address: accountAddress,
isConnected: accountIsConnected,
status: accountStatus,
} = useAccount();
const { chainId, setChainId, setIsConnected, setAddress } = useUserStore();

const isNavigating = useRef(false);
const previousAccountStatus = useRef<string | undefined>(undefined);

// init store's chain from location (once at mount time)
useEffect(() => {
setChainId(INITIAL_CHAIN.id);
}, []);

// update store with user account's state
useEffect(() => {
setIsConnected(accountIsConnected);
setAddress(accountAddress);
if (accountChain?.id && chainId !== accountChain?.id) {
setChainId(accountChain?.id);
}
}, [
accountAddress,
accountIsConnected,
setIsConnected,
setAddress,
chainId,
accountChain?.id,
setChainId,
]);

// request chain change if the user connects on chain different from the active chain
useEffect(() => {
// auto reconnection case connect to the initial chain
if (
(previousAccountStatus.current === undefined ||
previousAccountStatus.current === 'reconnecting') &&
accountChain?.id &&
INITIAL_CHAIN.id !== accountChain?.id
) {
switchChain(wagmiAdapter.wagmiConfig, { chainId: INITIAL_CHAIN.id });
}
// connection case connect to the selected chain
if (
previousAccountStatus.current === 'connecting' &&
chainId &&
accountChain?.id &&
chainId !== accountChain?.id
) {
switchChain(wagmiAdapter.wagmiConfig, { chainId });
}

previousAccountStatus.current = accountStatus;
}, [accountChain?.id, chainId, accountStatus]);

// Sync URL with store's chain
useEffect(() => {
if (!chainId) {
return;
}
const slug = getChainFromId(chainId)?.slug;

if (slug !== chainSlug && !isNavigating.current) {
const [, ...rest] = pathname.split('/').filter(Boolean);
const newPath = `/${slug}/${rest.join('/')}`;
isNavigating.current = true;
const navigationResult = navigate({ to: newPath, replace: true });
if (navigationResult instanceof Promise) {
navigationResult.finally(() => {
isNavigating.current = false;
});
} else {
isNavigating.current = false;
}
}
}, [chainId, chainSlug, navigate, pathname]);

return null;
}
24 changes: 24 additions & 0 deletions src/hooks/useChainSwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { switchChain } from '@wagmi/core';
import { useAccount } from 'wagmi';
import useUserStore from '@/stores/useUser.store';
import { wagmiAdapter } from '@/utils/wagmiConfig';

export function useChainSwitch() {
const { isConnected } = useAccount();
const { setChainId } = useUserStore();
/**
* request a chain change
*
* the change is either:
* - immediately effective if the user is not connected
* - delegated to the user's account provider if the user is connected
*/
async function requestChainChange(chainId: number) {
if (isConnected) {
switchChain(wagmiAdapter.wagmiConfig, { chainId });
} else {
setChainId(chainId);
}
}
return { requestChainChange };
}
21 changes: 0 additions & 21 deletions src/hooks/useSyncAccountWithUserStore.ts

This file was deleted.

57 changes: 0 additions & 57 deletions src/hooks/useSyncChain.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/routes/$chainSlug/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { useSyncChain } from '@/hooks/useSyncChain';

export const Route = createFileRoute('/$chainSlug/_layout')({
component: RouteComponent,
});

function RouteComponent() {
useSyncChain();

return <Outlet />;
}
5 changes: 2 additions & 3 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
import { Footer } from '@/components/Footer';
import { UnsupportedChain } from '@/components/UnsupportedChain';
import { Navbar } from '@/components/navbar/NavBar';
import { useSyncAccountWithUserStore } from '@/hooks/useSyncAccountWithUserStore';
import { ChainSyncManager } from '@/hooks/ChainSyncManger';

export const Route = createRootRoute({
component: Root,
});

function Root() {
useSyncAccountWithUserStore();

return (
<div className="mx-auto mb-20 w-full px-6 md:px-10 lg:px-20">
<ChainSyncManager />
<Navbar />
<UnsupportedChain />
<Outlet />
Expand Down
12 changes: 3 additions & 9 deletions src/stores/useUser.store.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { SUPPORTED_CHAINS } from '@/config';
import { Address } from '@/types';
import type { Connector } from 'wagmi';
import { create } from 'zustand';

type UserState = {
connector: Connector | undefined;
setConnector: (param: Connector | undefined) => void;
isConnected: boolean;
setIsConnected: (param: boolean) => void;
isInitialized: boolean;
setInitialized: (isInitialized: boolean) => void;
address: Address | undefined;
setAddress: (param: Address | undefined) => void;
chainId: number;
chainId: number | undefined;
setChainId: (param: number) => void;
};

const useUserStore = create<UserState>((set) => ({
connector: undefined,
setConnector: (connector: Connector | undefined) => set({ connector }),
isConnected: false,
setIsConnected: (isConnected: boolean) => set({ isConnected }),
isInitialized: false,
Expand All @@ -27,8 +21,8 @@ const useUserStore = create<UserState>((set) => ({
setAddress: (address: Address | undefined) => {
set({ address: address?.toLowerCase() as Address });
},
chainId: SUPPORTED_CHAINS[0].id,
setChainId: (chainId: number | undefined) => {
chainId: undefined,
setChainId: (chainId: number) => {
set({ chainId: chainId });
},
}));
Expand Down
7 changes: 7 additions & 0 deletions src/utils/chain.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ export function getBlockExplorerUrl(chainId: number) {
const chain = getChainFromId(chainId);
return chain?.blockExplorerUrl ?? 'https://blockscout.com/';
}

/**
* initial chain evaluated once against the current location when the app loads
*/
export const INITIAL_CHAIN =
getChainFromSlug(new URL(window.location.href).pathname.split('/')[1]) ||
SUPPORTED_CHAINS[0];
Loading