Skip to content

Commit

Permalink
Merge pull request NomicFoundation#1366 from nomiclabs/cleanup-jsonrp…
Browse files Browse the repository at this point in the history
…c-input-types

Cleanup JSON-RPC related types
  • Loading branch information
fvictorio committed Apr 5, 2021
2 parents 767f68d + aa74b67 commit 8ac46c6
Show file tree
Hide file tree
Showing 78 changed files with 3,565 additions and 2,287 deletions.
6 changes: 3 additions & 3 deletions packages/hardhat-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
"@ethereumjs/block": "^3.2.0",
"@ethereumjs/blockchain": "^5.2.1",
"@ethereumjs/common": "^2.2.0",
"@ethereumjs/tx": "^3.1.1",
"@ethereumjs/vm": "^5.2.0",
"@ethereumjs/tx": "^3.1.2",
"@ethereumjs/vm": "^5.3.0",
"@sentry/node": "^5.18.1",
"@solidity-parser/parser": "^0.11.0",
"@types/bn.js": "^5.1.0",
Expand All @@ -96,7 +96,7 @@
"eth-sig-util": "^2.5.2",
"ethereum-cryptography": "^0.1.2",
"ethereumjs-abi": "^0.6.8",
"ethereumjs-util": "^7.0.9",
"ethereumjs-util": "^7.0.10",
"find-up": "^2.1.0",
"fp-ts": "1.19.3",
"fs-extra": "^7.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HARDHAT_NETWORK_NAME,
HARDHAT_NETWORK_SUPPORTED_HARDFORKS,
} from "../../constants";
import { optional } from "../../util/io-ts";
import { fromEntries } from "../../util/lang";
import { HardhatError } from "../errors";
import { ERRORS } from "../errors-list";
Expand Down Expand Up @@ -62,18 +63,6 @@ export const DotPathReporter: Reporter<string[]> = {
report: (validation) => validation.fold(failure, success),
};

function optional<TypeT, OutputT>(
codec: t.Type<TypeT, OutputT, unknown>,
name: string = `${codec.name} | undefined`
): t.Type<TypeT | undefined, OutputT | undefined, unknown> {
return new t.Type(
name,
(u: unknown): u is TypeT | undefined => u === undefined || codec.is(u),
(u, c) => (u === undefined ? t.success(u) : codec.validate(u, c)),
(a) => (a === undefined ? undefined : codec.encode(a))
);
}

const HEX_STRING_REGEX = /^(0x)?([0-9a-f]{2})+$/gi;

