Skip to content

Commit

Permalink
Merge pull request #5810 from NomicFoundation/move-providers-to-v3
Browse files Browse the repository at this point in the history
Move V2 wrapped providers to v3
  • Loading branch information
ChristopherDedominici authored Oct 17, 2024
2 parents ef2a5de + 6472432 commit 1ed9f9e
Show file tree
Hide file tree
Showing 27 changed files with 1,963 additions and 8 deletions.
15 changes: 15 additions & 0 deletions v-next/hardhat-errors/src/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,21 @@ If you want to use a different chain type, please update your networks config.`,
websiteTitle: "Invalid config override",
websiteDescription: `The configuration override you provided is invalid.`,
},
INVALID_GLOBAL_CHAIN_ID: {
number: 708,
messageTemplate:
"Hardhat was set to use chain id {configChainId}, but connected to a chain with id {connectionChainId}.",
websiteTitle: "Invalid global chain id",
websiteDescription: `Hardhat was set to use a chain id but connected to a chain with a different id`,
},
NO_REMOTE_ACCOUNT_AVAILABLE: {
number: 709,
messageTemplate:
"No local account was set and there are accounts in the remote node.",
websiteTitle: "No remote account available",
websiteDescription:
"No local account was set and there are accounts in the remote node",
},
},
KEYSTORE: {
INVALID_KEYSTORE_FILE_FORMAT: {
Expand Down
36 changes: 36 additions & 0 deletions v-next/hardhat-utils/src/hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,42 @@ export function hexStringToBigInt(hexString: string): bigint {
return bigInt;
}

/**
* Converts a hexadecimal string to a number. The string must be a valid
* hexadecimal string. The string may be prefixed with "0x" or not. The
* empty string is considered a valid hexadecimal string, so is the string
* "0x" and will be converted to 0.
*
* @param hexString The hexadecimal string to convert. It must be a valid
* hexadecimal string.
* @returns The number representation of the hexadecimal string.
* @throws InvalidParameterError If the input is not a hexadecimal string or the value exceeds the Number.MAX_SAFE_INTEGER limit.
*/
export function hexStringToNumber(hexString: string): number {
if (!isHexString(hexString)) {
throw new InvalidParameterError(
`Expected a valid hexadecimal string. Received: ${hexString}`,
);
}

// Prefix the string as it is required to make parseInt interpret it as a
// hexadecimal number.
let prefixedHexString = getPrefixedHexString(hexString);

// Handle the special case where the string is "0x".
prefixedHexString = prefixedHexString === "0x" ? "0x0" : prefixedHexString;

const numberValue = parseInt(prefixedHexString, 16);

if (numberValue > Number.MAX_SAFE_INTEGER) {
throw new InvalidParameterError(
`Value exceeds the safe integer limit. Received: ${hexString}`,
);
}

return numberValue;
}

/**
* Converts a Uint8Array to a hexadecimal string.
*
Expand Down
52 changes: 50 additions & 2 deletions v-next/hardhat-utils/test/hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getPrefixedHexString,
unpadHexString,
setLengthLeft,
hexStringToNumber,
} from "../src/hex.js";

describe("hex", () => {
Expand Down Expand Up @@ -72,8 +73,8 @@ describe("hex", () => {
});
});

describe("hexStringToNumber", () => {
it("Should convert a hexadecimal string to a number", () => {
describe("hexStringToBigInt", () => {
it("Should convert a hexadecimal string to a bigint", () => {
assert.equal(hexStringToBigInt("0x"), 0n);
assert.equal(hexStringToBigInt("0x0"), 0n);
assert.equal(hexStringToBigInt("0x1"), 1n);
Expand Down Expand Up @@ -107,6 +108,53 @@ describe("hex", () => {
});
});

describe("hexStringToNumber", () => {
it("Should convert a hexadecimal string to a number", () => {
assert.equal(hexStringToNumber("0x"), 0);
assert.equal(hexStringToNumber("0x0"), 0);
assert.equal(hexStringToNumber("0x1"), 1);
assert.equal(hexStringToNumber("0xf"), 15);
assert.equal(hexStringToNumber("0x10"), 16);
assert.equal(hexStringToNumber("0xff"), 255);
assert.equal(hexStringToNumber("0x100"), 256);
assert.equal(hexStringToNumber("0xffff"), 65535);
assert.equal(hexStringToNumber("0x10000"), 65536);
assert.equal(hexStringToNumber("0xffffffff"), 4294967295);
assert.equal(hexStringToNumber("0x100000000"), 4294967296);

assert.equal(hexStringToNumber(""), 0);
assert.equal(hexStringToNumber("0"), 0);
assert.equal(hexStringToNumber("1"), 1);
assert.equal(hexStringToNumber("f"), 15);
assert.equal(hexStringToNumber("10"), 16);
assert.equal(hexStringToNumber("ff"), 255);
assert.equal(hexStringToNumber("100"), 256);
assert.equal(hexStringToNumber("ffff"), 65535);
assert.equal(hexStringToNumber("10000"), 65536);
assert.equal(hexStringToNumber("ffffffff"), 4294967295);
assert.equal(hexStringToNumber("100000000"), 4294967296);
assert.equal(hexStringToNumber("ffffffff"), 4294967295);

// Max allowed value
assert.equal(hexStringToNumber("0x1fffffffffffff"), 9007199254740991);
});

it("Should throw InvalidParameterError if the input exceeds the safe integer limit", () => {
assert.throws(() => hexStringToNumber("0x20000000000000"), {
name: "InvalidParameterError",
message:
"Value exceeds the safe integer limit. Received: 0x20000000000000",
});
});

it("Should throw InvalidParameterError if the input is not a hexadecimal string", () => {
assert.throws(() => hexStringToNumber("invalid"), {
name: "InvalidParameterError",
message: "Expected a valid hexadecimal string. Received: invalid",
});
});
});

describe("bytesToHexString", () => {
it("Should convert a Uint8Array to a hexadecimal string", () => {
assert.equal(bytesToHexString(new Uint8Array([])), "0x");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {
JsonRpcRequest,
JsonRpcResponse,
} from "../../../../types/providers.js";
import type {
HookContext,
NetworkHooks,
} from "@ignored/hardhat-vnext/types/hooks";
import type {
ChainType,
NetworkConnection,
} from "@ignored/hardhat-vnext/types/network";

import { JsonRpcRequestModifier } from "../json-rpc-request-modifiers/json-rpc-request-modifier.js";

export default async (): Promise<Partial<NetworkHooks>> => {
// This map is necessary because Hardhat V3 supports multiple network connections, requiring us to track them
// to apply the appropriate modifiers to each request.
// When a connection is closed, it is removed from the map. Refer to "closeConnection" at the end of the file.
const jsonRpcRequestModifiers: Map<number, JsonRpcRequestModifier> =
new Map();

const handlers: Partial<NetworkHooks> = {
async onRequest<ChainTypeT extends ChainType | string>(
context: HookContext,
networkConnection: NetworkConnection<ChainTypeT>,
jsonRpcRequest: JsonRpcRequest,
next: (
nextContext: HookContext,
nextNetworkConnection: NetworkConnection<ChainTypeT>,
nextJsonRpcRequest: JsonRpcRequest,
) => Promise<JsonRpcResponse>,
) {
let jsonRpcRequestModifier = jsonRpcRequestModifiers.get(
networkConnection.id,
);

if (jsonRpcRequestModifier === undefined) {
jsonRpcRequestModifier = new JsonRpcRequestModifier(networkConnection);

jsonRpcRequestModifiers.set(
networkConnection.id,
jsonRpcRequestModifier,
);
}

const newJsonRpcRequest =
await jsonRpcRequestModifier.createModifiedJsonRpcRequest(
jsonRpcRequest,
);

return next(context, networkConnection, newJsonRpcRequest);
},

async closeConnection<ChainTypeT extends ChainType | string>(
context: HookContext,
networkConnection: NetworkConnection<ChainTypeT>,
next: (
nextContext: HookContext,
nextNetworkConnection: NetworkConnection<ChainTypeT>,
) => Promise<void>,
): Promise<void> {
if (jsonRpcRequestModifiers.has(networkConnection.id) === true) {
jsonRpcRequestModifiers.delete(networkConnection.id);
}

return next(context, networkConnection);
},
};

return handlers;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const hardhatPlugin: HardhatPlugin = {
hookHandlers: {
config: import.meta.resolve("./hook-handlers/config.js"),
hre: import.meta.resolve("./hook-handlers/hre.js"),
network: import.meta.resolve("./hook-handlers/network.js"),
},
globalOptions: [
globalOption({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { EthereumProvider } from "../../../../../types/providers.js";

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

import { ChainId } from "./chain-id.js";

/**
* This class extends `ChainId` to validate that the current provider's chain ID matches
* an expected value. If the actual chain ID differs from the expected one, it throws a
* HardhatError to signal a network configuration mismatch. Once validated, further checks
* are skipped to avoid redundant validations.
*/
export class ChainIdValidator extends ChainId {
readonly #expectedChainId: number;

#alreadyValidated = false;
readonly #chainId: number | undefined;

constructor(provider: EthereumProvider, expectedChainId: number) {
super(provider);
this.#expectedChainId = expectedChainId;
}

public async validate(): Promise<void> {
if (!this.#alreadyValidated) {
const actualChainId = await this.getChainId();

if (actualChainId !== this.#expectedChainId) {
throw new HardhatError(
HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID,
{
configChainId: this.#expectedChainId,
connectionChainId: actualChainId,
},
);
}

this.#alreadyValidated = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { EthereumProvider } from "../../../../../types/providers.js";

import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors";
import {
hexStringToNumber,
isHexStringPrefixed,
} from "@ignored/hardhat-vnext-utils/hex";

/**
* This class is responsible for retrieving the chain ID of the network.
* It uses the provider to fetch the chain ID via two methods: 'eth_chainId' and,
* as a fallback, 'net_version' if the first one fails. The chain ID is cached
* after being retrieved to avoid redundant requests.
*/
export abstract class ChainId {
protected readonly provider: EthereumProvider;

#chainId: number | undefined;

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

protected async getChainId(): Promise<number> {
if (this.#chainId === undefined) {
try {
this.#chainId = await this.getChainIdFromEthChainId();
} catch {
// If eth_chainId fails we default to net_version
this.#chainId = await this.#getChainIdFromEthNetVersion();
}
}

return this.#chainId;
}

protected async getChainIdFromEthChainId(): Promise<number> {
const id = await this.provider.request({
method: "eth_chainId",
});

assertHardhatInvariant(typeof id === "string", "id should be a string");

return hexStringToNumber(id);
}

async #getChainIdFromEthNetVersion(): Promise<number> {
const id = await this.provider.request({
method: "net_version",
});

assertHardhatInvariant(typeof id === "string", "id should be a string");

// There's a node returning this as decimal instead of QUANTITY.
// TODO: from V2 - Document here which node does that
return isHexStringPrefixed(id) ? hexStringToNumber(id) : parseInt(id, 10);
}
}
Loading

0 comments on commit 1ed9f9e

Please sign in to comment.