import minimist from 'minimist'; import { FunctionArgs, deployContract } from '../internal/deploy-contract'; import { getDeployClient } from '../internal/client'; import { USAGE_COMMAND_PREFIX, getAndValidateString, getNetwork } from '../internal/utils'; import { DeployClient, DeploymentResponse, DeployMetadata, TxOverrides } from '@openzeppelin/defender-sdk-deploy-client'; import { NetworkClient } from '@openzeppelin/defender-sdk-network-client'; const USAGE = `${USAGE_COMMAND_PREFIX} deploy --contractName --contractPath --chainId --buildInfoFile [--constructorBytecode ] [--licenseType ] [--verifySourceCode ] [--relayerId ] [--salt ] [--createFactoryAddress ] [--gasLimit ] [--gasPrice ] [--maxFeePerGas ] [--maxPriorityFeePerGas ] [--metadata ] [--origin ]`; const DETAILS = ` Deploys a contract using OpenZeppelin Defender. Required options: --contractName Name of the contract to deploy. --contractPath Path to the contract file. --chainId Chain ID of the network to deploy to. --buildInfoFile Path to the build info file containing Solidity compiler input and output for the contract. Additional options: --constructorBytecode 0x-prefixed ABI encoded byte string representing the constructor arguments. Required if the constructor has arguments. --licenseType '' License type to display on block explorers for verified source code. See https://etherscan.io/contract-license-types for supported values and use the string found in brackets, e.g. 'MIT' --verifySourceCode Whether to verify source code on block explorers. Defaults to true. --relayerId Relayer ID to use for deployment. Defaults to the relayer configured for your deployment environment on Defender. --salt Salt to use for CREATE2 deployment. Defaults to a random salt. --createFactoryAddress Address of the CREATE2 factory to use for deployment. Defaults to the factory provided by Defender. --gasLimit Maximum amount of gas to allow the deployment transaction to use. --gasPrice Gas price for legacy transactions, in wei. --maxFeePerGas Maximum total fee per gas, in wei. --maxPriorityFeePerGas Maximum priority fee per gas, in wei. --metadata '' Use this to identify, tag, or classify deployments. See https://docs.openzeppelin.com/defender/module/deploy#metadata. Must be a JSON string, for example: --metadata '{ "commitHash": "4ae3e0d", "tag": "v1.0.0", "anyOtherField": "anyValue" }' --origin The client that made the deployment. For internal use only. Only 'Foundry' or 'SDK' are supported. Defaults to 'SDK'. `; export async function deploy(args: string[], deployClient?: DeployClient, networkClient?: NetworkClient): Promise { const { parsedArgs, extraArgs } = parseArgs(args); if (!help(parsedArgs)) { const functionArgs = await getFunctionArgs(parsedArgs, extraArgs, networkClient); const client = deployClient ?? getDeployClient(); const address = await deployContract(functionArgs, client); console.log(`Deployed to address: ${address}`); } } function parseArgs(args: string[]) { const parsedArgs = minimist(args, { boolean: [ 'help', 'verifySourceCode', ], string: ['contractName', 'contractPath', 'chainId', 'buildInfoFile', 'licenseType', 'constructorBytecode', 'relayerId', 'salt', 'createFactoryAddress', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas', 'metadata', 'origin'], alias: { h: 'help' }, default: { verifySourceCode: true }, }); const extraArgs = parsedArgs._; return { parsedArgs, extraArgs }; } function help(parsedArgs: minimist.ParsedArgs): boolean { if (!parsedArgs['help']) { return false; } else { console.log(USAGE); console.log(DETAILS); return true; } } /** * Gets and validates function arguments and options. * @returns Function arguments * @throws Error if any arguments or options are invalid. */ async function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: string[], networkClient?: NetworkClient): Promise { if (extraArgs.length !== 0) { throw new Error('The deploy command does not take any arguments, only options.'); } else { // Required options const contractName = getAndValidateString(parsedArgs, 'contractName', true)!; const contractPath = getAndValidateString(parsedArgs, 'contractPath', true)!; const networkString = getAndValidateString(parsedArgs, 'chainId', true)!; const network = await getNetwork(parseInt(networkString), networkClient); const buildInfoFile = getAndValidateString(parsedArgs, 'buildInfoFile', true)!; // Additional options const licenseType = getAndValidateString(parsedArgs, 'licenseType'); const constructorBytecode = parsedArgs['constructorBytecode']; const verifySourceCode = parsedArgs['verifySourceCode']; const relayerId = getAndValidateString(parsedArgs, 'relayerId'); const salt = getAndValidateString(parsedArgs, 'salt'); const createFactoryAddress = getAndValidateString(parsedArgs, 'createFactoryAddress'); const txOverrides: TxOverrides = { gasLimit: parseNumberOrUndefined(getAndValidateString(parsedArgs, 'gasLimit')), gasPrice: parseHexOrUndefined(getAndValidateString(parsedArgs, 'gasPrice')), maxFeePerGas: parseHexOrUndefined(getAndValidateString(parsedArgs, 'maxFeePerGas')), maxPriorityFeePerGas: parseHexOrUndefined(getAndValidateString(parsedArgs, 'maxPriorityFeePerGas')), }; const metadata = getAndValidateJsonString(parsedArgs, 'metadata'); const origin = getAndValidateOrigin(parsedArgs, 'origin'); checkInvalidArgs(parsedArgs); return { contractName, contractPath, network, buildInfoFile, licenseType, constructorBytecode, verifySourceCode, relayerId, salt, createFactoryAddress, txOverrides, metadata, origin }; } } function checkInvalidArgs(parsedArgs: minimist.ParsedArgs) { const invalidArgs = Object.keys(parsedArgs).filter( key => ![ 'help', 'h', '_', 'contractName', 'contractPath', 'chainId', 'buildInfoFile', 'licenseType', 'constructorBytecode', 'verifySourceCode', 'relayerId', 'salt', 'createFactoryAddress', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas', 'metadata', 'origin', ].includes(key), ); if (invalidArgs.length > 0) { throw new Error(`Invalid options: ${invalidArgs.join(', ')}`); } } function parseHexOrUndefined(value?: string): string | undefined { if (value !== undefined) { // If not a hex string, convert from decimal to hex as a string if (!value.startsWith('0x')) { return '0x' + Number(value).toString(16); } else { return value; } } else { return undefined; } } function parseNumberOrUndefined(value?: string): number | undefined { if (value !== undefined) { return Number(value); } else { return undefined; } } function getAndValidateJsonString(parsedArgs: minimist.ParsedArgs, option: string): DeployMetadata | undefined { const value = getAndValidateString(parsedArgs, option); if (value !== undefined) { try { return JSON.parse(value); } catch (e: any) { throw new Error(`Failed to parse ${option} option as JSON: ${e.message}`); } } else { return undefined; } } function getAndValidateOrigin(parsedArgs: minimist.ParsedArgs, option: string): DeploymentResponse['origin'] | undefined { const value = getAndValidateString(parsedArgs, option); const supportedOrigins: DeploymentResponse['origin'][] = ['Foundry', 'SDK']; if (value !== undefined) { if (!supportedOrigins.includes(value as DeploymentResponse['origin'])) { throw new Error(`Option --${option} only supports 'Foundry' or 'SDK'`); } return value as DeploymentResponse['origin']; } else { return undefined; } }