refactor: creates standalone JsonRpcContext

This commit is contained in:
Ben Kremer 2022-02-04 20:48:43 +01:00
parent 34e175f24c
commit 7eeefd4f65
3 changed files with 489 additions and 416 deletions

View File

@ -1,9 +1,6 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import * as encoding from "@walletconnect/encoding";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import { BigNumber } from "ethers";
// import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
@ -11,18 +8,7 @@ import Column from "./components/Column";
import Header from "./components/Header";
import Modal from "./components/Modal";
import { DEFAULT_MAIN_CHAINS, DEFAULT_TEST_CHAINS } from "./constants";
import {
AccountAction,
eip712,
hashPersonalMessage,
hashTypedDataMessage,
verifySignature,
formatTestTransaction,
ChainNamespaces,
setInitialStateTestnet,
getInitialStateTestnet,
getAllChainNamespaces,
} from "./helpers";
import { AccountAction, setInitialStateTestnet, getInitialStateTestnet } from "./helpers";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PairingModal from "./modals/PairingModal";
@ -38,27 +24,13 @@ import {
SToggleContainer,
} from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
interface FormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
import { useJsonRpc } from "./contexts/JsonRpcContext";
export default function App() {
const [pending, setPending] = useState(false);
const [isTestnet, setIsTestnet] = useState(getInitialStateTestnet());
const [modal, setModal] = useState("");
const [result, setResult] = useState<{
method: string;
valid: boolean;
} | null>();
const [chainData, setChainData] = useState<ChainNamespaces>({});
const closeModal = () => setModal("");
const openPairingModal = () => setModal("pairing");
const openPingModal = () => setModal("ping");
@ -77,9 +49,15 @@ export default function App() {
setChains,
} = useWalletConnectClient();
useEffect(() => {
loadChainData();
}, []);
const {
chainData,
ping,
testSendTransaction,
testSignPersonalMessage,
testSignTypedData,
isRpcRequestPending,
rpcResult,
} = useJsonRpc();
const onConnect = () => {
if (typeof client === "undefined") {
@ -91,399 +69,38 @@ export default function App() {
connect();
};
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);
};
// ----- EVM RPC -----
const createJsonRpcRequestHandler =
(rpcRequest: (...requestArgs: [any]) => Promise<FormattedRpcResponse>) =>
async (chainId: string) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
const result = await rpcRequest(chainId);
setResult(result);
} catch (err) {
console.error(err);
setResult(null);
} finally {
setPending(false);
}
};
const ping = async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
const onPing = async () => {
openPingModal();
let valid = false;
try {
await client.session.ping(session.topic);
valid = true;
} catch (e) {
valid = false;
}
// display result
setResult({
method: "ping",
valid,
});
} catch (e) {
console.error(e);
setResult(null);
} finally {
setPending(false);
}
await ping();
};
const testSendTransaction = createJsonRpcRequestHandler(async (chainId: string) => {
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// open modal
const onSendTransaction = async (chainId: string) => {
openRequestModal();
const tx = await formatTestTransaction(account);
const balance = BigNumber.from(balances[account][0].balance || "0");
if (balance.lt(BigNumber.from(tx.gasPrice).mul(tx.gasLimit))) {
return {
method: "eth_sendTransaction",
address,
valid: false,
result: "Insufficient funds for intrinsic transaction cost",
await testSendTransaction(chainId);
};
}
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_sendTransaction",
params: [tx],
},
});
// format displayed result
return {
method: "eth_sendTransaction",
address,
valid: true,
result,
};
});
const testSignPersonalMessage = createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = `My email is john@doe.com - ${Date.now()}`;
// encode message (hex)
const hexMsg = encoding.utf8ToHex(message, true);
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// personal_sign params
const params = [hexMsg, address];
// open modal
const onSignPersonalMessage = async (chainId: string) => {
openRequestModal();
// send message
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "personal_sign",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashPersonalMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "personal_sign",
address,
valid,
result,
await testSignPersonalMessage(chainId);
};
});
const testSignTypedData = createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = JSON.stringify(eip712.example);
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// eth_signTypedData params
const params = [address, message];
// open modal
const onSignTypedData = async (chainId: string) => {
openRequestModal();
// send message
const result = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_signTypedData",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashTypedDataMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "eth_signTypedData",
address,
valid,
result,
};
});
// ------ COSMOS RPC ------
const testSignDirect = async (chainId: string) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
// test direct sign doc inputs
const inputs = {
fee: [{ amount: "2000", denom: "ucosm" }],
pubkey: "AgSEjOuOr991QlHCORRmdE5ahVKeyBrmtgoYepCpQGOW",
gasLimit: 200000,
accountNumber: 1,
sequence: 1,
bodyBytes:
"0a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d120731323334353637",
authInfoBytes:
"0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c",
};
// split chainId
const [namespace, reference] = chainId.split(":");
// format sign doc
const signDoc = formatDirectSignDoc(
inputs.fee,
inputs.pubkey,
inputs.gasLimit,
inputs.accountNumber,
inputs.sequence,
inputs.bodyBytes,
reference,
);
// get cosmos address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// cosmos_signDirect params
const params = {
signerAddress: address,
signDoc: stringifySignDocValues(signDoc),
};
// open modal
openRequestModal();
// send message
const result = await client.request({
topic: session.topic,
chainId,
request: {
method: "cosmos_signDirect",
params,
},
});
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
const formattedResult = {
method: "cosmos_signDirect",
address,
valid,
result: result.signature.signature,
};
// display result
setResult(formattedResult);
} catch (e) {
console.error(e);
setResult(null);
} finally {
setPending(false);
}
};
const testSignAmino = async (chainId: string) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
// split chainId
const [namespace, reference] = chainId.split(":");
// test amino sign doc
const signDoc = {
msgs: [],
fee: { amount: [], gas: "23" },
chain_id: "foochain",
memo: "hello, world",
account_number: "7",
sequence: "54",
};
// get cosmos address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// cosmos_signAmino params
const params = { signerAddress: address, signDoc };
// open modal
openRequestModal();
// send message
const result = await client.request({
topic: session.topic,
chainId,
request: {
method: "cosmos_signAmino",
params,
},
});
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
const formattedResult = {
method: "cosmos_signAmino",
address,
valid,
result: result.signature.signature,
};
// display result
setResult(formattedResult);
} catch (e) {
console.error(e);
setResult(null);
} finally {
setPending(false);
}
await testSignTypedData(chainId);
};
const getEthereumActions = (): AccountAction[] => {
return [
{ method: "eth_sendTransaction", callback: testSendTransaction },
{ method: "personal_sign", callback: testSignPersonalMessage },
{ method: "eth_signTypedData", callback: testSignTypedData },
{ method: "eth_sendTransaction", callback: onSendTransaction },
{ method: "personal_sign", callback: onSignPersonalMessage },
{ method: "eth_signTypedData", callback: onSignTypedData },
];
};
const getCosmosActions = (): AccountAction[] => {
return [
{ method: "cosmos_signDirect", callback: testSignDirect },
{ method: "cosmos_signAmino", callback: testSignAmino },
// { method: "cosmos_signDirect", callback: testSignDirect },
// { method: "cosmos_signAmino", callback: testSignAmino },
];
};
@ -522,9 +139,9 @@ export default function App() {
}
return <PairingModal pairings={client.pairing.values} connect={connect} />;
case "request":
return <RequestModal pending={pending} result={result} />;
return <RequestModal pending={isRpcRequestPending} result={rpcResult} />;
case "ping":
return <PingModal pending={pending} result={result} />;
return <PingModal pending={isRpcRequestPending} result={rpcResult} />;
default:
return null;
}
@ -586,7 +203,7 @@ export default function App() {
return (
<SLayout>
<Column maxWidth={1000} spanHeight>
<Header ping={ping} disconnect={disconnect} session={session} />
<Header ping={onPing} disconnect={disconnect} session={session} />
<SContent>{loading ? "Loading..." : renderContent()}</SContent>
</Column>
<Modal show={!!modal} closeModal={closeModal}>

View File

@ -0,0 +1,453 @@
import { BigNumber } from "ethers";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import * as encoding from "@walletconnect/encoding";
import {
ChainNamespaces,
eip712,
formatTestTransaction,
getAllChainNamespaces,
hashPersonalMessage,
hashTypedDataMessage,
verifySignature,
} from "../helpers";
import { useWalletConnectClient } from "./ClientContext";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
/**
* Types
*/
interface IFormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
interface IRpcResult {
method: string;
valid: boolean;
}
interface IContext {
ping: () => Promise<void>;
testSendTransaction: (chainId: string) => Promise<void>;
testSignPersonalMessage: (chainId: string) => Promise<void>;
testSignTypedData: (chainId: string) => Promise<void>;
chainData: ChainNamespaces;
rpcResult?: IRpcResult | null;
isRpcRequestPending: boolean;
}
/**
* Context
*/
export const JsonRpcContext = createContext<IContext>({} as IContext);
/**
* Provider
*/
export function JsonRpcContextProvider({ children }: { children: ReactNode | ReactNode[] }) {
const [pending, setPending] = useState(false);
const [result, setResult] = useState<IRpcResult | null>();
const [chainData, setChainData] = useState<ChainNamespaces>({});
const { client, session, accounts, balances } = useWalletConnectClient();
useEffect(() => {
loadChainData();
}, []);
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 _createJsonRpcRequestHandler =
(rpcRequest: (...requestArgs: [any]) => Promise<IFormattedRpcResponse>) =>
async (chainId: string) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
const result = await rpcRequest(chainId);
setResult(result);
} catch (err) {
console.error(err);
setResult(null);
} finally {
setPending(false);
}
};
const ping = async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
let valid = false;
try {
await client.session.ping(session.topic);
valid = true;
} catch (e) {
valid = false;
}
// display result
setResult({
method: "ping",
valid,
});
} catch (e) {
console.error(e);
setResult(null);
} finally {
setPending(false);
}
};
const testSendTransaction = _createJsonRpcRequestHandler(async (chainId: string) => {
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
const tx = await formatTestTransaction(account);
const balance = BigNumber.from(balances[account][0].balance || "0");
if (balance.lt(BigNumber.from(tx.gasPrice).mul(tx.gasLimit))) {
return {
method: "eth_sendTransaction",
address,
valid: false,
result: "Insufficient funds for intrinsic transaction cost",
};
}
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_sendTransaction",
params: [tx],
},
});
// format displayed result
return {
method: "eth_sendTransaction",
address,
valid: true,
result,
};
});
const testSignPersonalMessage = _createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = `My email is john@doe.com - ${Date.now()}`;
// encode message (hex)
const hexMsg = encoding.utf8ToHex(message, true);
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// personal_sign params
const params = [hexMsg, address];
// send message
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "personal_sign",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashPersonalMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "personal_sign",
address,
valid,
result,
};
});
const testSignTypedData = _createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = JSON.stringify(eip712.example);
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
// eth_signTypedData params
const params = [address, message];
// send message
const result = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_signTypedData",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashTypedDataMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "eth_signTypedData",
address,
valid,
result,
};
});
return (
<JsonRpcContext.Provider
value={{
chainData,
ping,
testSendTransaction,
testSignPersonalMessage,
testSignTypedData,
rpcResult: result,
isRpcRequestPending: pending,
}}
>
{children}
</JsonRpcContext.Provider>
);
}
export function useJsonRpc() {
const context = useContext(JsonRpcContext);
if (context === undefined) {
throw new Error("useJsonRpc must be used within a JsonRpcContextProvider");
}
return context;
}
// ------ COSMOS RPC ------
// const testSignDirect = async (chainId: string) => {
// if (typeof client === "undefined") {
// throw new Error("WalletConnect is not initialized");
// }
// if (typeof session === "undefined") {
// throw new Error("Session is not connected");
// }
// try {
// // test direct sign doc inputs
// const inputs = {
// fee: [{ amount: "2000", denom: "ucosm" }],
// pubkey: "AgSEjOuOr991QlHCORRmdE5ahVKeyBrmtgoYepCpQGOW",
// gasLimit: 200000,
// accountNumber: 1,
// sequence: 1,
// bodyBytes:
// "0a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d120731323334353637",
// authInfoBytes:
// "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c",
// };
// // split chainId
// const [namespace, reference] = chainId.split(":");
// // format sign doc
// const signDoc = formatDirectSignDoc(
// inputs.fee,
// inputs.pubkey,
// inputs.gasLimit,
// inputs.accountNumber,
// inputs.sequence,
// inputs.bodyBytes,
// reference,
// );
// // get cosmos address
// const account = accounts.find(account => account.startsWith(chainId));
// if (account === undefined) throw new Error("Account is not found");
// const address = account.split(":").pop();
// if (address === undefined) throw new Error("Address is invalid");
// // cosmos_signDirect params
// const params = {
// signerAddress: address,
// signDoc: stringifySignDocValues(signDoc),
// };
// // open modal
// openRequestModal();
// // send message
// const result = await client.request({
// topic: session.topic,
// chainId,
// request: {
// method: "cosmos_signDirect",
// params,
// },
// });
// const targetChainData = chainData[namespace][reference];
// if (typeof targetChainData === "undefined") {
// throw new Error(`Missing chain data for chainId: ${chainId}`);
// }
// // TODO: check if valid
// const valid = true;
// // format displayed result
// const formattedResult = {
// method: "cosmos_signDirect",
// address,
// valid,
// result: result.signature.signature,
// };
// // display result
// setResult(formattedResult);
// } catch (e) {
// console.error(e);
// setResult(null);
// } finally {
// setPending(false);
// }
// };
// const testSignAmino = async (chainId: string) => {
// if (typeof client === "undefined") {
// throw new Error("WalletConnect is not initialized");
// }
// if (typeof session === "undefined") {
// throw new Error("Session is not connected");
// }
// try {
// // split chainId
// const [namespace, reference] = chainId.split(":");
// // test amino sign doc
// const signDoc = {
// msgs: [],
// fee: { amount: [], gas: "23" },
// chain_id: "foochain",
// memo: "hello, world",
// account_number: "7",
// sequence: "54",
// };
// // get cosmos address
// const account = accounts.find(account => account.startsWith(chainId));
// if (account === undefined) throw new Error("Account is not found");
// const address = account.split(":").pop();
// if (address === undefined) throw new Error("Address is invalid");
// // cosmos_signAmino params
// const params = { signerAddress: address, signDoc };
// // open modal
// openRequestModal();
// // send message
// const result = await client.request({
// topic: session.topic,
// chainId,
// request: {
// method: "cosmos_signAmino",
// params,
// },
// });
// const targetChainData = chainData[namespace][reference];
// if (typeof targetChainData === "undefined") {
// throw new Error(`Missing chain data for chainId: ${chainId}`);
// }
// // TODO: check if valid
// const valid = true;
// // format displayed result
// const formattedResult = {
// method: "cosmos_signAmino",
// address,
// valid,
// result: result.signature.signature,
// };
// // display result
// setResult(formattedResult);
// } catch (e) {
// console.error(e);
// setResult(null);
// } finally {
// setPending(false);
// }
// };

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components";
import { ClientContextProvider } from "./contexts/ClientContext";
import { JsonRpcContextProvider } from "./contexts/JsonRpcContext";
import HooksApp from "./HooksApp";
import { globalStyle } from "./styles";
@ -20,7 +21,9 @@ ReactDOM.render(
<>
<GlobalStyle />
<ClientContextProvider>
<JsonRpcContextProvider>
<HooksApp />
</JsonRpcContextProvider>
</ClientContextProvider>
</>,
document.getElementById("root"),