refactor: sets up ClientContext

This commit is contained in:
Ben Kremer 2022-02-04 16:02:24 +01:00
parent f819161934
commit 0732b09991
3 changed files with 227 additions and 85 deletions

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import QRCodeModal from "@walletconnect/legacy-modal"; import QRCodeModal from "@walletconnect/legacy-modal";
import { PairingTypes, SessionTypes } from "@walletconnect/types"; import { SessionTypes } from "@walletconnect/types";
import { ERROR, getAppMetadata } from "@walletconnect/utils"; import { ERROR, getAppMetadata } from "@walletconnect/utils";
import * as encoding from "@walletconnect/encoding"; import * as encoding from "@walletconnect/encoding";
import { apiGetChainNamespace, ChainsMap } from "caip-api"; import { apiGetChainNamespace, ChainsMap } from "caip-api";
@ -17,11 +16,8 @@ import Modal from "./components/Modal";
import { import {
DEFAULT_APP_METADATA, DEFAULT_APP_METADATA,
DEFAULT_MAIN_CHAINS, DEFAULT_MAIN_CHAINS,
DEFAULT_LOGGER,
DEFAULT_EIP155_METHODS, DEFAULT_EIP155_METHODS,
DEFAULT_COSMOS_METHODS, DEFAULT_COSMOS_METHODS,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
DEFAULT_TEST_CHAINS, DEFAULT_TEST_CHAINS,
} from "./constants"; } from "./constants";
import { import {
@ -52,6 +48,7 @@ import {
SLayout, SLayout,
SToggleContainer, SToggleContainer,
} from "./components/app"; } from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
interface FormattedRpcResponse { interface FormattedRpcResponse {
method: string; method: string;
@ -61,105 +58,59 @@ interface FormattedRpcResponse {
} }
export default function App() { export default function App() {
const [loading, setLoading] = useState(false);
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [fetching, setFetching] = useState(false);
const [isTestnet, setIsTestnet] = useState(getInitialStateTestnet()); const [isTestnet, setIsTestnet] = useState(getInitialStateTestnet());
const [modal, setModal] = useState(""); const [modal, setModal] = useState("");
const [client, setClient] = useState<Client>();
const [session, setSession] = useState<SessionTypes.Created>();
const [accounts, setAccounts] = useState<string[]>([]);
const [pairings, setPairings] = useState<string[]>([]);
const [result, setResult] = useState<{ const [result, setResult] = useState<{
method: string; method: string;
valid: boolean; valid: boolean;
} | null>(); } | null>();
const [balances, setBalances] = useState<AccountBalances>({});
const [chainData, setChainData] = useState<ChainNamespaces>({}); const [chainData, setChainData] = useState<ChainNamespaces>({});
const [chains, setChains] = useState<string[]>([]);
const closeModal = () => setModal(""); const closeModal = () => setModal("");
const openPairingModal = () => setModal("pairing"); const openPairingModal = () => setModal("pairing");
const openPingModal = () => setModal("ping"); const openPingModal = () => setModal("ping");
const openRequestModal = () => setModal("request"); const openRequestModal = () => setModal("request");
const init = async () => { const {
try { client,
setLoading(true); session,
await loadChainData(); chains,
accounts,
balances,
fetching,
loading,
setSession,
setPairings,
setAccounts,
setChains,
setFetching,
setBalances,
} = useWalletConnectClient();
const _client = await Client.init({ console.log({
logger: DEFAULT_LOGGER, client,
relayUrl: DEFAULT_RELAY_URL, session,
projectId: DEFAULT_PROJECT_ID, chains,
accounts,
balances,
fetching,
loading,
setSession,
setPairings,
setAccounts,
setChains,
setFetching,
setBalances,
}); });
setClient(_client);
await subscribeToEvents(_client);
await checkPersistedState(_client);
} catch (err) {
throw err;
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
init(); loadChainData();
}, []); }, []);
const subscribeToEvents = async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
let _session = {} as SessionTypes.Settled;
if (_client.session.topics.length) {
_session = await _client.session.get(_client.session.topics[0]);
}
_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 (proposal: PairingTypes.Settled) => {
if (typeof client === "undefined") return;
setPairings(client.pairing.topics);
});
_client.on(CLIENT_EVENTS.session.deleted, (deletedSession: SessionTypes.Settled) => {
if (deletedSession.topic !== _session?.topic) return;
console.log("EVENT", "session_deleted");
// TODO:
// this.resetApp();
window.location.reload();
});
};
const checkPersistedState = 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 chains = session.state.accounts.map(account =>
account.split(":").slice(0, -1).join(":"),
);
setAccounts(session.state.accounts);
setChains(chains);
onSessionConnected(session);
}
};
const connect = async (pairing?: { topic: string }) => { const connect = async (pairing?: { topic: string }) => {
if (typeof client === "undefined") { if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized"); throw new Error("WalletConnect is not initialized");

View File

@ -0,0 +1,188 @@
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import QRCodeModal from "@walletconnect/legacy-modal";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import { DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL } from "../constants";
import { AccountBalances, apiGetAccountAssets } from "../helpers";
/**
* Types
*/
interface IContext {
client: Client | undefined;
session: SessionTypes.Created | undefined;
loading: boolean;
fetching: boolean;
chains: string[];
pairings: string[];
accounts: string[];
balances: AccountBalances;
setSession: any;
setPairings: any;
setAccounts: any;
setChains: any;
setFetching: any;
setBalances: any;
}
/**
* 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>();
// UI State
const [fetching, setFetching] = useState(false);
const [loading, setLoading] = useState(false);
// Other entities
const [balances, setBalances] = useState<AccountBalances>({});
const [accounts, setAccounts] = useState<string[]>([]);
const [chains, setChains] = useState<string[]>([]);
const subscribeToEvents = async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
let _session = {} as SessionTypes.Settled;
if (_client.session.topics.length) {
_session = await _client.session.get(_client.session.topics[0]);
}
_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 (proposal: PairingTypes.Settled) => {
if (typeof client === "undefined") return;
setPairings(client.pairing.topics);
});
_client.on(CLIENT_EVENTS.session.deleted, (deletedSession: SessionTypes.Settled) => {
if (deletedSession.topic !== _session?.topic) return;
console.log("EVENT", "session_deleted");
// TODO:
// this.resetApp();
window.location.reload();
});
};
const checkPersistedState = 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 chains = session.state.accounts.map(account =>
account.split(":").slice(0, -1).join(":"),
);
setAccounts(session.state.accounts);
setChains(chains);
onSessionConnected(session);
}
};
const onSessionConnected = async (incomingSession: SessionTypes.Settled) => {
setSession(incomingSession);
onSessionUpdate(incomingSession.state.accounts, incomingSession.permissions.blockchain.chains);
};
const onSessionUpdate = async (accounts: string[], chains: string[]) => {
setChains(chains);
setAccounts(accounts);
await getAccountBalances(accounts);
};
const getAccountBalances = async (_accounts: string[]) => {
setFetching(true);
try {
const arr = await Promise.all(
_accounts.map(async account => {
const [namespace, reference, address] = account.split(":");
const chainId = `${namespace}:${reference}`;
const assets = await apiGetAccountAssets(address, chainId);
return { account, assets };
}),
);
const balances: AccountBalances = {};
arr.forEach(({ account, assets }) => {
balances[account] = assets;
});
setBalances(balances);
} catch (e) {
console.error(e);
} finally {
setFetching(false);
}
};
useEffect(() => {
const init = async () => {
try {
const _client = await Client.init({
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
projectId: DEFAULT_PROJECT_ID,
});
setClient(_client);
await subscribeToEvents(_client);
await checkPersistedState(_client);
} catch (err) {
throw err;
} finally {
setLoading(false);
}
};
init();
}, []);
return (
<ClientContext.Provider
value={{
pairings,
fetching,
loading,
balances,
accounts,
chains,
client,
session,
setSession,
setPairings,
setAccounts,
setChains,
setFetching,
setBalances,
}}
>
{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;
}

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components"; import { createGlobalStyle } from "styled-components";
import { ClientContextProvider } from "./contexts/ClientContext";
import HooksApp from "./HooksApp"; import HooksApp from "./HooksApp";
import { globalStyle } from "./styles"; import { globalStyle } from "./styles";
@ -18,7 +19,9 @@ declare global {
ReactDOM.render( ReactDOM.render(
<> <>
<GlobalStyle /> <GlobalStyle />
<ClientContextProvider>
<HooksApp /> <HooksApp />
</ClientContextProvider>
</>, </>,
document.getElementById("root"), document.getElementById("root"),
); );