import Client, { CLIENT_EVENTS } from "@walletconnect/client"; import EthereumProvider from "@walletconnect/ethereum-provider"; import { PairingTypes, SessionTypes } from "@walletconnect/types"; import QRCodeModal from "@walletconnect/qrcode-modal"; import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState, } from "react"; import Web3 from "web3"; import { apiGetChainNamespace, ChainsMap } from "caip-api"; import { DEFAULT_INFURA_ID, DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL, } from "../constants"; import { ChainNamespaces, getAllChainNamespaces } from "../helpers"; import { utils } from "ethers"; /** * Types */ interface IContext { client: Client | undefined; session: SessionTypes.Created | undefined; disconnect: () => Promise; isInitializing: boolean; chain: string; pairings: string[]; accounts: string[]; balances: { symbol: string; balance: string }[]; isFetchingBalances: boolean; chainData: ChainNamespaces; onEnable: (chainId: string) => Promise; web3Provider?: Web3; } /** * 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 [ethereumProvider, setEthereumProvider] = useState(); const [web3Provider, setWeb3Provider] = useState(); const [isFetchingBalances, setIsFetchingBalances] = useState(false); const [isInitializing, setIsInitializing] = useState(false); const [hasCheckedPersistedSession, setHasCheckedPersistedSession] = useState(false); const [balances, setBalances] = useState<{ symbol: string; balance: string }[]>([]); const [accounts, setAccounts] = useState([]); const [chainData, setChainData] = useState({}); const [chain, setChain] = useState(""); const resetApp = () => { setPairings([]); setSession(undefined); setBalances([]); setAccounts([]); setChain(""); }; const loadChainData = async () => { const namespaces = getAllChainNamespaces(); const chainData: ChainNamespaces = {}; await Promise.all( namespaces.map(async namespace => { let chains: ChainsMap | undefined; try { chains = await apiGetChainNamespace(namespace); } catch (e) { // ignore error } if (typeof chains !== "undefined") { chainData[namespace] = chains; } }), ); setChainData(chainData); }; const disconnect = useCallback(async () => { if (typeof ethereumProvider === "undefined") { throw new Error("ethereumProvider is not initialized"); } await ethereumProvider.disconnect(); }, [ethereumProvider]); const _subscribeToClientEvents = useCallback(async (_client: Client) => { if (typeof _client === "undefined") { throw new Error("WalletConnect is not initialized"); } _client.on(CLIENT_EVENTS.pairing.proposal, async (proposal: PairingTypes.Proposal) => { const { uri } = proposal.signal.params; console.log("EVENT", "QR Code Modal open"); QRCodeModal.open(uri, () => { console.log("EVENT", "QR Code Modal closed"); }); }); _client.on(CLIENT_EVENTS.pairing.created, async () => { setPairings(_client.pairing.topics); }); _client.on(CLIENT_EVENTS.session.deleted, () => { console.log("EVENT", "session_deleted"); resetApp(); }); }, []); const createClient = useCallback(async () => { try { setIsInitializing(true); const _client = await Client.init({ logger: DEFAULT_LOGGER, relayUrl: DEFAULT_RELAY_URL, projectId: DEFAULT_PROJECT_ID, }); setClient(_client); await _subscribeToClientEvents(_client); } catch (err) { throw err; } finally { setIsInitializing(false); } }, [_subscribeToClientEvents]); const onEnable = useCallback( async (caipChainId: string) => { if (!client) { throw new ReferenceError("WalletConnect Client is not initialized."); } const chainId = caipChainId.split(":").pop(); console.log("Enabling EthereumProvider for chainId: ", chainId); const customRpcs = Object.keys(chainData.eip155).reduce( (rpcs: Record, chainId) => { rpcs[chainId] = chainData.eip155[chainId].rpc[0]; return rpcs; }, {}, ); // Create WalletConnect Provider const ethereumProvider = new EthereumProvider({ chainId: Number(chainId), rpc: { infuraId: DEFAULT_INFURA_ID, custom: customRpcs, }, client, }); // FIXME: // Type 'EthereumProvider' is missing the following properties from type 'WebsocketProvider': // isConnecting, requestQueue, responseQueue, connection, and 5 more.ts(2345) // @ts-expect-error const web3Provider = new Web3(ethereumProvider); console.log(ethereumProvider); console.log(web3Provider); setEthereumProvider(ethereumProvider); setWeb3Provider(web3Provider); const _accounts = await ethereumProvider.enable(); const _session = await client.session.get(client.session.topics[0]); setAccounts(_accounts); setSession(_session); setChain(caipChainId); try { setIsFetchingBalances(true); const _balances = await Promise.all( _accounts.map(async account => { const balance = await web3Provider.eth.getBalance(account); return { symbol: "ETH", balance: utils.formatEther(balance) }; }), ); setBalances(_balances); } catch (error: any) { throw new Error(error); } finally { setIsFetchingBalances(false); } QRCodeModal.close(); }, [client, chainData.eip155], ); const _checkForPersistedSession = useCallback( async (_client: Client) => { if (typeof _client === "undefined") { throw new Error("WalletConnect is not initialized"); } // populates existing pairings to state setPairings(_client.pairing.topics); if (typeof session !== "undefined") return; // populates existing session to state (assume only the top one) if (_client.session.topics.length) { const _session = await _client.session.get(_client.session.topics[0]); const [namespace, chainId] = _session.state.accounts[0].split(":"); const caipChainId = `${namespace}:${chainId}`; onEnable(caipChainId); } }, [session, onEnable], ); useEffect(() => { loadChainData(); }, []); useEffect(() => { if (!client) { createClient(); } }, [client, createClient]); useEffect(() => { const getPersistedSession = async () => { if (client && !hasCheckedPersistedSession) { await _checkForPersistedSession(client); setHasCheckedPersistedSession(true); } }; getPersistedSession(); }, [client, _checkForPersistedSession, hasCheckedPersistedSession]); const value = useMemo( () => ({ pairings, isInitializing, balances, isFetchingBalances, accounts, chain, client, session, disconnect, chainData, onEnable, web3Provider, }), [ pairings, isInitializing, balances, isFetchingBalances, accounts, chain, client, session, disconnect, chainData, onEnable, web3Provider, ], ); return ( {children} ); } export function useWalletConnectClient() { const context = useContext(ClientContext); if (context === undefined) { throw new Error("useWalletConnectClient must be used within a ClientContextProvider"); } return context; }