Skip to content

Commit 95a42f9

Browse files
committed
feat: add ChainSelector component and related utilities for chain management
1 parent 18142ab commit 95a42f9

File tree

6 files changed

+203
-1
lines changed

6 files changed

+203
-1
lines changed

.vitepress/config.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ export default defineConfig({
9595
{ text: 'Build iApp', link: '/build-iapp/what-is-iapp' },
9696
{ text: 'Use iApp', link: '/use-iapp/introduction' },
9797
{ text: 'Protocol', link: '/protocol/sdk' },
98+
{
99+
component: 'ChainSelector',
100+
props: {
101+
className: 'w-48',
102+
},
103+
},
98104
],
99105
outline: {
100106
level: [2, 4],

.vitepress/theme/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import 'virtual:group-icons.css';
99
import '@shikijs/vitepress-twoslash/style.css';
1010
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
1111
import { WagmiPlugin } from '@wagmi/vue';
12-
import { wagmiAdapter } from '../../src/utils/wagmiConfig';
12+
import { createPinia } from 'pinia';
13+
import { wagmiAdapter } from '@/utils/wagmiConfig';
14+
import ChainSelector from '@/components/ChainSelector.vue';
1315
import './style.css';
1416

1517
export default {
@@ -19,11 +21,15 @@ export default {
1921
app.use(TwoslashFloatingVue as any);
2022

2123
const queryClient = new QueryClient();
24+
const pinia = createPinia();
2225

26+
app.use(pinia);
2327
app.use(VueQueryPlugin, { queryClient });
2428

2529
app.use(WagmiPlugin, { config: wagmiAdapter.wagmiConfig });
2630

31+
app.component('ChainSelector', ChainSelector);
32+
2733
googleAnalytics({
2834
id: 'GTM-P7KSD4T',
2935
});

src/components/ChainSelector.vue

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<template>
2+
<Select
3+
v-model="selectedChainId"
4+
:placeholder="'Select Chain'"
5+
:class="className"
6+
@update:model-value="handleChainChange"
7+
>
8+
<SelectItem
9+
v-for="chain in filteredChains"
10+
:key="chain.id"
11+
:value="chain.id.toString()"
12+
:class="className"
13+
>
14+
<div class="flex items-center gap-2">
15+
<img :src="chain.icon" class="size-4" :alt="chain.name" />
16+
{{ chain.name }}
17+
</div>
18+
</SelectItem>
19+
</Select>
20+
</template>
21+
22+
<script setup lang="ts">
23+
import { computed } from 'vue';
24+
import { useAccount } from '@wagmi/vue';
25+
import { useChainSwitch } from '@/hooks/useChainSwitch';
26+
import { getSupportedChains, getChainById } from '@/utils/chain.utils';
27+
import useUserStore from '@/stores/useUser.store';
28+
import Select from '@/components/ui/Select.vue';
29+
import SelectItem from '@/components/ui/SelectItem.vue';
30+
31+
interface Props {
32+
className?: string;
33+
}
34+
35+
defineProps<Props>();
36+
37+
// Composables
38+
const { chainId } = useAccount();
39+
const { requestChainChange } = useChainSwitch();
40+
const userStore = useUserStore();
41+
42+
// Data
43+
const filteredChains = getSupportedChains();
44+
45+
// Computed
46+
const selectedChainId = computed({
47+
get: () => {
48+
return (chainId.value || userStore.chainId || -1).toString();
49+
},
50+
set: (value: string) => {
51+
const numericValue = Number(value);
52+
if (numericValue !== -1) {
53+
const chain = getChainById(numericValue);
54+
if (chain) {
55+
userStore.setSelectedChain(chain);
56+
}
57+
}
58+
},
59+
});
60+
61+
// Methods
62+
async function handleChainChange(value: string) {
63+
const numericValue = Number(value);
64+
65+
if (numericValue === -1) return;
66+
67+
const chain = getChainById(numericValue);
68+
if (chain) {
69+
userStore.setSelectedChain(chain);
70+
71+
await requestChainChange(numericValue);
72+
}
73+
}
74+
</script>

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/vue';
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/stores/useUser.store.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defineStore } from 'pinia';
2+
import { ref } from 'vue';
3+
import type { Chain } from '@/utils/chain.utils';
4+
5+
export const useUserStore = defineStore('user', () => {
6+
// State
7+
const chainId = ref<number | undefined>(undefined);
8+
const selectedChain = ref<Chain | undefined>(undefined);
9+
10+
// Actions
11+
function setChainId(newChainId: number) {
12+
chainId.value = newChainId;
13+
}
14+
15+
function setSelectedChain(chain: Chain) {
16+
selectedChain.value = chain;
17+
chainId.value = chain.id;
18+
}
19+
20+
function clearChain() {
21+
chainId.value = undefined;
22+
selectedChain.value = undefined;
23+
}
24+
25+
// Getters
26+
const getCurrentChainId = () => chainId.value;
27+
const getCurrentChain = () => selectedChain.value;
28+
29+
return {
30+
// State
31+
chainId,
32+
selectedChain,
33+
// Actions
34+
setChainId,
35+
setSelectedChain,
36+
clearChain,
37+
// Getters
38+
getCurrentChainId,
39+
getCurrentChain,
40+
};
41+
});
42+
43+
export default useUserStore;

src/utils/chain.utils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { arbitrum } from 'viem/chains';
2+
import { bellecour } from './bellecourChainConfig';
3+
4+
export interface Chain {
5+
id: number;
6+
name: string;
7+
icon: string;
8+
nativeCurrency: {
9+
name: string;
10+
symbol: string;
11+
decimals: number;
12+
};
13+
rpcUrls: {
14+
default: {
15+
http: readonly string[];
16+
};
17+
};
18+
blockExplorers: {
19+
default: {
20+
name: string;
21+
url: string;
22+
};
23+
};
24+
}
25+
26+
export function getSupportedChains(): Chain[] {
27+
return [
28+
{
29+
id: arbitrum.id,
30+
name: arbitrum.name,
31+
icon: '/assets/icons/arbitrum.svg',
32+
nativeCurrency: arbitrum.nativeCurrency,
33+
rpcUrls: arbitrum.rpcUrls,
34+
blockExplorers: arbitrum.blockExplorers,
35+
},
36+
{
37+
id: bellecour.id,
38+
name: bellecour.name,
39+
icon: '/assets/icons/iexec-logo.png',
40+
nativeCurrency: bellecour.nativeCurrency,
41+
rpcUrls: bellecour.rpcUrls,
42+
blockExplorers: bellecour.blockExplorers,
43+
},
44+
];
45+
}
46+
47+
export function getChainById(chainId: number): Chain | undefined {
48+
return getSupportedChains().find((chain) => chain.id === chainId);
49+
}

0 commit comments

Comments
 (0)