Skip to content

Commit

Permalink
add automatic and fixed sender
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherDedominici committed Nov 27, 2024
1 parent 31720c3 commit ef072a8
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors";

import { SenderHandler } from "./sender.js";

/**
* This class automatically retrieves and caches the first available account from the connected provider.
* It overrides the getSender method of the base class to request the list of accounts if the first account has not been fetched yet,
* ensuring dynamic selection of the sender for all JSON-RPC requests without requiring manual input.
*/
export class AutomaticSenderHandler extends SenderHandler {
#alreadyFetchedAccounts = false;
#firstAccount: string | undefined;

protected async getSender(): Promise<string | undefined> {
if (this.#alreadyFetchedAccounts === false) {
const accounts = await this.provider.request({
method: "eth_accounts",
});

// TODO: This shouldn't be an exception but a failed JSON response!
assertHardhatInvariant(
Array.isArray(accounts),
"eth_accounts response should be an array",
);

this.#firstAccount = accounts[0];
this.#alreadyFetchedAccounts = true;
}

return this.#firstAccount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EthereumProvider } from "../../../../../../types/providers.js";

import { SenderHandler } from "./sender.js";

/**
* This class provides a fixed sender address for transactions.
* It overrides the getSender method of the base class to always return the sender address specified during instantiation,
* ensuring that all JSON-RPC requests use this fixed sender.
*/
export class FixedSenderHandler extends SenderHandler {
readonly #sender: string;

constructor(provider: EthereumProvider, sender: string) {
super(provider);
this.#sender = sender;
}

protected async getSender(): Promise<string | undefined> {
return this.#sender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { JsonRpcTransactionData } from "./types.js";
import type {
EthereumProvider,
JsonRpcRequest,
JsonRpcResponse,
} from "../../../../../../types/providers.js";
import type { RequestHandler } from "../../types.js";

import { HardhatError } from "@ignored/hardhat-vnext-errors";

import { getRequestParams } from "../../../json-rpc.js";

/**
* This class modifies JSON-RPC requests.
* It checks if the request is related to transactions and ensures that the "from" field is populated with a sender account if it's missing.
* If no account is available for sending transactions, it throws an error.
* The class also provides a mechanism to retrieve the sender account, which must be implemented by subclasses.
*/
export abstract class SenderHandler implements RequestHandler {
protected readonly provider: EthereumProvider;

constructor(provider: EthereumProvider) {
this.provider = provider;
}

public async handle(
jsonRpcRequest: JsonRpcRequest,
): Promise<JsonRpcRequest | JsonRpcResponse> {
const method = jsonRpcRequest.method;
const params = getRequestParams(jsonRpcRequest);

if (
method === "eth_sendTransaction" ||
method === "eth_call" ||
method === "eth_estimateGas"
) {
// TODO: from V2 - Should we validate this type?
const tx: JsonRpcTransactionData = params[0];

if (tx !== undefined && tx.from === undefined) {
const senderAccount = await this.getSender();

if (senderAccount !== undefined) {
tx.from = senderAccount;
} else if (method === "eth_sendTransaction") {
throw new HardhatError(
HardhatError.ERRORS.NETWORK.NO_REMOTE_ACCOUNT_AVAILABLE,
);
}
}
}

return jsonRpcRequest;
}

protected abstract getSender(): Promise<string | undefined>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface JsonRpcTransactionData {
from?: string;
to?: string;
gas?: string | number;
gasPrice?: string | number;
value?: string | number;
data?: string;
nonce?: string | number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { createMockedNetworkHre } from "./hooks-mock.js";
// are correctly modified in the "onRequest" hook handler.
// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)".
describe("request-handlers - e2e", () => {
it("should successfully executes all the handlers setting fixed values", async () => {
// should use the handlers in this order: ChainIdValidatorHandler, FixedGasHandler, FixedGasPriceHandler
it("should successfully executes all the handlers that set fixed values", async () => {
// should use the handlers in this order: ChainIdValidatorHandler, FixedGasHandler, FixedGasPriceHandler, FixedSenderHandler

const FIXED_GAS_LIMIT = 1231n;
const FIXED_GAS_PRICE = 1232n;
Expand All @@ -21,12 +21,14 @@ describe("request-handlers - e2e", () => {
localhost: {
gas: FIXED_GAS_LIMIT,
gasPrice: FIXED_GAS_PRICE,
from: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d",
type: "http",
url: "http://localhost:8545",
chainId: 1,
},
},
},
// List of methods that the handlers will call; we mock the responses
{
eth_chainId: "0x1",
},
Expand All @@ -40,20 +42,23 @@ describe("request-handlers - e2e", () => {
method: "eth_sendTransaction",
params: [
{
from: "0x0000000000000000000000000000000000000011",
to: "0x0000000000000000000000000000000000000012",
},
],
});

assert.ok(Array.isArray(res), "res should be an array");

// gas
assert.equal(res[0].gas, numberToHexString(FIXED_GAS_LIMIT));
// gasPrice
assert.equal(res[0].gasPrice, numberToHexString(FIXED_GAS_PRICE));
// sender
assert.equal(res[0].from, "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d");
});

it("should successfully executes all the handlers setting automatic values", async () => {
// should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler
it("should successfully executes all the handlers that set automatic values", async () => {
// should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler, AutomaticSenderHandler

const FIXED_GAS_LIMIT = 1231;
const GAS_MULTIPLIER = 1.337;
Expand All @@ -72,6 +77,7 @@ describe("request-handlers - e2e", () => {
},
},
{
// List of methods that the handlers will call; we mock the responses
eth_chainId: "0x1",
eth_getBlockByNumber: {
baseFeePerGas: "0x1",
Expand All @@ -87,6 +93,7 @@ describe("request-handlers - e2e", () => {
],
reward: [["0x4"]],
},
eth_accounts: ["0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"],
},
);

Expand All @@ -98,7 +105,6 @@ describe("request-handlers - e2e", () => {
method: "eth_sendTransaction",
params: [
{
from: "0x0000000000000000000000000000000000000011",
to: "0x0000000000000000000000000000000000000012",
maxFeePerGas: "0x99",
},
Expand All @@ -107,12 +113,15 @@ describe("request-handlers - e2e", () => {

assert.ok(Array.isArray(res), "res should be an array");

// gas
assert.equal(
res[0].gas,
numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)),
);

// gas price
assert.equal(res[0].maxPriorityFeePerGas, "0x4");
assert.equal(res[0].maxFeePerGas, "0x99");
// sender
assert.equal(res[0].from, "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.js";

import assert from "node:assert/strict";
import { before, describe, it } from "node:test";

import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex";

import {
getJsonRpcRequest,
getRequestParams,
} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js";
import { AutomaticSenderHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.js";
import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js";

describe("AutomaticSenderHandler", function () {
let automaticSenderHandler: AutomaticSenderHandler;
let mockedProvider: EthereumMockedProvider;
let tx: JsonRpcTransactionData;

before(() => {
mockedProvider = new EthereumMockedProvider();

mockedProvider.setReturnValue("eth_accounts", [
"0x123006d4548a3ac17d72b372ae1e416bf65b8eaf",
]);

automaticSenderHandler = new AutomaticSenderHandler(mockedProvider);

tx = {
to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead",
gas: numberToHexString(21000),
gasPrice: numberToHexString(678912),
nonce: numberToHexString(0),
value: numberToHexString(1),
};
});

it("should set the from value into the transaction", async () => {
const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]);

await automaticSenderHandler.handle(jsonRpcRequest);

assert.equal(
getRequestParams(jsonRpcRequest)[0].from,
"0x123006d4548a3ac17d72b372ae1e416bf65b8eaf",
);
});

it("should not replace transaction's from", async () => {
tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead";

const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]);

assert.equal(
getRequestParams(jsonRpcRequest)[0].from,
"0x000006d4548a3ac17d72b372ae1e416bf65b8ead",
);
});

it("should not fail on eth_calls if provider doesn't have any accounts", async () => {
mockedProvider.setReturnValue("eth_accounts", []);

tx.value = "asd";

const jsonRpcRequest = getJsonRpcRequest(1, "eth_call", [tx]);

await automaticSenderHandler.handle(jsonRpcRequest);

assert.equal(getRequestParams(jsonRpcRequest)[0].value, "asd");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.js";

import assert from "node:assert/strict";
import { before, describe, it } from "node:test";

import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex";

import {
getJsonRpcRequest,
getRequestParams,
} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js";
import { FixedSenderHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.js";
import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js";

describe("FixedSenderHandler", function () {
let fixedSenderHandler: FixedSenderHandler;
let mockedProvider: EthereumMockedProvider;
let tx: JsonRpcTransactionData;

before(() => {
mockedProvider = new EthereumMockedProvider();

fixedSenderHandler = new FixedSenderHandler(
mockedProvider,
"0x2a97a65d5673a2c61e95ce33cecadf24f654f96d",
);

tx = {
to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead",
gas: numberToHexString(21000),
gasPrice: numberToHexString(678912),
nonce: numberToHexString(0),
value: numberToHexString(1),
};
});

it("should set the from value into the transaction", async () => {
const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]);

await fixedSenderHandler.handle(jsonRpcRequest);

assert.equal(
getRequestParams(jsonRpcRequest)[0].from,
"0x2a97a65d5673a2c61e95ce33cecadf24f654f96d",
);
});

it("should not replace transaction's from", async () => {
tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead";

const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]);

await fixedSenderHandler.handle(jsonRpcRequest);

assert.equal(
getRequestParams(jsonRpcRequest)[0].from,
"0x000006d4548a3ac17d72b372ae1e416bf65b8ead",
);
});
});

0 comments on commit ef072a8

Please sign in to comment.