From c8a5403de67789ec3d0b163736de09c311dadc5c Mon Sep 17 00:00:00 2001 From: Kyrylo Riabov Date: Wed, 7 Feb 2024 16:18:03 +0200 Subject: [PATCH] Fixed a bug with the shallow copy and mutation of args (#70) * Fixed a bug with the shallow copy and mutation of args * Updated versions * Added ability to retrieve the class name from the contract instance. --- CHANGELOG.md | 9 +- package-lock.json | 4 +- package.json | 2 +- .../adapters/EthersContractAdapter.ts | 6 +- src/deployer/adapters/EthersFactoryAdapter.ts | 6 +- src/deployer/adapters/TruffleAdapter.ts | 13 +- src/utils.ts | 254 ------------------ src/utils/common.ts | 47 ++++ src/utils/error.ts | 50 ++++ src/utils/index.ts | 5 + src/utils/logging.ts | 24 ++ src/utils/migration.ts | 138 ++++++++++ src/utils/network.ts | 23 ++ 13 files changed, 311 insertions(+), 270 deletions(-) delete mode 100644 src/utils.ts create mode 100644 src/utils/common.ts create mode 100644 src/utils/error.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/logging.ts create mode 100644 src/utils/migration.ts create mode 100644 src/utils/network.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8366a62..0a88c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Changelog +## Version 2.1.1 + +* Fixed a bug related to the shallow copying and mutation of arguments within the `getMethodString` function. +* Added ability to retrieve the class name from the contract instance. + ## Version 2.1.0 -* Updated packages to their latest versions (updated `@nomicfoundation/hardhat-verify` to version `2.0.4`) -* Added a `save` function to the Deployer class to enable saving the contract to storage without deploying it +* Updated packages to their latest versions (updated `@nomicfoundation/hardhat-verify` to version `2.0.4`). +* Added a `save` function to the Deployer class to enable saving the contract to storage without deploying it. ## Version 2.0.1 diff --git a/package-lock.json b/package-lock.json index 05945fa..6124e04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solarity/hardhat-migrate", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solarity/hardhat-migrate", - "version": "2.1.0", + "version": "2.1.1", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index d3d05cb..2e4ebfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/hardhat-migrate", - "version": "2.1.0", + "version": "2.1.1", "description": "Automatic deployment and verification of smart contracts", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/src/deployer/adapters/EthersContractAdapter.ts b/src/deployer/adapters/EthersContractAdapter.ts index 7be62f3..1dc89b3 100644 --- a/src/deployer/adapters/EthersContractAdapter.ts +++ b/src/deployer/adapters/EthersContractAdapter.ts @@ -2,9 +2,7 @@ import { BaseContract, ContractRunner, Interface } from "ethers"; import { AbstractEthersAdapter } from "./AbstractEthersAdapter"; -import { catchError, getSignerHelper } from "../../utils"; - -import { UNKNOWN_CONTRACT_NAME } from "../../constants"; +import { catchError, getInstanceNameFromClass, getSignerHelper } from "../../utils"; import { EthersContract } from "../../types/adapter"; import { OverridesAndName } from "../../types/deployer"; @@ -33,7 +31,7 @@ export class EthersContractAdapter extends AbstractEthersAdapter { return (instance as any).contractName; } - return UNKNOWN_CONTRACT_NAME; + return getInstanceNameFromClass(instance); } } diff --git a/src/deployer/adapters/EthersFactoryAdapter.ts b/src/deployer/adapters/EthersFactoryAdapter.ts index aac28ce..52bb34c 100644 --- a/src/deployer/adapters/EthersFactoryAdapter.ts +++ b/src/deployer/adapters/EthersFactoryAdapter.ts @@ -4,9 +4,7 @@ import { AbstractEthersAdapter } from "./AbstractEthersAdapter"; import { OverridesAndName } from "../../types/deployer"; -import { catchError } from "../../utils"; - -import { UNKNOWN_CONTRACT_NAME } from "../../constants"; +import { catchError, getInstanceNameFromClass } from "../../utils"; import { ArtifactProcessor } from "../../tools/storage/ArtifactProcessor"; @@ -32,7 +30,7 @@ export class EthersFactoryAdapter extends AbstractEthersAdapter { return (instance as any).contractName; } - return UNKNOWN_CONTRACT_NAME; + return getInstanceNameFromClass(instance); } } diff --git a/src/deployer/adapters/TruffleAdapter.ts b/src/deployer/adapters/TruffleAdapter.ts index 01c9a64..a3520ea 100644 --- a/src/deployer/adapters/TruffleAdapter.ts +++ b/src/deployer/adapters/TruffleAdapter.ts @@ -14,9 +14,16 @@ import { Adapter } from "./Adapter"; import { MinimalContract } from "../MinimalContract"; -import { bytecodeToString, catchError, fillParameters, getMethodString, getSignerHelper } from "../../utils"; +import { + bytecodeToString, + catchError, + fillParameters, + getInstanceNameFromClass, + getMethodString, + getSignerHelper, +} from "../../utils"; -import { UNKNOWN_CONTRACT_NAME, UNKNOWN_TRANSACTION_NAME } from "../../constants"; +import { UNKNOWN_TRANSACTION_NAME } from "../../constants"; import { KeyTransactionFields, MigrationMetadata } from "../../types/tools"; import { EthersContract, Instance, TruffleFactory } from "../../types/adapter"; @@ -82,7 +89,7 @@ export class TruffleAdapter extends Adapter { return this._getFullyQualifiedName(instance as any) || (instance as any).contractName; } - return UNKNOWN_CONTRACT_NAME; + return getInstanceNameFromClass(instance); } } diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index d5d5ead..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { join } from "path"; -import { realpathSync, existsSync } from "fs"; -import { - id, - hexlify, - toBigInt, - ethers, - Overrides, - Interface, - Fragment, - AddressLike, - InterfaceAbi, - JsonFragment, - FunctionFragment, - ConstructorFragment, -} from "ethers"; - -import { isBytes } from "@ethersproject/bytes"; -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { MigrateError } from "./errors"; -import { UNKNOWN_CONTRACT_NAME } from "./constants"; - -import { Bytecode } from "./types/deployer"; -import { KeyDeploymentFields, KeyTransactionFields } from "./types/tools"; - -import { networkManager } from "./tools/network/NetworkManager"; - -export async function getSignerHelper(from?: null | AddressLike): Promise { - if (!from) { - return networkManager!.provider.getSigner(); - } - - const address = await ethers.resolveAddress(from, networkManager!.provider); - - return networkManager!.provider.getSigner(address); -} - -export async function fillParameters(parameters: Overrides): Promise { - if (parameters.from === undefined) { - parameters.from = await (await networkManager!.provider.getSigner()).getAddress(); - } - - if (parameters.chainId === undefined) { - parameters.chainId = await getChainId(); - } - - if (parameters.value === undefined) { - parameters.value = 0; - } - - return parameters; -} - -export function underline(str: string): string { - return `\u001b[4m${str}\u001b[0m`; -} - -export function resolvePathToFile(path: string, file: string = ""): string { - if (!existsSync(path)) { - path = "./"; - } - - return join(realpathSync(path), file); -} - -export async function getChainId(): Promise { - return toBigInt(await networkManager!.provider.send("eth_chainId")); -} - -export function toJSON(data: any): string { - return JSON.stringify(data, JSONConvertor); -} - -export function JSONConvertor(_key: any, value: any) { - if (typeof value === "bigint") { - return value.toString(); - } - - return value; -} - -export function bytecodeHash(bytecode: any): string { - return id(bytecodeToString(bytecode)); -} - -export function createKeyDeploymentFieldsHash(keyTxFields: KeyDeploymentFields): string { - const obj: KeyDeploymentFields = { - name: keyTxFields.name, - data: keyTxFields.data, - from: keyTxFields.from, - chainId: keyTxFields.chainId, - value: keyTxFields.value, - }; - - return id(toJSON(obj)); -} - -export function createKeyTxFieldsHash(keyTxFields: KeyTransactionFields): string { - const obj: KeyTransactionFields = { - data: keyTxFields.data, - from: keyTxFields.from, - chainId: keyTxFields.chainId, - to: keyTxFields.to, - value: keyTxFields.value, - name: keyTxFields.name, - }; - - return id(toJSON(obj)); -} - -export async function isDeployedContractAddress(address: string): Promise { - return (await networkManager!.provider.getCode(address)) !== "0x"; -} - -export function bytecodeToString(bytecode: Bytecode): string { - let bytecodeHex: string; - - if (typeof bytecode === "string") { - bytecodeHex = bytecode; - } else if (isBytes(bytecode)) { - bytecodeHex = hexlify(bytecode); - } else { - throw new MigrateError(`Invalid bytecode: ${bytecode}`); - } - - // Make sure it is 0x prefixed - if (bytecodeHex.substring(0, 2) !== "0x") { - bytecodeHex = "0x" + bytecodeHex; - } - - return bytecodeHex; -} - -export function getMethodString( - contractName: string, - methodName: string, - methodFragment: FunctionFragment = {} as FunctionFragment, - args: any[] = [], -): string { - if (contractName === UNKNOWN_CONTRACT_NAME) { - contractName = "Unidentified Contract"; - } - - if (methodFragment.inputs.length + 1 === args.length) { - args.pop(); - } - - if (methodFragment.inputs === undefined) { - return `${contractName}.${methodName}()`; - } - const argsString = methodFragment.inputs.map((input, i) => `${input.name}:${args[i]}`).join(", "); - - const methodSting = `${contractName}.${methodName}(${argsString})`; - - if (methodSting.length > 60) { - const shortenMethodString = `${contractName}.${methodName}(${args.length} arguments)`; - - if (shortenMethodString.length > 60) { - return `${contractName.split(":").pop()}.${methodName}(${args.length} arguments)`; - } - } - - return methodSting; -} - -export function catchError(target: any, propertyName?: string, descriptor?: PropertyDescriptor) { - // Method decorator - if (descriptor) { - _generateDescriptor(propertyName!, descriptor); - } - // Class decorator - else { - for (const propertyName of Reflect.ownKeys(target.prototype).filter((prop) => prop !== "constructor")) { - const desc = Object.getOwnPropertyDescriptor(target.prototype, propertyName)!; - const isMethod = desc.value instanceof Function; - if (!isMethod) continue; - Object.defineProperty( - target.prototype, - propertyName, - _generateDescriptor(`${target.prototype.constructor.name}.${propertyName.toString()}`, desc), - ); - } - } -} - -export function getInterfaceOnlyWithConstructor(fragments: InterfaceAbi): Interface { - let abi: ReadonlyArray; - if (typeof fragments === "string") { - abi = JSON.parse(fragments); - } else { - abi = fragments; - } - - const fragment = abi.find((a: any) => a.type === "constructor"); - - if (fragment !== undefined) { - return Interface.from([fragment]); - } - - return new Interface([ConstructorFragment.from("constructor()")]); -} - -/* eslint-disable no-console */ -export function suppressLogs(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value; - - descriptor.value = function (...args: any[]) { - const log = console.log; - - console.log(); - console.log = () => {}; - - const result = originalMethod.apply(this, args); - - console.log = log; - - return result; - }; - - return descriptor; -} -/* eslint-enable no-console */ - -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -function _generateDescriptor(propertyName: string, descriptor: PropertyDescriptor): PropertyDescriptor { - const method = descriptor.value; - - descriptor.value = function ___ErrorCatcher(...args: any[]) { - try { - const result = method.apply(this, args); - - // Check if the method is asynchronous - if (result && result instanceof Promise) { - // Return promise - return result.catch((e: any) => { - _handleError(propertyName, e); - }); - } - - // Return actual result - return result; - } catch (e: any) { - _handleError(propertyName, e); - } - }; - - return descriptor; -} - -function _handleError(propertyName: string, error: any) { - throw new MigrateError(`${propertyName}(): ${error.message ?? error}`, { cause: error }); -} diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..27a334c --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,47 @@ +import { join } from "path"; +import { realpathSync, existsSync } from "fs"; +import { UNKNOWN_CONTRACT_NAME } from "../constants"; + +export function resolvePathToFile(path: string, file: string = ""): string { + if (!existsSync(path)) { + path = "./"; + } + + return join(realpathSync(path), file); +} + +export function getInstanceNameFromClass(instance: any): string { + const className = parseClassName(instance.toString()); + + return className === undefined ? UNKNOWN_CONTRACT_NAME : className.replace("__factory", ""); +} + +function parseClassName(classDefinitionString: string) { + // Regular expression to match the class name + const classNameRegex = /class\s+([^\s]+)\s*\{/; + + const match = classDefinitionString.match(classNameRegex); + if (match && match.length > 1) { + return match[1]; + } + + return undefined; +} + +export function deepCopy(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function toJSON(data: any): string { + return JSON.stringify(data, JSONConvertor); +} + +function JSONConvertor(_key: any, value: any) { + if (typeof value === "bigint") { + return value.toString(); + } + + return value; +} diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..e901b64 --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,50 @@ +import { MigrateError } from "../errors"; + +export function catchError(target: any, propertyName?: string, descriptor?: PropertyDescriptor) { + // Method decorator + if (descriptor) { + _generateDescriptor(propertyName!, descriptor); + } + // Class decorator + else { + for (const propertyName of Reflect.ownKeys(target.prototype).filter((prop) => prop !== "constructor")) { + const desc = Object.getOwnPropertyDescriptor(target.prototype, propertyName)!; + const isMethod = desc.value instanceof Function; + if (!isMethod) continue; + Object.defineProperty( + target.prototype, + propertyName, + _generateDescriptor(`${target.prototype.constructor.name}.${propertyName.toString()}`, desc), + ); + } + } +} + +function _generateDescriptor(propertyName: string, descriptor: PropertyDescriptor): PropertyDescriptor { + const method = descriptor.value; + + descriptor.value = function ___ErrorCatcher(...args: any[]) { + try { + const result = method.apply(this, args); + + // Check if the method is asynchronous + if (result && result instanceof Promise) { + // Return promise + return result.catch((e: any) => { + _handleError(propertyName, e); + }); + } + + // Return actual result + return result; + } catch (e: any) { + _handleError(propertyName, e); + } + }; + + return descriptor; +} + +function _handleError(propertyName: string, error: any) { + throw new MigrateError(`${propertyName}(): ${error.message ?? error}`, { cause: error }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..50a526c --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./common"; +export * from "./error"; +export * from "./logging"; +export * from "./migration"; +export * from "./network"; diff --git a/src/utils/logging.ts b/src/utils/logging.ts new file mode 100644 index 0000000..e5cde1a --- /dev/null +++ b/src/utils/logging.ts @@ -0,0 +1,24 @@ +export function underline(str: string): string { + return `\u001b[4m${str}\u001b[0m`; +} + +/* eslint-disable no-console */ +export function suppressLogs(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: any[]) { + const log = console.log; + + console.log(); + console.log = () => {}; + + const result = originalMethod.apply(this, args); + + console.log = log; + + return result; + }; + + return descriptor; +} +/* eslint-enable no-console */ diff --git a/src/utils/migration.ts b/src/utils/migration.ts new file mode 100644 index 0000000..0d46a0a --- /dev/null +++ b/src/utils/migration.ts @@ -0,0 +1,138 @@ +import { + id, + hexlify, + Fragment, + Interface, + Overrides, + InterfaceAbi, + JsonFragment, + FunctionFragment, + ConstructorFragment, +} from "ethers"; +import { isBytes } from "@ethersproject/bytes"; + +import { getChainId } from "./network"; +import { deepCopy, toJSON } from "./common"; + +import { MigrateError } from "../errors"; +import { UNKNOWN_CONTRACT_NAME } from "../constants"; + +import { networkManager } from "../tools/network/NetworkManager"; + +import { Bytecode } from "../types/deployer"; +import { KeyDeploymentFields, KeyTransactionFields } from "../types/tools"; + +export async function fillParameters(parameters: Overrides): Promise { + if (parameters.from === undefined) { + parameters.from = await (await networkManager!.provider.getSigner()).getAddress(); + } + + if (parameters.chainId === undefined) { + parameters.chainId = await getChainId(); + } + + if (parameters.value === undefined) { + parameters.value = 0; + } + + return parameters; +} + +export function getInterfaceOnlyWithConstructor(fragments: InterfaceAbi): Interface { + let abi: ReadonlyArray; + if (typeof fragments === "string") { + abi = JSON.parse(fragments); + } else { + abi = fragments; + } + + const fragment = abi.find((a: any) => a.type === "constructor"); + + if (fragment !== undefined) { + return Interface.from([fragment]); + } + + return new Interface([ConstructorFragment.from("constructor()")]); +} + +export function bytecodeHash(bytecode: any): string { + return id(bytecodeToString(bytecode)); +} + +export function createKeyDeploymentFieldsHash(keyTxFields: KeyDeploymentFields): string { + const obj: KeyDeploymentFields = { + name: keyTxFields.name, + data: keyTxFields.data, + from: keyTxFields.from, + chainId: keyTxFields.chainId, + value: keyTxFields.value, + }; + + return id(toJSON(obj)); +} + +export function createKeyTxFieldsHash(keyTxFields: KeyTransactionFields): string { + const obj: KeyTransactionFields = { + data: keyTxFields.data, + from: keyTxFields.from, + chainId: keyTxFields.chainId, + to: keyTxFields.to, + value: keyTxFields.value, + name: keyTxFields.name, + }; + + return id(toJSON(obj)); +} + +export function bytecodeToString(bytecode: Bytecode): string { + let bytecodeHex: string; + + if (typeof bytecode === "string") { + bytecodeHex = bytecode; + } else if (isBytes(bytecode)) { + bytecodeHex = hexlify(bytecode); + } else { + throw new MigrateError(`Invalid bytecode: ${bytecode}`); + } + + // Make sure it is 0x prefixed + if (bytecodeHex.substring(0, 2) !== "0x") { + bytecodeHex = "0x" + bytecodeHex; + } + + return bytecodeHex; +} + +export function getMethodString( + contractName: string, + methodName: string, + methodFragment: FunctionFragment = {} as FunctionFragment, + args: any[] = [], +): string { + if (contractName === UNKNOWN_CONTRACT_NAME) { + contractName = "Unidentified Contract"; + } + + const copyOfArgs = deepCopy(args); + + if (methodFragment.inputs.length + 1 === copyOfArgs.length) { + copyOfArgs.pop(); + } + + if (methodFragment.inputs === undefined) { + return `${contractName}.${methodName}()`; + } + const argsString = methodFragment.inputs.map((input, i) => `${input.name}:${copyOfArgs[i]}`).join(", "); + + const methodSting = `${contractName}.${methodName}(${argsString})`; + + if (methodSting.length > 60) { + const shortenMethodString = `${contractName}.${methodName}(${copyOfArgs.length} arguments)`; + + if (shortenMethodString.length > 60) { + return `${contractName.split(":").pop()}.${methodName}(${copyOfArgs.length} arguments)`; + } + } + + return methodSting; +} diff --git a/src/utils/network.ts b/src/utils/network.ts new file mode 100644 index 0000000..148d785 --- /dev/null +++ b/src/utils/network.ts @@ -0,0 +1,23 @@ +import { AddressLike, ethers, toBigInt } from "ethers"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { networkManager } from "../tools/network/NetworkManager"; + +export async function getChainId(): Promise { + return toBigInt(await networkManager!.provider.send("eth_chainId")); +} + +export async function isDeployedContractAddress(address: string): Promise { + return (await networkManager!.provider.getCode(address)) !== "0x"; +} + +export async function getSignerHelper(from?: null | AddressLike): Promise { + if (!from) { + return networkManager!.provider.getSigner(); + } + + const address = await ethers.resolveAddress(from, networkManager!.provider); + + return networkManager!.provider.getSigner(address); +}