import Client from "@walletconnect/sign-client"; import { PairingTypes, SessionTypes } from "@walletconnect/types"; import QRCodeModal from "@walletconnect/qrcode-modal"; import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState, } from "react"; import { PublicKey } from "@solana/web3.js"; import { DEFAULT_APP_METADATA, DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL, } from "../constants"; import { AccountBalances, apiGetAccountBalance } from "../helpers"; import { getAppMetadata, getSdkError } from "@walletconnect/utils"; import { getPublicKeysFromAccounts } from "../helpers/solana"; import { getRequiredNamespaces } from "../helpers/namespaces"; /** * Types */ interface IContext { client: Client | undefined; session: SessionTypes.Struct | undefined; connect: (pairing?: { topic: string }) => Promise; disconnect: () => Promise; isInitializing: boolean; chains: string[]; pairings: PairingTypes.Struct[]; accounts: string[]; solanaPublicKeys?: Record; balances: AccountBalances; isFetchingBalances: boolean; setChains: any; } /** * Context */ export const ClientContext = createContext({} as IContext); /** * Provider */ export function ClientContextProvider({ children }: { children: ReactNode | ReactNode[] }) { const [client, setClient] = useState(); const [pairings, setPairings] = useState([]); const [session, setSession] = useState(); const [isFetchingBalances, setIsFetchingBalances] = useState(false); const [isInitializing, setIsInitializing] = useState(false); const [balances, setBalances] = useState({}); const [accounts, setAccounts] = useState([]); const [solanaPublicKeys, setSolanaPublicKeys] = useState>(); const [chains, setChains] = useState([]); const reset = () => { setSession(undefined); setBalances({}); setAccounts([]); setChains([]); }; const getAccountBalances = async (_accounts: string[]) => { setIsFetchingBalances(true); try { const arr = await Promise.all( _accounts.map(async account => { const [namespace, reference, address] = account.split(":"); const chainId = `${namespace}:${reference}`; const assets = await apiGetAccountBalance(address, chainId); return { account, assets: [assets] }; }), ); const balances: AccountBalances = {}; arr.forEach(({ account, assets }) => { balances[account] = assets; }); setBalances(balances); } catch (e) { console.error(e); } finally { setIsFetchingBalances(false); } }; const onSessionConnected = useCallback(async (_session: SessionTypes.Struct) => { const allNamespaceAccounts = Object.values(_session.namespaces) .map(namespace => namespace.accounts) .flat(); const allNamespaceChains = Object.keys(_session.namespaces); setSession(_session); setChains(allNamespaceChains); setAccounts(allNamespaceAccounts); setSolanaPublicKeys(getPublicKeysFromAccounts(allNamespaceAccounts)); await getAccountBalances(allNamespaceAccounts); }, []); const connect = useCallback( async pairing => { if (typeof client === "undefined") { throw new Error("WalletConnect is not initialized"); } console.log("connect, pairing topic is:", pairing?.topic); try { const requiredNamespaces = getRequiredNamespaces(chains); console.log("requiredNamespaces config for connect:", requiredNamespaces); const { uri, approval } = await client.connect({ pairingTopic: pairing?.topic, requiredNamespaces, }); // Open QRCode modal if a URI was returned (i.e. we're not connecting an existing pairing). if (uri) { QRCodeModal.open(uri, () => { console.log("EVENT", "QR Code Modal closed"); }); } const session = await approval(); console.log("Established session:", session); await onSessionConnected(session); // Update known pairings after session is connected. setPairings(client.pairing.getAll({ active: true })); } catch (e) { console.error(e); // ignore rejection } finally { // close modal in case it was open QRCodeModal.close(); } }, [chains, client, onSessionConnected], ); const disconnect = useCallback(async () => { if (typeof client === "undefined") { throw new Error("WalletConnect is not initialized"); } if (typeof session === "undefined") { throw new Error("Session is not connected"); } await client.disconnect({ topic: session.topic, reason: getSdkError("USER_DISCONNECTED"), }); // Reset app state after disconnect. reset(); }, [client, session]); const _subscribeToEvents = useCallback( async (_client: Client) => { if (typeof _client === "undefined") { throw new Error("WalletConnect is not initialized"); } _client.on("session_ping", args => { console.log("EVENT", "session_ping", args); }); _client.on("session_event", args => { console.log("EVENT", "session_event", args); }); _client.on("session_update", ({ topic, params }) => { console.log("EVENT", "session_update", { topic, params }); const { namespaces } = params; const _session = _client.session.get(topic); const updatedSession = { ..._session, namespaces }; onSessionConnected(updatedSession); }); _client.on("session_delete", () => { console.log("EVENT", "session_delete"); reset(); }); }, [onSessionConnected], ); const _checkPersistedState = useCallback( async (_client: Client) => { if (typeof _client === "undefined") { throw new Error("WalletConnect is not initialized"); } // populates existing pairings to state setPairings(_client.pairing.getAll({ active: true })); console.log("RESTORED PAIRINGS: ", _client.pairing.getAll({ active: true })); if (typeof session !== "undefined") return; // populates (the last) existing session to state if (_client.session.length) { const lastKeyIndex = _client.session.keys.length - 1; const _session = _client.session.get(_client.session.keys[lastKeyIndex]); console.log("RESTORED SESSION:", _session); await onSessionConnected(_session); return _session; } }, [session, onSessionConnected], ); const createClient = useCallback(async () => { try { setIsInitializing(true); const _client = await Client.init({ logger: DEFAULT_LOGGER, relayUrl: DEFAULT_RELAY_URL, projectId: DEFAULT_PROJECT_ID, metadata: getAppMetadata() || DEFAULT_APP_METADATA, }); console.log("CREATED CLIENT: ", _client); setClient(_client); await _subscribeToEvents(_client); await _checkPersistedState(_client); } catch (err) { throw err; } finally { setIsInitializing(false); } }, [_checkPersistedState, _subscribeToEvents]); useEffect(() => { if (!client) { createClient(); } }, [client, createClient]); const value = useMemo( () => ({ pairings, isInitializing, balances, isFetchingBalances, accounts, solanaPublicKeys, chains, client, session, connect, disconnect, setChains, }), [ pairings, isInitializing, balances, isFetchingBalances, accounts, solanaPublicKeys, chains, client, session, connect, disconnect, setChains, ], ); return ( {children} ); } export function useWalletConnectClient() { const context = useContext(ClientContext); if (context === undefined) { throw new Error("useWalletConnectClient must be used within a ClientContextProvider"); } return context; }