diff --git a/packages/utils-evm-hardhat/.gitignore b/packages/utils-evm-hardhat/.gitignore index 5e4659675..803d4166c 100644 --- a/packages/utils-evm-hardhat/.gitignore +++ b/packages/utils-evm-hardhat/.gitignore @@ -1 +1,3 @@ -cache \ No newline at end of file +artifacts +cache +deployments \ No newline at end of file diff --git a/packages/utils-evm-hardhat/hardhat.config.ts b/packages/utils-evm-hardhat/hardhat.config.ts index fbfa518bc..62d525b25 100644 --- a/packages/utils-evm-hardhat/hardhat.config.ts +++ b/packages/utils-evm-hardhat/hardhat.config.ts @@ -1,3 +1,4 @@ +import "hardhat-deploy" import { HardhatUserConfig } from "hardhat/types" /** diff --git a/packages/utils-evm-hardhat/src/errors.ts b/packages/utils-evm-hardhat/src/errors.ts new file mode 100644 index 000000000..8118da574 --- /dev/null +++ b/packages/utils-evm-hardhat/src/errors.ts @@ -0,0 +1,3 @@ +"use strict" + +export class ConfigurationError extends Error {} diff --git a/packages/utils-evm-hardhat/src/runtime.ts b/packages/utils-evm-hardhat/src/runtime.ts index 4f36e1fc6..e6534048f 100644 --- a/packages/utils-evm-hardhat/src/runtime.ts +++ b/packages/utils-evm-hardhat/src/runtime.ts @@ -1,51 +1,76 @@ -import type { Network, HardhatRuntimeEnvironment, EthereumProvider, EIP1193Provider } from "hardhat/types" -import { DeploymentsManager } from "hardhat-deploy/dist/src/DeploymentsManager" -import { createProvider } from "hardhat/internal/core/providers/construction" +import type { HardhatRuntimeEnvironment, EIP1193Provider } from "hardhat/types" -import assert from "assert" -import memoize from "micro-memoize" import pMemoize from "p-memoize" -import { Signer } from "@ethersproject/abstract-signer" -import { Provider, JsonRpcProvider, Web3Provider } from "@ethersproject/providers" -import { Contract, ContractFactory } from "ethers" -import { DeploymentsExtension } from "hardhat-deploy/types" +import { Web3Provider } from "@ethersproject/providers" +import { ConfigurationError } from "./errors" +import { HardhatContext } from "hardhat/internal/context" +import { Environment as HardhatRuntimeEnvironmentImplementation } from "hardhat/internal/core/runtime-environment" /** * Helper type for when we need to grab something asynchronously by the network name */ export type GetByNetwork = (networkName: string) => Promise -export type GetContract = (contractName: string, signerOrProvider?: Signer | Provider) => Promise - -export type GetContractFactory = (contractName: string, signer?: Signer) => Promise - -export type MinimalNetwork = Pick - /** - * Factory function creator for providers that are not on the network - * that hardhat has been configured with. - * - * This function returns the EIP1193 provider (that hardhat uses internally) that - * needs to be wrapped for use with ethers (see `wrapEIP1193Provider`) + * Creates a clone of the HardhatRuntimeEnvironment for a particular network * * ```typescript - * const getProvider = createGetEthereumProvider(hre); - * const provider = await getProvider("bsc-testnet"); - * const ethersProvider = wrapEIP1193Provider(provider); + * const env = getEnvironment("bsc-testnet"); + * + * // All the ususal properties are present + * env.deployments.get("MyContract") * ``` * - * @param hre `HardhatRuntimeEnvironment` - * @returns `GetByNetwork` */ -export const createGetEthereumProvider = memoize( - (hre: HardhatRuntimeEnvironment): GetByNetwork => - pMemoize((networkName) => { - const networkConfig = hre.config.networks[networkName] - assert(networkConfig, `Missing network config for '${networkName}'`) - - return createProvider(hre.config, networkName, hre.artifacts) - }) -) +export const getNetworkRuntimeEnvironment: GetByNetwork = pMemoize(async (networkName) => { + // The first step is to get the hardhat context + // + // Context is registered globally as a singleton and can be accessed + // using the static methods of the HardhatContext class + // + // In our case we require the context to exist, the other option would be + // to create it and set it up - see packages/hardhat-core/src/register.ts for an example setup + let context: HardhatContext + try { + context = HardhatContext.getHardhatContext() + } catch (error: unknown) { + throw new ConfigurationError(`Could not get Hardhat context: ${error}`) + } + + // We also require the hardhat environment to already exist + // + // Again, we could create it but that means we'd need to duplicate the bootstrap code + // that hardhat does when setting up the environment + let environment: HardhatRuntimeEnvironment + try { + environment = context.getHardhatRuntimeEnvironment() + } catch (error: unknown) { + throw new ConfigurationError(`Could not get Hardhat Runtime Environment: ${error}`) + } + + try { + // The last step is to create a duplicate enviornment that mimics the original one + // with one crucial difference - the network setup + return new HardhatRuntimeEnvironmentImplementation( + environment.config, + { + ...environment.hardhatArguments, + network: networkName, + }, + environment.tasks, + environment.scopes, + context.environmentExtenders, + context.experimentalHardhatNetworkMessageTraceHooks, + environment.userConfig, + context.providerExtenders + // This is a bit annoying - the environmentExtenders are not stronly typed + // so TypeScript complains that the properties required by HardhatRuntimeEnvironment + // are not present on HardhatRuntimeEnvironmentImplementation + ) as unknown as HardhatRuntimeEnvironment + } catch (error: unknown) { + throw new ConfigurationError(`Could not setup Hardhat Runtime Environment: ${error}`) + } +}) /** * Helper function that wraps an EIP1193Provider with Web3Provider @@ -55,170 +80,3 @@ export const createGetEthereumProvider = memoize( * @returns `Web3Provider` */ export const wrapEIP1193Provider = (provider: EIP1193Provider): Web3Provider => new Web3Provider(provider) - -/** - * Factory function for trimmed-down `MinimalNetwork` objects that are not one the network - * that hardhat has been configured with. - * - * ```typescript - * const getNetwork = createGetNetwork(hre); - * const network = await getNetwork("bsc-testnet"); - * ``` - * - * @param hre `HardhatRuntimeEnvironment` - * @returns `GetByNetwork` - */ -export const createGetNetwork = memoize( - (hre: HardhatRuntimeEnvironment, getProvider = createGetEthereumProvider(hre)): GetByNetwork => - pMemoize(async (networkName) => { - const networkConfig = hre.config.networks[networkName] - const networkProvider = await getProvider(networkName) - - return { - name: networkName, - config: networkConfig, - provider: networkProvider, - saveDeployments: networkConfig.saveDeployments, - } - }) -) - -/** - * Factory function for `DeploymentsExtension` objects that are not one the network - * that hardhat has been configured with. - * - * ```typescript - * const getDeployments = createGetDeployments(hre); - * const deployments = await getDeployments("bsc-testnet"); - * const factoryDeploymentOnBscTestnet = await deployments.get("Factory"); - * ``` - * - * @param hre `HardhatRuntimeEnvironment` - * @returns `GetByNetwork` - */ -export const createGetDeployments = memoize( - (hre: HardhatRuntimeEnvironment, getNetwork = createGetNetwork(hre)): GetByNetwork => - pMemoize(async (networkName) => { - const network = await getNetwork(networkName) - - return new DeploymentsManager(hre, network as Network).deploymentsExtension - }) -) - -/** - * Factory function for `Contract` instances that are not one the network - * that hardhat has been configured with. - * - * - * ```typescript - * const getContract = createGetContract(hre); - * const getContractOnBscTestnet = await getContract("bsc-testnet") - * - * const router = await getContractOnBscTestnet("Router"); - * - * // To get a connected instance, a provider or a signer needs to be passed in - * const routerWithProvider = getContractOnBscTestnet("Router", provider) - * const routerWithSigner = getContractOnBscTestnet("Router", signer) - * ``` - * - * @param hre `HardhatRuntimeEnvironment` - * @returns `GetByNetwork` - */ -export const createGetContract = memoize( - (hre: HardhatRuntimeEnvironment, getDeployments = createGetDeployments(hre)): GetByNetwork => - pMemoize(async (networkName) => { - const deployments = await getDeployments(networkName) - - return async (contractName, signerOrProvider) => { - const { address, abi } = await deployments.get(contractName) - - return new Contract(address, abi, signerOrProvider) - } - }) -) - -/** - * Factory function for `ContractFactory` instances that are not one the network - * that hardhat has been configured with. - * - * - * ```typescript - * const getContractFactory = createGetContractFactory(hre); - * const getContractFactoryOnBscTestnet = await getContractFactory("bsc-testnet") - * - * const router = await getContractFactoryOnBscTestnet("Router"); - * - * // To get a connected instance, a signer needs to be passed in - * const routerWithSigner = getContractOnBscTestnet("Router", signer) - * ``` - * - * @param hre `HardhatRuntimeEnvironment` - * @returns `GetByNetwork` - */ -export const createGetContractFactory = memoize( - (hre: HardhatRuntimeEnvironment, getDeployments = createGetDeployments(hre)): GetByNetwork => - pMemoize(async (networkName) => { - const deployments = await getDeployments(networkName) - - return async (contractName, signer) => { - const { abi, bytecode } = await deployments.getArtifact(contractName) - - return new ContractFactory(abi, bytecode, signer) - } - }) -) - -export interface NetworkEnvironment { - network: MinimalNetwork - provider: JsonRpcProvider - deployments: DeploymentsExtension - getContract: GetContract - getContractFactory: GetContractFactory -} - -/** - * Creates a whole per-network environment for a particular network: - * - * ```typescript - * const getEnvironment = createGetNetworkEnvironment(hre); - * const environment = await getEnvironment("bsc-testnet") - * - * const provider = environment.provider - * const signer = provider.getSigner() - * const router = environment.getContract("Router") - * const routerWithProvider = environment.getContract("Router", provider) - * const routerWithSigner = environment.getContract("Router", signer) - * const factoryDeployment = await environment.deployments.get("Factory") - * ``` - * - * @param hre `HardhatRuntimeEnvironment` - * @param getProvider `GetByNetwork` - * @param getNetwork `GetByNetwork` - * @param getDeployments `GetByNetwork` - * @param getContract `GetByNetwork` - * - * @returns `GetByNetwork` - */ -export const createGetNetworkEnvironment = memoize( - ( - hre: HardhatRuntimeEnvironment, - getProvider = createGetEthereumProvider(hre), - getNetwork = createGetNetwork(hre, getProvider), - getDeployments = createGetDeployments(hre, getNetwork), - getContract = createGetContract(hre, getDeployments), - getContractFactory = createGetContractFactory(hre, getDeployments) - ): GetByNetwork => - pMemoize(async (networkName) => { - const provider = await getProvider(networkName).then(wrapEIP1193Provider) - const network = await getNetwork(networkName) - const deployments = await getDeployments(networkName) - - return { - network, - provider, - deployments, - getContract: await getContract(networkName), - getContractFactory: await getContractFactory(networkName), - } - }) -) diff --git a/packages/utils-evm-hardhat/test/runtime.test.ts b/packages/utils-evm-hardhat/test/runtime.test.ts index b28120da0..262b5ef0a 100644 --- a/packages/utils-evm-hardhat/test/runtime.test.ts +++ b/packages/utils-evm-hardhat/test/runtime.test.ts @@ -1,238 +1,50 @@ import chai from "chai" import chaiAsPromised from "chai-as-promised" -import hre from "hardhat" -import sinon from "sinon" import { expect } from "chai" -import { - createGetDeployments, - createGetNetwork, - createGetNetworkEnvironment, - createGetEthereumProvider, - wrapEIP1193Provider, -} from "../src/runtime" -import * as providersConstruction from "hardhat/internal/core/providers/construction" -import { verifyMessage } from "@ethersproject/wallet" +import { getNetworkRuntimeEnvironment } from "../src/runtime" +import { DeploymentSubmission } from "hardhat-deploy/dist/types" chai.use(chaiAsPromised) describe("runtime", () => { - let createProviderStub: sinon.SinonStub - - beforeEach(() => { - createProviderStub = sinon.stub(providersConstruction, "createProvider") - - // We want to clear the memoization cache before running the test suite - createGetEthereumProvider.cache.keys.length = 0 - createGetEthereumProvider.cache.values.length = 0 - createGetNetwork.cache.keys.length = 0 - createGetNetwork.cache.values.length = 0 - createGetNetworkEnvironment.cache.keys.length = 0 - createGetNetworkEnvironment.cache.values.length = 0 - }) - - afterEach(() => { - createProviderStub.restore() - }) - - describe("createGetEthereumProvider", () => { - it("should reject if network does not exist", async () => { - const getProvider = createGetEthereumProvider(hre) - - await expect(getProvider("not-existent-in-hardhat-config")).to.eventually.be.rejected - }) - - it("should reject if createProvider rejects", async () => { - const error = new Error("oh no oh no") - createProviderStub.rejects(error) - - const getProvider = createGetEthereumProvider(hre) - - await expect(getProvider("ethereum-mainnet")).to.eventually.be.rejectedWith(error) - }) - - it("should resolve with provider", async () => { - createProviderStub.restore() - - const getProvider = createGetEthereumProvider(hre) - const provider = await getProvider("ethereum-mainnet") - - expect(provider).not.to.be.undefined - }) - - it("should cache the result", async () => { - createProviderStub.restore() - - const getProvider = createGetEthereumProvider(hre) - const provider1 = await getProvider("ethereum-mainnet") - const provider2 = await getProvider("ethereum-mainnet") - const provider3 = await getProvider("bsc-testnet") - const provider4 = await getProvider("bsc-testnet") - - expect(provider1).to.equal(provider2) - expect(provider3).to.equal(provider4) - expect(provider1).not.to.equal(provider4) - }) - - it("should cache the factory", () => { - expect(createGetEthereumProvider(hre)).to.eql(createGetEthereumProvider(hre)) - }) - - describe("getSigner()", () => { - it("should sign a transaction", async () => { - createProviderStub.restore() - - const getProvider = createGetEthereumProvider(hre) - const provider = await getProvider("bsc-testnet").then(wrapEIP1193Provider) - const signer = provider.getSigner() - - const message = "hello" - const signature = await signer.signMessage(message) - const address = verifyMessage(message, signature) - - expect(address).to.equal(await signer.getAddress()) - }) - - it("should throw an error if there is no mnemonic", async () => { - createProviderStub.restore() - - const getProvider = createGetEthereumProvider(hre) - const provider = await getProvider("ethereum-mainnet").then(wrapEIP1193Provider) - const signer = provider.getSigner() - - await expect(signer.signMessage("hello")).to.eventually.be.rejected - }) - }) - }) - - describe("createGetNetwork", () => { - it("should reject if the network is not defined in hardhat config", async () => { - const getNetwork = createGetNetwork(hre) - - await expect(getNetwork("not-existent-in-hardhat-config")).to.eventually.be.rejected - }) - - it("should reject if createProvider rejects", async () => { - const error = new Error("oh no oh no") - createProviderStub.rejects(error) - - const getNetwork = createGetNetwork(hre) - - await expect(getNetwork("ethereum-mainnet")).to.eventually.be.rejectedWith(error) - }) - - it("should resolve with network", async () => { - const getProvider = createGetEthereumProvider(hre) - const getNetwork = createGetNetwork(hre) - - const provider = await getProvider("ethereum-mainnet") - const network = await getNetwork("ethereum-mainnet") - - expect(network).to.eql({ - name: "ethereum-mainnet", - config: hre.config.networks["ethereum-mainnet"], - provider, - saveDeployments: true, - }) - }) - - it("should cache the result", async () => { - createProviderStub.restore() - - const getNetwork = createGetNetwork(hre) - const network1 = await getNetwork("ethereum-mainnet") - const network2 = await getNetwork("ethereum-mainnet") - const network3 = await getNetwork("bsc-testnet") - const network4 = await getNetwork("bsc-testnet") - - expect(network1).to.equal(network2) - expect(network3).to.equal(network4) - expect(network1).not.to.equal(network4) - }) - - it("should cache the factory", () => { - expect(createGetNetwork(hre)).to.eql(createGetNetwork(hre)) - }) - }) - - describe("createGetDeployments", () => { - it("should reject if the network is not defined in hardhat config", async () => { - const getDeployments = createGetDeployments(hre) - - await expect(getDeployments("not-existent-in-hardhat-config")).to.eventually.be.rejected - }) - - it("should reject if createProvider rejects", async () => { - const error = new Error("oh no oh no") - createProviderStub.rejects(error) - - const getDeployments = createGetDeployments(hre) - - await expect(getDeployments("ethereum-mainnet")).to.eventually.be.rejectedWith(error) - }) - - it("should resolve with network", async () => { - const getDeployments = createGetDeployments(hre) - const deployments = await getDeployments("ethereum-mainnet") - - expect(deployments).to.have.property("get") - expect(deployments).to.have.property("getOrNull") - expect(deployments).to.have.property("save") - }) - - it("should cache the factory", () => { - expect(createGetDeployments(hre)).to.eql(createGetDeployments(hre)) - }) - }) - - describe("createGetNetworkEnvironment", () => { - beforeEach(() => { - createProviderStub.restore() + describe("getNetworkRuntimeEnvironment", () => { + it("should reject with an invalid network", async () => { + await expect(getNetworkRuntimeEnvironment("not-in-hardhat-config")).to.eventually.be.rejected }) - it("should reject if the network is not defined in hardhat config", async () => { - const getNetworkEnvironment = createGetNetworkEnvironment(hre) + it("should return a HardhatRuntimeEnvironment with correct network", async () => { + const runtime = await getNetworkRuntimeEnvironment("ethereum-mainnet") - await expect(getNetworkEnvironment("not-existent-in-hardhat-config")).to.eventually.be.rejected + expect(runtime.network.name).to.eql("ethereum-mainnet") + expect(runtime.deployments).to.be.an("object") }) - it("should reject if createProvider rejects", async () => { - const error = new Error("oh no oh no") - const mockGetProvider = sinon.stub().throws(error) + it("should have the config setup correctly", async () => { + const ethRuntime = await getNetworkRuntimeEnvironment("ethereum-mainnet") + const bscRuntime = await getNetworkRuntimeEnvironment("bsc-testnet") - const getNetworkEnvironment = createGetNetworkEnvironment(hre, mockGetProvider) - - await expect(getNetworkEnvironment("ethereum-mainnet")).to.eventually.be.rejectedWith(error) - }) - - it("should reject if createNetwork rejects", async () => { - const error = new Error("oh no oh no") - const mockGetNetwork = sinon.stub().throws(error) - - const getNetworkEnvironment = createGetNetworkEnvironment(hre, undefined, mockGetNetwork) - - await expect(getNetworkEnvironment("ethereum-mainnet")).to.eventually.be.rejectedWith(error) + expect(ethRuntime.network.config.saveDeployments).to.be.true + expect(bscRuntime.network.config.saveDeployments).to.be.undefined }) - it("should reject if createDeployments rejects", async () => { - const error = new Error("oh no oh no") - const mockGetDeployments = sinon.stub().throws(error) + it("should save the deployment to correct network", async () => { + const bscRuntime = await getNetworkRuntimeEnvironment("bsc-testnet") + const ethRuntime = await getNetworkRuntimeEnvironment("ethereum-mainnet") + const now = Date.now() + const deploymentSubmission = { + args: ["bsc-testnet", now], + } as DeploymentSubmission - const getNetworkEnvironment = createGetNetworkEnvironment(hre, undefined, undefined, mockGetDeployments) + // First we want to save the deployment for bsc-testnet + await bscRuntime.deployments.save("Mock", deploymentSubmission) - await expect(getNetworkEnvironment("ethereum-mainnet")).to.eventually.be.rejectedWith(error) - }) - - it("should resolve with network environment", async () => { - const getNetworkEnvironment = createGetNetworkEnvironment(hre) - const env = await getNetworkEnvironment("ethereum-mainnet") - - expect(env).to.have.property("network") - expect(env).to.have.property("deployments") - expect(env).to.have.property("provider") - }) + // Then we check whether it has been saved + const deployment = await bscRuntime.deployments.get("Mock") + expect(deployment.args).to.eql(deploymentSubmission.args) - it("should cache the factory", () => { - expect(createGetNetworkEnvironment(hre)).to.eql(createGetNetworkEnvironment(hre)) + // And finally we check whether it was not by accident saved for ethereum-mainnet + const nonExistentDeployment = await ethRuntime.deployments.getOrNull("Mock") + expect(nonExistentDeployment?.args).not.to.eql(deploymentSubmission.args) }) }) })