Skip to content
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/hardhat-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
185 changes: 82 additions & 103 deletions packages/hardhat-utils/src/config.ts
Original file line number Diff line number Diff line change
@@ -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 `<THardhatUserConfig extends HardhatUserConfig>(config: THardhatUserConfig): THardhatUserConfig` Hardhat config decorator
*/
export const withExternalDeployments =
(deploymentsDir: string) =>
<THardhatUserConfig extends HardhatUserConfig>(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 <THardhatUserConfig extends HardhatUserConfig>(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
}
}
4 changes: 4 additions & 0 deletions packages/hardhat-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Hardhat type augmentation
import "./type-extensions"

// Regular exports
export * from "./config"
export * from "./logger"
export * from "./runtime"
Expand Down
19 changes: 19 additions & 0 deletions packages/hardhat-utils/src/type-extensions.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
88 changes: 46 additions & 42 deletions packages/hardhat-utils/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -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")],
},
},
})
Expand All @@ -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"])
})
})
})
Loading