Skip to content

Commit

Permalink
Support private networks and forked networks (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericglau authored Mar 11, 2024
1 parent 4606f76 commit d4c7cef
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 47 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.0.1-alpha.5 (2024-03-11)

- Support private networks and forked networks. ([#7](https://github.com/OpenZeppelin/defender-deploy-client-cli/pull/7))

## 0.0.1-alpha.4 (2024-02-14)

- Add commands to get approval process information. ([#4](https://github.com/OpenZeppelin/defender-deploy-client-cli/pull/4))
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ DEFENDER_KEY=<Your API key>
DEFENDER_SECRET<Your API secret>
```

## Network Selection

The network that is used with OpenZeppelin Defender is determined by the `chainId` parameter in the below commands.
If you want to ensure that a specific network is used with Defender, set the `DEFENDER_NETWORK` environment variable, for example:
```
DEFENDER_NETWORK=my-mainnet-fork
```
If set, this must be the name of a public, private or forked network in Defender. If the `chainId` parameter corresponds to a different network while this is set, the deployment will not occur and will throw an error instead.

> **Note**
> This is required if you have multiple forked networks in Defender with the same chainId, in which case the one with name matching the `DEFENDER_NETWORK` environment variable will be used.

## Usage

```
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/defender-deploy-client-cli",
"version": "0.0.1-alpha.4",
"version": "0.0.1-alpha.5",
"description": "CLI for deployments using OpenZeppelin Defender SDK",
"repository": "https://github.com/OpenZeppelin/defender-deploy-client-cli",
"license": "MIT",
Expand All @@ -24,8 +24,9 @@
},
"dependencies": {
"minimist": "^1.2.8",
"@openzeppelin/defender-sdk-deploy-client": "^1.9.0",
"@openzeppelin/defender-sdk-base-client": "^1.9.0",
"@openzeppelin/defender-sdk-deploy-client": "^1.10.0",
"@openzeppelin/defender-sdk-base-client": "^1.10.0",
"@openzeppelin/defender-sdk-network-client": "^1.10.0",
"dotenv": "^16.3.1"
}
}
9 changes: 5 additions & 4 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FunctionArgs, deployContract } from '../internal/deploy-contract';
import { getDeployClient } from '../internal/client';
import { USAGE_COMMAND_PREFIX, getAndValidateString, getNetwork } from '../internal/utils';
import { DeployClient } from '@openzeppelin/defender-sdk-deploy-client';
import { NetworkClient } from '@openzeppelin/defender-sdk-network-client';

const USAGE = `${USAGE_COMMAND_PREFIX} deploy --contractName <CONTRACT_NAME> --contractPath <CONTRACT_PATH> --chainId <CHAIN_ID> --buildInfoFile <BUILD_INFO_FILE_PATH> [--constructorBytecode <CONSTRUCTOR_ARGS>] [--licenseType <LICENSE>] [--verifySourceCode <true|false>] [--relayerId <RELAYER_ID>] [--salt <SALT>] [--createFactoryAddress <CREATE_FACTORY_ADDRESS>]`;
const DETAILS = `
Expand All @@ -23,11 +24,11 @@ Additional options:
--createFactoryAddress <CREATE_FACTORY_ADDRESS> Address of the CREATE2 factory to use for deployment. Defaults to the factory provided by Defender.
`;

export async function deploy(args: string[], deployClient?: DeployClient): Promise<void> {
export async function deploy(args: string[], deployClient?: DeployClient, networkClient?: NetworkClient): Promise<void> {
const { parsedArgs, extraArgs } = parseArgs(args);

if (!help(parsedArgs)) {
const functionArgs = getFunctionArgs(parsedArgs, extraArgs);
const functionArgs = await getFunctionArgs(parsedArgs, extraArgs, networkClient);
const client = deployClient ?? getDeployClient();
const address = await deployContract(functionArgs, client);

Expand Down Expand Up @@ -64,7 +65,7 @@ function help(parsedArgs: minimist.ParsedArgs): boolean {
* @returns Function arguments
* @throws Error if any arguments or options are invalid.
*/
function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): FunctionArgs {
async function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[], networkClient?: NetworkClient): Promise<FunctionArgs> {
if (extraArgs.length !== 0) {
throw new Error('The deploy command does not take any arguments, only options.');
} else {
Expand All @@ -73,7 +74,7 @@ function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]):
const contractPath = getAndValidateString(parsedArgs, 'contractPath', true)!;

const networkString = getAndValidateString(parsedArgs, 'chainId', true)!;
const network = getNetwork(parseInt(networkString));
const network = await getNetwork(parseInt(networkString), networkClient);

const buildInfoFile = getAndValidateString(parsedArgs, 'buildInfoFile', true)!;

Expand Down
9 changes: 5 additions & 4 deletions src/commands/get-approval-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getDeployClient } from '../internal/client';
import { USAGE_COMMAND_PREFIX, getAndValidateString, getNetwork } from '../internal/utils';
import { ApprovalProcessResponse, DeployClient } from '@openzeppelin/defender-sdk-deploy-client';
import { Network } from '@openzeppelin/defender-sdk-base-client';
import { NetworkClient } from '@openzeppelin/defender-sdk-network-client';

const USAGE_DEPLOY = `${USAGE_COMMAND_PREFIX} getDeployApprovalProcess --chainId <CHAIN_ID>`;
const DETAILS_DEPLOY = `
Expand All @@ -23,11 +24,11 @@ Required options:

export type Command = 'getDeployApprovalProcess' | 'getUpgradeApprovalProcess';

export async function getApprovalProcess(command: Command, args: string[], deployClient?: DeployClient): Promise<void> {
export async function getApprovalProcess(command: Command, args: string[], deployClient?: DeployClient, networkClient?: NetworkClient): Promise<void> {
const { parsedArgs, extraArgs } = parseArgs(args);

if (!help(command, parsedArgs)) {
const network = getFunctionArgs(command, parsedArgs, extraArgs);
const network = await getFunctionArgs(command, parsedArgs, extraArgs, networkClient);
const client = deployClient ?? getDeployClient();

let response: ApprovalProcessResponse;
Expand Down Expand Up @@ -89,12 +90,12 @@ function help(command: Command, parsedArgs: minimist.ParsedArgs): boolean {
* @returns Function arguments
* @throws Error if any arguments or options are invalid.
*/
export function getFunctionArgs(command: Command, parsedArgs: minimist.ParsedArgs, extraArgs: string[]): Network {
export async function getFunctionArgs(command: Command, parsedArgs: minimist.ParsedArgs, extraArgs: string[], networkClient?: NetworkClient): Promise<string> {
if (extraArgs.length !== 0) {
throw new Error(`The ${command} command does not take any arguments, only options.`);
} else {
const networkString = getAndValidateString(parsedArgs, 'chainId', true)!;
const network = getNetwork(parseInt(networkString));
const network = await getNetwork(parseInt(networkString), networkClient);

checkInvalidArgs(parsedArgs);

Expand Down
9 changes: 5 additions & 4 deletions src/commands/propose-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FunctionArgs, upgradeContract } from '../internal/upgrade-contract';
import { getDeployClient } from '../internal/client';
import { USAGE_COMMAND_PREFIX, getAndValidateString, getNetwork } from '../internal/utils';
import { DeployClient } from '@openzeppelin/defender-sdk-deploy-client';
import { NetworkClient } from '@openzeppelin/defender-sdk-network-client';

const USAGE = `${USAGE_COMMAND_PREFIX} proposeUpgrade --proxyAddress <PROXY_ADDRESS> --newImplementationAddress <NEW_IMPLEMENTATION_ADDRESS> --chainId <CHAIN_ID> [--proxyAdminAddress <PROXY_ADMIN_ADDRESS>] [--contractArtifactFile <CONTRACT_ARTIFACT_FILE_PATH>] [--approvalProcessId <UPGRADE_APPROVAL_PROCESS_ID>]`;
const DETAILS = `
Expand All @@ -19,11 +20,11 @@ Additional options:
--approvalProcessId <UPGRADE_APPROVAL_PROCESS_ID> The ID of the upgrade approval process. Defaults to the upgrade approval process configured for your deployment environment on Defender.
`;

export async function proposeUpgrade(args: string[], deployClient?: DeployClient): Promise<void> {
export async function proposeUpgrade(args: string[], deployClient?: DeployClient, networkClient?: NetworkClient): Promise<void> {
const { parsedArgs, extraArgs } = parseArgs(args);

if (!help(parsedArgs)) {
const functionArgs = getFunctionArgs(parsedArgs, extraArgs);
const functionArgs = await getFunctionArgs(parsedArgs, extraArgs, networkClient);
const client = deployClient ?? getDeployClient();
const upgradeResponse = await upgradeContract(functionArgs, client);

Expand Down Expand Up @@ -62,7 +63,7 @@ function help(parsedArgs: minimist.ParsedArgs): boolean {
* @returns Function arguments
* @throws Error if any arguments or options are invalid.
*/
function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): FunctionArgs {
async function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[], networkClient?: NetworkClient): Promise<FunctionArgs> {
if (extraArgs.length !== 0) {
throw new Error('The proposeUpgrade command does not take any arguments, only options.');
} else {
Expand All @@ -71,7 +72,7 @@ function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]):
const newImplementationAddress = getAndValidateString(parsedArgs, 'newImplementationAddress', true)!;

const networkString = getAndValidateString(parsedArgs, 'chainId', true)!;
const network = getNetwork(parseInt(networkString));
const network = await getNetwork(parseInt(networkString), networkClient);

// Additional options
const proxyAdminAddress = getAndValidateString(parsedArgs, 'proxyAdminAddress');
Expand Down
11 changes: 10 additions & 1 deletion src/internal/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { DeployClient } from "@openzeppelin/defender-sdk-deploy-client";
import { NetworkClient } from "@openzeppelin/defender-sdk-network-client";

export function getNetworkClient(): NetworkClient {
return new NetworkClient(getDefenderApiKey());
}

export function getDeployClient(): DeployClient {
return new DeployClient(getDefenderApiKey());
}

function getDefenderApiKey() {
require('dotenv').config();
const apiKey = process.env.DEFENDER_KEY as string;
const apiSecret = process.env.DEFENDER_SECRET as string;
Expand All @@ -9,5 +18,5 @@ export function getDeployClient(): DeployClient {
throw new Error('DEFENDER_KEY and DEFENDER_SECRET must be set in environment variables.');
}

return new DeployClient({ apiKey, apiSecret });
return { apiKey, apiSecret };
}
9 changes: 9 additions & 0 deletions src/internal/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function noDetails() {
return '';
}

export class ErrorWithDetails extends Error {
constructor(message: string, details = noDetails) {
super(message + '\n\n' + details());
}
}
81 changes: 75 additions & 6 deletions src/internal/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import minimist from "minimist";
import { Network, fromChainId } from "@openzeppelin/defender-sdk-base-client";
import { fromChainId } from "@openzeppelin/defender-sdk-base-client";
import { getNetworkClient } from "./client";
import { NetworkClient } from "@openzeppelin/defender-sdk-network-client";
import { ErrorWithDetails } from "./error";

export function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: string, required = false): string | undefined {
const value = parsedArgs[option];
Expand All @@ -11,12 +14,78 @@ export function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: st
return value;
}

export function getNetwork(chainId: number): Network {
const network = fromChainId(chainId);
if (network === undefined) {
throw new Error(`Network ${chainId} is not supported by OpenZeppelin Defender`);
/**
* Gets the network name for the given chainId.
*
* @param chainId Chain ID
* @param networkClient Overrides the default network client. For testing only.
* @param requireNetwork Overrides the DEFENDER_NETWORK environment variable. For testing only.
* @returns Network name
*/
export async function getNetwork(chainId: number, networkClient?: NetworkClient, requireNetwork?: string): Promise<string> {
const networkNames = await getNetworkNames(chainId, networkClient);

require('dotenv').config();
const userConfigNetwork = requireNetwork ?? process.env.DEFENDER_NETWORK as string;

if (networkNames.length === 0) {
throw new ErrorWithDetails(
`The current network with chainId ${chainId} is not supported by OpenZeppelin Defender`,
() => `If this is a private or forked network, add it in Defender from the Manage tab.`,
);
} else if (networkNames.length === 1) {
const network = networkNames[0];
if (userConfigNetwork !== undefined && network !== userConfigNetwork) {
throw new ErrorWithDetails(
`Detected network ${network} does not match specified network: ${userConfigNetwork}`,
() =>
`The current chainId ${chainId} is detected as ${network} on OpenZeppelin Defender, but the DEFENDER_NETWORK environment variable specifies network: ${userConfigNetwork}.\nEnsure you are connected to the correct network.`,
);
}
return network;
} else {
if (userConfigNetwork === undefined) {
throw new ErrorWithDetails(
`Detected multiple networks with the same chainId ${chainId} on OpenZeppelin Defender: ${Array.from(networkNames).join(', ')}`,
() =>
`Specify the network that you want to use in the DEFENDER_NETWORK environment variable.`,
);
} else if (!networkNames.includes(userConfigNetwork)) {
throw new ErrorWithDetails(
`Specified network ${userConfigNetwork} does not match any of the detected networks for chainId ${chainId}: ${Array.from(networkNames).join(', ')}`,
() =>
`Ensure you are connected to the correct network, or specify one of the detected networks in the DEFENDER_NETWORK environment variable.`,
);
}
return userConfigNetwork;
}
}

async function getNetworkNames(chainId: number, networkClient?: NetworkClient) {
const matchingNetworks = [];

const knownNetwork = fromChainId(chainId);
if (knownNetwork !== undefined) {
matchingNetworks.push(knownNetwork);
}
return network;

const client = networkClient ?? getNetworkClient();

const forkedNetworks = await client.listForkedNetworks();
for (const network of forkedNetworks) {
if (network.chainId === chainId) {
matchingNetworks.push(network.name);
}
}

const privateNetworks = await client.listPrivateNetworks();
for (const network of privateNetworks) {
if (network.chainId === chainId) {
matchingNetworks.push(network.name);
}
}

return matchingNetworks;
}

export const USAGE_COMMAND_PREFIX = 'Usage: npx @openzeppelin/defender-deploy-client-cli';
15 changes: 12 additions & 3 deletions test/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ test.beforeEach(t => {
});
t.context.deployContractStub = deployContractStub;

t.context.fakeDefenderClient = {
t.context.fakeDeployClient = {
deployContract: deployContractStub,
getDeployedContract: getDeployedContractStub,
};

t.context.fakeNetworkClient = {
listForkedNetworks: () => {
return [];
},
listPrivateNetworks: () => {
return [];
},
};
});

test.afterEach.always(t => {
Expand All @@ -51,7 +60,7 @@ test.afterEach.always(t => {
test('deploy required args', async t => {
const args = ['--contractName', 'MyContract', '--contractPath', 'contracts/MyContract.sol', '--chainId', FAKE_CHAIN_ID, '--buildInfoFile', 'test/input/build-info.json'];

await deploy(args, t.context.fakeDefenderClient);
await deploy(args, t.context.fakeDeployClient, t.context.fakeNetworkClient);

t.is(t.context.deployContractStub.callCount, 1);

Expand All @@ -72,7 +81,7 @@ test('deploy required args', async t => {
test('deploy all args', async t => {
const args = ['--contractName', 'MyContract', '--contractPath', 'contracts/MyContract.sol', '--chainId', FAKE_CHAIN_ID, '--buildInfoFile', 'test/input/build-info.json', '--constructorBytecode', '0x1234', '--licenseType', 'MIT', '--verifySourceCode', 'false', '--relayerId', 'my-relayer-id', '--salt', '0x4567', '--createFactoryAddress', '0x0000000000000000000000000000000000098765'];

await deploy(args, t.context.fakeDefenderClient);
await deploy(args, t.context.fakeDeployClient, t.context.fakeNetworkClient);

t.is(t.context.deployContractStub.callCount, 1);

Expand Down
15 changes: 12 additions & 3 deletions test/get-approval-process.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,19 @@ test.beforeEach(t => {
viaType: 'Multisig',
});

t.context.fakeDefenderClient = {
t.context.fakeDeployClient = {
getDeployApprovalProcess: t.context.getDeployApprovalProcessStub,
getUpgradeApprovalProcess: t.context.getUpgradeApprovalProcessStub,
};

t.context.fakeNetworkClient = {
listForkedNetworks: () => {
return [];
},
listPrivateNetworks: () => {
return [];
},
};
});

test.afterEach.always(t => {
Expand All @@ -56,7 +65,7 @@ test.afterEach.always(t => {
test('getDeployApprovalProcess args', async t => {
const args = ['--chainId', FAKE_CHAIN_ID];

await getApprovalProcess('getDeployApprovalProcess', args, t.context.fakeDefenderClient);
await getApprovalProcess('getDeployApprovalProcess', args, t.context.fakeDeployClient, t.context.fakeNetworkClient);

t.is(t.context.getDeployApprovalProcessStub.callCount, 1);
t.is(t.context.getUpgradeApprovalProcessStub.callCount, 0);
Expand All @@ -67,7 +76,7 @@ test('getDeployApprovalProcess args', async t => {
test('getUpgradeApprovalProcess args', async t => {
const args = ['--chainId', FAKE_CHAIN_ID];

await getApprovalProcess('getUpgradeApprovalProcess', args, t.context.fakeDefenderClient);
await getApprovalProcess('getUpgradeApprovalProcess', args, t.context.fakeDeployClient, t.context.fakeNetworkClient);

t.is(t.context.getDeployApprovalProcessStub.callCount, 0);
t.is(t.context.getUpgradeApprovalProcessStub.callCount, 1);
Expand Down
Loading

0 comments on commit d4c7cef

Please sign in to comment.