Skip to content

Commit 59a355d

Browse files
refactor: introduce ChainSyncManager to keep chain state up to date
1 parent 862ac61 commit 59a355d

File tree

14 files changed

+174
-125
lines changed

14 files changed

+174
-125
lines changed

src/components/UnsupportedChain.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { SUPPORTED_CHAINS } from '@/config';
22
import { AlertOctagon } from 'lucide-react';
3-
import useUserStore from '@/stores/useUser.store';
3+
import { useAccount } from 'wagmi';
44
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
55

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

88
export function UnsupportedChain() {
9-
const { isConnected, chainId } = useUserStore();
10-
9+
const { isConnected, chainId } = useAccount();
1110
const isChainSupported =
1211
chainId !== undefined && SUPPORTED_CHAIN_IDS.includes(chainId);
1312

14-
if (isChainSupported || !isConnected) {
13+
if (!isConnected || isChainSupported) {
1514
return null;
1615
}
1716

src/components/navbar/ChainSelector.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SUPPORTED_CHAINS } from '@/config.ts';
2-
import { useRouter } from '@tanstack/react-router';
2+
import { useChainSwitch } from '@/hooks/useChainSwitch.ts';
33
import useUserStore from '@/stores/useUser.store.ts';
44
import {
55
Select,
@@ -11,23 +11,17 @@ import {
1111

1212
export function ChainSelector() {
1313
const { chainId } = useUserStore();
14-
const { navigate } = useRouter();
15-
14+
const { requestChainChange } = useChainSwitch();
1615
const handleChainChange = async (value: string) => {
17-
const newChainSlug = SUPPORTED_CHAINS.find(
18-
(chain) => chain.id === Number(value)
19-
)?.slug;
20-
const pathParts = location.pathname.split('/').filter(Boolean);
21-
const newPath =
22-
pathParts.length > 1
23-
? `/${newChainSlug}/${pathParts.slice(1).join('/')}`
24-
: `/${newChainSlug}`;
25-
26-
navigate({ to: newPath });
16+
requestChainChange(Number(value));
2717
};
2818

2919
return (
30-
<Select value={chainId.toString()} onValueChange={handleChainChange}>
20+
<Select
21+
value={chainId?.toString()}
22+
onValueChange={handleChainChange}
23+
defaultValue="-1"
24+
>
3125
<SelectTrigger>
3226
<SelectValue placeholder="Select Chain" />
3327
</SelectTrigger>

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import arbitrumSepoliaIcon from './assets/chain-icons/arbitrum-sepolia.svg';
22
import iexecLogo from './assets/iexec-logo.svg';
3+
import { bellecour, arbitrumSepolia } from './utils/wagmiNetworks';
34

45
export const PREVIEW_TABLE_LENGTH = 5;
56
export const TABLE_LENGTH = 16;
@@ -14,6 +15,7 @@ export const SUPPORTED_CHAINS = [
1415
icon: iexecLogo,
1516
blockExplorerUrl: 'https://blockscout-bellecour.iex.ec',
1617
subgraphUrl: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5',
18+
wagmiNetwork: bellecour,
1719
},
1820
{
1921
id: 421614,
@@ -24,5 +26,6 @@ export const SUPPORTED_CHAINS = [
2426
blockExplorerUrl: 'https://sepolia.arbiscan.io/',
2527
subgraphUrl:
2628
'http://localhost:8080/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz',
29+
wagmiNetwork: arbitrumSepolia,
2730
},
2831
];

src/graphql/execute.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import type { TypedDocumentString } from './graphql'
33

44
export async function execute<TResult, TVariables>(
55
query: TypedDocumentString<TResult, TVariables>,
6-
chainId: number,
6+
chainId?: number,
77
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
88
) {
9+
if (!chainId) {
10+
throw Error('Missing chainId')
11+
}
912
const subgraphUrl = getSubgraphUrl(chainId);
1013
const response = await fetch(subgraphUrl, {
1114
method: 'POST',

src/hooks/ChainSyncManger.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useParams, useRouter } from '@tanstack/react-router';
2+
import { switchChain } from '@wagmi/core';
3+
import { useEffect, useRef } from 'react';
4+
import { useAccount } from 'wagmi';
5+
import useUserStore from '@/stores/useUser.store';
6+
import { getChainFromId, INITIAL_CHAIN } from '@/utils/chain.utils';
7+
import { wagmiAdapter } from '@/utils/wagmiConfig';
8+
9+
/**
10+
* Synchronize URL, account and app state
11+
*
12+
* - keep the global store up to date with user's account state
13+
* - keep the URL up to date with the chain
14+
* - request chain changes when user's account connects to the wrong chain
15+
*/
16+
export function ChainSyncManager() {
17+
const { chainSlug } = useParams({ from: '/$chainSlug' });
18+
const { navigate, history } = useRouter();
19+
const { pathname } = history.location;
20+
const {
21+
chain: accountChain,
22+
address: accountAddress,
23+
isConnected: accountIsConnected,
24+
status: accountStatus,
25+
} = useAccount();
26+
const { chainId, setChainId, setIsConnected, setAddress } = useUserStore();
27+
28+
const isNavigating = useRef(false);
29+
const previousAccountStatus = useRef<string | undefined>(undefined);
30+
31+
// init store's chain from location (once at mount time)
32+
useEffect(() => {
33+
setChainId(INITIAL_CHAIN.id);
34+
}, []);
35+
36+
// update store with user account's state
37+
useEffect(() => {
38+
setIsConnected(accountIsConnected);
39+
setAddress(accountAddress);
40+
if (accountChain?.id && chainId !== accountChain?.id) {
41+
setChainId(accountChain?.id);
42+
}
43+
}, [
44+
accountAddress,
45+
accountIsConnected,
46+
setIsConnected,
47+
setAddress,
48+
chainId,
49+
accountChain?.id,
50+
setChainId,
51+
]);
52+
53+
// request chain change if the user connects on chain different from the active chain
54+
useEffect(() => {
55+
// auto reconnection case connect to the initial chain
56+
if (
57+
(previousAccountStatus.current === undefined ||
58+
previousAccountStatus.current === 'reconnecting') &&
59+
accountChain?.id &&
60+
INITIAL_CHAIN.id !== accountChain?.id
61+
) {
62+
switchChain(wagmiAdapter.wagmiConfig, { chainId: INITIAL_CHAIN.id });
63+
}
64+
// connection case connect to the selected chain
65+
if (
66+
previousAccountStatus.current === 'connecting' &&
67+
chainId &&
68+
accountChain?.id &&
69+
chainId !== accountChain?.id
70+
) {
71+
switchChain(wagmiAdapter.wagmiConfig, { chainId });
72+
}
73+
74+
previousAccountStatus.current = accountStatus;
75+
}, [accountChain?.id, chainId, accountStatus]);
76+
77+
// Sync URL with store's chain
78+
useEffect(() => {
79+
if (!chainId) {
80+
return;
81+
}
82+
const slug = getChainFromId(chainId)?.slug;
83+
84+
if (slug !== chainSlug && !isNavigating.current) {
85+
const [, ...rest] = pathname.split('/').filter(Boolean);
86+
const newPath = `/${slug}/${rest.join('/')}`;
87+
isNavigating.current = true;
88+
const navigationResult = navigate({ to: newPath, replace: true });
89+
if (navigationResult instanceof Promise) {
90+
navigationResult.finally(() => {
91+
isNavigating.current = false;
92+
});
93+
} else {
94+
isNavigating.current = false;
95+
}
96+
}
97+
}, [chainId, chainSlug, navigate, pathname]);
98+
99+
return null;
100+
}

src/hooks/useChainSwitch.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { switchChain } from '@wagmi/core';
2+
import { useAccount } from 'wagmi';
3+
import useUserStore from '@/stores/useUser.store';
4+
import { wagmiAdapter } from '@/utils/wagmiConfig';
5+
6+
export function useChainSwitch() {
7+
const { isConnected } = useAccount();
8+
const { setChainId } = useUserStore();
9+
/**
10+
* request a chain change
11+
*
12+
* the change is either:
13+
* - immediately effective if the user is not connected
14+
* - delegated to the user's account provider if the user is connected
15+
*/
16+
async function requestChainChange(chainId: number) {
17+
if (isConnected) {
18+
switchChain(wagmiAdapter.wagmiConfig, { chainId });
19+
} else {
20+
setChainId(chainId);
21+
}
22+
}
23+
return { requestChainChange };
24+
}

src/hooks/useSyncAccountWithUserStore.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/hooks/useSyncChain.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

src/routes/$chainSlug/_layout.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { createFileRoute, Outlet } from '@tanstack/react-router';
2-
import { useSyncChain } from '@/hooks/useSyncChain';
32

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

87
function RouteComponent() {
9-
useSyncChain();
10-
118
return <Outlet />;
129
}

src/routes/__root.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
33
import { Footer } from '@/components/Footer';
44
import { UnsupportedChain } from '@/components/UnsupportedChain';
55
import { Navbar } from '@/components/navbar/NavBar';
6-
import { useSyncAccountWithUserStore } from '@/hooks/useSyncAccountWithUserStore';
6+
import { ChainSyncManager } from '@/hooks/ChainSyncManger';
77

88
export const Route = createRootRoute({
99
component: Root,
1010
});
1111

1212
function Root() {
13-
useSyncAccountWithUserStore();
14-
1513
return (
1614
<div className="mx-auto mb-20 w-full px-6 md:px-10 lg:px-20">
15+
<ChainSyncManager />
1716
<Navbar />
1817
<UnsupportedChain />
1918
<Outlet />

0 commit comments

Comments
 (0)