From fd105122361de5332ea12d85443a37056df707ac Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 18:49:58 -0400 Subject: [PATCH 1/3] feat: Add ChugSplash config parsing with handlebars --- packages/contracts/package.json | 3 +- packages/contracts/src/chugsplash/config.ts | 94 +++++ packages/contracts/src/chugsplash/index.ts | 1 + packages/contracts/src/index.ts | 1 + .../contracts/test/chugsplash/config.spec.ts | 375 ++++++++++++++++++ yarn.lock | 2 +- 6 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/src/chugsplash/config.ts create mode 100644 packages/contracts/src/chugsplash/index.ts create mode 100644 packages/contracts/test/chugsplash/config.spec.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 18d155a11567..fbc9a98ec11a 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -51,7 +51,8 @@ "@openzeppelin/contracts-upgradeable": "^3.3.0", "@typechain/hardhat": "^1.0.1", "ganache-core": "^2.13.2", - "glob": "^7.1.6" + "glob": "^7.1.6", + "handlebars": "^4.7.7" }, "devDependencies": { "@eth-optimism/hardhat-ovm": "^0.1.0", diff --git a/packages/contracts/src/chugsplash/config.ts b/packages/contracts/src/chugsplash/config.ts new file mode 100644 index 000000000000..00f5819a4b35 --- /dev/null +++ b/packages/contracts/src/chugsplash/config.ts @@ -0,0 +1,94 @@ +/* Imports: External */ +import * as Handlebars from 'handlebars' +import { ethers } from 'ethers' + +type SolidityVariable = + | string + | number + | Array + | { + [name: string]: SolidityVariable + } + +export interface ChugSplashConfig { + contracts: { + [name: string]: { + address: string + source: string + variables?: { + [name: string]: SolidityVariable + } + } + } +} + +/** + * Parses a ChugSplash config file by replacing template values. + * @param config Unparsed config file to parse. + * @param env Environment variables to inject into the file. + * @return Parsed config file with template variables replaced. + */ +export const parseChugSplashConfig = ( + config: ChugSplashConfig, + env: any = {} +): ChugSplashConfig => { + config.contracts = config.contracts || {} + + const contracts = {} + for (const [contractName, contractConfig] of Object.entries( + config.contracts + )) { + // Block people from accidentally using templates in contract names. + if (contractName.includes('{') || contractName.includes('}')) { + throw new Error( + `cannot use template strings in contract names: ${contractName}` + ) + } + + // Block people from accidentally using templates in contract names. + if ( + contractConfig.source.includes('{') || + contractConfig.source.includes('}') + ) { + throw new Error( + `cannot use template strings in contract source names: ${contractConfig.source}` + ) + } + + // Make sure addresses are fixed and are actually addresses. + if (!ethers.utils.isAddress(contractConfig.address)) { + throw new Error( + `contract address is not a valid address: ${contractConfig.address}` + ) + } + + contracts[contractName] = contractConfig.address + } + + return JSON.parse( + Handlebars.compile(JSON.stringify(config))({ + env: new Proxy(env, { + get: (target, prop) => { + const val = target[prop] + if (val === undefined) { + throw new Error( + `attempted to access unknown env value: ${prop as any}` + ) + } + return val + }, + }), + contracts: new Proxy(contracts, { + get: (target, prop) => { + const val = target[prop] + if (val === undefined) { + throw new Error( + `attempted to access unknown contract: ${prop as any}` + ) + } + return val + }, + }), + }) + ) +} diff --git a/packages/contracts/src/chugsplash/index.ts b/packages/contracts/src/chugsplash/index.ts new file mode 100644 index 000000000000..f934b01b6f50 --- /dev/null +++ b/packages/contracts/src/chugsplash/index.ts @@ -0,0 +1 @@ +export * from './config' diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index ba9a1959a568..71c73b725826 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -2,3 +2,4 @@ export * from './contract-defs' export { getLatestStateDump, StateDump } from './contract-dumps' export * from './contract-deployment' export * from './predeploys' +export * from './chugsplash' diff --git a/packages/contracts/test/chugsplash/config.spec.ts b/packages/contracts/test/chugsplash/config.spec.ts new file mode 100644 index 000000000000..1ce6f151274c --- /dev/null +++ b/packages/contracts/test/chugsplash/config.spec.ts @@ -0,0 +1,375 @@ +import { expect } from '../setup' + +/* Imports: Internal */ +import { parseChugSplashConfig } from '../../src' + +describe('ChugSplash config parsing', () => { + describe('parseChugSplashConfig', () => { + it('should correctly parse a basic config file with no template variables', () => { + expect( + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + }, + }) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + }, + }) + }) + + it('should correctly parse a basic config file with multiple input contracts', () => { + expect( + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: {}, + }, + }, + }) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: {}, + }, + }, + }) + }) + + it('should correctly parse a config file with a templated variable', () => { + expect( + parseChugSplashConfig( + { + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '{{ env.MY_VARIABLE_VALUE }}', + }, + }, + }, + }, + { + MY_VARIABLE_VALUE: '1234', + } + ) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '1234', + }, + }, + }, + }) + }) + + it('should correctly parse a config file with multiple templated values', () => { + expect( + parseChugSplashConfig( + { + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '{{ env.MY_VARIABLE_VALUE }}', + mySecondVariable: '{{ env.MY_SECOND_VARIABLE_VALUE }}', + myThirdVariable: '{{ env.MY_THIRD_VARIABLE_VALUE }}', + }, + }, + }, + }, + { + MY_VARIABLE_VALUE: '1234', + MY_SECOND_VARIABLE_VALUE: 'banana', + MY_THIRD_VARIABLE_VALUE: 'cake', + } + ) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '1234', + mySecondVariable: 'banana', + myThirdVariable: 'cake', + }, + }, + }, + }) + }) + + it('should correctly parse a config file with a templated contract address', () => { + expect( + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: '{{ contracts.MyContract }}', + }, + }, + }, + }) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: `0x${'11'.repeat(20)}`, + }, + }, + }, + }) + }) + + it('should correctly parse a config file with a templated contract address', () => { + expect( + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: '{{ contracts.MyContract }}', + }, + }, + MyThirdContract: { + address: `0x${'33'.repeat(20)}`, + source: 'MyThirdContract', + variables: { + myContractAddress: '{{ contracts.MyContract }}', + myOtherContractAddress: '{{ contracts.MyOtherContract }}', + }, + }, + }, + }) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: `0x${'11'.repeat(20)}`, + }, + }, + MyThirdContract: { + address: `0x${'33'.repeat(20)}`, + source: 'MyThirdContract', + variables: { + myContractAddress: `0x${'11'.repeat(20)}`, + myOtherContractAddress: `0x${'22'.repeat(20)}`, + }, + }, + }, + }) + }) + + it('should correctly parse a config file with a contract referencing its own address', () => { + expect( + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myContractAddress: '{{ contracts.MyContract }}', + }, + }, + }, + }) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myContractAddress: `0x${'11'.repeat(20)}`, + }, + }, + }, + }) + }) + + it('should correctly parse a config file with a templated contract address and a templated variable', () => { + expect( + parseChugSplashConfig( + { + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: '{{ contracts.MyContract }}', + myVariable: '{{ env.MY_VARIABLE_VALUE }}', + }, + }, + }, + }, + { + MY_VARIABLE_VALUE: '0x1234', + } + ) + ).to.deep.equal({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: {}, + }, + MyOtherContract: { + address: `0x${'22'.repeat(20)}`, + source: 'MyOtherContract', + variables: { + myContractAddress: `0x${'11'.repeat(20)}`, + myVariable: '0x1234', + }, + }, + }, + }) + }) + + it('should throw an error when a variable is not supplied', () => { + expect(() => { + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '{{ env.MY_FAKE_VARIABLE_VALUE }}', + }, + }, + }, + }) + }).to.throw( + 'attempted to access unknown env value: MY_FAKE_VARIABLE_VALUE' + ) + }) + + it('should throw an error when accessing a contract that does not exist', () => { + expect(() => { + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '{{ contracts.MyFakeContract }}', + }, + }, + }, + }) + }).to.throw('attempted to access unknown contract: MyFakeContract') + }) + + it('should throw an error if trying to use a template in an address', () => { + expect(() => { + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `{{ env.NOT_AN_ADDRESS }}`, + source: 'MyContract', + variables: { + myVariable: '{{ contracts.MyFakeContract }}', + }, + }, + }, + }) + }).to.throw( + 'contract address is not a valid address: {{ env.NOT_AN_ADDRESS }}' + ) + }) + + it('should throw an error if trying to use a template in a contract source', () => { + expect(() => { + parseChugSplashConfig({ + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: '{{ env.MY_CONTRACT_SOURCE }}', + variables: { + myVariable: '{{ contracts.MyFakeContract }}', + }, + }, + }, + }) + }).to.throw( + 'cannot use template strings in contract source names: {{ env.MY_CONTRACT_SOURCE }}' + ) + }) + + it('should throw an error if trying to use a template in a contract name', () => { + expect(() => { + parseChugSplashConfig({ + contracts: { + '{{ env.MY_CONTRACT_NAME }}': { + address: `0x${'11'.repeat(20)}`, + source: 'MyContract', + variables: { + myVariable: '{{ contracts.MyFakeContract }}', + }, + }, + }, + }) + }).to.throw( + 'cannot use template strings in contract names: {{ env.MY_CONTRACT_NAME }}' + ) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 004891bc3cc6..61447ea7dd63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6979,7 +6979,7 @@ gtoken@^5.0.4: google-p12-pem "^3.0.3" jws "^4.0.0" -handlebars@^4.0.1, handlebars@^4.7.6: +handlebars@^4.0.1, handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== From 12e70df05035e7df77d3a4948948d40fd871540a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 18:59:07 -0400 Subject: [PATCH 2/3] break out validation function --- packages/contracts/src/chugsplash/config.ts | 34 +++++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/contracts/src/chugsplash/config.ts b/packages/contracts/src/chugsplash/config.ts index 00f5819a4b35..2667dc75247e 100644 --- a/packages/contracts/src/chugsplash/config.ts +++ b/packages/contracts/src/chugsplash/config.ts @@ -23,18 +23,14 @@ export interface ChugSplashConfig { } /** - * Parses a ChugSplash config file by replacing template values. - * @param config Unparsed config file to parse. - * @param env Environment variables to inject into the file. - * @return Parsed config file with template variables replaced. + * Validates a ChugSplash config file. + * @param config Config file to validate. */ -export const parseChugSplashConfig = ( - config: ChugSplashConfig, - env: any = {} -): ChugSplashConfig => { - config.contracts = config.contracts || {} +const validateChugSplashConfig = (config: ChugSplashConfig) => { + if (config.contracts === undefined) { + throw new Error('contracts field must be defined in ChugSplash config') + } - const contracts = {} for (const [contractName, contractConfig] of Object.entries( config.contracts )) { @@ -61,7 +57,25 @@ export const parseChugSplashConfig = ( `contract address is not a valid address: ${contractConfig.address}` ) } + } +} +/** + * Parses a ChugSplash config file by replacing template values. + * @param config Unparsed config file to parse. + * @param env Environment variables to inject into the file. + * @return Parsed config file with template variables replaced. + */ +export const parseChugSplashConfig = ( + config: ChugSplashConfig, + env: any = {} +): ChugSplashConfig => { + validateChugSplashConfig(config) + + const contracts = {} + for (const [contractName, contractConfig] of Object.entries( + config.contracts + )) { contracts[contractName] = contractConfig.address } From 4e94b7aadb1252ae8b955741d587dbf869242f99 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 19:00:45 -0400 Subject: [PATCH 3/3] fix: minor typo in config tests --- packages/contracts/test/chugsplash/config.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/test/chugsplash/config.spec.ts b/packages/contracts/test/chugsplash/config.spec.ts index 1ce6f151274c..a111d47d1dab 100644 --- a/packages/contracts/test/chugsplash/config.spec.ts +++ b/packages/contracts/test/chugsplash/config.spec.ts @@ -163,7 +163,7 @@ describe('ChugSplash config parsing', () => { }) }) - it('should correctly parse a config file with a templated contract address', () => { + it('should correctly parse a config file with multiple templated contract addresses', () => { expect( parseChugSplashConfig({ contracts: {