function isHexString(v: unknown): v is string {
Expand Down
10 changes: 10 additions & 0 deletions packages/hardhat-core/src/internal/core/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,16 @@ Please make sure your node is running, and check your internet connection and ne
Try using another mnemonic or deriving less keys.`,
shouldBeReported: false,
},
INVALID_RPC_DATA_VALUE: {
number: 112,
message:
"Received invalid value `%value%` from/to the node's JSON-RPC, but a Data was expected.",
title: "Invalid JSON-RPC value",
description: `One of your calls sent or received an invalid JSON-RPC DATA value.
Please double check your calls' parameters and keep your Ethereum node up to date.`,
shouldBeReported: false,
},
},
TASK_DEFINITIONS: {
PARAM_AFTER_VARIADIC: {
Expand Down
134 changes: 134 additions & 0 deletions packages/hardhat-core/src/internal/core/jsonrpc/types/base-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { BN, bufferToHex, isValidAddress, toBuffer } from "ethereumjs-util";
import * as t from "io-ts";

import { assertHardhatInvariant, HardhatError } from "../../errors";
import { ERRORS } from "../../errors-list";

const ADDRESS_LENGTH_BYTES = 20;
const HASH_LENGTH_BYTES = 32;

export const rpcQuantity = new t.Type<BN>(
"QUANTITY",
BN.isBN,
(u, c) =>
isRpcQuantityString(u) ? t.success(new BN(toBuffer(u))) : t.failure(u, c),
t.identity
);

export const rpcData = new t.Type<Buffer>(
"DATA",
Buffer.isBuffer,
(u, c) => (isRpcDataString(u) ? t.success(toBuffer(u)) : t.failure(u, c)),
t.identity
);

export const rpcHash = new t.Type<Buffer>(
"HASH",
(v): v is Buffer => Buffer.isBuffer(v) && v.length === HASH_LENGTH_BYTES,
(u, c) => (isRpcHashString(u) ? t.success(toBuffer(u)) : t.failure(u, c)),
t.identity
);

export const rpcAddress = new t.Type<Buffer>(
"ADDRESS",
(v): v is Buffer => Buffer.isBuffer(v) && v.length === ADDRESS_LENGTH_BYTES,
(u, c) => (isRpcAddressString(u) ? t.success(toBuffer(u)) : t.failure(u, c)),
t.identity
);

export const rpcUnsignedInteger = new t.Type<number>(
"Unsigned integer",
isInteger,
(u, c) => (isInteger(u) && u >= 0 ? t.success(u) : t.failure(u, c)),
t.identity
);

// Conversion functions

/**
* Transforms a QUANTITY into a number. It should only be used if you are 100% sure that the value
* fits in a number.
*/
export function rpcQuantityToNumber(quantity: string): number {
return rpcQuantityToBN(quantity).toNumber();
}

export function rpcQuantityToBN(quantity: string): BN {
// We validate it in case a value gets here through a cast or any
if (!isRpcQuantityString(quantity)) {
throw new HardhatError(ERRORS.NETWORK.INVALID_RPC_QUANTITY_VALUE, {
value: quantity,
});
}

const buffer = toBuffer(quantity);
return new BN(buffer);
}

export function numberToRpcQuantity(n: number | BN): string {
assertHardhatInvariant(
typeof n === "number" || BN.isBN(n),
"Expected number"
);

return `0x${n.toString(16)}`;
}

/**
* Transforms a DATA into a number. It should only be used if you are 100% sure that the data
* represents a value fits in a number.
*/
export function rpcDataToNumber(data: string): number {
return rpcDataToBN(data).toNumber();
}

export function rpcDataToBN(data: string): BN {
return new BN(rpcDataToBuffer(data));
}

export function bufferToRpcData(
buffer: Buffer,
padToBytes: number = 0
): string {
let s = bufferToHex(buffer);
if (padToBytes > 0 && s.length < padToBytes * 2 + 2) {
s = `0x${"0".repeat(padToBytes * 2 + 2 - s.length)}${s.slice(2)}`;
}
return s;
}

export function rpcDataToBuffer(data: string): Buffer {
// We validate it in case a value gets here through a cast or any
if (!isRpcDataString(data)) {
throw new HardhatError(ERRORS.NETWORK.INVALID_RPC_DATA_VALUE, {
value: data,
});
}

return toBuffer(data);
}

// Type guards

function isRpcQuantityString(u: unknown): u is string {
return (
typeof u === "string" &&
u.match(/^0x(?:0|(?:[1-9a-fA-F][0-9a-fA-F]*))$/) !== null
);
}

function isRpcDataString(u: unknown): u is string {
return typeof u === "string" && u.match(/^0x(?:[0-9a-fA-F]{2})*$/) !== null;
}

function isRpcHashString(u: unknown): u is string {
return typeof u === "string" && u.length === 66 && isRpcDataString(u);
}

function isRpcAddressString(u: unknown): u is string {
return typeof u === "string" && isValidAddress(u);
}

function isInteger(num: unknown): num is number {
return Number.isInteger(num);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcData, rpcQuantity } from "../base-types";

export const rpcNewBlockTagObjectWithNumber = t.type({
blockNumber: rpcQuantity,
});

export const rpcNewBlockTagObjectWithHash = t.type({
blockHash: rpcData,
requireCanonical: optional(t.boolean),
});

export const rpcBlockTagName = t.keyof({
earliest: null,
latest: null,
pending: null,
});

// This is the new kind of block tag as defined by https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md
export const rpcNewBlockTag = t.union([
rpcQuantity,
rpcNewBlockTagObjectWithNumber,
rpcNewBlockTagObjectWithHash,
rpcBlockTagName,
]);

export type RpcNewBlockTag = t.TypeOf<typeof rpcNewBlockTag>;

export const optionalRpcNewBlockTag = optional(rpcNewBlockTag);

export type OptionalRpcNewBlockTag = t.TypeOf<typeof optionalRpcNewBlockTag>;

// This is the old kind of block tag which is described in the ethereum wiki
export const rpcOldBlockTag = t.union([rpcQuantity, rpcBlockTagName]);

export type RpcOldBlockTag = t.TypeOf<typeof rpcOldBlockTag>;

export const optionalRpcOldBlockTag = optional(rpcOldBlockTag);

export type OptionalRpcOldBlockTag = t.TypeOf<typeof optionalRpcOldBlockTag>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcAddress, rpcData, rpcQuantity } from "../base-types";

// Type used by eth_call and eth_estimateGas
// TODO: Update to Berlin
export const rpcCallRequest = t.type(
{
from: optional(rpcAddress),
to: optional(rpcAddress),
gas: optional(rpcQuantity),
gasPrice: optional(rpcQuantity),
value: optional(rpcQuantity),
data: optional(rpcData),
},
"RpcCallRequest"
);

// This type represents possibly valid inputs to rpcCallRequest.
// TODO: It can probably be inferred by io-ts.
export interface RpcCallRequestInput {
from?: string;
to: string;
gas?: string;
gasPrice?: string;
value?: string;
data?: string;
}

export type RpcCallRequest = t.TypeOf<typeof rpcCallRequest>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcHash } from "../base-types";

