diff --git a/packages/cli/src/commands/governance/build-proposal.ts b/packages/cli/src/commands/governance/build-proposal.ts index 858f8aeea08..7235153e1eb 100644 --- a/packages/cli/src/commands/governance/build-proposal.ts +++ b/packages/cli/src/commands/governance/build-proposal.ts @@ -2,6 +2,7 @@ import { InteractiveProposalBuilder, ProposalBuilder } from '@celo/governance/li import { flags } from '@oclif/command' import { writeFileSync } from 'fs-extra' import { BaseCommand } from '../../base' +import { checkProposal } from '../../utils/governance' export default class BuildProposal extends BaseCommand { static description = 'Interactively build a governance proposal' @@ -27,5 +28,10 @@ export default class BuildProposal extends BaseCommand { const output = await promptBuilder.promptTransactions() console.info(`Outputting proposal to ${res.flags.output}`) writeFileSync(res.flags.output!, JSON.stringify(output)) + + output.forEach((tx) => builder.addJsonTx(tx)) + const proposal = await builder.build() + + await checkProposal(proposal, this.kit) } } diff --git a/packages/cli/src/commands/governance/hashhotfix.ts b/packages/cli/src/commands/governance/hashhotfix.ts index 23b643cc601..673583b1037 100644 --- a/packages/cli/src/commands/governance/hashhotfix.ts +++ b/packages/cli/src/commands/governance/hashhotfix.ts @@ -4,6 +4,7 @@ import { flags } from '@oclif/command' import { readFileSync } from 'fs-extra' import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' +import { checkProposal } from '../../utils/governance' export default class HashHotfix extends BaseCommand { static description = 'Hash a governance hotfix specified by JSON and a salt' @@ -14,6 +15,7 @@ export default class HashHotfix extends BaseCommand { required: true, description: 'Path to json transactions of the hotfix', }), + force: flags.boolean({ description: 'Skip execution check', default: false }), salt: flags.string({ required: true, description: 'Secret salt associated with hotfix' }), } @@ -31,6 +33,13 @@ export default class HashHotfix extends BaseCommand { jsonTransactions.forEach((tx) => builder.addJsonTx(tx)) const hotfix = await builder.build() + if (!res.flags.force) { + const ok = await checkProposal(hotfix, this.kit) + if (!ok) { + return + } + } + // Combine with the salt and hash the proposal. const saltBuff = Buffer.from(trimLeading0x(res.flags.salt), 'hex') console.log(`salt: ${res.flags.salt}, buf: ${saltBuff.toString('hex')}`) diff --git a/packages/cli/src/commands/governance/propose.ts b/packages/cli/src/commands/governance/propose.ts index f981a821251..c5ebb057e67 100644 --- a/packages/cli/src/commands/governance/propose.ts +++ b/packages/cli/src/commands/governance/propose.ts @@ -6,6 +6,7 @@ import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx, printValueMapRecursive } from '../../utils/cli' import { Flags } from '../../utils/command' +import { checkProposal } from '../../utils/governance' export default class Propose extends BaseCommand { static description = 'Submit a governance proposal' @@ -17,6 +18,7 @@ export default class Propose extends BaseCommand { }), deposit: flags.string({ required: true, description: 'Amount of Gold to attach to proposal' }), from: Flags.address({ required: true, description: "Proposer's address" }), + force: flags.boolean({ description: 'Skip execution check', default: false }), descriptionURL: flags.string({ required: true, description: 'A URL where further information about the proposal can be viewed', @@ -55,6 +57,14 @@ export default class Propose extends BaseCommand { printValueMapRecursive(await proposalToJSON(this.kit, proposal)) const governance = await this.kit.contracts.getGovernance() + + if (!res.flags.force) { + const ok = await checkProposal(proposal, this.kit) + if (!ok) { + return + } + } + await displaySendTx( 'proposeTx', governance.propose(proposal, res.flags.descriptionURL), diff --git a/packages/cli/src/commands/governance/test-proposal.ts b/packages/cli/src/commands/governance/test-proposal.ts new file mode 100644 index 00000000000..069181b922d --- /dev/null +++ b/packages/cli/src/commands/governance/test-proposal.ts @@ -0,0 +1,43 @@ +import { ProposalBuilder, proposalToJSON, ProposalTransactionJSON } from '@celo/governance' +import { flags } from '@oclif/command' +import { readFileSync } from 'fs' +import { BaseCommand } from '../../base' +import { printValueMapRecursive } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { executeProposal } from '../../utils/governance' +export default class TestProposal extends BaseCommand { + static description = 'Test a governance proposal' + + static hidden = true + + static flags = { + ...BaseCommand.flags, + jsonTransactions: flags.string({ + required: true, + description: 'Path to json transactions', + }), + from: Flags.address({ required: true, description: "Proposer's address" }), + } + + static examples = [ + 'test-proposal --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --jsonTransactions proposal.json', + ] + + async run() { + const res = this.parse(TestProposal) + const account = res.flags.from + this.kit.defaultAccount = account + + const builder = new ProposalBuilder(this.kit) + + // BUILD FROM JSON + const jsonString = readFileSync(res.flags.jsonTransactions).toString() + const jsonTransactions: ProposalTransactionJSON[] = JSON.parse(jsonString) + jsonTransactions.forEach((tx) => builder.addJsonTx(tx)) + + const proposal = await builder.build() + printValueMapRecursive(await proposalToJSON(this.kit, proposal)) + + await executeProposal(proposal, this.kit, account) + } +} diff --git a/packages/cli/src/utils/governance.ts b/packages/cli/src/utils/governance.ts new file mode 100644 index 00000000000..71b8195120b --- /dev/null +++ b/packages/cli/src/utils/governance.ts @@ -0,0 +1,53 @@ +import { toTxResult } from '@celo/connect' +import { ContractKit } from '@celo/contractkit' +import { ProposalTransaction } from '@celo/contractkit/src/wrappers/Governance' +import chalk from 'chalk' + +export async function checkProposal(proposal: ProposalTransaction[], kit: ContractKit) { + const governance = await kit.contracts.getGovernance() + return tryProposal(proposal, kit, governance.address, true) +} + +export async function executeProposal( + proposal: ProposalTransaction[], + kit: ContractKit, + from: string +) { + return tryProposal(proposal, kit, from, false) +} + +async function tryProposal( + proposal: ProposalTransaction[], + kit: ContractKit, + from: string, + call: boolean +) { + console.log('Simulating proposal execution') + let ok = true + for (const [i, tx] of proposal.entries()) { + if (!tx.to) { + continue + } + + try { + if (call) { + await kit.web3.eth.call({ + to: tx.to, + from, + value: tx.value, + data: tx.input, + }) + } else { + const txRes = toTxResult( + kit.web3.eth.sendTransaction({ to: tx.to, from, value: tx.value, data: tx.input }) + ) + await txRes.waitReceipt() + } + console.log(chalk.green(` ${chalk.bold('✔')} Transaction ${i} success!`)) + } catch (err) { + console.log(chalk.red(` ${chalk.bold('✘')} Transaction ${i} failure: ${err.toString()}`)) + ok = false + } + } + return ok +} diff --git a/packages/docs/command-line-interface/governance.md b/packages/docs/command-line-interface/governance.md index cdd1eaa007d..745f714a2d3 100644 --- a/packages/docs/command-line-interface/governance.md +++ b/packages/docs/command-line-interface/governance.md @@ -105,6 +105,7 @@ USAGE $ celocli governance:hashhotfix OPTIONS + --force Skip execution check --globalHelp View all available global flags --jsonTransactions=jsonTransactions (required) Path to json transactions of the @@ -189,6 +190,8 @@ OPTIONS information about the proposal can be viewed + --force Skip execution check + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Proposer's address --globalHelp View all available global flags