feat(dapp-v2): integrates solana + RPC methods

This commit is contained in:
Ben Kremer
2022-03-17 11:08:47 +01:00
parent 86ef897b4c
commit 863e0f8bf8
7 changed files with 237 additions and 13 deletions
+30 -5
View File
@@ -6,8 +6,8 @@ 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 { DEFAULT_MAIN_CHAINS, DEFAULT_SOLANA_METHODS, DEFAULT_TEST_CHAINS } from "./constants";
import { AccountAction, setLocaleStorageTestnetFlag } from "./helpers";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PairingModal from "./modals/PairingModal";
@@ -26,8 +26,6 @@ import { useWalletConnectClient } from "./contexts/ClientContext";
import { useJsonRpc } from "./contexts/JsonRpcContext";
export default function App() {
const [isTestnet, setIsTestnet] = useState(getLocalStorageTestnetFlag());
const [modal, setModal] = useState("");
const closeModal = () => setModal("");
@@ -50,7 +48,17 @@ export default function App() {
} = useWalletConnectClient();
// Use `JsonRpcContext` to provide us with relevant RPC methods and states.
const { chainData, ping, ethereumRpc, cosmosRpc, isRpcRequestPending, rpcResult } = useJsonRpc();
const {
chainData,
ping,
ethereumRpc,
cosmosRpc,
solanaRpc,
isRpcRequestPending,
rpcResult,
isTestnet,
setIsTestnet,
} = useJsonRpc();
// Close the pairing modal after a session is established.
useEffect(() => {
@@ -122,6 +130,21 @@ export default function App() {
];
};
const getSolanaActions = (): AccountAction[] => {
const onSignTransaction = async (chainId: string, address: string) => {
openRequestModal();
await solanaRpc.testSignTransaction(chainId, address);
};
const onSignMessage = async (chainId: string, address: string) => {
openRequestModal();
await solanaRpc.testSignMessage(chainId, address);
};
return [
{ method: DEFAULT_SOLANA_METHODS.SOL_SIGN_TRANSACTION, callback: onSignTransaction },
{ method: DEFAULT_SOLANA_METHODS.SOL_SIGN_MESSAGE, callback: onSignMessage },
];
};
const getBlockchainActions = (chainId: string) => {
const [namespace] = chainId.split(":");
switch (namespace) {
@@ -129,6 +152,8 @@ export default function App() {
return getEthereumActions();
case "cosmos":
return getCosmosActions();
case "solana":
return getSolanaActions();
default:
break;
}
+19
View File
@@ -1,5 +1,24 @@
import { ChainsMap } from "caip-api";
import { NamespaceMetadata, ChainMetadata } from "../helpers";
// TODO: add `solana` namespace to `caip-api` package to avoid manual specification here.
export const SolanaChainData: ChainsMap = {
"4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ": {
id: "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ",
name: "Solana Mainnet",
rpc: ["https://api.mainnet-beta.solana.com", "https://solana-api.projectserum.com"],
slip44: 501,
testnet: false,
},
"8E9rvCKLFQia2Y35HXjjpWzj8weVo44K": {
id: "solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K",
name: "Solana Devnet",
rpc: ["https://api.devnet.solana.com"],
slip44: 501,
testnet: true,
},
};
export const SolanaMetadata: NamespaceMetadata = {
// Solana Mainnet
"4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ": {
@@ -10,6 +10,8 @@ import {
useMemo,
useState,
} from "react";
import { PublicKey } from "@solana/web3.js";
import {
DEFAULT_APP_METADATA,
DEFAULT_COSMOS_METHODS,
@@ -17,9 +19,11 @@ import {
DEFAULT_LOGGER,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
DEFAULT_SOLANA_METHODS,
} from "../constants";
import { AccountBalances, apiGetAccountBalance } from "../helpers";
import { ERROR, getAppMetadata } from "@walletconnect/utils";
import { getPublicKeysFromAccounts } from "../helpers/solana";
/**
* Types
@@ -33,6 +37,7 @@ interface IContext {
chains: string[];
pairings: string[];
accounts: string[];
solanaPublicKeys?: Record<string, PublicKey>;
balances: AccountBalances;
isFetchingBalances: boolean;
setChains: any;
@@ -56,6 +61,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
const [balances, setBalances] = useState<AccountBalances>({});
const [accounts, setAccounts] = useState<string[]>([]);
const [solanaPublicKeys, setSolanaPublicKeys] = useState<Record<string, PublicKey>>();
const [chains, setChains] = useState<string[]>([]);
const resetApp = () => {
@@ -110,6 +116,8 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
return DEFAULT_EIP155_METHODS;
case "cosmos":
return DEFAULT_COSMOS_METHODS;
case "solana":
return Object.values(DEFAULT_SOLANA_METHODS);
default:
throw new Error(`No default methods for namespace: ${namespace}`);
}
@@ -119,11 +127,12 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
return supportedMethods;
};
const onSessionConnected = useCallback(async (incomingSession: SessionTypes.Settled) => {
setSession(incomingSession);
setChains(incomingSession.permissions.blockchain.chains);
setAccounts(incomingSession.state.accounts);
await getAccountBalances(incomingSession.state.accounts);
const onSessionConnected = useCallback(async (_session: SessionTypes.Settled) => {
setSession(_session);
setChains(_session.permissions.blockchain.chains);
setAccounts(_session.state.accounts);
setSolanaPublicKeys(getPublicKeysFromAccounts(_session.state.accounts));
await getAccountBalances(_session.state.accounts);
}, []);
const connect = useCallback(
@@ -135,6 +144,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
try {
const supportedNamespaces = getSupportedNamespaces();
const methods = getSupportedMethods(supportedNamespaces);
const session = await client.connect({
metadata: getAppMetadata() || DEFAULT_APP_METADATA,
pairing,
@@ -254,6 +264,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
balances,
isFetchingBalances,
accounts,
solanaPublicKeys,
chains,
client,
session,
@@ -267,6 +278,7 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
balances,
isFetchingBalances,
accounts,
solanaPublicKeys,
chains,
client,
session,
@@ -2,18 +2,24 @@ import { BigNumber, utils } from "ethers";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import * as encoding from "@walletconnect/encoding";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import bs58 from "bs58";
import { verifyMessageSignature } from "solana-wallet";
import {
ChainNamespaces,
eip712,
formatTestTransaction,
getAllChainNamespaces,
getLocalStorageTestnetFlag,
hashPersonalMessage,
verifySignature,
} from "../helpers";
import { useWalletConnectClient } from "./ClientContext";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import { TypedDataField } from "@ethersproject/abstract-signer";
import { DEFAULT_SOLANA_METHODS } from "../constants";
import { clusterApiUrl, Connection, Keypair, SystemProgram, Transaction } from "@solana/web3.js";
import { SolanaChainData } from "../chains/solana";
/**
* Types
@@ -45,9 +51,15 @@ interface IContext {
testSignDirect: TRpcRequestCallback;
testSignAmino: TRpcRequestCallback;
};
solanaRpc: {
testSignMessage: TRpcRequestCallback;
testSignTransaction: TRpcRequestCallback;
};
chainData: ChainNamespaces;
rpcResult?: IRpcResult | null;
isRpcRequestPending: boolean;
isTestnet: boolean;
setIsTestnet: (isTestnet: boolean) => void;
}
/**
@@ -62,8 +74,9 @@ export function JsonRpcContextProvider({ children }: { children: ReactNode | Rea
const [pending, setPending] = useState(false);
const [result, setResult] = useState<IRpcResult | null>();
const [chainData, setChainData] = useState<ChainNamespaces>({});
const [isTestnet, setIsTestnet] = useState(getLocalStorageTestnetFlag());
const { client, session, accounts, balances } = useWalletConnectClient();
const { client, session, accounts, balances, solanaPublicKeys } = useWalletConnectClient();
useEffect(() => {
loadChainData();
@@ -76,7 +89,11 @@ export function JsonRpcContextProvider({ children }: { children: ReactNode | Rea
namespaces.map(async namespace => {
let chains: ChainsMap | undefined;
try {
chains = await apiGetChainNamespace(namespace);
if (namespace === "solana") {
chains = SolanaChainData;
} else {
chains = await apiGetChainNamespace(namespace);
}
} catch (e) {
// ignore error
}
@@ -85,6 +102,7 @@ export function JsonRpcContextProvider({ children }: { children: ReactNode | Rea
}
}),
);
setChainData(chainData);
};
@@ -437,6 +455,112 @@ export function JsonRpcContextProvider({ children }: { children: ReactNode | Rea
}),
};
// -------- SOLANA RPC METHODS --------
const solanaRpc = {
testSignTransaction: _createJsonRpcRequestHandler(
async (chainId: string, address: string): Promise<IFormattedRpcResponse> => {
if (!solanaPublicKeys) {
throw new Error("Could not find Solana PublicKeys.");
}
const senderPublicKey = solanaPublicKeys[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: DEFAULT_SOLANA_METHODS.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: DEFAULT_SOLANA_METHODS.SOL_SIGN_TRANSACTION,
address,
valid,
result: signature,
};
} catch (error: any) {
throw new Error(error);
}
},
),
testSignMessage: _createJsonRpcRequestHandler(
async (chainId: string, address: string): Promise<IFormattedRpcResponse> => {
if (!solanaPublicKeys) {
throw new Error("Could not find Solana PublicKeys.");
}
const senderPublicKey = solanaPublicKeys[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: DEFAULT_SOLANA_METHODS.SOL_SIGN_MESSAGE,
params: {
pubkey: senderPublicKey.toBase58(),
message,
},
},
});
const valid = verifyMessageSignature(senderPublicKey.toBase58(), signature, message);
return {
method: DEFAULT_SOLANA_METHODS.SOL_SIGN_MESSAGE,
address,
valid,
result: signature,
};
} catch (error: any) {
throw new Error(error);
}
},
),
};
return (
<JsonRpcContext.Provider
value={{
@@ -444,8 +568,11 @@ export function JsonRpcContextProvider({ children }: { children: ReactNode | Rea
ping,
ethereumRpc,
cosmosRpc,
solanaRpc,
rpcResult: result,
isRpcRequestPending: pending,
isTestnet,
setIsTestnet,
}}
>
{children}
+18
View File
@@ -0,0 +1,18 @@
import { PublicKey } from "@solana/web3.js";
export function getPublicKeysFromAccounts(accounts: string[]) {
return (
accounts
// Filter out any non-solana accounts.
.filter(account => account.startsWith("solana:"))
// Create a map of Solana address -> publicKey.
.reduce((map: Record<string, PublicKey>, account) => {
const address = account.split(":").pop();
if (!address) {
throw new Error(`Could not derive Solana address from CAIP account: ${account}`);
}
map[address] = new PublicKey(address);
return map;
}, {})
);
}