UNPKG

@solarity/hardhat-migrate

Version:
366 lines (311 loc) 13.5 kB
import { isAbsolute } from "path"; import type { ConfigurationVariableResolver, HardhatConfig, HardhatUserConfig } from "hardhat/types/config"; import type { ConfigHooks, HardhatUserConfigValidationError } from "hardhat/types/hooks"; import { validateUserConfigZodType } from "@nomicfoundation/hardhat-zod-utils"; import { z } from "zod"; import type { MigrateConfig, MigrateConfigArgs, MigrateUserConfig, MigrateVerifyConfig } from "./types/index.js"; import { PLUGIN_ID } from "./constants.js"; export default async (): Promise<Partial<ConfigHooks>> => ({ validateUserConfig, resolveUserConfig, }); const userConfigType = z.object({ migrate: z .object({ filter: z .object({ from: z.number().int().optional(), to: z.number().int().optional(), only: z.number().int().optional(), skip: z.number().int().optional(), }) .optional(), verification: z .object({ verify: z.boolean().optional(), verificationDelay: z.number().int().optional(), verifyParallel: z.number().int().optional(), verifyAttempts: z.number().int().optional(), }) .optional(), paths: z .object({ pathToMigrations: z .string() .refine((p) => !isAbsolute(p), "Expected a relative path") .optional(), namespace: z.string().optional(), reportPath: z .string() .refine((p) => !isAbsolute(p), "Expected a relative path") .optional(), reportFormat: z.enum(["json", "md"]).optional(), }) .optional(), execution: z .object({ force: z.boolean().optional(), continue: z.boolean().optional(), wait: z.number().int().optional(), transactionStatusCheckInterval: z.number().int().optional(), withoutCLIReporting: z.boolean().optional(), }) .optional(), castWallet: z .object({ passwordFile: z.string().optional(), keystore: z.string().optional(), account: z.string().optional(), }) .optional(), trezorWallet: z .object({ enabled: z.boolean().optional(), mnemonicIndex: z.number().int().optional(), }) .optional(), }) .optional(), }); export async function validateUserConfig(userConfig: HardhatUserConfig): Promise<HardhatUserConfigValidationError[]> { return validateUserConfigZodType(userConfig, userConfigType); } export async function resolveUserConfig( userConfig: HardhatUserConfig, resolveConfigurationVariable: ConfigurationVariableResolver, next: ( nextUserConfig: HardhatUserConfig, nextResolveConfigurationVariable: ConfigurationVariableResolver, ) => Promise<HardhatConfig>, ): Promise<HardhatConfig> { const resolvedConfig = await next(userConfig, resolveConfigurationVariable); const migrate = await resolveMigrateConfig(userConfig.migrate, resolveConfigurationVariable); return { ...resolvedConfig, migrate, }; } async function resolveMigrateConfig( externalConfig: MigrateUserConfig | undefined, _resolveConfigurationVariable: ConfigurationVariableResolver, ): Promise<MigrateConfig> { const defaults: MigrateConfig = { filter: { from: -1, to: -1, only: -1, skip: -1, }, verification: { verify: false, verificationDelay: 5000, verifyParallel: 1, verifyAttempts: 3, }, paths: { pathToMigrations: "./deploy", namespace: "", reportPath: "cache", reportFormat: "md", }, execution: { force: false, continue: false, wait: 1, transactionStatusCheckInterval: 2000, withoutCLIReporting: false, }, castWallet: {}, trezorWallet: { enabled: false, mnemonicIndex: 0, }, }; if (externalConfig === undefined) { return defaults; } const resolved: MigrateConfig = JSON.parse(JSON.stringify(defaults)); // Filter if (externalConfig.filter) { if (typeof externalConfig.filter.from === "number") resolved.filter.from = externalConfig.filter.from; if (typeof externalConfig.filter.to === "number") resolved.filter.to = externalConfig.filter.to; if (typeof externalConfig.filter.only === "number") resolved.filter.only = externalConfig.filter.only; if (typeof externalConfig.filter.skip === "number") resolved.filter.skip = externalConfig.filter.skip; } // Verification if (externalConfig.verification) { const v = externalConfig.verification; if (typeof v.verify === "boolean") resolved.verification.verify = v.verify; if (typeof v.verificationDelay === "number") resolved.verification.verificationDelay = v.verificationDelay; if (typeof v.verifyParallel === "number") resolved.verification.verifyParallel = v.verifyParallel; if (typeof v.verifyAttempts === "number") resolved.verification.verifyAttempts = v.verifyAttempts; } // Paths if (externalConfig.paths) { const p = externalConfig.paths; if (typeof p.pathToMigrations === "string") resolved.paths.pathToMigrations = p.pathToMigrations; if (typeof p.namespace === "string") resolved.paths.namespace = p.namespace; if (typeof p.reportPath === "string") resolved.paths.reportPath = p.reportPath; if (typeof p.reportFormat === "string") { resolved.paths.reportFormat = p.reportFormat; } } // Execution if (externalConfig.execution) { const e = externalConfig.execution; if (typeof e.force === "boolean") resolved.execution.force = e.force; if (typeof e.continue === "boolean") resolved.execution.continue = e.continue; if (typeof e.wait === "number") resolved.execution.wait = e.wait; if (typeof e.transactionStatusCheckInterval === "number") resolved.execution.transactionStatusCheckInterval = e.transactionStatusCheckInterval; if (typeof e.withoutCLIReporting === "boolean") resolved.execution.withoutCLIReporting = e.withoutCLIReporting; } // Cast wallet if (externalConfig.castWallet) { if (typeof externalConfig.castWallet.passwordFile === "string") { resolved.castWallet.passwordFile = externalConfig.castWallet.passwordFile; } if (typeof externalConfig.castWallet.keystore === "string") { resolved.castWallet.keystore = externalConfig.castWallet.keystore; } if (typeof externalConfig.castWallet.account === "string") { resolved.castWallet.account = externalConfig.castWallet.account; } } // Trezor wallet if (externalConfig.trezorWallet) { const t = externalConfig.trezorWallet; if (typeof t.enabled === "boolean") resolved.trezorWallet.enabled = t.enabled; if (typeof t.mnemonicIndex === "number") resolved.trezorWallet.mnemonicIndex = t.mnemonicIndex; } return resolved; } export const mergeConfigs = ( externalConfig: Partial<MigrateConfig> | undefined, migrateConfig: MigrateConfig, ): MigrateConfig => { if (externalConfig) { return deepMerge(migrateConfig, externalConfig); } return migrateConfig; }; export function convertFlatToNested(flatConfig: MigrateConfigArgs): Partial<MigrateConfig> { const result: Partial<MigrateConfig> = {}; // NOTE: Using truthy checks here means CLI flags that explicitly set `0` (e.g. `--from 0`) // will be ignored because `0` is falsy. If zero should be a valid override we need // to switch to `"key" in flatConfig` style checks. if (flatConfig.from || flatConfig.to || flatConfig.only || flatConfig.skip) { result.filter = {} as any; } if (flatConfig.from) result.filter!.from = flatConfig.from; if (flatConfig.to) result.filter!.to = flatConfig.to; if (flatConfig.only) result.filter!.only = flatConfig.only; if (flatConfig.skip) result.filter!.skip = flatConfig.skip; if (flatConfig.verify || flatConfig.verificationDelay || flatConfig.verifyParallel || flatConfig.verifyAttempts) { result.verification = {} as any; } if (flatConfig.verify) result.verification!.verify = flatConfig.verify; if (flatConfig.verificationDelay) result.verification!.verificationDelay = flatConfig.verificationDelay; if (flatConfig.verifyParallel) result.verification!.verifyParallel = flatConfig.verifyParallel; if (flatConfig.verifyAttempts) result.verification!.verifyAttempts = flatConfig.verifyAttempts; if (flatConfig.pathToMigrations || flatConfig.namespace || flatConfig.reportPath || flatConfig.reportFormat) { result.paths = {} as any; } if (flatConfig.pathToMigrations) result.paths!.pathToMigrations = flatConfig.pathToMigrations; if (flatConfig.namespace) result.paths!.namespace = flatConfig.namespace; if (flatConfig.reportPath) result.paths!.reportPath = flatConfig.reportPath; if (flatConfig.reportFormat) result.paths!.reportFormat = flatConfig.reportFormat; if (flatConfig.force || flatConfig.continue || flatConfig.wait || flatConfig.transactionStatusCheckInterval) { result.execution = {} as any; } if (flatConfig.force) result.execution!.force = flatConfig.force; if (flatConfig.continue) result.execution!.continue = flatConfig.continue; if (flatConfig.wait) result.execution!.wait = flatConfig.wait; if (flatConfig.transactionStatusCheckInterval) result.execution!.transactionStatusCheckInterval = flatConfig.transactionStatusCheckInterval; if (flatConfig.passwordFile || flatConfig.keystore || flatConfig.account) { result.castWallet = {} as any; } if (flatConfig.passwordFile) result.castWallet!.passwordFile = flatConfig.passwordFile; if (flatConfig.keystore) result.castWallet!.keystore = flatConfig.keystore; if (flatConfig.account) result.castWallet!.account = flatConfig.account; if (flatConfig.trezorEnabled || flatConfig.trezorMnemonicIndex) { result.trezorWallet = {} as any; } if (flatConfig.trezorEnabled) result.trezorWallet!.enabled = flatConfig.trezorEnabled; if (flatConfig.trezorMnemonicIndex) result.trezorWallet!.mnemonicIndex = flatConfig.trezorMnemonicIndex; return result; } // Deep merge utility for nested objects function deepMerge(target: any, source: any): any { const output = { ...target }; if (isObject(target) && isObject(source)) { Object.keys(source).forEach((key) => { if (isObject(source[key])) { if (!(key in target)) { Object.assign(output, { [key]: source[key] }); } else { output[key] = deepMerge(target[key], source[key]); } } else { Object.assign(output, { [key]: source[key] }); } }); } return output; } function isObject(item: any): boolean { return item && typeof item === "object" && !Array.isArray(item); } export const validateConfig = (config: MigrateConfig): void => { if (config.execution.wait !== undefined && config.execution.wait < 1) { throw new Error(`[${PLUGIN_ID}] config.migrate.execution.wait must be greater than 0`); } if (config.verification.verifyParallel !== undefined && config.verification.verifyParallel < 1) { throw new Error(`[${PLUGIN_ID}] config.migrate.verification.verifyParallel must be greater than 0`); } if (config.verification.verifyAttempts !== undefined && config.verification.verifyAttempts < 1) { throw new Error(`[${PLUGIN_ID}] config.migrate.verification.verifyAttempts must be greater than 0`); } if (config.paths.pathToMigrations !== undefined && !isRelativePath(config.paths.pathToMigrations)) { throw new Error(`[${PLUGIN_ID}] config.migrate.paths.pathToMigrations must be a relative path`); } if (config.paths.reportFormat !== undefined && !["json", "md"].includes(config.paths.reportFormat)) { throw new Error(`[${PLUGIN_ID}] config.migrate.paths.reportFormat must be either 'json' or 'md'`); } if (config.trezorWallet.enabled && (config.castWallet.account || config.castWallet.keystore)) { throw new Error( `[${PLUGIN_ID}] config.migrate.trezorWallet.enabled cannot be used with config.migrate.castWallet.account or config.migrate.castWallet.keystore at the same time`, ); } if (config.castWallet.account && config.castWallet.keystore) { throw new Error( `[${PLUGIN_ID}] config.migrate.castWallet.account and config.migrate.castWallet.keystore cannot be enabled at the same time`, ); } if ( config.execution.transactionStatusCheckInterval !== undefined && config.execution.transactionStatusCheckInterval < 1000 ) { throw new Error( `[${PLUGIN_ID}] config.migrate.execution.transactionStatusCheckInterval must be greater or equal to 1000`, ); } }; export const extendVerifyConfigs = (cliArgs: Partial<MigrateVerifyConfig> | undefined): MigrateVerifyConfig => { const defaultVerifyConfig: MigrateVerifyConfig = { inputFile: undefined, parallel: 1, attempts: 3, }; const config = cliArgs === undefined ? defaultVerifyConfig : { ...defaultVerifyConfig, ...definedProps(cliArgs) }; if (config.parallel !== undefined && config.parallel < 1) { throw new Error(`[${PLUGIN_ID}] parallel must be greater than 0`); } if (config.attempts !== undefined && config.attempts < 1) { throw new Error(`[${PLUGIN_ID}] attempts must be greater than 0`); } return config; }; const definedProps = (obj: any): any => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); const isRelativePath = (path?: string): boolean => path === undefined || !isAbsolute(path);