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

306 lines
9.0 KiB
TypeScript

import React, { useState } from "react";
import { version } from "@walletconnect/client/package.json";
import { clusterApiUrl, Connection, Keypair, SystemProgram, Transaction } from "@solana/web3.js";
import bs58 from "bs58";
import { verifyMessageSignature } from "solana-wallet";
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 RequestModal from "./modals/RequestModal";
import PingModal from "./modals/PingModal";
import {
SAccounts,
SAccountsContainer,
SButtonContainer,
SContent,
SLanding,
SLayout,
SToggleContainer,
} from "./components/app";
import { SolanaRpcMethod, useWalletConnectClient } from "./contexts/ClientContext";
import Toggle from "./components/Toggle";
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,
publicKeys,
balances,
chainData,
isInitializing,
onEnable,
} = 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 testSignTransaction = async (account: string): Promise<IFormattedRpcResponse> => {
if (!client || !publicKeys || !session) {
throw new Error("WalletConnect Client not initialized properly.");
}
const address = account.split(":").pop();
if (!address) {
throw new Error(`Could not derive Solana address from CAIP account: ${account}`);
}
const senderPublicKey = publicKeys[address];
const connection = new Connection(clusterApiUrl(isTestnet ? "testnet" : "mainnet-beta"));
// Using deprecated `getRecentBlockhash` over `getLatestBlockhash` here, since `mainnet-beta`
// cluster only seems to support `connection.getRecentBlockhash` currently.
const { blockhash } = await connection.getRecentBlockhash();
const transaction = new Transaction({
feePayer: senderPublicKey,
recentBlockhash: blockhash,
}).add(
SystemProgram.transfer({
fromPubkey: senderPublicKey,
toPubkey: Keypair.generate().publicKey,
lamports: 1,
}),
);
try {
const { signature } = await client.request({
topic: session.topic,
request: {
method: SolanaRpcMethod.SOL_SIGN_TRANSACTION,
params: {
feePayer: transaction.feePayer!.toBase58(),
recentBlockhash: transaction.recentBlockhash,
instructions: transaction.instructions.map(i => ({
programId: i.programId.toBase58(),
data: bs58.encode(i.data),
keys: i.keys.map(k => ({
isSigner: k.isSigner,
isWritable: k.isWritable,
pubkey: k.pubkey.toBase58(),
})),
})),
},
},
});
// We only need `Buffer.from` here to satisfy the `Buffer` param type for `addSignature`.
// The resulting `UInt8Array` is equivalent to just `bs58.decode(...)`.
transaction.addSignature(senderPublicKey, Buffer.from(bs58.decode(signature)));
const valid = transaction.verifySignatures();
return {
method: SolanaRpcMethod.SOL_SIGN_TRANSACTION,
address,
valid,
result: signature,
};
} catch (error: any) {
throw new Error(error);
}
};
const testSignMessage = async (account: string): Promise<IFormattedRpcResponse> => {
if (!client || !publicKeys || !session) {
throw new Error("WalletConnect Client not initialized properly.");
}
const address = account.split(":").pop();
if (!address) {
throw new Error(`Could not derive Solana address from CAIP account: ${account}`);
}
const senderPublicKey = publicKeys[address];
// Encode message to `UInt8Array` first via `TextEncoder` so we can pass it to `bs58.encode`.
const message = bs58.encode(
new TextEncoder().encode(`This is an example message to be signed - ${Date.now()}`),
);
try {
const { signature } = await client.request({
topic: session.topic,
request: {
method: SolanaRpcMethod.SOL_SIGN_MESSAGE,
params: {
pubkey: senderPublicKey.toBase58(),
message,
},
},
});
const valid = verifyMessageSignature(senderPublicKey.toBase58(), signature, message);
return {
method: SolanaRpcMethod.SOL_SIGN_MESSAGE,
address,
valid,
result: signature,
};
} catch (error: any) {
throw new Error(error);
}
};
const getSolanaActions = (): AccountAction[] => {
const wrapRpcRequest =
(rpcRequest: (account: string) => Promise<IFormattedRpcResponse>) =>
async (account: string) => {
openRequestModal();
try {
setIsRpcRequestPending(true);
const result = await rpcRequest(account);
setRpcResult(result);
} catch (error) {
console.error("RPC request failed:", error);
setRpcResult({ result: error as string });
} finally {
setIsRpcRequestPending(false);
}
};
return [
{
method: SolanaRpcMethod.SOL_SIGN_TRANSACTION,
callback: wrapRpcRequest(testSignTransaction),
},
{
method: SolanaRpcMethod.SOL_SIGN_MESSAGE,
callback: wrapRpcRequest(testSignMessage),
},
];
};
// 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;
}
};
// Toggle between displaying testnet or mainnet chains as selection options.
const toggleTestnets = () => {
const nextIsTestnetState = !isTestnet;
setIsTestnet(nextIsTestnetState);
setLocaleStorageTestnetFlag(nextIsTestnetState);
};
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 chain:</h6>
<SToggleContainer>
<p>Testnet Only?</p>
<Toggle active={isTestnet} onClick={toggleTestnets} />
</SToggleContainer>
{chainOptions.map(chainId => (
<Blockchain
key={chainId}
chainId={chainId}
chainData={chainData}
isTestnet={isTestnet}
onClick={onEnable}
/>
))}
</SButtonContainer>
</SLanding>
) : (
<SAccountsContainer>
<h3>Account</h3>
<SAccounts>
{accounts.map(account => {
return (
<Blockchain
key={account}
active={true}
chainData={chainData}
address={account}
chainId={chain}
balances={balances}
isTestnet={isTestnet}
actions={getSolanaActions()}
/>
);
})}
</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>
);
}