{isConnected && (
)}
-
+
{isConnected ? (
@@ -89,9 +75,9 @@ export function Navbar() {
-
+
-
+
{isConnected ? (
@@ -117,18 +103,9 @@ export function Navbar() {
asChild
className="justify-baseline px-3 text-white"
>
-
iExec Account
+
iExec Account
-
+
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
index 4c402f57..ea17954f 100644
--- a/src/components/ui/alert.tsx
+++ b/src/components/ui/alert.tsx
@@ -3,13 +3,13 @@ import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
const alertVariants = cva(
- 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+ 'relative w-full rounded border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
{
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
- 'text-destructive bg-danger border-danger-border [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
+ 'text-destructive bg-danger border-danger-border [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90 border-l-4',
},
},
defaultVariants: {
diff --git a/src/config.ts b/src/config.ts
index 962a75ca..75e7d6e6 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,3 +1,7 @@
+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;
export const PREVIEW_TABLE_REFETCH_INTERVAL = 10_000;
@@ -6,8 +10,22 @@ export const SUPPORTED_CHAINS = [
{
id: 134,
name: 'Bellecour',
- icon: 'src/assets/iexec-logo.svg',
+ slug: 'bellecour',
+ color: '#F4942566',
+ icon: iexecLogo,
blockExplorerUrl: 'https://blockscout-bellecour.iex.ec',
subgraphUrl: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5',
+ wagmiNetwork: bellecour,
+ },
+ {
+ id: 421614,
+ name: 'Arbitrum Sepolia',
+ slug: 'arbitrum-sepolia',
+ color: '#28A0F080',
+ icon: arbitrumSepoliaIcon,
+ blockExplorerUrl: 'https://sepolia.arbiscan.io/',
+ subgraphUrl:
+ 'http://localhost:8080/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz',
+ wagmiNetwork: arbitrumSepolia,
},
];
diff --git a/src/graphql/execute.ts b/src/graphql/execute.ts
index 7bfe7188..fe979c44 100644
--- a/src/graphql/execute.ts
+++ b/src/graphql/execute.ts
@@ -1,11 +1,15 @@
+import { getSubgraphUrl } from '@/utils/chain.utils';
import type { TypedDocumentString } from './graphql'
export async function execute
(
query: TypedDocumentString,
+ chainId?: number,
...[variables]: TVariables extends Record ? [] : [TVariables]
) {
- const subgraphUrl = import.meta.env.VITE_POCO_SUBGRAPH_URL;
-
+ if (!chainId) {
+ throw Error('Missing chainId')
+ }
+ const subgraphUrl = getSubgraphUrl(chainId);
const response = await fetch(subgraphUrl, {
method: 'POST',
headers: {
diff --git a/src/hooks/ChainSyncManger.ts b/src/hooks/ChainSyncManger.ts
new file mode 100644
index 00000000..76e5d806
--- /dev/null
+++ b/src/hooks/ChainSyncManger.ts
@@ -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(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;
+}
diff --git a/src/hooks/useChainSwitch.ts b/src/hooks/useChainSwitch.ts
new file mode 100644
index 00000000..f7ff97b7
--- /dev/null
+++ b/src/hooks/useChainSwitch.ts
@@ -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 };
+}
diff --git a/src/hooks/useSyncAccountWithUserStore.ts b/src/hooks/useSyncAccountWithUserStore.ts
deleted file mode 100644
index 16e1b0ba..00000000
--- a/src/hooks/useSyncAccountWithUserStore.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useEffect } from 'react';
-import { useAccount } from 'wagmi';
-import useUserStore from '@/stores/useUser.store';
-
-export function useSyncAccountWithUserStore() {
- const { connector, status, address, chain, isConnected } = useAccount();
- const { setConnector, setIsConnected, setAddress, setChainId } =
- useUserStore();
-
- useEffect(() => {
- // Update userStore
- setConnector(connector);
- setIsConnected(isConnected);
- setAddress(address);
- setChainId(chain?.id);
- }, [
- connector,
- status,
- address,
- chain,
- isConnected,
- setConnector,
- setIsConnected,
- setAddress,
- setChainId,
- ]);
-}
diff --git a/src/index.css b/src/index.css
index 3c1376fd..8f97243b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -152,9 +152,9 @@
--color-success-foreground: var(--color-green-600);
--color-success-border: var(--color-green-600);
- --color-danger: var(--color-red-100);
- --color-danger-foreground: var(--color-red-600);
- --color-danger-border: var(--color-red-600);
+ --color-danger: var(--color-red-900);
+ --color-danger-foreground: var(--color-red-100);
+ --color-danger-border: var(--color-red-500);
--color-warning: var(--color-orange-100);
--color-warning-foreground: var(--color-orange-600);
diff --git a/src/modules/apps/AppsPreviewTable.tsx b/src/modules/apps/AppsPreviewTable.tsx
index 0a6458bf..c7dcb54c 100644
--- a/src/modules/apps/AppsPreviewTable.tsx
+++ b/src/modules/apps/AppsPreviewTable.tsx
@@ -2,21 +2,26 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/execute';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
-import { Link } from '@tanstack/react-router';
import { Box, LoaderCircle, Terminal } from 'lucide-react';
-import { CircularLoader } from '@/components/CircularLoader';
+import { ChainLink } from '@/components/ChainLink';
import { DataTable } from '@/components/DataTable';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
+import useUserStore from '@/stores/useUser.store';
import { appsQuery } from './appsQuery';
import { columns } from './appsTable/columns';
export function AppsPreviewTable({ className }: { className?: string }) {
+ const { chainId } = useUserStore();
const apps = useQuery({
- queryKey: ['apps_preview'],
+ queryKey: [chainId, 'apps_preview'],
queryFn: () =>
- execute(appsQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }),
+ execute(appsQuery, chainId, {
+ length: PREVIEW_TABLE_LENGTH,
+ skip: 0,
+ }),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
+ enabled: !!chainId,
});
const formattedData =
@@ -39,7 +44,7 @@ export function AppsPreviewTable({ className }: { className?: string }) {
{apps.isFetching && }
{(apps.isError || apps.errorUpdateCount > 0) && !apps.data ? (
diff --git a/src/modules/datasets/DatasetsPreviewTable.tsx b/src/modules/datasets/DatasetsPreviewTable.tsx
index 2dda14a9..1a298ea1 100644
--- a/src/modules/datasets/DatasetsPreviewTable.tsx
+++ b/src/modules/datasets/DatasetsPreviewTable.tsx
@@ -2,21 +2,26 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/execute';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
-import { Link } from '@tanstack/react-router';
import { Box, LoaderCircle, Terminal } from 'lucide-react';
-import { CircularLoader } from '@/components/CircularLoader';
+import { ChainLink } from '@/components/ChainLink';
import { DataTable } from '@/components/DataTable';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
+import useUserStore from '@/stores/useUser.store';
import { datasetsQuery } from './datasetsQuery';
import { columns } from './datasetsTable/columns';
export function DatasetsPreviewTable({ className }: { className?: string }) {
+ const { chainId } = useUserStore();
const datasets = useQuery({
- queryKey: ['datasets_preview'],
+ queryKey: [chainId, 'datasets_preview'],
queryFn: () =>
- execute(datasetsQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }),
+ execute(datasetsQuery, chainId, {
+ length: PREVIEW_TABLE_LENGTH,
+ skip: 0,
+ }),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
+ enabled: !!chainId,
});
const formattedData =
@@ -39,7 +44,7 @@ export function DatasetsPreviewTable({ className }: { className?: string }) {
{datasets.isFetching &&
}
{(datasets.isError || datasets.errorUpdateCount > 0) && !datasets.data ? (
diff --git a/src/modules/deals/DealsPreviewTable.tsx b/src/modules/deals/DealsPreviewTable.tsx
index a4d778fa..e98e7c36 100644
--- a/src/modules/deals/DealsPreviewTable.tsx
+++ b/src/modules/deals/DealsPreviewTable.tsx
@@ -2,21 +2,26 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/execute';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
-import { Link } from '@tanstack/react-router';
import { Box, LoaderCircle, Terminal } from 'lucide-react';
-import { CircularLoader } from '@/components/CircularLoader';
+import { ChainLink } from '@/components/ChainLink';
import { DataTable } from '@/components/DataTable';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
+import useUserStore from '@/stores/useUser.store';
import { dealsQuery } from './dealsQuery';
import { columns } from './dealsTable/columns';
export function DealsPreviewTable({ className }: { className?: string }) {
+ const { chainId } = useUserStore();
const deals = useQuery({
- queryKey: ['deals_preview'],
+ queryKey: [chainId, 'deals_preview'],
queryFn: () =>
- execute(dealsQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }),
+ execute(dealsQuery, chainId, {
+ length: PREVIEW_TABLE_LENGTH,
+ skip: 0,
+ }),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
+ enabled: !!chainId,
});
const formattedData =
@@ -39,7 +44,7 @@ export function DealsPreviewTable({ className }: { className?: string }) {
{deals.isFetching &&