Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions provider/implementations/js/src/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -112,4 +117,12 @@ export class Connection {
);
}
}

public getSignerType(): SignerType {
if (Signer.isSigner(this._config.signer)) {
return SignerType.CUSTOM_SIGNER
}

return SignerType.PROVIDER_SIGNER
}
}
103 changes: 80 additions & 23 deletions provider/implementations/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -24,7 +30,7 @@ export class EthereumProviderPlugin extends Module<ProviderConfig> {
private _connections: Connections;

constructor(config: ProviderConfig) {
super(config)
super(config);
this._connections = config.connections;
}

Expand All @@ -33,7 +39,7 @@ export class EthereumProviderPlugin extends Module<ProviderConfig> {
_client: CoreClient
): Promise<string> {
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
Expand All @@ -42,8 +48,50 @@ export class EthereumProviderPlugin extends Module<ProviderConfig> {
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) {
/**
Expand All @@ -52,17 +100,14 @@ export class EthereumProviderPlugin extends Module<ProviderConfig> {
* 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);
Expand Down Expand Up @@ -116,35 +161,47 @@ export class EthereumProviderPlugin extends Module<ProviderConfig> {
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<Connection> {
return this._connections.getConnection(
connection ?? this.env.connection
);
private async _getConnection(
connection?: SchemaConnection | null
): Promise<Connection> {
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<string, any> = { ...tx, r: undefined, s: undefined, v: undefined };
let request: Record<string, any> = {
...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<string, unknown>)
}, {} as Record<string, unknown>);

return request;
}
}

export const ethereumProviderPlugin: PluginFactory<ProviderConfig> = (
config: ProviderConfig
) => new PluginPackage<ProviderConfig>(new EthereumProviderPlugin(config), manifest);
) =>
new PluginPackage<ProviderConfig>(
new EthereumProviderPlugin(config),
manifest
);

export const plugin = ethereumProviderPlugin;

120 changes: 120 additions & 0 deletions provider/implementations/js/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>).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;
}
}