From 666906075e421e51605a56008e69d81f2dc05b3f Mon Sep 17 00:00:00 2001 From: Ryan Pate Date: Fri, 11 Nov 2022 12:34:07 -0800 Subject: [PATCH] Added Docker configuration for executor --- .changeset/spicy-geckos-greet.md | 6 ++ packages/executor/.dockerignore | 3 + packages/executor/.env.example | 8 ++ packages/executor/Dockerfile | 9 +++ packages/executor/package.json | 8 +- packages/executor/src/index.ts | 130 ++++++++++++++++++++++--------- packages/executor/tsconfig.json | 22 +++++- packages/plugins/.gitignore | 3 +- 8 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 .changeset/spicy-geckos-greet.md create mode 100644 packages/executor/.dockerignore create mode 100644 packages/executor/.env.example create mode 100644 packages/executor/Dockerfile diff --git a/.changeset/spicy-geckos-greet.md b/.changeset/spicy-geckos-greet.md new file mode 100644 index 000000000..fc5820e55 --- /dev/null +++ b/.changeset/spicy-geckos-greet.md @@ -0,0 +1,6 @@ +--- +'@chugsplash/executor': patch +'@chugsplash/plugins': patch +--- + +Add Docker configuration for executor diff --git a/packages/executor/.dockerignore b/packages/executor/.dockerignore new file mode 100644 index 000000000..9c97bbd46 --- /dev/null +++ b/packages/executor/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/packages/executor/.env.example b/packages/executor/.env.example new file mode 100644 index 000000000..6c219e289 --- /dev/null +++ b/packages/executor/.env.example @@ -0,0 +1,8 @@ +CHUGSPLASH_EXECUTOR__PRIVATE_KEY= +CHUGSPLASH_EXECUTOR__NETWORK= +CHUGSPLASH_EXECUTOR__AMPLITUDE_KEY= +IPFS_PROJECT_ID= +IPFS_API_KEY_SECRET= +OPT_ETHERSCAN_API_KEY= +ETH_ETHERSCAN_API_KEY= +INFURA_API_KEY= diff --git a/packages/executor/Dockerfile b/packages/executor/Dockerfile new file mode 100644 index 000000000..560f1d098 --- /dev/null +++ b/packages/executor/Dockerfile @@ -0,0 +1,9 @@ +FROM node:16.16.0 + +COPY package.json package.json + +RUN npm install + +COPY . . + +CMD [ "yarn", "start" ] diff --git a/packages/executor/package.json b/packages/executor/package.json index 0539d7e32..c0c7b1b16 100644 --- a/packages/executor/package.json +++ b/packages/executor/package.json @@ -18,7 +18,9 @@ "lint:check": "yarn lint:ts:check", "lint:ts:fix": "yarn lint:ts:check --fix", "lint:ts:check": "eslint . --max-warnings=0", - "pre-commit": "lint-staged" + "pre-commit": "lint-staged", + "build-container": "docker build --tag chugsplash-executor .", + "run-container": "docker run --env-file ./.env chugsplash-executor" }, "homepage": "https://github.com/smartcontracts/chugsplash/tree/develop/packages/executor#readme", "license": "MIT", @@ -40,5 +42,9 @@ "hardhat": "^2.10.0", "ipfs-http-client": "56.0.3", "undici": "^5.12.0" + }, + "dependencies": { + "@amplitude/node": "^1.10.2", + "ts-node": "^10.9.1" } } diff --git a/packages/executor/src/index.ts b/packages/executor/src/index.ts index 96aeb42aa..6d8b8b71c 100644 --- a/packages/executor/src/index.ts +++ b/packages/executor/src/index.ts @@ -8,11 +8,14 @@ import { } from '@chugsplash/contracts' import { ChugSplashBundleState } from '@chugsplash/core' import { getChainId } from '@eth-optimism/core-utils' +import * as Amplitude from '@amplitude/node' import { compileRemoteBundle, verifyChugSplashConfig } from './utils' type Options = { network: string + privateKey: string + amplitudeKey: string } type Metrics = {} @@ -20,14 +23,18 @@ type Metrics = {} type State = { registry: ethers.Contract wallet: ethers.Wallet - ess: string[] - eps: string[] + lastBlockNumber: number + amplitudeClient: Amplitude.NodeClient } +// TODO: +// Add logging agent for docker container and connect to a managed sink such as logz.io +// Refactor chugsplash commands to decide whether to use the executor based on the target network + export class ChugSplashExecutor extends BaseServiceV2 { constructor(options?: Partial) { super({ - name: 'executor', + name: 'chugsplash-executor', // eslint-disable-next-line @typescript-eslint/no-var-requires version: require('../package.json').version, loop: true, @@ -37,18 +44,26 @@ export class ChugSplashExecutor extends BaseServiceV2 { network: { desc: 'network for the chain to run the executor on', validator: validators.str, - default: 'http://localhost:8545', }, - // key: { - // desc: 'private key to use for signing transactions', - // validator: validators.str, - // }, + privateKey: { + desc: 'private key used for deployments', + validator: validators.str, + }, + amplitudeKey: { + desc: 'API key to send data to Amplitude', + validator: validators.str, + default: 'disabled', + }, }, metricsSpec: {}, }) } async init() { + if (this.options.amplitudeKey !== 'disabled') { + this.state.amplitudeClient = Amplitude.init(this.options.amplitudeKey) + } + const reg = CHUGSPLASH_REGISTRY_PROXY_ADDRESS const provider = ethers.getDefaultProvider(this.options.network) this.state.registry = new ethers.Contract( @@ -56,17 +71,31 @@ export class ChugSplashExecutor extends BaseServiceV2 { ChugSplashRegistryABI, provider ) - - this.state.wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider) + this.state.lastBlockNumber = -1 + this.state.wallet = new ethers.Wallet(this.options.privateKey, provider) } async main() { - // Find all active upgrades that have not yet been started + // Find all active upgrades that have not yet been executed in blocks after the stored hash const approvalAnnouncementEvents = await this.state.registry.queryFilter( - this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved') + this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved'), + this.state.lastBlockNumber + 1 ) + // If none found, return + if (approvalAnnouncementEvents.length === 0) { + this.logger.info('no events found') + return + } + + this.logger.info(`${approvalAnnouncementEvents.length} events found`) + + // store last block number + this.state.lastBlockNumber = approvalAnnouncementEvents.at(-1).blockNumber + + // execute all approved bundles for (const approvalAnnouncementEvent of approvalAnnouncementEvents) { + // fetch manager for relevant project const signer = this.state.wallet const manager = new ethers.Contract( approvalAnnouncementEvent.args.manager, @@ -74,52 +103,77 @@ export class ChugSplashExecutor extends BaseServiceV2 { signer ) + // get active bundle id for this project const activeBundleId = await manager.activeBundleId() if (activeBundleId === ethers.constants.HashZero) { - console.log('no active bundle') + this.logger.error(`Error: No active bundle id found in manager`) continue } + // fetch bundle state const bundleState: ChugSplashBundleState = await manager.bundles( activeBundleId ) - // TODO: Add this to the ChugSplashManager contract - const selectedExecutor = await manager.getSelectedExecutor(activeBundleId) - if (selectedExecutor !== ethers.constants.AddressZero) { - // Someone else has been selected to execute the upgrade, so we can skip it. - continue - } - + // get proposal event and compile const proposalEvents = await manager.queryFilter( manager.filters.ChugSplashBundleProposed(activeBundleId) ) - - if (proposalEvents.length !== 1) { - // TODO: throw an error here or skip - } - const proposalEvent = proposalEvents[0] const { bundle, canonicalConfig } = await compileRemoteBundle( hre, proposalEvent.args.configUri ) + + // ensure compiled bundle matches proposed bundle if (bundle.root !== proposalEvent.args.bundleRoot) { - // TODO: throw an error here or skip + // log error and continue + this.logger.error( + 'Error: Compiled bundle root does not match proposal event bundle root', + canonicalConfig.options + ) + continue + } + + // execute bundle + try { + await hre.run('chugsplash-execute', { + chugSplashManager: manager, + bundleState, + bundle, + parsedConfig: canonicalConfig, + deployer: signer, + hide: false, + }) + this.logger.info('Successfully executed') + } catch (e) { + // log error and continue + this.logger.error('Error: execution error', e, canonicalConfig.options) + continue + } + + // verify on etherscan + try { + if ((await getChainId(this.state.wallet.provider)) !== 31337) { + await verifyChugSplashConfig(hre, proposalEvent.args.configUri) + this.logger.info('Successfully verified') + } + } catch (e) { + this.logger.error( + 'Error: verification error', + e, + canonicalConfig.options + ) } - // todo call chugsplash-execute if deploying locally - await hre.run('chugsplash-execute', { - chugSplashManager: manager, - bundleState, - bundle, - parsedConfig: canonicalConfig, - deployer: signer, - hide: false, - }) - - if ((await getChainId(this.state.wallet.provider)) !== 31337) { - await verifyChugSplashConfig(hre, proposalEvent.args.configUri) + if (this.options.amplitudeKey !== 'disabled') { + this.state.amplitudeClient.logEvent({ + event_type: 'ChugSplash Executed', + user_id: canonicalConfig.options.projectOwner, + event_properties: { + projectName: canonicalConfig.options.projectName, + }, + }) } } } diff --git a/packages/executor/tsconfig.json b/packages/executor/tsconfig.json index a58b672ec..8665fe49c 100644 --- a/packages/executor/tsconfig.json +++ b/packages/executor/tsconfig.json @@ -1,11 +1,27 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "sourceMap": true, + "esModuleInterop": true, + "composite": true, + "resolveJsonModule": true, + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "typeRoots": [ + "node_modules/@types" + ], "rootDir": "./src", "outDir": "./dist" }, - + "exclude": [ + "node_modules", + "dist" + ], "include": [ "src/**/*" ] diff --git a/packages/plugins/.gitignore b/packages/plugins/.gitignore index 33d75fd5f..e72db0c0e 100644 --- a/packages/plugins/.gitignore +++ b/packages/plugins/.gitignore @@ -3,4 +3,5 @@ dist/ .deployed artifacts cache -deployments +deployments/ +deployed/