Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON-RPC eth_signTypedData_v4 method to Hardhat Network. #1189

Merged
merged 8 commits into from
Feb 9, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,19 @@ export class EthModule {
throw new MethodNotSupportedError(method);

case "eth_signTypedData":
return this._signTypedDataAction(...this._signTypedDataParams(params));
throw new MethodNotSupportedError(method);

case "eth_signTypedData_v3":
throw new MethodNotSupportedError(method);

// TODO: we're currently mimicking the MetaMask implementation here.
// The EIP 712 is still a draft. It doesn't actually distinguish different versions
// of the eth_signTypedData API.
// Also, note that go-ethereum implemented this in a clef JSON-RPC API: account_signTypedData.
case "eth_signTypedData_v4":
return this._signTypedDataV4Action(
...this._signTypedDataV4Params(params)
);

case "eth_submitHashrate":
throw new MethodNotSupportedError(method);
Expand Down Expand Up @@ -881,17 +893,34 @@ export class EthModule {

// eth_signTransaction

// eth_signTypedData
// eth_signTypedData_v4

private _signTypedDataParams(params: any[]): [Buffer, any] {
private _signTypedDataV4Params(params: any[]): [Buffer, any] {
// Validation of the TypedData parameter is handled by eth-sig-util
return validateParams(params, rpcAddress, rpcUnknown);
}

private async _signTypedDataAction(
private async _signTypedDataV4Action(
address: Buffer,
typedData: any
): Promise<string> {
return this._node.signTypedData(address, typedData);
let typedMessage: any = typedData;

// According to the MetaMask implementation,
// the message parameter may be JSON stringified in versions later than V1
// See https://github.com/MetaMask/metamask-extension/blob/0dfdd44ae7728ed02cbf32c564c75b74f37acf77/app/scripts/metamask-controller.js#L1736
// In fact, ethers.js JSON stringifies the message at the time of writing.
if (typeof typedData === "string") {
try {
typedMessage = JSON.parse(typedData);
} catch (error) {
throw new InvalidInputError(
`The message parameter is an invalid JSON. Either pass a valid JSON or a plain object conforming to EIP712 TypedData schema.`
);
}
}

return this._node.signTypedDataV4(address, typedMessage);
}

// eth_submitHashrate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RunBlockResult } from "@nomiclabs/ethereumjs-vm/dist/runBlock";
import { StateManager } from "@nomiclabs/ethereumjs-vm/dist/state";
import chalk from "chalk";
import debug from "debug";
import * as ethSigUtil from "eth-sig-util";
import Common from "ethereumjs-common";
import { FakeTransaction, Transaction } from "ethereumjs-tx";
import {
Expand Down Expand Up @@ -75,10 +76,6 @@ import { putGenesisBlock } from "./utils/putGenesisBlock";

const log = debug("hardhat:core:hardhat-network:node");

// This library's types are wrong, they don't type check
// tslint:disable-next-line no-var-requires
const ethSigUtil = require("eth-sig-util");

export const COINBASE_ADDRESS = toBuffer(
"0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e"
);
Expand Down Expand Up @@ -623,7 +620,10 @@ export class HardhatNode extends EventEmitter {
return ecsign(messageHash, privateKey);
}

public async signTypedData(address: Buffer, typedData: any): Promise<string> {
public async signTypedDataV4(
address: Buffer,
typedData: any
): Promise<string> {
const privateKey = this._getLocalAccountPrivateKey(address);

return ethSigUtil.signTypedData_v4(privateKey, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { assert } from "chai";
import { recoverTypedSignature, recoverTypedSignature_v4 } from "eth-sig-util";
import Common from "ethereumjs-common";
import { Transaction } from "ethereumjs-tx";
import { BN, bufferToHex, toBuffer, zeroAddress } from "ethereumjs-util";
Expand Down Expand Up @@ -3390,8 +3391,102 @@ describe("Eth module", function () {
});
});

describe("eth_signTypedData", async function () {
// TODO: Test this. Note that it just forwards to/from eth-sign-util
describe("eth_signTypedData", function () {
it("is not supported", async function () {
await assertNotSupported(this.provider, "eth_signTypedData");
});
});

describe("eth_signTypedData_v3", function () {
it("is not supported", async function () {
await assertNotSupported(this.provider, "eth_signTypedData_v3");
});
});

describe("eth_signTypedData_v4", function () {
// See https://eips.ethereum.org/EIPS/eip-712#parameters
// There's a json schema and an explanation for each field.
const typedMessage = {
domain: {
chainId: 31337,
name: "Hardhat Network test suite",
},
message: {
name: "Translation",
start: {
x: 200,
y: 600,
},
end: {
x: 300,
y: 350,
},
cost: 50,
},
primaryType: "WeightedVector",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "chainId", type: "uint256" },
],
WeightedVector: [
{ name: "name", type: "string" },
{ name: "start", type: "Point" },
{ name: "end", type: "Point" },
{ name: "cost", type: "uint256" },
],
Point: [
{ name: "x", type: "uint256" },
{ name: "y", type: "uint256" },
],
},
};
const [address] = DEFAULT_ACCOUNTS_ADDRESSES;

it("should sign a message", async function () {
const signature = await this.provider.request({
method: "eth_signTypedData_v4",
params: [address, typedMessage],
});
const signedMessage = {
data: typedMessage,
sig: signature,
};

const recoveredAddress = recoverTypedSignature_v4(
signedMessage as any
);
assert.equal(address.toLowerCase(), recoveredAddress.toLowerCase());
});

it("should sign a message that is JSON stringified", async function () {
const signature = await this.provider.request({
method: "eth_signTypedData_v4",
params: [address, JSON.stringify(typedMessage)],
});
const signedMessage = {
data: typedMessage,
sig: signature,
};

const recoveredAddress = recoverTypedSignature_v4(
signedMessage as any
);
assert.equal(address.toLowerCase(), recoveredAddress.toLowerCase());
});

it("should fail with an invalid JSON", async function () {
try {
const signature = await this.provider.request({
method: "eth_signTypedData_v4",
params: [address, "{an invalid JSON"],
});
} catch (error) {
assert.include(error.message, "is an invalid JSON");
return;
}
assert.fail("should have failed with an invalid JSON");
});
});

describe("eth_submitHashrate", async function () {
Expand Down
10 changes: 8 additions & 2 deletions packages/hardhat-ethers/src/signers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ethers } from "ethers";

export class SignerWithAddress extends ethers.Signer {
public static async create(signer: ethers.Signer) {
public static async create(signer: ethers.providers.JsonRpcSigner) {
return new SignerWithAddress(await signer.getAddress(), signer);
}

private constructor(
public readonly address: string,
private readonly _signer: ethers.Signer
private readonly _signer: ethers.providers.JsonRpcSigner
) {
super();
(this as any).provider = _signer.provider;
Expand Down Expand Up @@ -37,6 +37,12 @@ export class SignerWithAddress extends ethers.Signer {
return new SignerWithAddress(this.address, this._signer.connect(provider));
}

public _signTypedData(
...params: Parameters<ethers.providers.JsonRpcSigner["_signTypedData"]>
): Promise<string> {
return this._signer._signTypedData(...params);
}

public toJSON() {
return `<SignerWithAddress ${this.address}>`;
}
Expand Down
53 changes: 53 additions & 0 deletions packages/hardhat-ethers/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,5 +773,58 @@ describe("Ethers plugin", function () {
code = await this.env.ethers.provider.getCode(receipt.contractAddress);
assert.lengthOf(code, 2);
});

it("_signTypedData integration test", async function () {
// See https://eips.ethereum.org/EIPS/eip-712#parameters
// There's a json schema and an explanation for each field.
const typedMessage = {
domain: {
chainId: 31337,
name: "Hardhat Network test suite",
},
message: {
name: "Translation",
start: {
x: 200,
y: 600,
},
end: {
x: 300,
y: 350,
},
cost: 50,
},
primaryType: "WeightedVector",
types: {
// ethers.js derives the EIP712Domain type from the domain object itself
// EIP712Domain: [
// { name: "name", type: "string" },
// { name: "chainId", type: "uint256" },
// ],
WeightedVector: [
{ name: "name", type: "string" },
{ name: "start", type: "Point" },
{ name: "end", type: "Point" },
{ name: "cost", type: "uint256" },
],
Point: [
{ name: "x", type: "uint256" },
{ name: "y", type: "uint256" },
],
},
};
const [signer] = await this.env.ethers.getSigners();

const signature = await signer._signTypedData(
typedMessage.domain,
typedMessage.types,
typedMessage.message
);

const byteToHex = 2;
const hexPrefix = 2;
const signatureSizeInBytes = 65;
assert.lengthOf(signature, signatureSizeInBytes * byteToHex + hexPrefix);
});
});
});