Skip to content

Commit

Permalink
🎁 External deployments in hardhat config (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Nov 10, 2023
1 parent f2a7e4e commit 982172d
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 145 deletions.
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

0 comments on commit 982172d

Please sign in to comment.