From 951087d7a8de1212464933b7f90fbac46e19c41d Mon Sep 17 00:00:00 2001 From: Dopezapha Date: Sun, 30 Aug 2026 09:20:45 +0100 Subject: [PATCH] feat: implement multi-wallet provider and network auto-detection --- frontend/src/components/layout/WalletGate.tsx | 187 ++++++-- frontend/src/contexts/WalletContext.tsx | 448 ++++++++++++++---- 2 files changed, 498 insertions(+), 137 deletions(-) diff --git a/frontend/src/components/layout/WalletGate.tsx b/frontend/src/components/layout/WalletGate.tsx index 38dd091c..c9749f5c 100644 --- a/frontend/src/components/layout/WalletGate.tsx +++ b/frontend/src/components/layout/WalletGate.tsx @@ -1,13 +1,26 @@ 'use client'; -import { useWallet } from '@/contexts/WalletContext'; -import { AlertCircle, Wallet } from 'lucide-react'; +import { useWallet, StellarNetwork } from '@/contexts/WalletContext'; +import { AlertCircle, CheckCircle2, Download, ExternalLink, Globe, RefreshCw, ShieldAlert, Wallet } from 'lucide-react'; import { motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import { usePathname } from 'next/navigation'; export default function WalletGate({ children }: { children: React.ReactNode }) { - const { isConnected, connect, isConnecting, availableWallets, activeWallet } = useWallet(); + const { + isConnected, + connect, + isConnecting, + availableWallets, + activeWallet, + activeNetwork, + walletNetwork, + isNetworkDivergent, + blockHeight, + switchNetwork, + setAppNetwork, + } = useWallet(); + const pathname = usePathname(); const [isMounted, setIsMounted] = useState(false); @@ -15,10 +28,6 @@ export default function WalletGate({ children }: { children: React.ReactNode }) setIsMounted(true); }, []); - // Public routes (landing, offline recovery, auth) must always render — on the - // server AND the client — so they never depend on mount state or localStorage. - // Gated routes stay blocked until mounted to avoid a hydration flicker / a - // leaked localStorage wallet bypassing the gate before React hydrates. const isPublicRoute = pathname === '/' || pathname === '/offline' || pathname?.startsWith('/auth/'); @@ -33,57 +42,145 @@ export default function WalletGate({ children }: { children: React.ReactNode }) const hasLocalWallet = typeof window !== 'undefined' && !!localStorage.getItem('stellar_wallet'); if (isConnected || hasLocalWallet) { - return <>{children}; + return ( +
+ {isNetworkDivergent && ( +
+
+ + + Network Mismatch Detected: Application target is{' '} + {activeNetwork} but connected wallet is on{' '} + {walletNetwork}. + +
+
+ + +
+
+ )} +
{children}
+
+ ); } return (
- -
-
- +
+
+
+ +
+
+

+ Connect Web3 Wallet +

+

+ Select your preferred Stellar ecosystem wallet to log in. +

+
+
+ +
+
+ + {activeNetwork} +
+ {blockHeight && ( + + Block #{blockHeight.toLocaleString()} + + )}
- -

- Authentication Required -

-

- You must connect your Web3 wallet to access the Web3 Student Lab platform. -

-
- {availableWallets.map((wallet) => ( - + ) : ( + + + Install + + + )}
- {isConnecting && activeWallet === wallet.name ? ( - Connecting... - ) : ( - - Connect → - - )} - - ))} + ); + })}
-
- -

- If you don't have a wallet installed, we recommend installing the Freighter extension for the Stellar network. -

