300 lines
8.0 KiB
TypeScript
300 lines
8.0 KiB
TypeScript
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<void>;
|
|
isInitializing: boolean;
|
|
chain: string;
|
|
pairings: string[];
|
|
accounts: string[];
|
|
balances: { symbol: string; balance: string }[];
|
|
isFetchingBalances: boolean;
|
|
chainData: ChainNamespaces;
|
|
onEnable: (chainId: string) => Promise<void>;
|
|
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<string[]>([]);
|
|
const [session, setSession] = useState<SessionTypes.Created>();
|
|
|
|
const [ethereumProvider, setEthereumProvider] = useState<EthereumProvider>();
|
|
const [web3Provider, setWeb3Provider] = useState<Web3>();
|
|
|
|
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<string[]>([]);
|
|
const [chainData, setChainData] = useState<ChainNamespaces>({});
|
|
const [chain, setChain] = useState<string>("");
|
|
|
|
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<string, string>, 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 (
|
|
<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;
|
|
}
|