diff --git a/packages/hardhat-utils/package.json b/packages/hardhat-utils/package.json index e9a45ed48d..0e2d3982c8 100644 --- a/packages/hardhat-utils/package.json +++ b/packages/hardhat-utils/package.json @@ -32,6 +32,7 @@ "@ethersproject/providers": "^5.7.0", "@ethersproject/wallet": "^5.7.0", "@layerzerolabs/lz-definitions": "~1.5.58", + "@layerzerolabs/lz-evm-sdk-v1": "~1.5.58", "@types/chai-as-promised": "^7.1.7", "chai": "^4.3.10", "chai-as-promised": "^7.1.1", diff --git a/packages/hardhat-utils/src/config.ts b/packages/hardhat-utils/src/config.ts index 9477388ff6..934d89aa65 100644 --- a/packages/hardhat-utils/src/config.ts +++ b/packages/hardhat-utils/src/config.ts @@ -1,126 +1,105 @@ import "hardhat-deploy/dist/src/type-extensions" -import { chainAndStageToNetwork, networkToStage, Chain, Stage } from "@layerzerolabs/lz-definitions" -import { HardhatUserConfig, HttpNetworkAccountsUserConfig, NetworksConfig } from "hardhat/types" -import { join } from "path" - -export const getMnemonic = (networkName: string): string | undefined => - process.env[`MNEMONIC_${networkName}`] || - process.env[`MNEMONIC_${networkName.toUpperCase()}`] || - process.env[`MNEMONIC_${networkName.toUpperCase().replace(/-/g, "_")}`] || - process.env.MNEMONIC - -export const getAccounts = (networkName: string): HttpNetworkAccountsUserConfig => { - const mnemonic = getMnemonic(networkName) - if (mnemonic == null) return [] - - return { mnemonic } -} +import { endpointIdToNetwork } from "@layerzerolabs/lz-definitions" +import { HardhatUserConfig } from "hardhat/types" +import { join, dirname } from "path" +import { createNetworkLogger } from "./logger" /** - * Adds external deployments directories for all configured networks. + * Helper utility that adds external deployment paths for all LayzerZero enabled networks. + * This will make LayerZero contracts available in your deploy scripts and tasks. * - * This function takes the root `deploymentsDir` path to the deployments folder - * and maps all configured networks to point to the network directories under this root path. + * ``` + * // hardhat.config.ts + * import { EndpointId } from "@layerzerolabs/lz-definitions" * - * ```typescript - * const config = { + * const config: HardhatUserConfig = { * networks: { - * "slipknot-testnet": {} - * } - * } - * - * const configWithExternals = withExternalDeployments("./path/to/some/deployments/folder")(config); - * - * // The configWithExternals will now look like this: - * - * { - * external: { - * deployments: { - * "slipknot-testnet": ["./path/to/some/deployments/folder/slipknot-testnet"] + * arbitrum: { + * endpointId: EndpointId.ARBITRUM_MAINNET + * }, + * fuji: { + * endpointId: EndpointId.AVALANCHE_TESTNET * } - * }, - * networks: { - * "slipknot-testnet": {} * } * } + * + * export default withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1") * ``` * - * @param deploymentsDir Path to the external deployments directory + * @param packageNames `string[]` List of @layerzerolabs package names that contain deployments directory * - * @returns `HardhatUserConfig` + * @returns `(config: THardhatUserConfig): THardhatUserConfig` Hardhat config decorator */ -export const withExternalDeployments = - (deploymentsDir: string) => - (config: THardhatUserConfig): THardhatUserConfig => ({ +export const withLayerZeroDeployments = (...packageNames: string[]) => { + // The first thing we do is we resolve the paths to LayerZero packages + const resolvedDeploymentsDirectories = packageNames + // The tricky bit here is the fact that if we resolve packages by their package name, + // we might be pointed to a file in some dist directory - node will just pick up the `main` + // entry in package.json and point us there + // + // So in order to get a stable path we choose package.json, pretty solid choice + .map((packageName) => join(packageName, "package.json")) + // We now resolve the path to package.json + .map((packageJsonPath) => require.resolve(packageJsonPath)) + // Take its directory + .map(dirname) + // And navigate to the deployments folder + .map((resolvedPackagePath) => join(resolvedPackagePath, "deployments")) + + // We return a function that will enrich hardhat config with the external deployments configuration + // + // This is a pretty standard way of enriching configuration files that leads to quite nice consumer code + return (config: THardhatUserConfig): THardhatUserConfig => ({ ...config, external: { ...config.external, + // Now for the meat of the operation, we'll enrich the external.deployments object deployments: Object.fromEntries( - // Map the configured networks into entries for the external deployments object - Object.keys(config.networks ?? {}).map((networkName: string) => { - return [ - // The external deployments object is keyed by network names - networkName, - // And its values are arrays of filesystem paths referring to individual network deployment directories - Array.from( - // Since we want the paths to be unique, we'll put everything we have into a Set, then convert back to array - new Set( - // These are the external deployments already configured - config.external?.deployments?.[networkName] ?? [] - ).add( - // And we're going to add a new one by concatenating the root deployments directory with the network name - join(deploymentsDir, networkName) - ) - ), - ] + Object.entries(config.networks ?? {}).flatMap(([networkName, networkConfig]) => { + const endpointId = networkConfig?.endpointId + const networkLogger = createNetworkLogger(networkName) + + // Let's first check whether endpointId is defined on the network config + if (endpointId == null) { + networkLogger.debug("Endpoint ID not specified in hardhat config, skipping external deployment configuration") + + return [] + } + + try { + // This operation is unsafe and can throw - let's make sure we don't explode with some unreadable error + const layerZeroNetworkName = endpointIdToNetwork(endpointId) + const layerZeroNetworkDeploymentsDirectories = resolvedDeploymentsDirectories.map((deploymentsDirectory) => + join(deploymentsDirectory, layerZeroNetworkName) + ) + + return [ + [ + // The external deployments object is keyed by local network names + // which do not necessarily match the LayerZero ones + networkName, + // And its values are arrays of filesystem paths referring to individual network deployment directories + Array.from( + // Since we want the paths to be unique, we'll put everything we have into a Set, then convert back to array + new Set([ + // These are the external deployments already configured + ...(config.external?.deployments?.[networkName] ?? []), + // And these are the new ones + ...layerZeroNetworkDeploymentsDirectories, + ]) + ), + ], + ] + } catch (error) { + networkLogger.error( + `Invalid endpoint ID specified in hardhat config (${endpointId}), skipping external deployment configuration` + ) + + return [] + } }) ), }, }) - -/** - * Helper utility that takes in an array of Chain identifiers - * and maps them to network names. - * - * If there are no chains defined, the defaults are supplied from the network config - * - * @param config `NetworksConfig` - * @param stage `Stage` - * - * @returns `(chains: Chain[] | null | undefined) => string[]` - */ -export const createGetDefinedNetworkNamesOnStage = - (config: NetworksConfig) => - (stage: Stage, chains: Chain[] | null | undefined): string[] => { - const definedNetworks = Object.keys(config).sort() - const definedNetworksSet = new Set(definedNetworks) - - return ( - chains - // We map the chains (e.g. bsc, avalanche) to network names (e.g. bsc-testnet) - ?.map((chain: Chain) => chainAndStageToNetwork(chain, stage)) - // Filter out networks that have not been defined in the config - // (since we just created them with the correct stage, we don't need to filter by stage) - .filter((networkName) => definedNetworksSet.has(networkName)) ?? - // But if we nothing has been provided, we take all the networks from hardhat config - definedNetworks - // And filter out the networks for this stage (since we know all of thse have been defined) - .filter(isNetworkOnStage(stage)) - ) - } - -/** - * Helper utility that safely calls networkToStage - * to determine whether a network name is on stage - * - * @param stage `Stage` - * @returns `true` if network is a valid network name and is on stage, `false` otherwise - */ -const isNetworkOnStage = (stage: Stage) => (networkName: string) => { - try { - return networkToStage(networkName) === stage - } catch { - return false - } } diff --git a/packages/hardhat-utils/src/index.ts b/packages/hardhat-utils/src/index.ts index 5711bd0275..1dea7cf150 100644 --- a/packages/hardhat-utils/src/index.ts +++ b/packages/hardhat-utils/src/index.ts @@ -1,3 +1,7 @@ +// Hardhat type augmentation +import "./type-extensions" + +// Regular exports export * from "./config" export * from "./logger" export * from "./runtime" diff --git a/packages/hardhat-utils/src/type-extensions.ts b/packages/hardhat-utils/src/type-extensions.ts new file mode 100644 index 0000000000..125dbb9d8d --- /dev/null +++ b/packages/hardhat-utils/src/type-extensions.ts @@ -0,0 +1,19 @@ +import { EndpointId } from "@layerzerolabs/lz-definitions" + +declare module "hardhat/types/config" { + interface HardhatNetworkUserConfig { + endpointId?: never + } + + interface HardhatNetworkConfig { + endpointId?: never + } + + interface HttpNetworkUserConfig { + endpointId?: EndpointId + } + + interface HttpNetworkConfig { + endpointId?: EndpointId + } +} diff --git a/packages/hardhat-utils/test/config.test.ts b/packages/hardhat-utils/test/config.test.ts index 0bfe4f4a90..84c75ab221 100644 --- a/packages/hardhat-utils/test/config.test.ts +++ b/packages/hardhat-utils/test/config.test.ts @@ -1,57 +1,79 @@ -import { Chain, Stage } from "@layerzerolabs/lz-definitions" -import hre from "hardhat" +import { EndpointId } from "@layerzerolabs/lz-definitions" import { expect } from "chai" import { describe } from "mocha" -import { createGetDefinedNetworkNamesOnStage, withExternalDeployments } from "../src/config" +import { withLayerZeroDeployments } from "../src/config" +import { dirname, join } from "path" describe("config", () => { describe("withExternalDeployments()", () => { + const resolvedLzEvmSdkPackageJson = dirname(require.resolve(join("@layerzerolabs/lz-evm-sdk-v1", "package.json"))) + it("should add no external deployments if no networks have been specified", () => { const config = {} - expect(withExternalDeployments("some/path")(config)).to.eql({ + expect(withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1")(config)).to.eql({ external: { deployments: {}, }, }) }) - it("should add external deployments for all networks", () => { + it("should not add external deployments for networks without endpointId", () => { const config = { networks: { "vengaboys-testnet": {}, }, } - expect(withExternalDeployments("some/path")(config)).to.eql({ + expect(withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1")(config)).to.eql({ networks: { "vengaboys-testnet": {}, }, external: { - deployments: { - "vengaboys-testnet": ["some/path/vengaboys-testnet"], + deployments: {}, + }, + }) + }) + + it("should not add external deployments for networks with invalid endpointId", () => { + const config = { + networks: { + "vengaboys-testnet": { + endpointId: 0, }, }, + } + + expect(withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1")(config)).to.eql({ + networks: { + "vengaboys-testnet": { + endpointId: 0, + }, + }, + external: { + deployments: {}, + }, }) }) it("should append external deployments for all networks", () => { const config = { networks: { - "vengaboys-testnet": {}, + "vengaboys-testnet": { + endpointId: EndpointId.ARBITRUM_MAINNET, + }, }, } - const configWithSomePath = withExternalDeployments("some/path")(config) - const configWithSomeOtherPath = withExternalDeployments("some/other/path")(configWithSomePath) - - expect(configWithSomeOtherPath).to.eql({ + expect(withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1")(config)).to.eql({ networks: { - "vengaboys-testnet": {}, + "vengaboys-testnet": { + endpointId: EndpointId.ARBITRUM_MAINNET, + }, }, external: { deployments: { - "vengaboys-testnet": ["some/path/vengaboys-testnet", "some/other/path/vengaboys-testnet"], + "vengaboys-testnet": [join(resolvedLzEvmSdkPackageJson, "deployments", "arbitrum-mainnet")], }, }, }) @@ -60,45 +82,27 @@ describe("config", () => { it("should not append duplicate external deployments for all networks", () => { const config = { networks: { - "vengaboys-testnet": {}, + "vengaboys-testnet": { + endpointId: EndpointId.BSC_TESTNET, + }, }, } - const configWithSomePath = withExternalDeployments("some/path")(config) - const configWithSomeOtherPath = withExternalDeployments("some/other/path")(configWithSomePath) - const configWithSomePathAgain = withExternalDeployments("some/path")(configWithSomeOtherPath) + const configWithSomePath = withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1", "@layerzerolabs/lz-evm-sdk-v1")(config) + const configWithSomePathAgain = withLayerZeroDeployments("@layerzerolabs/lz-evm-sdk-v1")(configWithSomePath) expect(configWithSomePathAgain).to.eql({ networks: { - "vengaboys-testnet": {}, + "vengaboys-testnet": { + endpointId: EndpointId.BSC_TESTNET, + }, }, external: { deployments: { - "vengaboys-testnet": ["some/path/vengaboys-testnet", "some/other/path/vengaboys-testnet"], + "vengaboys-testnet": [join(resolvedLzEvmSdkPackageJson, "deployments", "bsc-testnet")], }, }, }) }) }) - - describe("createGetDefinedNetworkNamesOnStage()", () => { - const getNetworkNames = createGetDefinedNetworkNamesOnStage(hre.config.networks) - - it("should return all network names on the stage if called with null/undefined", () => { - expect(getNetworkNames(Stage.TESTNET, null)).to.eql(["bsc-testnet"]) - expect(getNetworkNames(Stage.TESTNET, undefined)).to.eql(["bsc-testnet"]) - expect(getNetworkNames(Stage.MAINNET, null)).to.eql(["ethereum-mainnet"]) - expect(getNetworkNames(Stage.MAINNET, undefined)).to.eql(["ethereum-mainnet"]) - }) - - it("should return an empty array if called with an empty array", () => { - expect(getNetworkNames(Stage.TESTNET, [])).to.eql([]) - expect(getNetworkNames(Stage.MAINNET, [])).to.eql([]) - }) - - it("should return an array of defined networks if called with an non-empty array", () => { - expect(getNetworkNames(Stage.TESTNET, [Chain.BSC, Chain.AAVEGOTCHI, Chain.ETHEREUM])).to.eql(["bsc-testnet"]) - expect(getNetworkNames(Stage.MAINNET, [Chain.BSC, Chain.AAVEGOTCHI, Chain.ETHEREUM])).to.eql(["ethereum-mainnet"]) - }) - }) }) diff --git a/yarn.lock b/yarn.lock index 8fac7a2870..35d57096d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -925,6 +925,15 @@ dependencies: tiny-invariant "^1.3.1" +"@layerzerolabs/lz-evm-sdk-v1@~1.5.58": + version "1.5.59" + resolved "https://registry.yarnpkg.com/@layerzerolabs/lz-evm-sdk-v1/-/lz-evm-sdk-v1-1.5.59.tgz#f10587548cbba39f1ab69dde4f6c5263bc7310b7" + integrity sha512-m7WiaaslH9QitgSOiXygcGGnINTJgW4rpI7BBxS9jDkuBf7om3chuhJPysxS4TztWC8U/11HrfcpxHdfkkJP3g== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/providers" "^5.7.2" + ethers "^5.7.2" + "@layerzerolabs/lz-sdk@0.0.12": version "0.0.12" resolved "https://registry.npmjs.org/@layerzerolabs/lz-sdk/-/lz-sdk-0.0.12.tgz#b41b5b266d41f89c0a720de48581e0a8910180a8"