wallet-connect-web-examples/dapps/react-dapp-v2-with-web3js/src/contexts/ClientContext.tsx
Gancho Radkov 2810f0178f
chore: updates @walletconnect deps (#157)
* chore: updates `@walletconnect` deps

* fix: cosmos provider

* fix: updates web3wallet example

* fix: eip155 legacy session request type

---------

Co-authored-by: Gancho Radkov <ganchoradkov@gmail.com>
2023-05-09 16:51:32 +03:00

372 lines
10 KiB
TypeScript

import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { Web3Modal } from "@web3modal/standalone";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import UniversalProvider from "@walletconnect/universal-provider";
import Client from "@walletconnect/sign-client";
import Web3 from "web3";
import { DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL } from "../constants";
import { utils } from "ethers";
import { AccountBalances, ChainNamespaces, getAllChainNamespaces } from "../helpers";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
/**
* Types
*/
interface IContext {
client: Client | undefined;
session: SessionTypes.Struct | undefined;
connect: (caipChainId: string, pairing?: { topic: string }) => Promise<void>;
disconnect: () => Promise<void>;
isInitializing: boolean;
chain: string;
pairings: PairingTypes.Struct[];
accounts: string[];
balances: AccountBalances;
isFetchingBalances: boolean;
chainData: ChainNamespaces;
web3Provider?: Web3;
}
/**
* Context
*/
export const ClientContext = createContext<IContext>({} as IContext);
/**
* Provider
*/
export function ClientContextProvider({ children }: { children: ReactNode | ReactNode[] }) {
const [client, setClient] = useState<Client>();
const [pairings, setPairings] = useState<PairingTypes.Struct[]>([]);
const [session, setSession] = useState<SessionTypes.Struct>();
const [ethereumProvider, setEthereumProvider] = useState<UniversalProvider>();
const [web3Provider, setWeb3Provider] = useState<Web3>();
const [isFetchingBalances, setIsFetchingBalances] = useState(false);
const [isInitializing, setIsInitializing] = useState(false);
const [hasCheckedPersistedSession, setHasCheckedPersistedSession] = useState(false);
const [balances, setBalances] = useState<AccountBalances>({});
const [accounts, setAccounts] = useState<string[]>([]);
const [chainData, setChainData] = useState<ChainNamespaces>({});
const [chain, setChain] = useState<string>("");
const [web3Modal, setWeb3Modal] = useState<Web3Modal>();
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();
resetApp();
}, [ethereumProvider]);
const _subscribeToProviderEvents = useCallback(
async (_client: UniversalProvider) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
_client.on("display_uri", async (uri: string) => {
console.log("EVENT", "QR Code Modal open");
web3Modal?.openModal({ uri });
});
// Subscribe to session ping
_client.on("session_ping", ({ id, topic }: { id: number; topic: string }) => {
console.log("EVENT", "session_ping");
console.log(id, topic);
});
// Subscribe to session event
_client.on("session_event", ({ event, chainId }: { event: any; chainId: string }) => {
console.log("EVENT", "session_event");
console.log(event, chainId);
});
// Subscribe to session update
_client.on(
"session_update",
({ topic, session }: { topic: string; session: SessionTypes.Struct }) => {
console.log("EVENT", "session_updated");
setSession(session);
},
);
// Subscribe to session delete
_client.on("session_delete", ({ id, topic }: { id: number; topic: string }) => {
console.log("EVENT", "session_deleted");
console.log(id, topic);
resetApp();
});
},
[web3Modal],
);
const createClient = useCallback(async () => {
try {
setIsInitializing(true);
const provider = await UniversalProvider.init({
projectId: DEFAULT_PROJECT_ID,
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
});
const web3Modal = new Web3Modal({
projectId: DEFAULT_PROJECT_ID || "",
walletConnectVersion: 2,
});
setEthereumProvider(provider);
setClient(provider.client);
setWeb3Modal(web3Modal);
} catch (err) {
throw err;
} finally {
setIsInitializing(false);
}
}, []);
const createWeb3Provider = useCallback((ethereumProvider: UniversalProvider) => {
const web3Provider = new Web3(ethereumProvider);
setWeb3Provider(web3Provider);
}, []);
const connect = useCallback(
async (caipChainId: string, pairing?: { topic: string }) => {
if (!ethereumProvider) {
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<string, string>, chainId) => {
rpcs[chainId] = chainData.eip155[chainId].rpc[0];
return rpcs;
},
{},
);
const session = await ethereumProvider.connect({
namespaces: {
eip155: {
methods: [
"eth_sendTransaction",
"eth_signTransaction",
"eth_sign",
"personal_sign",
"eth_signTypedData",
],
chains: [`eip155:${chainId}`],
events: ["chainChanged", "accountsChanged"],
rpcMap: customRpcs,
},
},
pairingTopic: pairing?.topic,
});
createWeb3Provider(ethereumProvider);
const _accounts = await ethereumProvider.enable();
console.log("_accounts", _accounts);
setAccounts(_accounts);
setSession(session);
setChain(caipChainId);
web3Modal?.closeModal();
},
[ethereumProvider, chainData.eip155, createWeb3Provider, web3Modal],
);
const onSessionConnected = useCallback(
async (_session: SessionTypes.Struct) => {
if (!ethereumProvider) {
throw new ReferenceError("EthereumProvider is not initialized.");
}
const allNamespaceAccounts = Object.values(_session.namespaces)
.map(namespace => namespace.accounts)
.flat();
const allNamespaceChains = Object.keys(_session.namespaces);
const chainData = allNamespaceAccounts[0].split(":");
const caipChainId = `${chainData[0]}:${chainData[1]}`;
console.log("restored caipChainId", caipChainId);
setChain(caipChainId);
setSession(_session);
setAccounts(allNamespaceAccounts.map(account => account.split(":")[2]));
console.log("RESTORED", allNamespaceChains, allNamespaceAccounts);
createWeb3Provider(ethereumProvider);
},
[ethereumProvider, createWeb3Provider],
);
const _checkForPersistedSession = useCallback(
async (provider: UniversalProvider) => {
if (typeof provider === "undefined") {
throw new Error("WalletConnect is not initialized");
}
const pairings = provider.client.pairing.getAll({ active: true });
// populates existing pairings to state
setPairings(pairings);
console.log("RESTORED PAIRINGS: ", pairings);
if (typeof session !== "undefined") return;
// populates (the last) existing session to state
if (ethereumProvider?.session) {
const _session = ethereumProvider?.session;
console.log("RESTORED SESSION:", _session);
await onSessionConnected(_session);
return _session;
}
},
[session, ethereumProvider, onSessionConnected],
);
useEffect(() => {
loadChainData();
}, []);
useEffect(() => {
if (!client) {
createClient();
}
}, [client, createClient]);
useEffect(() => {
if (ethereumProvider && web3Modal) _subscribeToProviderEvents(ethereumProvider);
}, [_subscribeToProviderEvents, ethereumProvider, web3Modal]);
useEffect(() => {
const fetchBalances = async () => {
if (!web3Provider || !accounts) return;
try {
setIsFetchingBalances(true);
const _balances = await Promise.all(
accounts.map(async account => {
const balance = await web3Provider.eth.getBalance(account);
return {
account,
symbol: "ETH",
balance: utils.formatEther(balance),
contractAddress: "",
};
}),
);
const balancesByAccount = _balances.reduce((obj, balance) => {
obj[balance.account] = balance;
return obj;
}, {} as AccountBalances);
setBalances(balancesByAccount);
} catch (error: any) {
throw new Error(error);
} finally {
setIsFetchingBalances(false);
}
};
fetchBalances();
}, [web3Provider, accounts]);
useEffect(() => {
const getPersistedSession = async () => {
if (!ethereumProvider) return;
await _checkForPersistedSession(ethereumProvider);
setHasCheckedPersistedSession(true);
};
if (ethereumProvider && chainData && !hasCheckedPersistedSession) {
getPersistedSession();
}
}, [ethereumProvider, chainData, _checkForPersistedSession, hasCheckedPersistedSession]);
const value = useMemo(
() => ({
pairings,
isInitializing,
balances,
isFetchingBalances,
accounts,
chain,
client,
session,
disconnect,
connect,
chainData,
web3Provider,
}),
[
pairings,
isInitializing,
balances,
isFetchingBalances,
accounts,
chain,
client,
session,
disconnect,
connect,
chainData,
web3Provider,
],
);
return (
<ClientContext.Provider
value={{
...value,
}}
>
{children}
</ClientContext.Provider>
);
}
export function useWalletConnectClient() {
const context = useContext(ClientContext);
if (context === undefined) {
throw new Error("useWalletConnectClient must be used within a ClientContextProvider");
}
return context;
}