@nomicfoundation/hardhat-ethers
Version:
Hardhat plugin for ethers
482 lines (419 loc) • 11.8 kB
text/typescript
// these helpers functions were copied verbatim from ethers
import type {
TransactionRequest,
PreparedTransactionRequest,
BlockParams,
TransactionResponseParams,
TransactionReceiptParams,
LogParams,
JsonRpcTransactionRequest,
AuthorizationLike,
SignatureLike,
Authorization,
} from "ethers";
import {
accessListify,
assert,
assertArgument,
authorizationify,
getAddress,
getBigInt,
getCreateAddress,
getNumber,
hexlify,
isHexString,
Signature,
toQuantity,
} from "ethers";
import { HardhatEthersError } from "./errors";
export type FormatFunc = (value: any) => any;
export function copyRequest(
req: TransactionRequest
): PreparedTransactionRequest {
const result: any = {};
// These could be addresses, ENS names or Addressables
if (req.to !== null && req.to !== undefined) {
result.to = req.to;
}
if (req.from !== null && req.from !== undefined) {
result.from = req.from;
}
if (req.data !== null && req.data !== undefined) {
result.data = hexlify(req.data);
}
const bigIntKeys =
"chainId,gasLimit,gasPrice,maxFeePerGas,maxPriorityFeePerGas,value".split(
/,/
);
for (const key of bigIntKeys) {
if (
!(key in req) ||
(req as any)[key] === null ||
(req as any)[key] === undefined
) {
continue;
}
result[key] = getBigInt((req as any)[key], `request.${key}`);
}
const numberKeys = "type,nonce".split(/,/);
for (const key of numberKeys) {
if (
!(key in req) ||
(req as any)[key] === null ||
(req as any)[key] === undefined
) {
continue;
}
result[key] = getNumber((req as any)[key], `request.${key}`);
}
if (req.accessList !== null && req.accessList !== undefined) {
result.accessList = accessListify(req.accessList);
}
if (req.authorizationList !== null && req.authorizationList !== undefined) {
result.authorizationList = req.authorizationList;
}
if ("blockTag" in req) {
result.blockTag = req.blockTag;
}
if ("enableCcipRead" in req) {
result.enableCcipReadEnabled = Boolean(req.enableCcipRead);
}
if ("customData" in req) {
result.customData = req.customData;
}
return result;
}
export async function resolveProperties<T>(value: {
[P in keyof T]: T[P] | Promise<T[P]>;
}): Promise<T> {
const keys = Object.keys(value);
const results = await Promise.all(
keys.map((k) => Promise.resolve(value[k as keyof T]))
);
return results.reduce((accum: any, v, index) => {
accum[keys[index]] = v;
return accum;
}, {} as { [P in keyof T]: T[P] });
}
export function formatBlock(value: any): BlockParams {
const result = _formatBlock(value);
result.transactions = value.transactions.map(
(tx: string | TransactionResponseParams) => {
if (typeof tx === "string") {
return tx;
}
return formatTransactionResponse(tx);
}
);
return result;
}
const _formatBlock = object({
hash: allowNull(formatHash),
parentHash: formatHash,
number: getNumber,
timestamp: getNumber,
nonce: allowNull(formatData),
difficulty: getBigInt,
gasLimit: getBigInt,
gasUsed: getBigInt,
miner: allowNull(getAddress),
extraData: formatData,
baseFeePerGas: allowNull(getBigInt),
});
function object(
format: Record<string, FormatFunc>,
altNames?: Record<string, string[]>
): FormatFunc {
return (value: any) => {
const result: any = {};
// eslint-disable-next-line guard-for-in
for (const key in format) {
let srcKey = key;
if (altNames !== undefined && key in altNames && !(srcKey in value)) {
for (const altKey of altNames[key]) {
if (altKey in value) {
srcKey = altKey;
break;
}
}
}
try {
const nv = format[key](value[srcKey]);
if (nv !== undefined) {
result[key] = nv;
}
} catch (error) {
const message = error instanceof Error ? error.message : "not-an-error";
assert(
false,
`invalid value for value.${key} (${message})`,
"BAD_DATA",
{ value }
);
}
}
return result;
};
}
function allowNull(format: FormatFunc, nullValue?: any): FormatFunc {
return function (value: any) {
// eslint-disable-next-line eqeqeq
if (value === null || value === undefined) {
return nullValue;
}
return format(value);
};
}
function formatHash(value: any): string {
assertArgument(isHexString(value, 32), "invalid hash", "value", value);
return value;
}
function formatData(value: string): string {
assertArgument(isHexString(value, true), "invalid data", "value", value);
return value;
}
export function formatTransactionResponse(
value: any
): TransactionResponseParams {
// Some clients (TestRPC) do strange things like return 0x0 for the
// 0 address; correct this to be a real address
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (value.to && getBigInt(value.to) === 0n) {
value.to = "0x0000000000000000000000000000000000000000";
}
const result = object(
{
hash: formatHash,
type: (v: any) => {
// eslint-disable-next-line eqeqeq
if (v === "0x" || v == null) {
return 0;
}
return getNumber(v);
},
accessList: allowNull(accessListify, null),
authorizationList: allowNull(convertToTxAuthorizationList, null),
blockHash: allowNull(formatHash, null),
blockNumber: allowNull(getNumber, null),
transactionIndex: allowNull(getNumber, null),
from: getAddress,
// either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas) must be set
gasPrice: allowNull(getBigInt),
maxPriorityFeePerGas: allowNull(getBigInt),
maxFeePerGas: allowNull(getBigInt),
gasLimit: getBigInt,
to: allowNull(getAddress, null),
value: getBigInt,
nonce: getNumber,
data: formatData,
creates: allowNull(getAddress, null),
chainId: allowNull(getBigInt, null),
},
{
data: ["input"],
gasLimit: ["gas"],
}
)(value);
// If to and creates are empty, populate the creates from the value
// eslint-disable-next-line eqeqeq
if (result.to == null && result.creates == null) {
result.creates = getCreateAddress(result);
}
// @TODO: Check fee data
// Add an access list to supported transaction types
// eslint-disable-next-line eqeqeq
if ((value.type === 1 || value.type === 2) && value.accessList == null) {
result.accessList = [];
}
// eslint-disable-next-line eqeqeq
if (value.type === 4 && value.authorizationList == null) {
result.authorizationList = [];
}
// Compute the signature
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (value.signature) {
result.signature = Signature.from(value.signature);
} else {
result.signature = Signature.from(value);
}
// Some backends omit ChainId on legacy transactions, but we can compute it
// eslint-disable-next-line eqeqeq
if (result.chainId == null) {
const chainId = result.signature.legacyChainId;
// eslint-disable-next-line eqeqeq
if (chainId != null) {
result.chainId = chainId;
}
}
// 0x0000... should actually be null
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (result.blockHash && getBigInt(result.blockHash) === 0n) {
result.blockHash = null;
}
return result;
}
function arrayOf(format: FormatFunc): FormatFunc {
return (array: any) => {
if (!Array.isArray(array)) {
throw new HardhatEthersError("not an array");
}
return array.map((i) => format(i));
};
}
const _formatReceiptLog = object(
{
transactionIndex: getNumber,
blockNumber: getNumber,
transactionHash: formatHash,
address: getAddress,
topics: arrayOf(formatHash),
data: formatData,
index: getNumber,
blockHash: formatHash,
},
{
index: ["logIndex"],
}
);
const _formatTransactionReceipt = object(
{
to: allowNull(getAddress, null),
from: allowNull(getAddress, null),
contractAddress: allowNull(getAddress, null),
// should be allowNull(hash), but broken-EIP-658 support is handled in receipt
index: getNumber,
root: allowNull(hexlify),
gasUsed: getBigInt,
logsBloom: allowNull(formatData),
blockHash: formatHash,
hash: formatHash,
logs: arrayOf(formatReceiptLog),
blockNumber: getNumber,
cumulativeGasUsed: getBigInt,
effectiveGasPrice: allowNull(getBigInt),
status: allowNull(getNumber),
type: allowNull(getNumber, 0),
},
{
effectiveGasPrice: ["gasPrice"],
hash: ["transactionHash"],
index: ["transactionIndex"],
}
);
export function formatTransactionReceipt(value: any): TransactionReceiptParams {
return _formatTransactionReceipt(value);
}
export function formatReceiptLog(value: any): LogParams {
return _formatReceiptLog(value);
}
function formatBoolean(value: any): boolean {
switch (value) {
case true:
case "true":
return true;
case false:
case "false":
return false;
}
assertArgument(
false,
`invalid boolean; ${JSON.stringify(value)}`,
"value",
value
);
}
const _formatLog = object(
{
address: getAddress,
blockHash: formatHash,
blockNumber: getNumber,
data: formatData,
index: getNumber,
removed: formatBoolean,
topics: arrayOf(formatHash),
transactionHash: formatHash,
transactionIndex: getNumber,
},
{
index: ["logIndex"],
}
);
export function formatLog(value: any): LogParams {
return _formatLog(value);
}
export function getRpcTransaction(
tx: TransactionRequest
): JsonRpcTransactionRequest {
const result: JsonRpcTransactionRequest = {};
// JSON-RPC now requires numeric values to be "quantity" values
[
"chainId",
"gasLimit",
"gasPrice",
"type",
"maxFeePerGas",
"maxPriorityFeePerGas",
"nonce",
"value",
].forEach((key) => {
if ((tx as any)[key] === null || (tx as any)[key] === undefined) {
return;
}
let dstKey = key;
if (key === "gasLimit") {
dstKey = "gas";
}
(result as any)[dstKey] = toQuantity(
getBigInt((tx as any)[key], `tx.${key}`)
);
});
// Make sure addresses and data are lowercase
["from", "to", "data"].forEach((key) => {
if ((tx as any)[key] === null || (tx as any)[key] === undefined) {
return;
}
(result as any)[key] = hexlify((tx as any)[key]);
});
// Normalize the access list object
if (tx.accessList !== null && tx.accessList !== undefined) {
result.accessList = accessListify(tx.accessList);
}
// Normalize the authorization list
if (tx.authorizationList !== null && tx.authorizationList !== undefined) {
result.authorizationList = authorizationListify(tx.authorizationList);
}
return result;
}
function authorizationListify(authorizationList: AuthorizationLike[]) {
return authorizationList.map((el) => {
const auth = authorizationify(el);
return {
address: auth.address,
nonce: toQuantity(auth.nonce),
chainId: toQuantity(auth.chainId),
r: auth.signature.r,
s: auth.signature.s,
yParity: toQuantity(auth.signature.yParity),
};
});
}
function convertToTxAuthorizationList(
authorizationList: Array<
{
address: string;
nonce: string;
chainId: string;
} & SignatureLike
>
): Authorization[] {
const authorizations: Authorization[] = [];
for (const auth of authorizationList) {
authorizations.push({
address: auth.address,
nonce: getBigInt(auth.nonce),
chainId: getBigInt(auth.chainId),
signature: Signature.from(auth),
});
}
return authorizations;
}