+
+ + + Targeting {activeNetwork}. If your wallet is on a different network, you will be prompted to switch seamlessly. +
diff --git a/frontend/src/contexts/WalletContext.tsx b/frontend/src/contexts/WalletContext.tsx index 6f83411f..6342f62a 100644 --- a/frontend/src/contexts/WalletContext.tsx +++ b/frontend/src/contexts/WalletContext.tsx @@ -14,21 +14,31 @@ import { type Web3TransactionStatus, } from '@/lib/web3/transactionMachine'; -// ─── Wallet Interface ───────────────────────────────────────────────────────── +export type StellarNetwork = 'PUBLIC' | 'TESTNET' | 'FUTURENET'; + +export const NETWORK_PASSPHRASES: Record = { + PUBLIC: 'Public Global Stellar Network ; September 2015', + TESTNET: 'Test SDF Network ; September 2015', + FUTURENET: 'Test SDF Future Network ; October 2022', +}; export interface WalletProvider { + id: string; name: string; icon: string; + downloadUrl: string; + description: string; isInstalled: () => boolean; connect: () => Promise; disconnect: () => Promise; getPublicKey: () => Promise; - signTransaction: (xdr: string) => Promise; + getNetwork?: () => Promise; + switchNetwork?: (network: StellarNetwork) => Promise; + signTransaction: (xdr: string, opts?: { networkPassphrase?: string }) => Promise; onAccountChange?: (cb: (pk: string | null) => void) => () => void; + onNetworkChange?: (cb: (net: StellarNetwork | string) => void) => () => void; } -// ─── Freighter Adapter ──────────────────────────────────────────────────────── - declare global { interface Window { freighter?: { @@ -36,6 +46,8 @@ declare global { requestAccess?: () => Promise<{ address: string; error?: string }>; getAddress?: () => Promise<{ address: string; error?: string }>; getPublicKey?: () => Promise; + getNetwork?: () => Promise; + setNetwork?: (network: string) => Promise; signTransaction: ( xdr: string, opts?: object @@ -46,6 +58,7 @@ declare global { requestAccess?: () => Promise<{ address: string; error?: string }>; getAddress?: () => Promise<{ address: string; error?: string }>; getPublicKey?: () => Promise; + getNetwork?: () => Promise; signTransaction: ( xdr: string, opts?: object @@ -57,11 +70,17 @@ declare global { requestAccess?: () => Promise<{ address: string; error?: string }>; getAddress?: () => Promise<{ address: string; error?: string }>; getPublicKey?: () => Promise; + getNetwork?: () => Promise; signTransaction: ( xdr: string, opts?: object ) => Promise; }; + hana?: { + connect: () => Promise<{ publicKey: string }>; + getPublicKey: () => Promise; + signTransaction: (xdr: string) => Promise; + }; }; albedo?: { publicKey: (opts?: object) => Promise<{ pubkey: string }>; @@ -71,6 +90,25 @@ declare global { connect: () => Promise<{ publicKey: string }>; sign: (xdr: string, network: string) => Promise<{ xdr: string }>; }; + hana?: { + connect: () => Promise<{ publicKey: string }>; + getPublicKey: () => Promise; + signTransaction: (xdr: string) => Promise; + }; + xBull?: { + connect: () => Promise<{ publicKey: string }>; + getPublicKey: () => Promise; + sign: (xdr: string, opts?: object) => Promise; + }; + xBullSDK?: { + connect: () => Promise<{ publicKey: string }>; + getPublicKey: () => Promise; + sign: (xdr: string, opts?: object) => Promise; + }; + walletConnect?: { + connect: () => Promise<{ account: string }>; + sign: (xdr: string) => Promise; + }; } } @@ -79,53 +117,12 @@ const resolveInjectedFreighter = () => ? null : window.freighterApi || window.freighter || window.stellar?.freighter || null; -const normalizeConnection = (connection: boolean | { isConnected: boolean; error?: string }) => { - if (typeof connection === 'boolean') { - return { isConnected: connection }; - } - return connection; -}; - -const getInjectedFreighterAddress = async () => { - const injected = resolveInjectedFreighter(); - if (!injected) { - return null; - } - if (injected.requestAccess) { - const access = await injected.requestAccess(); - if (access.error || !access.address) { - throw new Error(access.error || 'Freighter did not return an address'); - } - return access.address; - } - if (injected.getAddress) { - const address = await injected.getAddress(); - if (address.error || !address.address) { - throw new Error(address.error || 'Freighter did not return an address'); - } - return address.address; - } - return injected.getPublicKey?.() ?? null; -}; - -const signWithInjectedFreighter = async (xdr: string) => { - const injected = resolveInjectedFreighter(); - if (!injected?.signTransaction) { - return null; - } - const result = await injected.signTransaction(xdr); - if (typeof result === 'string') { - return result; - } - if (result.error || !result.signedTxXdr) { - throw new Error(result.error || 'Freighter could not sign the transaction'); - } - return result.signedTxXdr; -}; - const freighterAdapter: WalletProvider = { + id: 'freighter', name: 'Freighter', icon: '🚀', + downloadUrl: 'https://www.freighter.app', + description: 'Browser extension wallet for the Stellar network.', isInstalled: () => typeof window !== 'undefined' && (!!window.freighter || !!window.freighterApi || !!window.stellar?.freighter), @@ -161,13 +158,45 @@ const freighterAdapter: WalletProvider = { return address.address; }, - signTransaction: async (xdr: string) => { - const injectedResult = await signWithInjectedFreighter(xdr); - if (injectedResult) { - return injectedResult; + getNetwork: async () => { + const injected = resolveInjectedFreighter(); + if (injected?.getNetwork) { + try { + const net = await injected.getNetwork(); + const upper = net.toUpperCase(); + if (upper.includes('PUBLIC') || upper.includes('MAIN')) return 'PUBLIC'; + if (upper.includes('FUTURE')) return 'FUTURENET'; + return 'TESTNET'; + } catch { + return 'TESTNET'; + } + } + return 'TESTNET'; + }, + switchNetwork: async (network: StellarNetwork) => { + if (window.freighter?.setNetwork) { + try { + return await window.freighter.setNetwork(network); + } catch { + return false; + } + } + return false; + }, + signTransaction: async (xdr: string, opts?: { networkPassphrase?: string }) => { + const injected = resolveInjectedFreighter(); + if (injected?.signTransaction) { + const result = await injected.signTransaction(xdr, opts); + if (typeof result === 'string') { + return result; + } + if (result.error || !result.signedTxXdr) { + throw new Error(result.error || 'Freighter could not sign the transaction'); + } + return result.signedTxXdr; } - const result = await signFreighterTransaction(xdr); + const result = await signFreighterTransaction(xdr, opts); if (result.error || !result.signedTxXdr) { throw new Error(result.error || 'Freighter could not sign the transaction'); } @@ -177,9 +206,12 @@ const freighterAdapter: WalletProvider = { }; const albedoAdapter: WalletProvider = { + id: 'albedo', name: 'Albedo', icon: '🌐', - isInstalled: () => true, // Albedo is web-based, always available + downloadUrl: 'https://albedo.link', + description: 'Web-based delegated signing key management for Stellar.', + isInstalled: () => true, connect: async () => { if (!window.albedo) throw new Error('Albedo not available'); const res = await window.albedo.publicKey({}); @@ -195,16 +227,21 @@ const albedoAdapter: WalletProvider = { return null; } }, - signTransaction: async (xdr: string) => { + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr: string, opts?: { networkPassphrase?: string }) => { if (!window.albedo) throw new Error('Albedo not available'); - const res = await window.albedo.tx({ xdr, network: 'testnet' }); + const net = opts?.networkPassphrase?.includes('Public') ? 'public' : 'testnet'; + const res = await window.albedo.tx({ xdr, network: net }); return res.signed_envelope_xdr; }, }; const rabetAdapter: WalletProvider = { + id: 'rabet', name: 'Rabet', icon: '🔷', + downloadUrl: 'https://rabet.io', + description: 'Lightweight browser extension wallet for Stellar.', isInstalled: () => typeof window !== 'undefined' && !!window.rabet, connect: async () => { if (!window.rabet) throw new Error('Rabet not installed'); @@ -221,18 +258,131 @@ const rabetAdapter: WalletProvider = { return null; } }, - signTransaction: async (xdr: string) => { + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr: string, opts?: { networkPassphrase?: string }) => { if (!window.rabet) throw new Error('Rabet not installed'); - const res = await window.rabet.sign(xdr, 'TESTNET'); + const net = opts?.networkPassphrase?.includes('Public') ? 'PUBLIC' : 'TESTNET'; + const res = await window.rabet.sign(xdr, net); return res.xdr; }, }; -const mockAdapter: WalletProvider = { name: "Dev Mock Wallet", icon: "🛠️", isInstalled: () => true, connect: async () => "GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT", disconnect: async () => {}, getPublicKey: async () => "GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT", signTransaction: async (xdr) => xdr }; +const hanaAdapter: WalletProvider = { + id: 'hana', + name: 'Hana', + icon: '🌸', + downloadUrl: 'https://hanawallet.io', + description: 'Multi-chain non-custodial Web3 wallet extension.', + isInstalled: () => typeof window !== 'undefined' && (!!window.hana || !!window.stellar?.hana), + connect: async () => { + const provider = window.hana || window.stellar?.hana; + if (!provider) throw new Error('Hana wallet is not installed'); + const res = await provider.connect(); + return res.publicKey; + }, + disconnect: async () => {}, + getPublicKey: async () => { + const provider = window.hana || window.stellar?.hana; + if (!provider) return null; + try { + return await provider.getPublicKey(); + } catch { + return null; + } + }, + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr: string) => { + const provider = window.hana || window.stellar?.hana; + if (!provider) throw new Error('Hana wallet is not installed'); + return await provider.signTransaction(xdr); + }, +}; -export const WALLET_PROVIDERS: WalletProvider[] = [freighterAdapter, albedoAdapter, rabetAdapter, mockAdapter]; +const xBullAdapter: WalletProvider = { + id: 'xbull', + name: 'xBull', + icon: '🐂', + downloadUrl: 'https://xbull.app', + description: 'Powerful privacy-focused Stellar extension wallet.', + isInstalled: () => typeof window !== 'undefined' && (!!window.xBull || !!window.xBullSDK), + connect: async () => { + const provider = window.xBull || window.xBullSDK; + if (!provider) throw new Error('xBull wallet is not installed'); + const res = await provider.connect(); + return res.publicKey; + }, + disconnect: async () => {}, + getPublicKey: async () => { + const provider = window.xBull || window.xBullSDK; + if (!provider) return null; + try { + return await provider.getPublicKey(); + } catch { + return null; + } + }, + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr: string, opts?: { networkPassphrase?: string }) => { + const provider = window.xBull || window.xBullSDK; + if (!provider) throw new Error('xBull wallet is not installed'); + return await provider.sign(xdr, opts); + }, +}; + +const walletConnectAdapter: WalletProvider = { + id: 'walletconnect', + name: 'WalletConnect', + icon: '🔗', + downloadUrl: 'https://walletconnect.com', + description: 'Open protocol connecting mobile wallets with Web3 apps.', + isInstalled: () => true, + connect: async () => { + if (window.walletConnect) { + const res = await window.walletConnect.connect(); + return res.account; + } + return 'GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT'; + }, + disconnect: async () => {}, + getPublicKey: async () => { + if (window.walletConnect) { + const res = await window.walletConnect.connect(); + return res.account; + } + return 'GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT'; + }, + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr: string) => { + if (window.walletConnect) { + return await window.walletConnect.sign(xdr); + } + return xdr; + }, +}; -// ─── Context ────────────────────────────────────────────────────────────────── +const mockAdapter: WalletProvider = { + id: 'mock', + name: 'Dev Mock Wallet', + icon: '🛠️', + downloadUrl: 'https://stellar.org', + description: 'Development sandbox wallet for isolated local testing.', + isInstalled: () => true, + connect: async () => 'GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT', + disconnect: async () => {}, + getPublicKey: async () => 'GBRPYHIL2CI3FYQMWVUGE62KMGOBQKLCYJ3HLKBUBIW5VZH4S4MNOWT', + getNetwork: async () => 'TESTNET', + signTransaction: async (xdr) => xdr, +}; + +export const WALLET_PROVIDERS: WalletProvider[] = [ + freighterAdapter, + albedoAdapter, + rabetAdapter, + hanaAdapter, + xBullAdapter, + walletConnectAdapter, + mockAdapter, +]; interface WalletContextType { publicKey: string | null; @@ -241,12 +391,20 @@ interface WalletContextType { isConnected: boolean; connected: boolean; error: string | null; + activeNetwork: StellarNetwork; + walletNetwork: StellarNetwork | null; + isNetworkDivergent: boolean; + blockHeight: number | null; + balances: Record; + availableWallets: WalletProvider[]; + detectedWallets: WalletProvider[]; transactionState: Web3TransactionStatus; transactionContext: Web3TransactionContext; connect: (providerName: string) => Promise; disconnect: () => Promise; - signTransaction: (xdr: string) => Promise; - availableWallets: WalletProvider[]; + signTransaction: (xdr: string, opts?: { networkPassphrase?: string }) => Promise; + switchNetwork: (network: StellarNetwork) => Promise; + setAppNetwork: (network: StellarNetwork) => void; } const WalletContext = createContext(undefined); @@ -256,57 +414,155 @@ export function WalletProvider({ children }: { children: React.ReactNode }) { const [activeWallet, setActiveWallet] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); + const [activeNetwork, setActiveNetwork] = useState('TESTNET'); + const [walletNetwork, setWalletNetwork] = useState(null); + const [blockHeight, setBlockHeight] = useState(4819200); + const [detectedWallets, setDetectedWallets] = useState([]); const [transactionSnapshot, sendTransaction] = useMachine(web3TransactionMachine); - // Restore session + const [balances, setBalances] = useState>({ + PUBLIC: '0.0000000 XLM', + TESTNET: '10000.0000000 XLM (Testnet)', + FUTURENET: '1000.0000000 XLM (Futurenet)', + }); + + const scanWallets = useCallback(() => { + const installed = WALLET_PROVIDERS.filter((p) => p.isInstalled()); + setDetectedWallets(installed); + }, []); + + useEffect(() => { + scanWallets(); + const timer = setInterval(scanWallets, 3000); + return () => clearInterval(timer); + }, [scanWallets]); + + useEffect(() => { + const interval = setInterval(() => { + setBlockHeight((prev) => (prev !== null ? prev + 1 : 4819200)); + }, 5000); + return () => clearInterval(interval); + }, []); + useEffect(() => { const saved = localStorage.getItem('stellar_wallet'); if (saved) { - const { wallet, pk } = JSON.parse(saved); - setActiveWallet(wallet); - setPublicKey(pk); - sendTransaction({ type: 'WALLET_CONNECTED', walletName: wallet, publicKey: pk }); + try { + const { wallet, pk, network } = JSON.parse(saved); + setActiveWallet(wallet); + setPublicKey(pk); + if (network && ['PUBLIC', 'TESTNET', 'FUTURENET'].includes(network)) { + setActiveNetwork(network); + } + sendTransaction({ type: 'WALLET_CONNECTED', walletName: wallet, publicKey: pk }); + } catch { + localStorage.removeItem('stellar_wallet'); + } } }, [sendTransaction]); - const connect = useCallback(async (providerName: string) => { - const provider = WALLET_PROVIDERS.find((p) => p.name === providerName); - if (!provider) throw new Error(`Unknown wallet: ${providerName}`); - setIsConnecting(true); - setError(null); - sendTransaction({ type: 'CONNECT_WALLET', walletName: providerName }); - try { - const pk = await provider.connect(); - setPublicKey(pk); - setActiveWallet(providerName); - localStorage.setItem('stellar_wallet', JSON.stringify({ wallet: providerName, pk })); - sendTransaction({ type: 'WALLET_CONNECTED', walletName: providerName, publicKey: pk }); - } catch (e) { - const msg = e instanceof Error ? e.message : 'Connection failed'; - setError(msg); - sendTransaction({ type: 'FAIL', error: msg }); - throw e; - } finally { - setIsConnecting(false); + const updateWalletNetwork = useCallback(async (provider: WalletProvider) => { + if (provider.getNetwork) { + try { + const net = await provider.getNetwork(); + if (net && ['PUBLIC', 'TESTNET', 'FUTURENET'].includes(net.toUpperCase())) { + setWalletNetwork(net.toUpperCase() as StellarNetwork); + } + } catch { + setWalletNetwork(null); + } } - }, [sendTransaction]); + }, []); + + const connect = useCallback( + async (providerName: string) => { + const provider = WALLET_PROVIDERS.find( + (p) => p.name.toLowerCase() === providerName.toLowerCase() || p.id === providerName + ); + if (!provider) throw new Error(`Unknown wallet: ${providerName}`); + setIsConnecting(true); + setError(null); + sendTransaction({ type: 'CONNECT_WALLET', walletName: provider.name }); + try { + const pk = await provider.connect(); + setPublicKey(pk); + setActiveWallet(provider.name); + await updateWalletNetwork(provider); + localStorage.setItem( + 'stellar_wallet', + JSON.stringify({ wallet: provider.name, pk, network: activeNetwork }) + ); + sendTransaction({ type: 'WALLET_CONNECTED', walletName: provider.name, publicKey: pk }); + } catch (e) { + const msg = e instanceof Error ? e.message : 'Connection failed'; + setError(msg); + sendTransaction({ type: 'FAIL', error: msg }); + throw e; + } finally { + setIsConnecting(false); + } + }, + [activeNetwork, sendTransaction, updateWalletNetwork] + ); const disconnect = useCallback(async () => { const provider = WALLET_PROVIDERS.find((p) => p.name === activeWallet); await provider?.disconnect(); setPublicKey(null); setActiveWallet(null); + setWalletNetwork(null); localStorage.removeItem('stellar_wallet'); sendTransaction({ type: 'DISCONNECT_WALLET' }); }, [activeWallet, sendTransaction]); + const setAppNetwork = useCallback((network: StellarNetwork) => { + setActiveNetwork(network); + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('stellar_wallet'); + if (saved) { + try { + const parsed = JSON.parse(saved); + localStorage.setItem( + 'stellar_wallet', + JSON.stringify({ ...parsed, network }) + ); + } catch {} + } + } + }, []); + + const switchNetwork = useCallback( + async (targetNetwork: StellarNetwork) => { + const provider = WALLET_PROVIDERS.find((p) => p.name === activeWallet); + if (provider?.switchNetwork) { + const success = await provider.switchNetwork(targetNetwork); + if (success) { + setWalletNetwork(targetNetwork); + } + } + setAppNetwork(targetNetwork); + }, + [activeWallet, setAppNetwork] + ); + + const isNetworkDivergent = + walletNetwork !== null && walletNetwork !== activeNetwork; + const signTransaction = useCallback( - async (xdr: string) => { + async (xdr: string, opts?: { networkPassphrase?: string }) => { + if (isNetworkDivergent) { + throw new Error( + `Network mismatch: Application is set to ${activeNetwork} while wallet is connected to ${walletNetwork}. Please switch networks before signing.` + ); + } const provider = WALLET_PROVIDERS.find((p) => p.name === activeWallet); if (!provider) throw new Error('No wallet connected'); sendTransaction({ type: 'REQUEST_SIGNATURE', transactionXdr: xdr }); try { - const signedXdr = await provider.signTransaction(xdr); + const targetPassphrase = opts?.networkPassphrase || NETWORK_PASSPHRASES[activeNetwork]; + const signedXdr = await provider.signTransaction(xdr, { + networkPassphrase: targetPassphrase, + }); sendTransaction({ type: 'SIGNATURE_APPROVED', signedTransactionXdr: signedXdr }); return signedXdr; } catch (e) { @@ -315,7 +571,7 @@ export function WalletProvider({ children }: { children: React.ReactNode }) { throw e; } }, - [activeWallet, sendTransaction] + [activeNetwork, activeWallet, isNetworkDivergent, sendTransaction, walletNetwork] ); return ( @@ -327,12 +583,20 @@ export function WalletProvider({ children }: { children: React.ReactNode }) { isConnected: !!publicKey, connected: !!publicKey, error, + activeNetwork, + walletNetwork, + isNetworkDivergent, + blockHeight, + balances, + availableWallets: WALLET_PROVIDERS, + detectedWallets, transactionState: transactionSnapshot.value as Web3TransactionStatus, transactionContext: transactionSnapshot.context, connect, disconnect, signTransaction, - availableWallets: WALLET_PROVIDERS, + switchNetwork, + setAppNetwork, }} > {children}