wallet-connect-web-examples/dapps/react-dapp-v2-with-web3js/src/App.tsx

344 lines
9.8 KiB
TypeScript

import React, { useState } from "react";
import { version } from "@walletconnect/client/package.json";
import * as encoding from "@walletconnect/encoding";
import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
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, getLocalStorageTestnetFlag, setLocaleStorageTestnetFlag } from "./helpers";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PingModal from "./modals/PingModal";
import {
SAccounts,
SAccountsContainer,
SButtonContainer,
SContent,
SLanding,
SLayout,
SToggleContainer,
} from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
import { utils } from "ethers";
interface IFormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
export default function App() {
const [isTestnet, setIsTestnet] = useState(getLocalStorageTestnetFlag());
const [isRpcRequestPending, setIsRpcRequestPending] = useState(false);
const [rpcResult, setRpcResult] = useState<IFormattedRpcResponse | null>();
const [modal, setModal] = useState("");
const closeModal = () => setModal("");
const openPingModal = () => setModal("ping");
const openRequestModal = () => setModal("request");
// Initialize the WalletConnect client.
const {
client,
session,
disconnect,
chain,
accounts,
balances,
chainData,
isFetchingBalances,
isInitializing,
onEnable,
web3Provider,
} = useWalletConnectClient();
const ping = async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect Client is not initialized");
}
try {
setIsRpcRequestPending(true);
const _session = await client.session.get(client.session.topics[0]);
await client.session.ping(_session.topic);
setRpcResult({
address: "",
method: "ping",
valid: true,
result: "success",
});
} catch (error) {
console.error("RPC request failed:", error);
} finally {
setIsRpcRequestPending(false);
}
};
const onPing = async () => {
openPingModal();
await ping();
};
const testSendTransaction: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
const [address] = await web3Provider.eth.getAccounts();
const tx = {
from: address,
to: address,
gasPrice: "20000000000",
value: "0",
};
const { transactionHash } = await web3Provider.eth.sendTransaction(tx);
return {
method: "eth_sendTransaction",
address,
valid: true,
result: transactionHash,
};
};
const testSignTransaction: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
const [address] = await web3Provider.eth.getAccounts();
const tx = {
from: address,
to: address,
gasPrice: "20000000000",
value: "0",
};
// The return signature here is `RLPEncodedTransaction` but it actually returns as string (?).
const result = (await web3Provider.eth.signTransaction(tx)) as unknown as string;
return {
method: "eth_signTransaction",
address,
valid: true,
result,
};
};
const testSignMessage: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
const msg = "hello world";
const hexMsg = encoding.utf8ToHex(msg, true);
const [address] = await web3Provider.eth.getAccounts();
const signature = await web3Provider.eth.personal.sign(hexMsg, address, "");
const valid = utils.verifyMessage(msg, signature) === address;
return {
method: "personal_sign",
address,
valid,
result: signature,
};
};
const testEthSign: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
const msg = "hello world";
const hexMsg = encoding.utf8ToHex(msg, true);
const [address] = await web3Provider.eth.getAccounts();
const signature = await web3Provider.eth.sign(hexMsg, address);
const valid = utils.verifyMessage(msg, signature) === address;
return {
method: "eth_sign (standard)",
address,
valid,
result: signature,
};
};
const testSignTypedData: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
if (!web3Provider.currentProvider) {
throw new Error("web3Provider.currentProvider is not set");
}
const typedData = {
types: {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" },
],
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person" },
{ name: "contents", type: "string" },
],
},
primaryType: "Mail",
domain: {
name: "Ether Mail",
version: "1",
chainId: 1,
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
},
message: {
from: {
name: "Cow",
wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
},
to: {
name: "Bob",
wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
},
contents: "Hello, Bob!",
},
};
const message = JSON.stringify(typedData);
const [address] = await web3Provider.eth.getAccounts();
// eth_signTypedData params
const params = [address, message];
// FIXME:
// Property 'request' does not exist on type 'string | HttpProvider | IpcProvider | WebsocketProvider | AbstractProvider'.
// Property 'request' does not exist on type 'string'.ts(2339)
// @ts-expect-error
const signature = await web3Provider.currentProvider.request({
method: "eth_signTypedData",
params,
});
const valid =
utils.verifyTypedData(typedData.domain, typedData.types, typedData.message, signature) ===
address;
return {
method: "eth_signTypedData",
address,
valid,
result: signature,
};
};
const getEthereumActions = (): AccountAction[] => {
const wrapRpcRequest = (rpcRequest: () => Promise<IFormattedRpcResponse>) => async () => {
openRequestModal();
try {
setIsRpcRequestPending(true);
const result = await rpcRequest();
setRpcResult(result);
} catch (error) {
console.error("RPC request failed:", error);
} finally {
setIsRpcRequestPending(false);
}
};
return [
{ method: "eth_sendTransaction", callback: wrapRpcRequest(testSendTransaction) },
{ method: "eth_signTransaction", callback: wrapRpcRequest(testSignTransaction) },
{ method: "personal_sign", callback: wrapRpcRequest(testSignMessage) },
{ method: "eth_sign (standard)", callback: wrapRpcRequest(testEthSign) },
{ method: "eth_signTypedData", callback: wrapRpcRequest(testSignTypedData) },
];
};
const getBlockchainActions = (chainId: string) => {
const [namespace] = chainId.split(":");
switch (namespace) {
case "eip155":
return getEthereumActions();
case "cosmos":
return [];
default:
break;
}
};
// Toggle between displaying testnet or mainnet chains as selection options.
const toggleTestnets = () => {
const nextIsTestnetState = !isTestnet;
setIsTestnet(nextIsTestnetState);
setLocaleStorageTestnetFlag(nextIsTestnetState);
};
// Renders the appropriate model for the given request that is currently in-flight.
const renderModal = () => {
switch (modal) {
case "request":
return <RequestModal pending={isRpcRequestPending} result={rpcResult} />;
case "ping":
return <PingModal pending={isRpcRequestPending} result={rpcResult} />;
default:
return null;
}
};
const renderContent = () => {
const chainOptions = isTestnet ? DEFAULT_TEST_CHAINS : DEFAULT_MAIN_CHAINS;
return !accounts.length && !Object.keys(balances).length ? (
<SLanding center>
<Banner />
<h6>
<span>{`Using v${version || "2.0.0-beta"}`}</span>
</h6>
<SButtonContainer>
<h6>Select an Ethereum chain:</h6>
<SToggleContainer>
<p>Testnets Only?</p>
<Toggle active={isTestnet} onClick={toggleTestnets} />
</SToggleContainer>
{chainOptions.map(chainId => (
<Blockchain key={chainId} chainId={chainId} chainData={chainData} onClick={onEnable} />
))}
</SButtonContainer>
</SLanding>
) : (
<SAccountsContainer>
<h3>Account</h3>
<SAccounts>
{accounts.map(account => {
return (
<Blockchain
key={account}
active={true}
chainData={chainData}
fetching={isFetchingBalances}
address={account}
chainId={chain}
balances={balances}
actions={getBlockchainActions(chain)}
/>
);
})}
</SAccounts>
</SAccountsContainer>
);
};
return (
<SLayout>
<Column maxWidth={1000} spanHeight>
<Header ping={onPing} disconnect={disconnect} session={session} />
<SContent>{isInitializing ? "Loading..." : renderContent()}</SContent>
</Column>
<Modal show={!!modal} closeModal={closeModal}>
{renderModal()}
</Modal>
</SLayout>
);
}