Skip to content

Commit

Permalink
Support proposing upgrades (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericglau authored Feb 9, 2024
1 parent 075965b commit 5441451
Show file tree
Hide file tree
Showing 25 changed files with 668 additions and 43 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Test

on:
push:
branches: [main]
pull_request: {}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Run tests
run: yarn test
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

## 0.0.1-alpha.3 (2024-02-09)

- Support proposing upgrades. ([#2](https://github.com/OpenZeppelin/defender-deploy-client-cli/pull/2))

## 0.0.1-alpha.2 (2024-02-01)

- Enable constructorBytecode argument. ([#1](https://github.com/OpenZeppelin/defender-deploy-client-cli/pull/1))
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,20 @@ DEFENDER_SECRET<Your API secret>
## Usage

```
npx @openzeppelin/defender-deploy-client-cli deploy --contractName <CONTRACT_NAME> --contractPath <CONTRACT_PATH> --chainId <CHAIN_ID> --artifactFile <BUILD_INFO_FILE_PATH> [--constructorBytecode <CONSTRUCTOR_BYTECODE>] [--licenseType <LICENSE>] [--verifySourceCode <true|false>] [--relayerId <RELAYER_ID>] [--salt <SALT>] [--createFactoryAddress <CREATE_FACTORY_ADDRESS>]
Usage: npx @openzeppelin/defender-deploy-client-cli <COMMAND> <OPTIONS>
Performs actions using OpenZeppelin Defender.
Available commands:
deploy Deploys a contract.
proposeUpgrade Proposes an upgrade.
Run 'npx @openzeppelin/defender-deploy-client-cli <COMMAND> --help' for more information on a command.
```

### Deploying a contract
```
npx @openzeppelin/defender-deploy-client-cli deploy --contractName <CONTRACT_NAME> --contractPath <CONTRACT_PATH> --chainId <CHAIN_ID> --artifactFile <BUILD_INFO_FILE_PATH> [--constructorBytecode <CONSTRUCTOR_ARGS>] [--licenseType <LICENSE>] [--verifySourceCode <true|false>] [--relayerId <RELAYER_ID>] [--salt <SALT>] [--createFactoryAddress <CREATE_FACTORY_ADDRESS>]
Deploys a contract using OpenZeppelin Defender.
Expand All @@ -36,4 +49,21 @@ Additional options:
--relayerId <RELAYER_ID> Relayer ID to use for deployment. Defaults to the relayer configured for your deployment environment on Defender.
--salt <SALT> Salt to use for CREATE2 deployment. Defaults to a random salt.
--createFactoryAddress <CREATE_FACTORY_ADDRESS> Address of the CREATE2 factory to use for deployment. Defaults to the factory provided by Defender.
```

### Proposing an upgrade
```
npx @openzeppelin/defender-deploy-client-cli proposeUpgrade --proxyAddress <PROXY_ADDRESS> --newImplementationAddress <NEW_IMPLEMENTATION_ADDRESS> --chainId <CHAIN_ID> [--proxyAdminAddress <PROXY_ADMIN_ADDRESS>] [--abiFile <CONTRACT_ARTIFACT_FILE_PATH>] [--approvalProcessId <UPGRADE_APPROVAL_PROCESS_ID>]
Proposes an upgrade using OpenZeppelin Defender.
Required options:
--proxyAddress <PROXY_ADDRESS> Address of the proxy to upgrade.
--newImplementationAddress <NEW_IMPLEMENTATION_ADDRESS> Address of the new implementation contract.
--chainId <CHAIN_ID> Chain ID of the network to use.
Additional options:
--proxyAdminAddress <PROXY_ADMIN_ADDRESS> Address of the proxy's admin. Required if the proxy is a transparent proxy.
--abiFile <CONTRACT_ARTIFACT_FILE_PATH> Path to a JSON file that contains an "abi" entry, where its value will be used as the new implementation ABI.
--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.
```
3 changes: 3 additions & 0 deletions ava.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
snapshotDir: '.',
};
5 changes: 3 additions & 2 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.2",
"version": "0.0.1-alpha.3",
"description": "CLI for deployments using OpenZeppelin Defender SDK",
"repository": "https://github.com/OpenZeppelin/defender-deploy-client-cli",
"license": "MIT",
Expand All @@ -19,7 +19,8 @@
"@types/minimist": "^1.2.5",
"ava": "^6.0.0",
"rimraf": "^5.0.0",
"typescript": "^4.0.0"
"typescript": "^4.0.0",
"sinon": "^17.0.1"
},
"dependencies": {
"minimist": "^1.2.8",
Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

import { main } from './defender';
import { main } from "./command-selector";

const run = async () => {
await main(process.argv.slice(2));
Expand Down
34 changes: 34 additions & 0 deletions src/command-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import minimist from "minimist";
import { deploy } from './commands/deploy';
import { proposeUpgrade } from './commands/propose-upgrade';

const USAGE = 'Usage: npx @openzeppelin/defender-deploy-client-cli <COMMAND> <OPTIONS>';
const DETAILS = `
Performs actions using OpenZeppelin Defender.
Available commands:
deploy Deploys a contract.
proposeUpgrade Proposes an upgrade.
Run 'npx @openzeppelin/defender-deploy-client-cli <COMMAND> --help' for more information on a command.
`;

export async function main(args: string[]): Promise<void> {
const regularArgs = minimist(args)._;

if (regularArgs.length === 0) {
console.log(USAGE);
console.log(DETAILS);
} else {
if (regularArgs[0] === 'deploy') {
await deploy(args.slice(1));
} else if (regularArgs[0] === 'proposeUpgrade') {
await proposeUpgrade(args.slice(1));
} else {
throw new Error(`\
Unknown command: ${regularArgs[0]}
Run 'npx @openzeppelin/defender-deploy-client-cli --help' for usage.\
`);
}
}
}
37 changes: 9 additions & 28 deletions src/defender.ts → src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import minimist from 'minimist';
import { FunctionArgs, deployContract } from './deployContract';
import { Network, fromChainId } from '@openzeppelin/defender-sdk-base-client';
import { FunctionArgs, deployContract } from '../internal/deploy-contract';
import { getDeployClient } from '../internal/client';
import { getAndValidateString, getNetwork } from '../internal/utils';
import { DeployClient } from '@openzeppelin/defender-sdk-deploy-client';

const USAGE = 'Usage: npx @openzeppelin/defender-deploy-client-cli deploy --contractName <CONTRACT_NAME> --contractPath <CONTRACT_PATH> --chainId <CHAIN_ID> --artifactFile <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 @@ -21,13 +23,14 @@ 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 main(args: string[]): Promise<void> {
export async function deploy(args: string[], deployClient?: DeployClient): Promise<void> {
const { parsedArgs, extraArgs } = parseArgs(args);

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

const address = await deployContract(functionArgs);
console.log(`Deployed to address: ${address}`);
}
}
Expand All @@ -47,7 +50,7 @@ function parseArgs(args: string[]) {
}

function help(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): boolean {
if (extraArgs.length === 0 || parsedArgs['help']) {
if (parsedArgs['help']) {
console.log(USAGE);
console.log(DETAILS);
return true;
Expand All @@ -62,11 +65,7 @@ function help(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): boolean {
* @throws Error if any arguments or options are invalid.
*/
export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): FunctionArgs {
if (extraArgs.length === 0) {
throw new Error('Missing command. Supported commands are: validate');
} else if (extraArgs[0] !== 'deploy') {
throw new Error(`Invalid command: ${extraArgs[0]}. Supported commands are: deploy`);
} else if (extraArgs.length > 1) {
if (extraArgs.length !== 0) {
throw new Error('The deploy command does not take any arguments, only options.');
} else {
// Required options
Expand All @@ -92,16 +91,6 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
}
}

function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: string, required = false): string | undefined {
const value = parsedArgs[option];
if (value !== undefined && value.trim().length === 0) {
throw new Error(`Invalid option: --${option} cannot be empty`);
} else if (required && value === undefined) {
throw new Error(`Missing required option: --${option}`);
}
return value;
}

function checkInvalidArgs(parsedArgs: minimist.ParsedArgs) {
const invalidArgs = Object.keys(parsedArgs).filter(
key =>
Expand All @@ -125,11 +114,3 @@ function checkInvalidArgs(parsedArgs: minimist.ParsedArgs) {
throw new Error(`Invalid options: ${invalidArgs.join(', ')}`);
}
}

function getNetwork(chainId: number): Network {
const network = fromChainId(chainId);
if (network === undefined) {
throw new Error(`Network ${chainId} is not supported by OpenZeppelin Defender`);
}
return network;
}
107 changes: 107 additions & 0 deletions src/commands/propose-upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import minimist from 'minimist';
import { FunctionArgs, upgradeContract } from '../internal/upgrade-contract';
import { getDeployClient } from '../internal/client';
import { getAndValidateString, getNetwork } from '../internal/utils';
import { DeployClient } from '@openzeppelin/defender-sdk-deploy-client';

const USAGE = 'Usage: npx @openzeppelin/defender-deploy-client-cli proposeUpgrade --proxyAddress <PROXY_ADDRESS> --newImplementationAddress <NEW_IMPLEMENTATION_ADDRESS> --chainId <CHAIN_ID> [--proxyAdminAddress <PROXY_ADMIN_ADDRESS>] [--abiFile <CONTRACT_ARTIFACT_FILE_PATH>] [--approvalProcessId <UPGRADE_APPROVAL_PROCESS_ID>]';
const DETAILS = `
Proposes an upgrade using OpenZeppelin Defender.
Required options:
--proxyAddress <PROXY_ADDRESS> Address of the proxy to upgrade.
--newImplementationAddress <NEW_IMPLEMENTATION_ADDRESS> Address of the new implementation contract.
--chainId <CHAIN_ID> Chain ID of the network to use.
Additional options:
--proxyAdminAddress <PROXY_ADMIN_ADDRESS> Address of the proxy's admin. Required if the proxy is a transparent proxy.
--abiFile <CONTRACT_ARTIFACT_FILE_PATH> Path to a JSON file that contains an "abi" entry, where its value will be used as the new implementation ABI.
--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> {
const { parsedArgs, extraArgs } = parseArgs(args);

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

console.log(`Upgrade proposal created.`);
console.log(`Proposal ID: ${upgradeResponse.proposalId}`);
if (upgradeResponse.externalUrl !== undefined) {
console.log(`Proposal URL: ${upgradeResponse.externalUrl}`);
}
}
}

function parseArgs(args: string[]) {
const parsedArgs = minimist(args, {
boolean: [
'help',
],
string: ['proxyAddress', 'newImplementationAddress', 'chainId', 'proxyAdminAddress', 'abiFile', 'approvalProcessId'],
alias: { h: 'help' },
});
const extraArgs = parsedArgs._;
return { parsedArgs, extraArgs };
}

function help(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): boolean {
if (parsedArgs['help']) {
console.log(USAGE);
console.log(DETAILS);
return true;
} else {
return false;
}
}

/**
* Gets and validates function arguments and options.
* @returns Function arguments
* @throws Error if any arguments or options are invalid.
*/
export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): FunctionArgs {
if (extraArgs.length !== 0) {
throw new Error('The proposeUpgrade command does not take any arguments, only options.');
} else {
// Required options
const proxyAddress = getAndValidateString(parsedArgs, 'proxyAddress', true)!;
const newImplementationAddress = getAndValidateString(parsedArgs, 'newImplementationAddress', true)!;

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

// Additional options
const proxyAdminAddress = getAndValidateString(parsedArgs, 'proxyAdminAddress');
const abiFile = getAndValidateString(parsedArgs, 'abiFile');
const approvalProcessId = getAndValidateString(parsedArgs, 'approvalProcessId');

checkInvalidArgs(parsedArgs);

return { proxyAddress, newImplementationAddress, network, proxyAdminAddress, abiFile, approvalProcessId };
}
}



function checkInvalidArgs(parsedArgs: minimist.ParsedArgs) {
const invalidArgs = Object.keys(parsedArgs).filter(
key =>
![
'help',
'h',
'_',
'proxyAddress',
'newImplementationAddress',
'chainId',
'proxyAdminAddress',
'abiFile',
'approvalProcessId',
].includes(key),
);
if (invalidArgs.length > 0) {
throw new Error(`Invalid options: ${invalidArgs.join(', ')}`);
}
}
13 changes: 13 additions & 0 deletions src/internal/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DeployClient } from "@openzeppelin/defender-sdk-deploy-client";

export function getDeployClient(): DeployClient {
require('dotenv').config();
const apiKey = process.env.DEFENDER_KEY as string;
const apiSecret = process.env.DEFENDER_SECRET as string;

if (apiKey === undefined || apiSecret === undefined) {
throw new Error('DEFENDER_KEY and DEFENDER_SECRET must be set in environment variables.');
}

return new DeployClient({ apiKey, apiSecret });
}
12 changes: 1 addition & 11 deletions src/deployContract.ts → src/internal/deploy-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,7 @@ export interface FunctionArgs {
createFactoryAddress?: string;
}

export async function deployContract(args: FunctionArgs) {
require('dotenv').config();
const apiKey = process.env.DEFENDER_KEY as string;
const apiSecret = process.env.DEFENDER_SECRET as string;

if (apiKey === undefined || apiSecret === undefined) {
throw new Error('DEFENDER_KEY and DEFENDER_SECRET must be set in environment variables.');
}

const client = new DeployClient({ apiKey, apiSecret });

export async function deployContract(args: FunctionArgs, client: DeployClient) {
const buildInfoFileContents = await fs.readFile(args.artifactFile, 'utf8');

const deploymentRequest: DeployContractRequest = {
Expand Down
32 changes: 32 additions & 0 deletions src/internal/upgrade-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { promises as fs } from 'fs';

import { Network } from '@openzeppelin/defender-sdk-base-client';
import { DeployClient, UpgradeContractRequest, UpgradeContractResponse } from '@openzeppelin/defender-sdk-deploy-client';

export interface FunctionArgs {
proxyAddress: string;
newImplementationAddress: string;
network: Network;
proxyAdminAddress?: string;
abiFile?: string;
approvalProcessId?: string;
}

export async function upgradeContract(args: FunctionArgs, client: DeployClient): Promise<UpgradeContractResponse> {
let newImplementationABI: string | undefined;
if (args.abiFile !== undefined) {
const artifactObject = JSON.parse(await fs.readFile(args.abiFile, 'utf8'));
newImplementationABI = JSON.stringify(artifactObject.abi);
}

const deploymentRequest: UpgradeContractRequest = {
proxyAddress: args.proxyAddress,
newImplementationAddress: args.newImplementationAddress,
network: args.network,
proxyAdminAddress: args.proxyAdminAddress,
newImplementationABI: newImplementationABI,
approvalProcessId: args.approvalProcessId,
};

return client.upgradeContract(deploymentRequest);
}
Loading

0 comments on commit 5441451

Please sign in to comment.