UNPKG

@nomicfoundation/hardhat-ignition-viem

Version:

The Viem extension to Hardhat Ignition. Hardhat Ignition is a declarative system for deploying smart contracts on Ethereum. It enables you to define smart contract instances you want to deploy, and any operation you want to run on them. By taking over the

415 lines (370 loc) 12 kB
import type { ViemIgnitionHelper, IgnitionModuleResultsToViemContracts, } from "../types.js"; import type { GetContractReturnType } from "@nomicfoundation/hardhat-viem/types"; import type { ContractAtFuture, ContractDeploymentFuture, ContractFuture, DeployConfig, DeploymentParameters, EIP1193Provider, Future, IgnitionModule, IgnitionModuleResult, LibraryDeploymentFuture, NamedArtifactContractAtFuture, NamedArtifactContractDeploymentFuture, NamedArtifactLibraryDeploymentFuture, StrategyConfig, SuccessfulDeploymentResult, } from "@nomicfoundation/ignition-core"; import type { ArtifactManager } from "hardhat/types/artifacts"; import type { HardhatConfig } from "hardhat/types/config"; import type { HookManager, UserInterruptionHooks } from "hardhat/types/hooks"; import type { NetworkConnection, ChainType } from "hardhat/types/network"; import type { UserInterruptionManager } from "hardhat/types/user-interruptions"; import path from "node:path"; import { assertHardhatInvariant, HardhatError, } from "@nomicfoundation/hardhat-errors"; import { HardhatArtifactResolver, PrettyEventHandler, errorDeploymentResultToExceptionMessage, getUserInterruptionsHandlers, readDeploymentParameters, resolveDeploymentId, } from "@nomicfoundation/hardhat-ignition/helpers"; import { DeploymentResultType, FutureType, deploy, isContractFuture, } from "@nomicfoundation/ignition-core"; import { getContract } from "viem"; export class ViemIgnitionHelperImpl<ChainTypeT extends ChainType | string> implements ViemIgnitionHelper { public type: "viem" = "viem"; readonly #hardhatConfig: HardhatConfig; readonly #artifactsManager: ArtifactManager; readonly #connection: NetworkConnection<ChainTypeT>; readonly #userInterruptions: UserInterruptionManager; readonly #hooks: HookManager; readonly #config: Partial<DeployConfig> | undefined; readonly #provider: EIP1193Provider; #mutex: boolean = false; constructor( hardhatConfig: HardhatConfig, artifactsManager: ArtifactManager, connection: NetworkConnection<ChainTypeT>, userInterruptions: UserInterruptionManager, hooks: HookManager, config?: Partial<DeployConfig> | undefined, provider?: EIP1193Provider, ) { this.#hardhatConfig = hardhatConfig; this.#artifactsManager = artifactsManager; this.#connection = connection; this.#userInterruptions = userInterruptions; this.#hooks = hooks; this.#config = config; this.#provider = provider ?? this.#connection.provider; } /** * Deploys the given Ignition module and returns the results of the module * as Viem contract instances. * * @param ignitionModule - The Ignition module to deploy. * @param options - The options to use for the deployment. * @returns Viem contract instances for each contract returned by the module. */ public async deploy< ModuleIdT extends string, ContractNameT extends string, IgnitionModuleResultsT extends IgnitionModuleResult<ContractNameT>, StrategyT extends keyof StrategyConfig = "basic", >( ignitionModule: IgnitionModule< ModuleIdT, ContractNameT, IgnitionModuleResultsT >, { parameters = {}, config: perDeployConfig = {}, defaultSender = undefined, strategy, strategyConfig, deploymentId: givenDeploymentId = undefined, displayUi = false, }: { parameters?: DeploymentParameters | string; config?: Partial<DeployConfig>; defaultSender?: string; strategy?: StrategyT; strategyConfig?: StrategyConfig[StrategyT]; deploymentId?: string; displayUi?: boolean; } = { parameters: {}, config: {}, defaultSender: undefined, strategy: undefined, strategyConfig: undefined, deploymentId: undefined, displayUi: undefined, }, ): Promise< IgnitionModuleResultsToViemContracts<ContractNameT, IgnitionModuleResultsT> > { if (this.#mutex) { throw new HardhatError( HardhatError.ERRORS.IGNITION.DEPLOY.ALREADY_IN_PROGRESS, ); } this.#mutex = true; let userInterruptionsHandlers: UserInterruptionHooks | undefined; try { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- eth_accounts returns a string array const accounts: string[] = (await this.#connection.provider.request({ method: "eth_accounts", })) as string[]; const artifactResolver = new HardhatArtifactResolver( this.#artifactsManager, ); const resolvedConfig: Partial<DeployConfig> = this.getResolvedConfig(perDeployConfig); const resolvedStrategyConfig = ViemIgnitionHelperImpl.#resolveStrategyConfig<StrategyT>( this.#hardhatConfig, strategy, strategyConfig, ); const chainId = Number( await this.#connection.provider.request({ method: "eth_chainId", }), ); const deploymentId = resolveDeploymentId(givenDeploymentId, chainId); const deploymentDir = this.#connection.networkConfig.type === "edr-simulated" ? undefined : path.join( this.#hardhatConfig.paths.ignition, "deployments", deploymentId, ); const executionEventListener = displayUi ? new PrettyEventHandler(this.#userInterruptions) : undefined; if (executionEventListener !== undefined) { userInterruptionsHandlers = getUserInterruptionsHandlers(); this.#hooks.registerHandlers( "userInterruptions", userInterruptionsHandlers, ); } let deploymentParameters: DeploymentParameters; if (typeof parameters === "string") { deploymentParameters = await readDeploymentParameters(parameters); } else { deploymentParameters = parameters; } if ( resolvedConfig.maxRetries === undefined && this.#connection.networkConfig.ignition.maxRetries !== undefined ) { resolvedConfig.maxRetries = this.#connection.networkConfig.ignition.maxRetries; } if ( resolvedConfig.retryInterval === undefined && this.#connection.networkConfig.ignition.retryInterval !== undefined ) { resolvedConfig.retryInterval = this.#connection.networkConfig.ignition.retryInterval; } const result = await deploy({ config: resolvedConfig, provider: this.#provider, deploymentDir, executionEventListener, artifactResolver, ignitionModule, deploymentParameters, accounts, defaultSender, strategy, strategyConfig: resolvedStrategyConfig, maxFeePerGasLimit: this.#connection.networkConfig?.ignition.maxFeePerGasLimit, maxPriorityFeePerGas: this.#connection.networkConfig?.ignition.maxPriorityFeePerGas, }); if (result.type !== DeploymentResultType.SUCCESSFUL_DEPLOYMENT) { const message = errorDeploymentResultToExceptionMessage(result); throw new HardhatError( HardhatError.ERRORS.IGNITION.INTERNAL.DEPLOYMENT_ERROR, { message, }, ); } return await this.#toViemContracts( this.#connection, ignitionModule, result, ); } finally { if (userInterruptionsHandlers !== undefined) { this.#hooks.unregisterHandlers( "userInterruptions", userInterruptionsHandlers, ); } this.#mutex = false; } } public getResolvedConfig( perDeployConfig: Partial<DeployConfig>, ): Partial<DeployConfig> { return { ...this.#config, ...perDeployConfig, }; } async #toViemContracts< ModuleIdT extends string, ContractNameT extends string, IgnitionModuleResultsT extends IgnitionModuleResult<ContractNameT>, >( connection: NetworkConnection<ChainTypeT>, ignitionModule: IgnitionModule< ModuleIdT, ContractNameT, IgnitionModuleResultsT >, result: SuccessfulDeploymentResult, ): Promise< IgnitionModuleResultsToViemContracts<ContractNameT, IgnitionModuleResultsT> > { return Object.fromEntries( await Promise.all( Object.entries(ignitionModule.results).map( async ([name, contractFuture]) => [ name, await this.#getContract( connection, contractFuture, result.contracts[contractFuture.id], ), ], ), ), ); } async #getContract( connection: NetworkConnection<ChainTypeT>, future: Future, deployedContract: { address: string }, ): Promise<GetContractReturnType> { assertHardhatInvariant( isContractFuture(future), `Expected contract future but got ${future.id} with type ${future.type} instead`, ); return await this.#convertContractFutureToViemContract( connection, future, deployedContract, ); } async #convertContractFutureToViemContract( connection: NetworkConnection<ChainTypeT>, future: ContractFuture<string>, deployedContract: { address: string }, ) { switch (future.type) { case FutureType.NAMED_ARTIFACT_CONTRACT_DEPLOYMENT: case FutureType.NAMED_ARTIFACT_LIBRARY_DEPLOYMENT: case FutureType.NAMED_ARTIFACT_CONTRACT_AT: return await this.#convertHardhatContractToViemContract( connection, future, deployedContract, ); case FutureType.CONTRACT_DEPLOYMENT: case FutureType.LIBRARY_DEPLOYMENT: case FutureType.CONTRACT_AT: return await this.#convertArtifactToViemContract( connection, future, deployedContract, ); } } #convertHardhatContractToViemContract( connection: NetworkConnection<ChainTypeT>, future: | NamedArtifactContractDeploymentFuture<string> | NamedArtifactLibraryDeploymentFuture<string> | NamedArtifactContractAtFuture<string>, deployedContract: { address: string }, ): Promise<GetContractReturnType> { return connection.viem.getContractAt( future.contractName, ViemIgnitionHelperImpl.#ensureAddressFormat(deployedContract.address), ); } async #convertArtifactToViemContract( connection: NetworkConnection<ChainTypeT>, future: | ContractDeploymentFuture | LibraryDeploymentFuture | ContractAtFuture, deployedContract: { address: string }, ): Promise<GetContractReturnType> { const publicClient = await connection.viem.getPublicClient(); const [walletClient] = await connection.viem.getWalletClients(); if (walletClient === undefined) { throw new HardhatError( HardhatError.ERRORS.IGNITION.INTERNAL.NO_DEFAULT_VIEM_WALLET_CLIENT, ); } const contract = getContract({ address: ViemIgnitionHelperImpl.#ensureAddressFormat( deployedContract.address, ), abi: future.artifact.abi, client: { public: publicClient, wallet: walletClient, }, }); return contract; } static #ensureAddressFormat(address: string): `0x${string}` { if (!address.startsWith("0x")) { return `0x${address}`; } return `0x${address.slice(2)}`; } static #resolveStrategyConfig<StrategyT extends keyof StrategyConfig>( hardhatConfig: HardhatConfig, strategyName: StrategyT | undefined, strategyConfig: StrategyConfig[StrategyT] | undefined, ): StrategyConfig[StrategyT] | undefined { if (strategyName === undefined) { return undefined; } if (strategyConfig === undefined) { const fromHardhatConfig = hardhatConfig.ignition?.strategyConfig?.[strategyName]; return fromHardhatConfig; } return strategyConfig; } }