From ae2a3d2d60cdd9b4e5780a74265544e8de75022f Mon Sep 17 00:00:00 2001 From: Ben Kremer Date: Fri, 11 Mar 2022 18:15:56 +0100 Subject: [PATCH] feat(dapp-with-solana): sets up client and `sol_signTransaction` test --- .../src/App.tsx | 140 +++++++----------- .../src/contexts/ClientContext.tsx | 98 ++++++------ 2 files changed, 104 insertions(+), 134 deletions(-) diff --git a/dapps/react-dapp-v2-with-solana-web3js/src/App.tsx b/dapps/react-dapp-v2-with-solana-web3js/src/App.tsx index a52f294..cad0ec1 100644 --- a/dapps/react-dapp-v2-with-solana-web3js/src/App.tsx +++ b/dapps/react-dapp-v2-with-solana-web3js/src/App.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { version } from "@walletconnect/client/package.json"; -import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet"; +import { Keypair, SystemProgram, Transaction } from "@solana/web3.js"; +import bs58 from "bs58"; import Banner from "./components/Banner"; import Blockchain from "./components/Blockchain"; @@ -19,7 +20,7 @@ import { SLanding, SLayout, } from "./components/app"; -import { useWalletConnectClient } from "./contexts/ClientContext"; +import { SolanaRpcMethod, useWalletConnectClient } from "./contexts/ClientContext"; interface IFormattedRpcResponse { method?: string; @@ -28,14 +29,6 @@ interface IFormattedRpcResponse { result: string; } -interface CosmosRpcResponse { - pub_key: { - type: string; - value: string; - }; - signature: string; -} - export default function App() { const [isRpcRequestPending, setIsRpcRequestPending] = useState(false); const [rpcResult, setRpcResult] = useState(); @@ -53,11 +46,11 @@ export default function App() { disconnect, chain, accounts, + publicKey, balances, chainData, isInitializing, onEnable, - cosmosProvider, } = useWalletConnectClient(); const ping = async () => { @@ -87,90 +80,55 @@ export default function App() { await ping(); }; - const testSignDirect: () => Promise = async () => { - if (!cosmosProvider) { - throw new Error("cosmosProvider not connected"); + const testSignTransaction = async (): Promise => { + if (!client || !publicKey || !session) { + throw new Error("WalletConnect Client not initialized properly."); } - // test direct sign doc inputs - const inputs = { - fee: [{ amount: "2000", denom: "ucosm" }], - pubkey: "AgSEjOuOr991QlHCORRmdE5ahVKeyBrmtgoYepCpQGOW", - gasLimit: 200000, - accountNumber: 1, - sequence: 1, - bodyBytes: - "0a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d120731323334353637", - authInfoBytes: - "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c", - }; - - // format sign doc - const signDoc = formatDirectSignDoc( - inputs.fee, - inputs.pubkey, - inputs.gasLimit, - inputs.accountNumber, - inputs.sequence, - inputs.bodyBytes, - "cosmoshub-4", + const transaction = new Transaction({ feePayer: publicKey }).add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: Keypair.generate().publicKey, + lamports: 1, + }), ); - const [address] = cosmosProvider.accounts; + try { + const signature = await client.request({ + topic: session.topic, + request: { + method: SolanaRpcMethod.SOL_SIGN_TRANSACTION, + params: { + feePayer: transaction.feePayer!.toBase58(), + 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(), + })), + })), + recentBlockhash: transaction.recentBlockhash, + }, + }, + }); - // cosmos_signDirect params - const params = { - signerAddress: address, - signDoc: stringifySignDocValues(signDoc), - }; + // @ts-expect-error + transaction.addSignature(publicKey, bs58.decode(signature)); - const result = await cosmosProvider.request({ - method: "cosmos_signDirect", - params, - }); - - return { - method: "cosmos_signDirect", - address, - valid: true, - result: result.signature, - }; - }; - - const testSignAmino: () => Promise = async () => { - if (!cosmosProvider) { - throw new Error("cosmosProvider not connected"); + return { + method: SolanaRpcMethod.SOL_SIGN_TRANSACTION, + // address, + valid: true, + result: signature, + }; + } catch (error: any) { + throw new Error(error); } - - // test amino sign doc - const signDoc = { - msgs: [], - fee: { amount: [], gas: "23" }, - chain_id: "foochain", - memo: "hello, world", - account_number: "7", - sequence: "54", - }; - - const [address] = cosmosProvider.accounts; - - // cosmos_signAmino params - const params = { signerAddress: address, signDoc }; - - const result = await cosmosProvider.request({ - method: "cosmos_signAmino", - params, - }); - - return { - method: "cosmos_signAmino", - address, - valid: true, - result: result.signature, - }; }; - const getCosmosActions = (): AccountAction[] => { + const getSolanaActions = (): AccountAction[] => { const wrapRpcRequest = (rpcRequest: () => Promise) => async () => { openRequestModal(); try { @@ -186,8 +144,10 @@ export default function App() { }; return [ - { method: "cosmos_signDirect", callback: wrapRpcRequest(testSignDirect) }, - { method: "cosmos_signAmino", callback: wrapRpcRequest(testSignAmino) }, + { + method: SolanaRpcMethod.SOL_SIGN_TRANSACTION, + callback: wrapRpcRequest(testSignTransaction), + }, ]; }; @@ -212,7 +172,7 @@ export default function App() { {`Using v${version || "2.0.0-beta"}`} -
Select Cosmos chain:
+
Select chain:
{chainOptions.map(chainId => ( ))} @@ -231,7 +191,7 @@ export default function App() { address={account} chainId={chain} balances={balances} - actions={getCosmosActions()} + actions={getSolanaActions()} /> ); })} diff --git a/dapps/react-dapp-v2-with-solana-web3js/src/contexts/ClientContext.tsx b/dapps/react-dapp-v2-with-solana-web3js/src/contexts/ClientContext.tsx index 8eff908..e00ba51 100644 --- a/dapps/react-dapp-v2-with-solana-web3js/src/contexts/ClientContext.tsx +++ b/dapps/react-dapp-v2-with-solana-web3js/src/contexts/ClientContext.tsx @@ -1,6 +1,6 @@ import Client, { CLIENT_EVENTS } from "@walletconnect/client"; import { PairingTypes, SessionTypes } from "@walletconnect/types"; -import CosmosProvider from "@walletconnect/cosmos-provider"; +import { ERROR } from "@walletconnect/utils"; import QRCodeModal from "@walletconnect/qrcode-modal"; import { createContext, @@ -11,13 +11,24 @@ import { useMemo, useState, } from "react"; +import { apiGetChainNamespace, ChainsMap } from "caip-api"; +import { PublicKey } from "@solana/web3.js"; + import { DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL } from "../constants"; import { AccountBalances, ChainNamespaces, getAllChainNamespaces } from "../helpers"; -import { apiGetChainNamespace, ChainsMap } from "caip-api"; /** * Types */ + +export enum SolanaChainId { + Mainnet = "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ", + Devnet = "solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K", +} + +export enum SolanaRpcMethod { + SOL_SIGN_TRANSACTION = "sol_signTransaction", +} interface IContext { client: Client | undefined; session: SessionTypes.Created | undefined; @@ -25,11 +36,11 @@ interface IContext { isInitializing: boolean; chain: string; pairings: string[]; + publicKey?: PublicKey; accounts: string[]; balances: AccountBalances; chainData: ChainNamespaces; onEnable: (chainId: string) => Promise; - cosmosProvider?: CosmosProvider; } /** @@ -45,13 +56,12 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac const [pairings, setPairings] = useState([]); const [session, setSession] = useState(); - const [cosmosProvider, setCosmosProvider] = useState(); - const [isInitializing, setIsInitializing] = useState(false); const [hasCheckedPersistedSession, setHasCheckedPersistedSession] = useState(false); const [balances, setBalances] = useState({}); const [accounts, setAccounts] = useState([]); + const [publicKey, setPublicKey] = useState(); const [chainData, setChainData] = useState({}); const [chain, setChain] = useState(""); @@ -59,6 +69,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac setPairings([]); setSession(undefined); setBalances({}); + setPublicKey(undefined); setAccounts([]); setChain(""); }; @@ -83,11 +94,17 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac }; const disconnect = useCallback(async () => { - if (typeof cosmosProvider === "undefined") { - throw new Error("cosmosProvider is not initialized"); + if (typeof client === "undefined") { + throw new Error("WalletConnect is not initialized"); } - await cosmosProvider.disconnect(); - }, [cosmosProvider]); + if (typeof session === "undefined") { + throw new Error("Session is not connected"); + } + await client.disconnect({ + topic: session.topic, + reason: ERROR.USER_DISCONNECTED.format(), + }); + }, [client, session]); const _subscribeToClientEvents = useCallback(async (_client: Client) => { if (typeof _client === "undefined") { @@ -131,46 +148,41 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac } }, [_subscribeToClientEvents]); + const onSessionConnected = useCallback(async (_session: SessionTypes.Settled) => { + const account = _session.state.accounts[0].split(":").pop(); + if (!account) { + throw new Error("Could not derive account address from `session.state.accounts`."); + } + + const _publicKey = new PublicKey(account); + + setSession(_session); + setChain(_session.permissions.blockchain.chains[0]); + setAccounts(_session.state.accounts); + setPublicKey(_publicKey); + }, []); + const onEnable = useCallback( async (caipChainId: string) => { if (!client) { throw new ReferenceError("WalletConnect Client is not initialized."); } - const chainId = caipChainId.split(":").pop(); - - if (!chainId) { - throw new Error("Could not derive chainId from CAIP chainId"); - } - - console.log("Enabling cosmosProvider for chainId: ", chainId); - - // Create WalletConnect Provider - const cosmosProvider = new CosmosProvider({ - chains: [chainId], - client, - }); - - console.log(cosmosProvider); - setCosmosProvider(cosmosProvider); - try { - await cosmosProvider.connect(); + const _session = await client.connect({ + permissions: { + blockchain: { chains: [caipChainId] }, + jsonrpc: { methods: [SolanaRpcMethod.SOL_SIGN_TRANSACTION] }, + }, + }); + onSessionConnected(_session); } catch (error) { console.error(error); - return; + } finally { + QRCodeModal.close(); } - - const _accounts = cosmosProvider.accounts; - const _session = await client.session.get(client.session.topics[0]); - - setAccounts(_accounts); - setSession(_session); - setChain(caipChainId); - - QRCodeModal.close(); }, - [client], + [client, onSessionConnected], ); const _checkForPersistedSession = useCallback( @@ -184,12 +196,10 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac // 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); + onSessionConnected(_session); } }, - [session, onEnable], + [session, onSessionConnected], ); useEffect(() => { @@ -218,6 +228,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac pairings, isInitializing, balances, + publicKey, accounts, chain, client, @@ -225,12 +236,12 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac disconnect, chainData, onEnable, - cosmosProvider, }), [ pairings, isInitializing, balances, + publicKey, accounts, chain, client, @@ -238,7 +249,6 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac disconnect, chainData, onEnable, - cosmosProvider, ], );