import { optionalRpcOldBlockTag } from "./blockTag";
import { optionalRpcLogAddress } from "./logAddress";
import { optionalRpcLogTopics } from "./logTopics";

export const rpcFilterRequest = t.type(
{
fromBlock: optionalRpcOldBlockTag,
toBlock: optionalRpcOldBlockTag,
address: optionalRpcLogAddress,
topics: optionalRpcLogTopics,
blockHash: optional(rpcHash),
},
"RpcFilterRequest"
);

export type RpcFilterRequest = t.TypeOf<typeof rpcFilterRequest>;

export const optionalRpcFilterRequest = optional(rpcFilterRequest);

export type OptionalRpcFilterRequest = t.TypeOf<
typeof optionalRpcFilterRequest
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcUnsignedInteger } from "../base-types";

export const rpcForkConfig = optional(
t.type(
{
jsonRpcUrl: t.string,
blockNumber: optional(t.number),
},
"RpcForkConfig"
)
);

export type RpcForkConfig = t.TypeOf<typeof rpcForkConfig>;

export const rpcHardhatNetworkConfig = t.type(
{
forking: optional(rpcForkConfig),
},
"HardhatNetworkConfig"
);

export type RpcHardhatNetworkConfig = t.TypeOf<typeof rpcHardhatNetworkConfig>;

export const optionalRpcHardhatNetworkConfig = optional(
rpcHardhatNetworkConfig
);

const isNumberPair = (x: unknown): x is [number, number] =>
Array.isArray(x) &&
x.length === 2 &&
Number.isInteger(x[0]) &&
Number.isInteger(x[1]);

// TODO: This can be simplified
const rpcIntervalMiningRange = new t.Type<[number, number]>(
"Interval mining range",
isNumberPair,
(u, c) =>
isNumberPair(u) && u[0] >= 0 && u[1] >= u[0]
? t.success(u)
: t.failure(u, c),
t.identity
);

export const rpcIntervalMining = t.union([
rpcUnsignedInteger,
rpcIntervalMiningRange,
]);

export type RpcIntervalMining = t.TypeOf<typeof rpcIntervalMining>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcAddress } from "../base-types";

export const rpcLogAddress = t.union([rpcAddress, t.array(rpcAddress)]);

export type RpcLogAddress = t.TypeOf<typeof rpcLogAddress>;

export const optionalRpcLogAddress = optional(rpcLogAddress);

export type OptionalRpcLogAddress = t.TypeOf<typeof optionalRpcLogAddress>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as t from "io-ts";

import { optional } from "../../../../util/io-ts";
import { rpcHash } from "../base-types";

export const rpcLogTopics = t.array(
t.union([t.null, rpcHash, t.array(t.union([t.null, rpcHash]))])
);

export type RpcLogTopics = t.TypeOf<typeof rpcLogTopics>;

export const optionalRpcLogTopics = optional(rpcLogTopics);

export type OptionalRpcLogTopics = t.TypeOf<typeof optionalRpcLogTopics>;
Loading

0 comments on commit 8ac46c6

Please sign in to comment.