Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪚 Synthetic HardhatRuntimeEnvironment #31

Merged
merged 3 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ua-utils-evm-hardhat-test/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "hardhat-deploy"
import "hardhat-deploy-ethers"
import { withLayerZeroArtifacts } from "../utils-evm-hardhat/dist"
import { EndpointId } from "@layerzerolabs/lz-definitions"
import { HardhatUserConfig } from "hardhat/types"
Expand Down
1 change: 1 addition & 0 deletions packages/ua-utils-evm-hardhat-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ethers": "^5.7.0",
"hardhat": "^2.9.9",
"hardhat-deploy": "^0.11.22",
"hardhat-deploy-ethers": "^0.3.0-beta.12",
"sinon": "^17.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
Expand Down
12 changes: 5 additions & 7 deletions packages/ua-utils-evm-hardhat-test/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import hre from "hardhat"
import { expect } from "chai"
import { describe } from "mocha"
import { NetworkEnvironment, createGetNetworkEnvironment } from "../../utils-evm-hardhat/dist"
import { getNetworkRuntimeEnvironment } from "../../utils-evm-hardhat/dist"
import { HardhatRuntimeEnvironment } from "hardhat/types"

const NETWORK_NAMES = ["vengaboys", "britney"]

describe("config", () => {
NETWORK_NAMES.forEach((networkName) => {
const getEnvironment = createGetNetworkEnvironment(hre)

describe(`Network '${networkName}`, () => {
let environment: NetworkEnvironment
let environment: HardhatRuntimeEnvironment

before(async () => {
environment = await getEnvironment(networkName)
environment = await getNetworkRuntimeEnvironment(networkName)
})

it("should have an endpoint deployed", async () => {
const endpoint = await environment.getContract("EndpointV2", environment.provider)
const endpoint = await environment.ethers.getContract("EndpointV2")
const endpointId = await endpoint.eid()

expect(environment.network.config.endpointId).to.be.a("number")
Expand Down
4 changes: 3 additions & 1 deletion packages/utils-evm-hardhat/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
cache
artifacts
cache
deployments
1 change: 1 addition & 0 deletions packages/utils-evm-hardhat/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "hardhat-deploy"
import { HardhatUserConfig } from "hardhat/types"

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/utils-evm-hardhat/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use strict"

export class ConfigurationError extends Error {}
260 changes: 59 additions & 201 deletions packages/utils-evm-hardhat/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -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<TValue> = (networkName: string) => Promise<TValue>

export type GetContract = (contractName: string, signerOrProvider?: Signer | Provider) => Promise<Contract>

export type GetContractFactory = (contractName: string, signer?: Signer) => Promise<ContractFactory>

export type MinimalNetwork = Pick<Network, "name" | "config" | "provider" | "saveDeployments">

/**
* 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<EthereumProvider>`
*/
export const createGetEthereumProvider = memoize(
(hre: HardhatRuntimeEnvironment): GetByNetwork<EthereumProvider> =>
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<HardhatRuntimeEnvironment> = 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
Expand All @@ -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<MinimalNetwork>`
*/
export const createGetNetwork = memoize(
(hre: HardhatRuntimeEnvironment, getProvider = createGetEthereumProvider(hre)): GetByNetwork<MinimalNetwork> =>
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<DeploymentsExtension>`
*/
export const createGetDeployments = memoize(
(hre: HardhatRuntimeEnvironment, getNetwork = createGetNetwork(hre)): GetByNetwork<DeploymentsExtension> =>
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<GetContract>`
*/
export const createGetContract = memoize(
(hre: HardhatRuntimeEnvironment, getDeployments = createGetDeployments(hre)): GetByNetwork<GetContract> =>
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<GetContractFactory>`
*/
export const createGetContractFactory = memoize(
(hre: HardhatRuntimeEnvironment, getDeployments = createGetDeployments(hre)): GetByNetwork<GetContractFactory> =>
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<EthereumProvider>`
* @param getNetwork `GetByNetwork<MinimalNetwork>`
* @param getDeployments `GetByNetwork<DeploymentsExtension>`
* @param getContract `GetByNetwork<GetContract>`
*
* @returns `GetByNetwork<NetworkEnvironment>`
*/
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<NetworkEnvironment> =>
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),
}
})
)
Loading