From 87d66a77e968d4695c6e52dfb803666019e54629 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 3 Sep 2024 11:51:12 +0200 Subject: [PATCH] productionize request router --- src/beta.ts | 139 ++++----------------------------- src/chains/ethereum.ts | 29 +++++++ src/guards.ts | 11 +++ src/index.ts | 6 +- src/types.ts | 99 ++++++++++++++++++++++- src/utils/index.ts | 3 + src/utils/request.ts | 115 +++++++++++++++++++++++++++ tests/unit/ethereum.test.ts | 6 +- tests/unit/wc.handlers.test.ts | 120 +++++++++++++++------------- 9 files changed, 344 insertions(+), 184 deletions(-) create mode 100644 src/guards.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/request.ts diff --git a/src/beta.ts b/src/beta.ts index 8a71c58..e2d848b 100644 --- a/src/beta.ts +++ b/src/beta.ts @@ -1,126 +1,10 @@ -import { - Hex, - Signature, - fromHex, - hashMessage, - hashTypedData, - keccak256, - serializeSignature, - serializeTransaction, -} from "viem"; -import { - addSignature, - populateTx, - relaySignedTransaction, - toPayload, -} from "./utils/transaction"; -import { NearEthTxData, RecoveryData } from "./types"; +import { Hex, Signature, serializeSignature } from "viem"; +import { addSignature, relaySignedTransaction } from "./utils/transaction"; +import { NearEthTxData, signMethods } from "./types"; import { NearEthAdapter } from "./chains/ethereum"; import { Web3WalletTypes } from "@walletconnect/web3wallet"; - -// Interface for Ethereum transaction parameters -export interface EthTransactionParams { - from: Hex; - to: Hex; - gas?: Hex; - value?: Hex; - data?: Hex; -} - -/// Interface for personal sign parameters (message and address) -export type PersonalSignParams = [Hex, Hex]; - -/// Interface for eth_sign parameters (address and message) -export type EthSignParams = [Hex, Hex]; - -/// Interface for complex structured parameters like EIP-712 -export type TypedDataParams = [Hex, string]; - -export type SessionRequestParams = - | EthTransactionParams[] - | PersonalSignParams - | EthSignParams - | TypedDataParams; - -export async function wcRouter( - method: string, - chainId: string, - params: SessionRequestParams -): Promise<{ - evmMessage: string; - payload: number[]; - recoveryData: RecoveryData; -}> { - switch (method) { - case "eth_sign": { - const [sender, messageHash] = params as EthSignParams; - return { - evmMessage: fromHex(messageHash, "string"), - payload: toPayload(hashMessage({ raw: messageHash })), - recoveryData: { - type: method, - data: { - address: sender, - message: { raw: messageHash }, - }, - }, - }; - } - case "personal_sign": { - const [messageHash, sender] = params as PersonalSignParams; - return { - evmMessage: fromHex(messageHash, "string"), - payload: toPayload(hashMessage({ raw: messageHash })), - recoveryData: { - type: method, - data: { - address: sender, - message: { raw: messageHash }, - }, - }, - }; - } - case "eth_sendTransaction": { - const tx = params[0] as EthTransactionParams; - const transaction = await populateTx( - { - to: tx.to, - chainId: parseInt(stripEip155Prefix(chainId)), - value: fromHex(tx.value || "0x0", "bigint"), - data: tx.data || "0x", - ...(tx.gas ? { gas: fromHex(tx.gas, "bigint") } : {}), - }, - tx.from - ); - const txHex = serializeTransaction(transaction); - return { - payload: toPayload(keccak256(serializeTransaction(transaction))), - evmMessage: txHex, - recoveryData: { - type: "eth_sendTransaction", - data: txHex, - }, - }; - } - case "eth_signTypedData": - case "eth_signTypedData_v4": { - const [sender, dataString] = params as TypedDataParams; - const typedData = JSON.parse(dataString); - return { - evmMessage: dataString, - payload: toPayload(hashTypedData(typedData)), - recoveryData: { - type: "eth_signTypedData", - data: { - address: sender, - ...typedData, - }, - }, - }; - } - } - throw new Error(`Unhandled session_request method: ${method}`); -} +import { isSignMethod } from "./guards"; +import { requestRouter } from "./utils/request"; function stripEip155Prefix(eip155Address: string): string { return eip155Address.split(":").pop() ?? ""; @@ -145,11 +29,16 @@ export class Beta { request: { method, params }, } = request.params!; console.log(`Session Request of type ${method} for chainId ${chainId}`); - const { evmMessage, payload, recoveryData } = await wcRouter( + if (!isSignMethod(method)) { + throw new Error( + `Unsupported sign method ${method}: Available sign methods ${signMethods}` + ); + } + const { evmMessage, payload, recoveryData } = await requestRouter({ method, - chainId, - params - ); + chainId: parseInt(stripEip155Prefix(chainId)), + params, + }); console.log("Parsed Request:", payload, recoveryData); return { nearPayload: await this.adapter.mpcContract.encodeSignatureRequestTx({ diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index 6862b70..84de6de 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -22,8 +22,11 @@ import { populateTx, toPayload, broadcastSignedTransaction, + SignRequestData, + NearEthTxData, } from ".."; import { Beta } from "../beta"; +import { requestRouter } from "../utils/request"; export class NearEthAdapter { readonly mpcContract: MpcContract; @@ -202,4 +205,30 @@ export class NearEthAdapter { }); return serializeSignature(signature); } + + /** + * Encodes a signature request for submission to the Near-Ethereum transaction MPC contract. + * + * @async + * @function encodeSignRequest + * @param {SignRequestData} signRequest - The signature request data containing method, chain ID, and parameters. + * @returns {Promise} + * - Returns a promise that resolves to an object containing the encoded Near-Ethereum transaction data, + * the original EVM message, and recovery data necessary for verifying or reconstructing the signature. + */ + async encodeSignRequest( + signRequest: SignRequestData + ): Promise { + const { evmMessage, payload, recoveryData } = + await requestRouter(signRequest); + return { + nearPayload: await this.mpcContract.encodeSignatureRequestTx({ + path: this.derivationPath, + payload, + key_version: 0, + }), + evmMessage, + recoveryData, + }; + } } diff --git a/src/guards.ts b/src/guards.ts new file mode 100644 index 0000000..28d0092 --- /dev/null +++ b/src/guards.ts @@ -0,0 +1,11 @@ +import { SignMethod } from "./types"; + +export function isSignMethod(method: string): method is SignMethod { + return [ + "eth_sign", + "personal_sign", + "eth_sendTransaction", + "eth_signTypedData", + "eth_signTypedData_v4", + ].includes(method); +} diff --git a/src/index.ts b/src/index.ts index 21c6bb6..5532448 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,11 +10,11 @@ import { export * from "./chains/ethereum"; export * from "./chains/near"; +export * from "./guards"; export * from "./mpcContract"; -export * from "./types"; -export * from "./utils/signature"; export * from "./network"; -export * from "./utils/transaction"; +export * from "./types"; +export * from "./utils"; /// Beta features export * from "./beta"; diff --git a/src/types.ts b/src/types.ts index b3f6990..54efa8b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,11 @@ import { MpcContract } from "./mpcContract"; -import { Hex, SignableMessage, Signature, TransactionSerializable } from "viem"; +import { + Address, + Hex, + SignableMessage, + Signature, + TransactionSerializable, +} from "viem"; /** * Borrowed from @near-wallet-selector/core @@ -179,3 +185,94 @@ export interface TransactionWithSignature { transaction: Hex; signature: Signature; } + +/// Below is hand-crafted types losely related to wallet connect + +/** + * Interface representing the parameters required for an Ethereum transaction. + * + * @property {Hex} from - The sender's Ethereum address in hexadecimal format. + * @property {Hex} to - The recipient's Ethereum address in hexadecimal format. + * @property {Hex} [gas] - Optional gas limit for the transaction in hexadecimal format. + * @property {Hex} [value] - Optional amount of Ether to send in hexadecimal format. + * @property {Hex} [data] - Optional data payload for the transaction in hexadecimal format, often used for contract interactions. */ +export interface EthTransactionParams { + from: Hex; + to: Hex; + gas?: Hex; + value?: Hex; + data?: Hex; +} + +/** + * Type representing the parameters for a personal_sign request. + * + * @type {[Hex, Address]} + * @property {Hex} 0 - The message to be signed in hexadecimal format. + * @property {Address} 1 - The address of the signer in hexadecimal format. + */ +export type PersonalSignParams = [Hex, Address]; + +/** + * Type representing the parameters for an eth_sign request. + * + * @type {[Address, Hex]} + * @property {Address} 0 - The address of the signer in hexadecimal format. + * @property {Hex} 1 - The message to be signed in hexadecimal format. + */ +export type EthSignParams = [Address, Hex]; + +/** + * Type representing the parameters for signing complex structured data (like EIP-712). + * + * @type {[Hex, string]} + * @property {Hex} 0 - The address of the signer in hexadecimal format. + * @property {string} 1 - The structured data in JSON string format to be signed. + */ +export type TypedDataParams = [Hex, string]; + +/** + * Type representing the possible request parameters for a signing session. + * + * @type {EthTransactionParams[] | Hex | PersonalSignParams | EthSignParams | TypedDataParams} + * @property {EthTransactionParams[]} - An array of Ethereum transaction parameters. + * @property {Hex} - A simple hexadecimal value representing RLP Encoded Ethereum Transaction. + * @property {PersonalSignParams} - Parameters for a personal sign request. + * @property {EthSignParams} - Parameters for an eth_sign request. + * @property {TypedDataParams} - Parameters for signing structured data. + */ +export type SessionRequestParams = + | EthTransactionParams[] + | Hex + | PersonalSignParams + | EthSignParams + | TypedDataParams; + +/** + * An array of supported signing methods. + */ +export const signMethods = [ + "eth_sign", + "personal_sign", + "eth_sendTransaction", + "eth_signTypedData", + "eth_signTypedData_v4", +] as const; + +/** + * Type representing one of the supported signing methods. + */ +export type SignMethod = (typeof signMethods)[number]; + +/** + * Interface representing the data required for a signature request. + * + * @property {SignMethods} method - The signing method to be used. + * @property {number} chainId - The ID of the Ethereum chain where the transaction or signing is taking place. + * @property {SessionRequestParams} params - The parameters required for the signing request, which vary depending on the method. + */ +export type SignRequestData = { + method: SignMethod; + chainId: number; + params: SessionRequestParams; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e756f28 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./request"; +export * from "./signature"; +export * from "./transaction"; diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..71c7782 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,115 @@ +import { + Hex, + fromHex, + hashMessage, + hashTypedData, + isHex, + keccak256, + serializeTransaction, +} from "viem"; +import { populateTx, toPayload } from "./transaction"; +import { + EthSignParams, + EthTransactionParams, + PersonalSignParams, + RecoveryData, + SignRequestData, + TypedDataParams, +} from "../types"; + +/** + * Handles routing of signature requests based on the provided method, chain ID, and parameters. + * + * @async + * @function requestRouter + * @param {SignRequestData} params - An object containing the method, chain ID, and request parameters. + * @returns {Promise<{ evmMessage: string; payload: number[]; recoveryData: RecoveryData }>} + * - Returns a promise that resolves to an object containing the Ethereum Virtual Machine (EVM) message, + * the payload (hashed data), and recovery data needed for reconstructing the signature request. + */ +export async function requestRouter({ + method, + chainId, + params, +}: SignRequestData): Promise<{ + evmMessage: string; + payload: number[]; + // We may eventually be able to abolish this. + recoveryData: RecoveryData; +}> { + switch (method) { + case "eth_sign": { + const [sender, messageHash] = params as EthSignParams; + return { + evmMessage: fromHex(messageHash, "string"), + payload: toPayload(hashMessage({ raw: messageHash })), + recoveryData: { + type: method, + data: { + address: sender, + message: { raw: messageHash }, + }, + }, + }; + } + case "personal_sign": { + const [messageHash, sender] = params as PersonalSignParams; + return { + evmMessage: fromHex(messageHash, "string"), + payload: toPayload(hashMessage({ raw: messageHash })), + recoveryData: { + type: method, + data: { + address: sender, + message: { raw: messageHash }, + }, + }, + }; + } + case "eth_sendTransaction": { + // We only support one transaction at a time! + const tx = params[0] as EthTransactionParams; + let rlpTx: Hex; + if (isHex(tx)) { + rlpTx = tx; + } else { + const transaction = await populateTx( + { + to: tx.to, + chainId, + value: fromHex(tx.value || "0x0", "bigint"), + data: tx.data || "0x", + ...(tx.gas ? { gas: fromHex(tx.gas, "bigint") } : {}), + }, + tx.from + ); + rlpTx = serializeTransaction(transaction); + } + + return { + payload: toPayload(keccak256(rlpTx)), + evmMessage: rlpTx, + recoveryData: { + type: "eth_sendTransaction", + data: rlpTx, + }, + }; + } + case "eth_signTypedData": + case "eth_signTypedData_v4": { + const [sender, dataString] = params as TypedDataParams; + const typedData = JSON.parse(dataString); + return { + evmMessage: dataString, + payload: toPayload(hashTypedData(typedData)), + recoveryData: { + type: "eth_signTypedData", + data: { + address: sender, + ...typedData, + }, + }, + }; + } + } +} diff --git a/tests/unit/ethereum.test.ts b/tests/unit/ethereum.test.ts index 7a67486..a814ea1 100644 --- a/tests/unit/ethereum.test.ts +++ b/tests/unit/ethereum.test.ts @@ -1,5 +1,5 @@ import { zeroAddress } from "viem"; -import { setupAdapter } from "../../src/"; +import { setupAdapter, signMethods } from "../../src/"; const accountId = "farmface.testnet"; const network = { @@ -37,7 +37,9 @@ describe("ethereum", () => { request: { method: "poop", params: [] }, }, }) - ).rejects.toThrow("Unhandled session_request method: poop"); + ).rejects.toThrow( + `Unsupported sign method poop: Available sign methods ${signMethods}` + ); const ethSign = await adapter.beta.handleSessionRequest({ params: { diff --git a/tests/unit/wc.handlers.test.ts b/tests/unit/wc.handlers.test.ts index ff51c55..7ea2b02 100644 --- a/tests/unit/wc.handlers.test.ts +++ b/tests/unit/wc.handlers.test.ts @@ -1,18 +1,19 @@ import { Hex, isHex, toHex } from "viem"; -import { wcRouter } from "../../src/beta"; +import { requestRouter } from "../../src"; describe("Wallet Connect", () => { - const chainId = "eip155:11155111"; + const chainId = 11155111; const from = "0xa61d98854f7ab25402e3d12548a2e93a080c1f97" as Hex; const to = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14" as Hex; - describe("wcRouter: eth_sign & personal_sign", () => { + describe("requestRouter: eth_sign & personal_sign", () => { it("hello message", async () => { const messageString = "Hello!"; - const { evmMessage, payload } = await wcRouter("eth_sign", chainId, [ - from, - toHex(messageString), - ]); + const { evmMessage, payload } = await requestRouter({ + method: "eth_sign", + chainId, + params: [from, toHex(messageString)], + }); expect(evmMessage).toEqual(messageString); expect(payload).toEqual([ 82, 182, 67, 125, 181, 109, 135, 245, 153, 29, 124, 23, 60, 241, 27, @@ -21,18 +22,15 @@ describe("Wallet Connect", () => { ]); }); - it("fail with wrong method", async () => { - const messageString = "Hello!"; - await expect( - wcRouter("eth_fail", chainId, [from, toHex(messageString)]) - ).rejects.toThrow("Unhandled session_request method: eth_fail"); - }); - it("opensea login", async () => { - const { evmMessage, payload } = await wcRouter("personal_sign", chainId, [ - "0x57656c636f6d6520746f204f70656e536561210a0a436c69636b20746f207369676e20696e20616e642061636365707420746865204f70656e536561205465726d73206f662053657276696365202868747470733a2f2f6f70656e7365612e696f2f746f732920616e64205072697661637920506f6c696379202868747470733a2f2f6f70656e7365612e696f2f70726976616379292e0a0a5468697320726571756573742077696c6c206e6f742074726967676572206120626c6f636b636861696e207472616e73616374696f6e206f7220636f737420616e792067617320666565732e0a0a57616c6c657420616464726573733a0a3078663131633232643631656364376231616463623662343335343266653861393662393332386463370a0a4e6f6e63653a0a32393731633731312d623739382d343433342d613633312d316333663133656665353365", - "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", - ]); + const { evmMessage, payload } = await requestRouter({ + method: "personal_sign", + chainId, + params: [ + "0x57656c636f6d6520746f204f70656e536561210a0a436c69636b20746f207369676e20696e20616e642061636365707420746865204f70656e536561205465726d73206f662053657276696365202868747470733a2f2f6f70656e7365612e696f2f746f732920616e64205072697661637920506f6c696379202868747470733a2f2f6f70656e7365612e696f2f70726976616379292e0a0a5468697320726571756573742077696c6c206e6f742074726967676572206120626c6f636b636861696e207472616e73616374696f6e206f7220636f737420616e792067617320666565732e0a0a57616c6c657420616464726573733a0a3078663131633232643631656364376231616463623662343335343266653861393662393332386463370a0a4e6f6e63653a0a32393731633731312d623739382d343433342d613633312d316333663133656665353365", + "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", + ], + }); expect(evmMessage).toEqual( `Welcome to OpenSea! @@ -54,10 +52,14 @@ Nonce: }); it("manifold login", async () => { - const { evmMessage, payload } = await wcRouter("personal_sign", chainId, [ - "0x506c65617365207369676e2074686973206d65737361676520746f20616363657373204d616e69666f6c642053747564696f0a0a4368616c6c656e67653a2034313133666333616232636336306635643539356232653535333439663165656335366664306337306434323837303831666537313536383438323633363236", - "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", - ]); + const { evmMessage, payload } = await requestRouter({ + method: "personal_sign", + chainId, + params: [ + "0x506c65617365207369676e2074686973206d65737361676520746f20616363657373204d616e69666f6c642053747564696f0a0a4368616c6c656e67653a2034313133666333616232636336306635643539356232653535333439663165656335366664306337306434323837303831666537313536383438323633363236", + "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", + ], + }); expect(evmMessage).toEqual( `Please sign this message to access Manifold Studio @@ -70,48 +72,60 @@ Challenge: 4113fc3ab2cc60f5d595b2e55349f1eec56fd0c70d4287081fe7156848263626` ]); }); }); - describe("wcRouter: eth_sendTransaction", () => { + describe("requestRouter: eth_sendTransaction", () => { /// can't test payload: its non-deterministic because of gas values! it("with value", async () => { - const { evmMessage } = await wcRouter("eth_sendTransaction", chainId, [ - { - gas: "0xd31d", - value: "0x16345785d8a0000", - from, - to, - data: "0xd0e30db0", - }, - ]); + const { evmMessage } = await requestRouter({ + method: "eth_sendTransaction", + chainId, + params: [ + { + gas: "0xd31d", + value: "0x16345785d8a0000", + from, + to, + data: "0xd0e30db0", + }, + ], + }); expect(isHex(evmMessage)).toBe(true); }); it("null value", async () => { - const { evmMessage } = await wcRouter("eth_sendTransaction", chainId, [ - { - gas: "0xa8c3", - from, - to, - data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000", - }, - ]); + const { evmMessage } = await requestRouter({ + method: "eth_sendTransaction", + chainId, + params: [ + { + gas: "0xa8c3", + from, + to, + data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000", + }, + ], + }); expect(isHex(evmMessage)).toBe(true); }); it("null data", async () => { - const { evmMessage } = await wcRouter("eth_sendTransaction", chainId, [ - { - gas: "0xa8c3", - from, - to, - value: "0x01", - }, - ]); + const { evmMessage } = await requestRouter({ + method: "eth_sendTransaction", + chainId, + params: [ + { + gas: "0xa8c3", + from, + to, + value: "0x01", + }, + ], + }); expect(isHex(evmMessage)).toBe(true); }); }); - describe("wcRouter: eth_signTypedData", () => { + describe("requestRouter: eth_signTypedData", () => { it("Cowswap Order", async () => { const jsonStr = `{ "types": { @@ -149,11 +163,11 @@ Challenge: 4113fc3ab2cc60f5d595b2e55349f1eec56fd0c70d4287081fe7156848263626` params: [from, jsonStr], }; - const { evmMessage, payload } = await wcRouter( - "eth_signTypedData_v4", + const { evmMessage, payload } = await requestRouter({ + method: "eth_signTypedData_v4", chainId, - [from, jsonStr] - ); + params: [from, jsonStr], + }); expect(evmMessage).toEqual(request.params[1]); expect(payload).toEqual([ 12, 48, 228, 237, 112, 46, 184, 104, 131, 82, 168, 85, 19, 250, 126, 53,