diff --git a/provider/implementations/js/src/Connection.ts b/provider/implementations/js/src/Connection.ts index 3735955..34aa32b 100644 --- a/provider/implementations/js/src/Connection.ts +++ b/provider/implementations/js/src/Connection.ts @@ -19,6 +19,11 @@ export interface ConnectionConfig { signer?: EthereumSigner; } +export enum SignerType { + CUSTOM_SIGNER, + PROVIDER_SIGNER +} + export class Connection { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: initialized within setProvider @@ -112,4 +117,12 @@ export class Connection { ); } } + + public getSignerType(): SignerType { + if (Signer.isSigner(this._config.signer)) { + return SignerType.CUSTOM_SIGNER + } + + return SignerType.PROVIDER_SIGNER + } } diff --git a/provider/implementations/js/src/index.ts b/provider/implementations/js/src/index.ts index a006f78..7f7ccae 100644 --- a/provider/implementations/js/src/index.ts +++ b/provider/implementations/js/src/index.ts @@ -9,10 +9,16 @@ import { Connection as SchemaConnection, Args_signerAddress, } from "./wrap"; -import { PluginFactory, PluginPackage } from "@polywrap/plugin-js"; -import { Connection } from "./Connection"; +import { Connection, SignerType } from "./Connection"; import { Connections } from "./Connections"; +import { + eth_sendTransaction, + eth_signTypedData +} from "./rpc"; + +import { PluginFactory, PluginPackage } from "@polywrap/plugin-js"; import { ethers } from "ethers"; + export * from "./Connection"; export * from "./Connections"; @@ -24,7 +30,7 @@ export class EthereumProviderPlugin extends Module { private _connections: Connections; constructor(config: ProviderConfig) { - super(config) + super(config); this._connections = config.connections; } @@ -33,7 +39,7 @@ export class EthereumProviderPlugin extends Module { _client: CoreClient ): Promise { const connection = await this._getConnection(args.connection); - const params = JSON.parse(args?.params ?? "[]"); + const params = args?.params ?? "[]"; const provider = connection.getProvider(); // Optimizations, utilizing the cache within ethers @@ -42,8 +48,50 @@ export class EthereumProviderPlugin extends Module { return JSON.stringify("0x" + network.chainId.toString(16)); } + if ( + args.method === "eth_sendTransaction" && + connection.getSignerType() == SignerType.CUSTOM_SIGNER + ) { + const signer = await connection.getSigner(); + const parameters = eth_sendTransaction.deserializeParameters( + params + ); + const request = eth_sendTransaction.toEthers( + parameters[0] + ); + const res = await signer.sendTransaction(request); + return JSON.stringify(res.hash); + } + + if ( + args.method === "eth_signTypedData" && + connection.getSignerType() == SignerType.CUSTOM_SIGNER + ) { + const signer = await connection.getSigner(); + const parameters = eth_signTypedData.deserializeParameters( + params + ); + let signature = ""; + // This is a hack because in ethers v5.7 this method is experimental + // when when we update to ethers v6 this wont be needed. More info: + // https://github.com/ethers-io/ethers.js/blob/ec1b9583039a14a0e0fa15d0a2a6082a2f41cf5b/packages/abstract-signer/src.ts/index.ts#L53 + if ("_signTypedData" in signer) { + const [_, data] = parameters + // @ts-ignore + signature = await signer._signTypedData( + data.domain, + data.types, + data.message + ) + } + return JSON.stringify(signature) + } + try { - const req = await provider.send(args.method, params); + const req = await provider.send( + args.method, + JSON.parse(params) + ); return JSON.stringify(req); } catch (err) { /** @@ -52,17 +100,14 @@ export class EthereumProviderPlugin extends Module { * as 0x02, but metamask expects it as 0x2, * hence, the need of this workaround. Related: * https://github.com/MetaMask/metamask-extension/issues/18076 - * + * * We check if the parameters comes as array, if the error * contains 0x2 and if the type is 0x02, then we change it */ const paramsIsArray = Array.isArray(params) && params.length > 0; - const messageContains0x2 = err && err.message && err.message.indexOf("0x2") > -1; - if ( - messageContains0x2 && - paramsIsArray && - params[0].type === "0x02" - ) { + const messageContains0x2 = + err && err.message && err.message.indexOf("0x2") > -1; + if (messageContains0x2 && paramsIsArray && params[0].type === "0x02") { params[0].type = "0x2"; const req = await provider.send(args.method, params); return JSON.stringify(req); @@ -116,27 +161,36 @@ export class EthereumProviderPlugin extends Module { const request = this._parseTransaction(args.rlp); const signedTxHex = await connection.getSigner().signTransaction(request); const signedTx = ethers.utils.parseTransaction(signedTxHex); - return ethers.utils.joinSignature(signedTx as { r: string; s: string; v: number | undefined }); + return ethers.utils.joinSignature( + signedTx as { r: string; s: string; v: number | undefined } + ); } - private async _getConnection(connection?: SchemaConnection | null): Promise { - return this._connections.getConnection( - connection ?? this.env.connection - ); + private async _getConnection( + connection?: SchemaConnection | null + ): Promise { + return this._connections.getConnection(connection ?? this.env.connection); } - private _parseTransaction(rlp: Uint8Array): ethers.providers.TransactionRequest { + private _parseTransaction( + rlp: Uint8Array + ): ethers.providers.TransactionRequest { const tx = ethers.utils.parseTransaction(rlp); // r, s, v can sometimes be set to 0, but ethers will throw if the keys exist at all - let request: Record = { ...tx, r: undefined, s: undefined, v: undefined }; + let request: Record = { + ...tx, + r: undefined, + s: undefined, + v: undefined, + }; // remove undefined and null values request = Object.keys(request).reduce((prev, curr) => { const val = request[curr]; - if (val !== undefined && val !== null) prev[curr] = val + if (val !== undefined && val !== null) prev[curr] = val; return prev; - }, {} as Record) + }, {} as Record); return request; } @@ -144,7 +198,10 @@ export class EthereumProviderPlugin extends Module { export const ethereumProviderPlugin: PluginFactory = ( config: ProviderConfig -) => new PluginPackage(new EthereumProviderPlugin(config), manifest); +) => + new PluginPackage( + new EthereumProviderPlugin(config), + manifest + ); export const plugin = ethereumProviderPlugin; - diff --git a/provider/implementations/js/src/rpc.ts b/provider/implementations/js/src/rpc.ts new file mode 100644 index 0000000..81b0023 --- /dev/null +++ b/provider/implementations/js/src/rpc.ts @@ -0,0 +1,120 @@ +import ethers from "ethers"; + +// Ref: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction +export namespace eth_sendTransaction { + export interface Transaction { + // DATA, 20 Bytes - The address the transaction is sent from. + from: string; + // DATA, 20 Bytes - (optional when creating new contract) The address the transaction is directed to. + to?: string; + // QUANTITY - (optional, default: 90000) Integer of the gas provided for the transaction execution. It will return unused gas. + gas?: string; + // QUANTITY - (optional, default: To-Be-Determined) Integer of the gasPrice used for each paid gas. + gasPrice?: string; + // QUANTITY - (optional) Integer of the value sent with this transaction. + value?: string; + // DATA - The compiled code of a contract OR the hash of the invoked method signature and encoded parameters. + data: string; + // QUANTITY - (optional) Integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce. + nonce?: string; + } + + export type Parameters = [Transaction]; + + // DATA, 32 Bytes - the transaction hash, or the zero hash if the transaction is not yet available. + export type Returns = string; + + export function deserializeParameters(input: string): Parameters { + const params = JSON.parse(input); + if (params.length < 1 || typeof params[0] !== "object") { + throw new Error( + "Invalid JSON-RPC parameters provided for eth_sendTransaction method. Reference: " + + "https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction" + ); + } + + const transaction: Transaction = params[0]; + + if (!transaction.from) { + throw new Error( + "The 'from' property on the transaction object parameter is required for the eth_sendTransaction method. Reference: " + + "https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction" + ); + } + + if (!transaction.data) { + throw new Error( + "The 'data' property on the transaction object parameter is required for the eth_sendTransaction method. Reference: " + + "https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction" + ); + } + + return [transaction]; + } + + export function toEthers( + transaction: Transaction + ): ethers.providers.TransactionRequest { + const result: ethers.providers.TransactionRequest = { + ...transaction, + // Ethers.js expects `gasLimit` instead of `gas` + gasLimit: transaction.gas + }; + + // Ethers.js expects "0" | "1" | "2" + // but it's being received as hex (e.g: "0x02") + if ("type" in transaction) { + result.type = parseInt( + (transaction as unknown as Record).type + ); + } + + return result; + } +} + +// Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md +export namespace eth_signTypedData { + export interface TypedData { + types: { + EIP712Domain: unknown[]; + [key: string]: { + name: string; + type: string; + [key: string]: unknown; + }[] | unknown; + }; + primaryType: string; + domain: { [key: string]: unknown }; + message: { [key: string]: unknown }; + [key: string]: unknown; + } + + export type Parameters = [ + // Address - 20 Bytes - Address of the account that will sign the messages. + string, + // TypedData - Typed structured data to be signed. + TypedData + ]; + + // DATA, 129 Bytes - the signature, as described here: + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#returns + export type Returns = string; + + export function deserializeParameters(input: string): Parameters { + const params = JSON.parse(input); + if ( + params.length < 2 || + typeof params[0] !== "string" || + typeof params[1] !== "object" + ) { + throw new Error( + "Invalid JSON-RPC parameters provided for eth_signTypedData method. Reference: " + + "https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#parameters" + ); + } + + return params; + } +} +