diff --git a/.circleci/config.yml b/.circleci/config.yml index 946d316eff3..e735eecf578 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -448,10 +448,12 @@ jobs: - attach_workspace: at: ~/app - run: - name: Test + name: Generate DevChain command: | - set -euo pipefail - yarn --cwd=packages/cli test + (cd packages/cli && yarn test:reset) + - run: + name: Run Tests + command: yarn --cwd=packages/cli test - run: name: Fail if someone forgot to commit CLI docs command: | diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index 7f32800acf2..846c3224039 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -7,3 +7,4 @@ node_modules oclif.manifest.json src/generated +.devchain/ \ No newline at end of file diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 2681a75d5a6..aa5c4c41f55 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -2,4 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['/src/**/?(*.)+(spec|test).ts?(x)'], + setupFilesAfterEnv: ['/src/test-utils/matchers.ts'], + globalSetup: '/src/test-utils/ganache.setup.ts', + globalTeardown: '/src/test-utils/ganache.teardown.ts', } diff --git a/packages/cli/package.json b/packages/cli/package.json index 571ac7f388f..723af7482a8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,7 +26,9 @@ "docs": "yarn oclif-dev readme --multi --dir=../docs/command-line-interface && yarn prettier ../docs/command-line-interface/*.md --write", "lint": "tslint -c tslint.json --project tsconfig.json", "prepack": "yarn run build && oclif-dev manifest && oclif-dev readme", - "test": "TZ=UTC jest" + "test:reset": "yarn --cwd ../protocol devchain generate .devchain", + "test:livechain": "yarn --cwd ../protocol devchain run .devchain", + "test": "TZ=UTC jest --runInBand" }, "dependencies": { "@celo/contractkit": "^0.1.6", diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts new file mode 100644 index 00000000000..1127bd6afcf --- /dev/null +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -0,0 +1,21 @@ +import Web3 from 'web3' +import { testWithGanache } from '../../test-utils/ganache-test' +import Authorize from './authorize' +import Register from './register' + +process.env.NO_SYNCCHECK = 'true' + +testWithGanache('account:authorize cmd', (web3: Web3) => { + test('can authorize account', async () => { + const accounts = await web3.eth.getAccounts() + await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) + await Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + }) + + test('fails if from is not an account', async () => { + const accounts = await web3.eth.getAccounts() + await expect( + Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + ).rejects.toThrow() + }) +}) diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index eaf496f0ffa..f34239dae7c 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -1,5 +1,6 @@ import { flags } from '@oclif/command' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -38,6 +39,11 @@ export default class Authorize extends BaseCommand { this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + + await newCheckBuilder(this) + .isAccount(res.flags.from) + .runChecks() + let tx: any if (res.flags.role === 'vote') { tx = await accounts.authorizeVoteSigner(res.flags.from, res.flags.to) diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/account/lock.ts similarity index 78% rename from packages/cli/src/commands/lockedgold/lock.ts rename to packages/cli/src/commands/account/lock.ts index d9f59f2bcaf..b46b49df012 100644 --- a/packages/cli/src/commands/lockedgold/lock.ts +++ b/packages/cli/src/commands/account/lock.ts @@ -2,7 +2,8 @@ import { Address } from '@celo/utils/lib/address' import { flags } from '@oclif/command' import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' -import { displaySendTx, failWith } from '../../utils/cli' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' import { LockedGoldArgs } from '../../utils/lockedgold' @@ -26,14 +27,15 @@ export default class Lock extends BaseCommand { const address: Address = res.flags.from this.kit.defaultAccount = address - const lockedGold = await this.kit.contracts.getLockedGold() - const value = new BigNumber(res.flags.value) - if (!value.gt(new BigNumber(0))) { - failWith(`Provided value must be greater than zero => [${value.toString()}]`) - } + await newCheckBuilder(this) + .addCheck(`Value [${value.toString()}] is >= 0`, () => value.gt(0)) + .isAccount(address) + .hasEnoughGold(address, value) + .runChecks() + const lockedGold = await this.kit.contracts.getLockedGold() const tx = lockedGold.lock() await displaySendTx('lock', tx, { value: value.toString() }) } diff --git a/packages/cli/src/commands/account/register.test.ts b/packages/cli/src/commands/account/register.test.ts new file mode 100644 index 00000000000..444dd71a0bc --- /dev/null +++ b/packages/cli/src/commands/account/register.test.ts @@ -0,0 +1,19 @@ +import Web3 from 'web3' +import { testWithGanache } from '../../test-utils/ganache-test' +import Register from './register' + +process.env.NO_SYNCCHECK = 'true' + +testWithGanache('account:register cmd', (web3: Web3) => { + test('can register account', async () => { + const accounts = await web3.eth.getAccounts() + + await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) + }) + + test('fails if from is missing', async () => { + // const accounts = await web3.eth.getAccounts() + + await expect(Register.run([])).rejects.toThrow('Missing required flag') + }) +}) diff --git a/packages/cli/src/commands/account/register.ts b/packages/cli/src/commands/account/register.ts index b12462caecf..ea121e9cdfc 100644 --- a/packages/cli/src/commands/account/register.ts +++ b/packages/cli/src/commands/account/register.ts @@ -1,5 +1,6 @@ import { flags } from '@oclif/command' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -20,7 +21,11 @@ export default class Register extends BaseCommand { const res = this.parse(Register) this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + + await newCheckBuilder(this) + .isNotAccount(res.flags.from) + .runChecks() await displaySendTx('register', accounts.createAccount()) - await displaySendTx('setName', accounts.setName(name)) + await displaySendTx('setName', accounts.setName(res.flags.name)) } } diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index 9c9c90c089c..b959faa599a 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,4 +1,5 @@ import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { printValueMapRecursive } from '../../utils/cli' import { Args } from '../../utils/command' @@ -14,10 +15,14 @@ export default class Show extends BaseCommand { static examples = ['show 0x5409ed021d9299bf6814279a6a1411a7e866a631'] async run() { - // tslint:disable-next-line const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() + + await newCheckBuilder(this) + .isAccount(args.account) + .runChecks() + printValueMapRecursive(await lockedGold.getAccountSummary(args.account)) } } diff --git a/packages/cli/src/commands/lockedgold/unlock.ts b/packages/cli/src/commands/lockedgold/unlock.ts index 1b04e9980c1..2438a474aac 100644 --- a/packages/cli/src/commands/lockedgold/unlock.ts +++ b/packages/cli/src/commands/lockedgold/unlock.ts @@ -1,5 +1,6 @@ import { flags } from '@oclif/command' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' import { LockedGoldArgs } from '../../utils/lockedgold' @@ -21,6 +22,11 @@ export default class Unlock extends BaseCommand { const res = this.parse(Unlock) this.kit.defaultAccount = res.flags.from const lockedgold = await this.kit.contracts.getLockedGold() + + await newCheckBuilder(this) + .isAccount(res.flags.from) + .runChecks() + await displaySendTx('unlock', lockedgold.unlock(res.flags.value)) } } diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index 06383dea399..6ad636ce8cf 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -1,4 +1,5 @@ import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -17,8 +18,12 @@ export default class Withdraw extends BaseCommand { const { flags } = this.parse(Withdraw) this.kit.defaultAccount = flags.from const lockedgold = await this.kit.contracts.getLockedGold() - const currentTime = Math.round(new Date().getTime() / 1000) + await newCheckBuilder(this) + .isAccount(flags.from) + .runChecks() + + const currentTime = Math.round(new Date().getTime() / 1000) while (true) { let madeWithdrawal = false const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) diff --git a/packages/cli/src/commands/validator/affiliate.ts b/packages/cli/src/commands/validator/affiliate.ts new file mode 100644 index 00000000000..26dda68f8a4 --- /dev/null +++ b/packages/cli/src/commands/validator/affiliate.ts @@ -0,0 +1,37 @@ +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Args, Flags } from '../../utils/command' + +export default class ValidatorAffiliate extends BaseCommand { + static description = 'Affiliate to a ValidatorGroup' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Signer or Validator's address" }), + } + + static args: IArg[] = [ + Args.address('groupAddress', { description: "ValidatorGroup's address", required: true }), + ] + + static examples = [ + 'affiliate --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 0x97f7333c51897469e8d98e7af8653aab468050a3', + ] + + async run() { + const res = this.parse(ValidatorAffiliate) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .isValidatorGroup(res.args.groupAddress) + .runChecks() + + await displaySendTx('affiliate', validators.affiliate(res.args.groupAddress)) + } +} diff --git a/packages/cli/src/commands/validator/affiliation.ts b/packages/cli/src/commands/validator/affiliation.ts deleted file mode 100644 index a2930f98bce..00000000000 --- a/packages/cli/src/commands/validator/affiliation.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class ValidatorAffiliate extends BaseCommand { - static description = 'Manage affiliation to a ValidatorGroup' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Validator's address" }), - unset: flags.boolean({ exclusive: ['set'], description: 'clear affiliation field' }), - set: Flags.address({ - description: 'set affiliation to given address', - exclusive: ['unset'], - }), - } - - static examples = [ - 'affiliation --set 0x97f7333c51897469e8d98e7af8653aab468050a3 --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', - 'affiliation --unset --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', - ] - - async run() { - const res = this.parse(ValidatorAffiliate) - - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - - if (!(res.flags.set || res.flags.unset)) { - this.error(`Specify action: --set or --unset`) - return - } - - if (res.flags.set) { - await displaySendTx('affiliate', validators.affiliate(res.flags.set)) - } else if (res.flags.unset) { - await displaySendTx('deaffiliate', validators.deaffiliate()) - } - } -} diff --git a/packages/cli/src/commands/validator/deaffiliate.ts b/packages/cli/src/commands/validator/deaffiliate.ts new file mode 100644 index 00000000000..54846cc13c4 --- /dev/null +++ b/packages/cli/src/commands/validator/deaffiliate.ts @@ -0,0 +1,29 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorDeAffiliate extends BaseCommand { + static description = 'DeAffiliate to a ValidatorGroup' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Signer or Validator's address" }), + } + + static examples = ['deaffiliate --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95'] + + async run() { + const res = this.parse(ValidatorDeAffiliate) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + await displaySendTx('deaffiliate', validators.deaffiliate()) + } +} diff --git a/packages/cli/src/commands/validator/deregister.ts b/packages/cli/src/commands/validator/deregister.ts new file mode 100644 index 00000000000..9c6184c73f1 --- /dev/null +++ b/packages/cli/src/commands/validator/deregister.ts @@ -0,0 +1,31 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorDeregister extends BaseCommand { + static description = 'Deregister a Validator' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Signer or Validator's address" }), + } + + static examples = ['deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95'] + + async run() { + const res = this.parse(ValidatorDeregister) + + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + const validator = await validators.signerToAccount(res.flags.from) + await displaySendTx('deregister', await validators.deregisterValidator(validator)) + } +} diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index 91b569bd255..bc50dd33dd6 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,4 +1,5 @@ import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' @@ -18,8 +19,16 @@ export default class ValidatorRegister extends BaseCommand { async run() { const res = this.parse(ValidatorRegister) this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() const accounts = await this.kit.contracts.getAccounts() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerMeetsValidatorBalanceRequirements() + .runChecks() + await displaySendTx( 'registerValidator', validators.registerValidator(res.flags.publicKey as any) diff --git a/packages/cli/src/commands/validator/requirements.ts b/packages/cli/src/commands/validator/requirements.ts new file mode 100644 index 00000000000..0cb871b268f --- /dev/null +++ b/packages/cli/src/commands/validator/requirements.ts @@ -0,0 +1,22 @@ +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' + +export default class ValidatorRequirements extends BaseCommand { + static description = 'Get Requirements for Validators' + + static flags = { + ...BaseCommand.flags, + } + + static examples = ['requirements'] + + async run() { + this.parse(ValidatorRequirements) + + const validators = await this.kit.contracts.getValidators() + + const requirements = await validators.getValidatorLockedGoldRequirements() + + printValueMap(requirements) + } +} diff --git a/packages/cli/src/commands/validator/show.ts b/packages/cli/src/commands/validator/show.ts index 8cb37c85c2f..17ed3bdcf29 100644 --- a/packages/cli/src/commands/validator/show.ts +++ b/packages/cli/src/commands/validator/show.ts @@ -1,5 +1,6 @@ import { IArg } from '@oclif/parser/lib/args' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' @@ -18,6 +19,11 @@ export default class ValidatorShow extends BaseCommand { const { args } = this.parse(ValidatorShow) const address = args.validatorAddress const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this) + .isValidator(address) + .runChecks() + const validator = await validators.getValidator(address) printValueMap(validator) } diff --git a/packages/cli/src/commands/validatorgroup/deregister.ts b/packages/cli/src/commands/validatorgroup/deregister.ts new file mode 100644 index 00000000000..57c0aeefdbb --- /dev/null +++ b/packages/cli/src/commands/validatorgroup/deregister.ts @@ -0,0 +1,32 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorGroupDeRegister extends BaseCommand { + static description = 'Deregister a ValidatorGroup' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Signer or ValidatorGroup's address" }), + } + + static examples = ['deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95'] + + async run() { + const res = this.parse(ValidatorGroupDeRegister) + + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + + const account = await validators.signerToAccount(res.flags.from) + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidatorGroup() + .runChecks() + + await displaySendTx('deregister', await validators.deregisterValidatorGroup(account)) + } +} diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 5470ece4b67..c4130c66dae 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -1,6 +1,7 @@ import { flags } from '@oclif/command' import { IArg } from '@oclif/parser/lib/args' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' @@ -42,22 +43,24 @@ export default class ValidatorGroupMembers extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidatorGroup() + .isValidator(res.args.validatorAddress) + .runChecks() + + const validatorGroup = await validators.signerToAccount(res.flags.from) if (res.flags.accept) { - const tx = await validators.addMember(res.flags.from, (res.args as any).validatorAddress) + const tx = await validators.addMember(validatorGroup, res.args.validatorAddress) await displaySendTx('addMember', tx) } else if (res.flags.remove) { - await displaySendTx( - 'removeMember', - validators.removeMember((res.args as any).validatorAddress) - ) + await displaySendTx('removeMember', validators.removeMember(res.args.validatorAddress)) } else if (res.flags.reorder != null) { await displaySendTx( 'reorderMember', - await validators.reorderMember( - res.flags.from, - (res.args as any).validatorAddress, - res.flags.reorder - ) + await validators.reorderMember(validatorGroup, res.args.validatorAddress, res.flags.reorder) ) } } diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index 1580cfaca30..ba4b6c17f45 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,6 +1,7 @@ import { flags } from '@oclif/command' import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -22,7 +23,16 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - const tx = await validators.registerValidatorGroup(new BigNumber(res.flags.commission)) + const commission = new BigNumber(res.flags.commission) + + await newCheckBuilder(this, res.flags.from) + .addCheck('Commission is in range [0,1]', () => commission.gte(0) && commission.lte(1)) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerMeetsValidatorGroupBalanceRequirements() + .runChecks() + + const tx = await validators.registerValidatorGroup(commission) await displaySendTx('registerValidatorGroup', tx) } } diff --git a/packages/cli/src/commands/validatorgroup/show.ts b/packages/cli/src/commands/validatorgroup/show.ts index 8093524e648..835d896d920 100644 --- a/packages/cli/src/commands/validatorgroup/show.ts +++ b/packages/cli/src/commands/validatorgroup/show.ts @@ -1,5 +1,6 @@ import { IArg } from '@oclif/parser/lib/args' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' @@ -17,6 +18,11 @@ export default class ValidatorGroupShow extends BaseCommand { async run() { const { args } = this.parse(ValidatorGroupShow) const validators = await this.kit.contracts.getValidators() + + await newCheckBuilder(this) + .isValidatorGroup(args.groupAddress) + .runChecks() + const validatorGroup = await validators.getValidatorGroup(args.groupAddress) printValueMap(validatorGroup) } diff --git a/packages/cli/src/test-utils/PromiEventStub.ts b/packages/cli/src/test-utils/PromiEventStub.ts new file mode 100644 index 00000000000..36890742970 --- /dev/null +++ b/packages/cli/src/test-utils/PromiEventStub.ts @@ -0,0 +1,42 @@ +import { EventEmitter } from 'events' +import PromiEvent from 'web3/promiEvent' +import { TransactionReceipt } from 'web3/types' + +interface PromiEventStub extends PromiEvent { + emitter: EventEmitter + resolveHash(hash: string): void + resolveReceipt(receipt: TransactionReceipt): void + rejectHash(error: any): void + rejectReceipt(receipt: TransactionReceipt, error: any): void +} +export function promiEventSpy(): PromiEventStub { + const ee = new EventEmitter() + const pe: PromiEventStub = { + catch: () => { + throw new Error('not implemented') + }, + then: () => { + throw new Error('not implemented') + }, + finally: () => { + throw new Error('not implemented') + }, + on: ((event: string, listener: (...args: any[]) => void) => ee.on(event, listener)) as any, + once: ((event: string, listener: (...args: any[]) => void) => ee.once(event, listener)) as any, + [Symbol.toStringTag]: 'Not Implemented', + emitter: ee, + resolveHash: (hash: string) => { + ee.emit('transactionHash', hash) + }, + resolveReceipt: (receipt: TransactionReceipt) => { + ee.emit('receipt', receipt) + }, + rejectHash: (error: any) => { + ee.emit('error', error, false) + }, + rejectReceipt: (receipt: TransactionReceipt, error: any) => { + ee.emit('error', error, receipt) + }, + } + return pe +} diff --git a/packages/cli/src/test-utils/ganache-test.ts b/packages/cli/src/test-utils/ganache-test.ts new file mode 100644 index 00000000000..6581e91d652 --- /dev/null +++ b/packages/cli/src/test-utils/ganache-test.ts @@ -0,0 +1,63 @@ +import Web3 from 'web3' +import { JsonRPCResponse } from 'web3/providers' + +export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promise { + return new Promise((resolve, reject) => { + web3.currentProvider.send( + { + id: new Date().getTime(), + jsonrpc: '2.0', + method, + params, + }, + (err: Error | null, res?: JsonRPCResponse) => { + if (err) { + reject(err) + } else if (!res) { + reject(new Error('no response')) + } else if (res.error) { + reject( + new Error( + `Failed JsonRPCResponse: method: ${method} params: ${params} error: ${JSON.stringify( + res.error + )}` + ) + ) + } else { + resolve(res.result) + } + } + ) + }) +} + +export function evmRevert(web3: Web3, snapId: string): Promise { + return jsonRpcCall(web3, 'evm_revert', [snapId]) +} + +export function evmSnapshot(web3: Web3) { + return jsonRpcCall(web3, 'evm_snapshot', []) +} + +export function testWithGanache(name: string, fn: (web3: Web3) => void) { + const web3 = new Web3('http://localhost:8545') + + describe(name, () => { + let snapId: string | null = null + + beforeEach(async () => { + if (snapId != null) { + await evmRevert(web3, snapId) + } + snapId = await evmSnapshot(web3) + }) + + afterAll(async () => { + if (snapId != null) { + await evmRevert(web3, snapId) + } + }) + + fn(web3) + }) +} diff --git a/packages/cli/src/test-utils/ganache.setup.ts b/packages/cli/src/test-utils/ganache.setup.ts new file mode 100644 index 00000000000..de02dd978c3 --- /dev/null +++ b/packages/cli/src/test-utils/ganache.setup.ts @@ -0,0 +1,61 @@ +// @ts-ignore +import * as ganache from '@celo/ganache-cli' +import * as path from 'path' + +const MNEMONIC = 'concert load couple harbor equip island argue ramp clarify fence smart topic' + +export async function startGanache(datadir: string, opts: { verbose?: boolean } = {}) { + const logFn = opts.verbose + ? // tslint:disable-next-line: no-console + (...args: any[]) => console.log(...args) + : () => { + /*nothing*/ + } + + const server = ganache.server({ + default_balance_ether: 1000000, + logger: { + log: logFn, + }, + network_id: 1101, + db_path: datadir, + mnemonic: MNEMONIC, + gasLimit: 7000000, + allowUnlimitedContractSize: true, + }) + + await new Promise((resolve, reject) => { + server.listen(8545, (err: any, blockchain: any) => { + if (err) { + reject(err) + } else { + resolve(blockchain) + } + }) + }) + + return () => + new Promise((resolve, reject) => { + server.close((err: any) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +export default function setup() { + const DATADIR = path.resolve(path.join(__dirname, '../../.devchain')) + // console.log('Starting Ganache: datadir=', DATADIR) + return startGanache(DATADIR) + .then((stopGanache) => { + ;(global as any).stopGanache = stopGanache + }) + .catch((err) => { + console.error('Error starting ganache, Doing `yarn test:prepare` might help') + console.error(err) + process.exit(1) + }) +} diff --git a/packages/cli/src/test-utils/ganache.teardown.ts b/packages/cli/src/test-utils/ganache.teardown.ts new file mode 100644 index 00000000000..27400b9a1aa --- /dev/null +++ b/packages/cli/src/test-utils/ganache.teardown.ts @@ -0,0 +1,7 @@ +export default function tearDown() { + console.log('Stopping ganache') + return (global as any).stopGanache().catch((err: any) => { + console.error('error stopping ganache') + console.error(err) + }) +} diff --git a/packages/cli/src/test-utils/matchers.ts b/packages/cli/src/test-utils/matchers.ts new file mode 100644 index 00000000000..992831e6c95 --- /dev/null +++ b/packages/cli/src/test-utils/matchers.ts @@ -0,0 +1,42 @@ +import BigNumber from 'bignumber.js' + +declare global { + namespace jest { + interface Matchers { + toBeBigNumber(): R + toEqBigNumber(expected: BigNumber | string | number): R + } + } +} + +expect.extend({ + toBeBigNumber(received: any) { + const pass = BigNumber.isBigNumber(received) + if (pass) { + return { + message: () => `expected ${received} not to be BigNumber`, + pass: true, + } + } else { + return { + message: () => `expected ${received} to be bigNumber`, + pass: false, + } + } + }, + toEqBigNumber(received: BigNumber, _expected: BigNumber | string | number) { + const expected = new BigNumber(_expected) + const pass = expected.eq(received) + if (pass) { + return { + message: () => `expected ${received.toString()} not to equal ${expected.toString()}`, + pass: true, + } + } else { + return { + message: () => `expected ${received.toString()} to equal ${expected.toString()}`, + pass: false, + } + } + }, +}) diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts new file mode 100644 index 00000000000..0e79ab09874 --- /dev/null +++ b/packages/cli/src/utils/checks.ts @@ -0,0 +1,171 @@ +import { Address } from '@celo/contractkit' +import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { LockedGoldWrapper } from '@celo/contractkit/lib/wrappers/LockedGold' +import { ValidatorsWrapper } from '@celo/contractkit/lib/wrappers/Validators' +import BigNumber from 'bignumber.js' +import chalk from 'chalk' +import { BaseCommand } from '../base' + +export interface CommandCheck { + name: string + run(): Promise | boolean +} + +export function check(name: string, predicate: () => Promise | boolean): CommandCheck { + return { + name, + run: predicate, + } +} + +const negate = (x: Promise) => x.then((y) => !y) + +type Resolve = A extends Promise ? T : A + +export function newCheckBuilder(cmd: BaseCommand, signer?: Address) { + return new CheckBuilder(cmd, signer) +} + +class CheckBuilder { + private checks: CommandCheck[] = [] + + constructor(private cmd: BaseCommand, private signer?: Address) {} + + get kit() { + return this.cmd.kit + } + + withValidators( + f: (validators: ValidatorsWrapper, signer: Address, account: Address) => A + ): () => Promise> { + return async () => { + const validators = await this.kit.contracts.getValidators() + if (this.signer) { + const account = await validators.signerToAccount(this.signer) + return f(validators, this.signer, account) as Resolve + } else { + return f(validators, '', '') as Resolve + } + } + } + + withLockedGold(f: (lockedGold: LockedGoldWrapper) => A): () => Promise> { + return async () => { + const lockedGold = await this.kit.contracts.getLockedGold() + return f(lockedGold) as Resolve + } + } + + withAccounts(f: (lockedGold: AccountsWrapper) => A): () => Promise> { + return async () => { + const accounts = await this.kit.contracts.getAccounts() + return f(accounts) as Resolve + } + } + + addCheck(name: string, predicate: () => Promise | boolean) { + this.checks.push(check(name, predicate)) + return this + } + + canSignValidatorTxs = () => + this.addCheck( + 'Signer can sign Validator Txs', + this.withAccounts((lg) => + lg + .activeValidationSignerToAccount(this.signer!) + .then(() => true) + .catch(() => false) + ) + ) + + signerAccountIsValidator = () => + this.addCheck( + `Signer account is Validator`, + this.withValidators((v, _s, account) => v.isValidator(account)) + ) + + signerAccountIsValidatorGroup = () => + this.addCheck( + `Signer account is ValidatorGroup`, + this.withValidators((v, _s, account) => v.isValidatorGroup(account)) + ) + + isValidator = (account: Address) => + this.addCheck(`${account} is Validator`, this.withValidators((v) => v.isValidator(account))) + + isValidatorGroup = (account: Address) => + this.addCheck( + `${account} is ValidatorGroup`, + this.withValidators((v) => v.isValidatorGroup(account)) + ) + + signerMeetsValidatorBalanceRequirements = () => + this.addCheck( + `Signer's account has enough locked gold for registration`, + this.withValidators((v, _signer, account) => v.meetsValidatorBalanceRequirements(account)) + ) + + signerMeetsValidatorGroupBalanceRequirements = () => + this.addCheck( + `Signer's account has enough locked gold for registration`, + this.withValidators((v, _signer, account) => + v.meetsValidatorGroupBalanceRequirements(account) + ) + ) + + isNotAccount = (address: Address) => + this.addCheck( + `${address} is not an Account`, + this.withAccounts((accs) => negate(accs.isAccount(address))) + ) + + isSignerOrAccount = () => + this.addCheck( + `${this.signer!} is Signer or Account`, + this.withAccounts(async (accs) => { + const res = (await accs.isAccount(this.signer!)) || (await accs.isSigner(this.signer!)) + return res + }) + ) + + isAccount = (address: Address) => + this.addCheck(`${address} is Account`, this.withAccounts((accs) => accs.isAccount(address))) + + hasEnoughGold = (account: Address, value: BigNumber) => { + const valueInEth = this.kit.web3.utils.fromWei(value.toFixed(), 'ether') + return this.addCheck(`Account has at least ${valueInEth} cGold`, () => + this.kit.contracts + .getGoldToken() + .then((gt) => gt.balanceOf(account)) + .then((balance) => balance.gte(value)) + ) + } + + async runChecks() { + console.log(`Running Checks:`) + let allPassed = true + for (const aCheck of this.checks) { + const passed = await aCheck.run() + const status︎Str = chalk.bold(passed ? '✔' : '✘') + const color = passed ? chalk.green : chalk.red + console.log(color(` ${status︎Str} ${aCheck.name}`)) + allPassed = allPassed && passed + } + + if (!allPassed) { + return this.cmd.error("Some checks didn't pass!") + } + } + + // async executeValidatorTx( + // name: string, + // f: ( + // validators: ValidatorsWrapper, + // signer: Address, + // account: Address + // ) => Promise> | CeloTransactionObject + // ) { + + // } +} diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index f97160513e6..988effe40f9 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -1,4 +1,5 @@ import { CeloTransactionObject } from '@celo/contractkit' +import { CLIError } from '@oclif/errors' import BigNumber from 'bignumber.js' import chalk from 'chalk' import Table from 'cli-table' @@ -36,8 +37,8 @@ export function printValueMapRecursive(valueMap: Record) { function toStringValueMapRecursive(valueMap: Record, prefix: string): string { const printValue = (v: any): string => { - if (typeof v === 'object') { - if (v instanceof BigNumber) return v.toString(10) + if (typeof v === 'object' && v != null) { + if (v instanceof BigNumber) return v.toFixed() return '\n' + toStringValueMapRecursive(v, prefix + ' ') } return chalk`${v}` @@ -56,6 +57,5 @@ export function printVTable(valueMap: Record) { } export function failWith(msg: string): never { - console.error(msg) - return process.exit(1) + throw new CLIError(msg) } diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index 742a3b64596..c1ad8113bfa 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,22 +1,22 @@ import { flags } from '@oclif/command' +import { CLIError } from '@oclif/errors' import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -import { failWith } from './cli' const parsePublicKey: ParseFn = (input) => { // Check that the string starts with 0x and has byte length of ecdsa pub key (64 bytes) + bls pub key (48 bytes) + proof of pos (96 bytes) if (Web3.utils.isHex(input) && input.length === 418 && input.startsWith('0x')) { return input } else { - return failWith(`${input} is not a public key`) + throw new CLIError(`${input} is not a public key`) } } const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { return input } else { - return failWith(`${input} is not a valid address`) + throw new CLIError(`${input} is not a valid address`) } } @@ -24,7 +24,7 @@ const parsePath: ParseFn = (input) => { if (pathExistsSync(input)) { return input } else { - return failWith(`File at "${input}" does not exist`) + throw new CLIError(`File at "${input}" does not exist`) } } @@ -37,7 +37,7 @@ const parseUrl: ParseFn = (input) => { if (URL_REGEX.test(input)) { return input } else { - return failWith(`"${input}" is not a valid URL`) + throw new CLIError(`"${input}" is not a valid URL`) } } diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index 775752e7170..d95c1354c57 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -25,6 +25,9 @@ export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { } export async function nodeIsSynced(web3: Web3): Promise { + if (process.env.NO_SYNCCHECK) { + return true + } try { // isSyncing() returns an object describing sync progress if syncing is actively // happening, and the boolean value `false` if not. diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index a13aec9b0d1..01afb340612 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -48,6 +48,15 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getValidationSigner ) + /** + * Returns the account address given the signer for validating + * @param signer Address that is authorized to sign the tx as validator + * @return The Account address + */ + activeValidationSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.activeValidationSignerToAccount + ) + /** * Check if an account already exists. * @param account The address of the account @@ -55,6 +64,13 @@ export class AccountsWrapper extends BaseWrapper { */ isAccount: (account: string) => Promise = proxyCall(this.contract.methods.isAccount) + /** + * Check if an address is a signer address + * @param address The address of the account + * @return Returns `true` if account exists. Returns `false` otherwise. + */ + isSigner: (address: string) => Promise = proxyCall(this.contract.methods.isAuthorized) + /** * Authorize an attestation signing key on behalf of this account to another address. * @param account Address of the active account. diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index 5b1f624ac78..ba9d9e7b920 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -36,6 +36,10 @@ export function parseNumber(input: NumberLike) { return new BigNumber(input).toString(10) } +export function parseBytes(input: string): Array { + return input as any +} + type Parser = (input: A) => B /** Identity Parser */ diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 49e9679b313..12740459b99 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -41,16 +41,6 @@ export interface LockedGoldConfig { * Contract for handling deposits needed for voting. */ export class LockedGoldWrapper extends BaseWrapper { - /** - * Unlocks gold that becomes withdrawable after the unlocking period. - * @param value The amount of gold to unlock. - */ - unlock: (value: NumberLike) => CeloTransactionObject = proxySend( - this.kit, - this.contract.methods.unlock, - tupleParser(parseNumber) - ) - /** * Withdraws a gold that has been unlocked after the unlocking period has passed. * @param index The index of the pending withdrawal to withdraw. @@ -59,10 +49,23 @@ export class LockedGoldWrapper extends BaseWrapper { this.kit, this.contract.methods.withdraw ) + /** - * @notice Locks gold to be used for voting. + * Locks gold to be used for voting. + * The gold to be locked, must be specified as the `tx.value` */ lock = proxySend(this.kit, this.contract.methods.lock) + + /** + * Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. + */ + unlock: (value: NumberLike) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.unlock, + tupleParser(parseNumber) + ) + /** * Relocks gold that has been unlocked but not withdrawn. * @param index The index of the pending withdrawal to relock. @@ -82,6 +85,7 @@ export class LockedGoldWrapper extends BaseWrapper { undefined, toBigNumber ) + /** * Returns the total amount of non-voting locked gold for an account. * @param account The account. diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 2ecbbce1fe2..a49ca798eb6 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -80,7 +80,9 @@ testWithGanache('Validators Wrapper', (web3) => { await setupGroup(groupAccount) await setupValidator(validatorAccount) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) - await (await validators.addMember(groupAccount, validatorAccount)).sendAndWaitForReceipt() + await (await validators.addMember(groupAccount, validatorAccount)).sendAndWaitForReceipt({ + from: groupAccount, + }) const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) expect(members).toContain(validatorAccount) @@ -99,7 +101,9 @@ testWithGanache('Validators Wrapper', (web3) => { for (const validator of [validator1, validator2]) { await setupValidator(validator) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) - await (await validators.addMember(groupAccount, validator)).sendAndWaitForReceipt() + await (await validators.addMember(groupAccount, validator)).sendAndWaitForReceipt({ + from: groupAccount, + }) } const members = await validators @@ -111,7 +115,7 @@ testWithGanache('Validators Wrapper', (web3) => { test('move last to first', async () => { await validators .reorderMember(groupAccount, validator2, 0) - .then((x) => x.sendAndWaitForReceipt()) + .then((x) => x.sendAndWaitForReceipt({ from: groupAccount })) const membersAfter = await validators .getValidatorGroup(groupAccount) @@ -122,7 +126,7 @@ testWithGanache('Validators Wrapper', (web3) => { test('move first to last', async () => { await validators .reorderMember(groupAccount, validator1, 1) - .then((x) => x.sendAndWaitForReceipt()) + .then((x) => x.sendAndWaitForReceipt({ from: groupAccount })) const membersAfter = await validators .getValidatorGroup(groupAccount) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index d2f3c303b97..c06c61fba75 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,3 +1,5 @@ +import { eqAddress } from '@celo/utils/lib/address' +import { zip } from '@celo/utils/lib/collections' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { Address, NULL_ADDRESS } from '../base' @@ -5,10 +7,13 @@ import { Validators } from '../generated/types/Validators' import { BaseWrapper, CeloTransactionObject, + parseBytes, proxyCall, proxySend, toBigNumber, + toNumber, toTransactionObject, + tupleParser, } from './BaseWrapper' export interface Validator { @@ -35,37 +40,16 @@ export interface ValidatorsConfig { maxGroupSize: BigNumber } +export interface GroupMembership { + epoch: number + group: Address +} + /** * Contract for voting for validators and managing validator groups. */ // TODO(asa): Support authorized validators export class ValidatorsWrapper extends BaseWrapper { - affiliate = proxySend(this.kit, this.contract.methods.affiliate) - deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) - removeMember = proxySend(this.kit, this.contract.methods.removeMember) - registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) - async registerValidatorGroup(commission: BigNumber): Promise> { - return toTransactionObject( - this.kit, - this.contract.methods.registerValidatorGroup(toFixed(commission).toFixed()) - ) - } - async addMember(group: string, member: string): Promise> { - const numMembers = await this.getGroupNumMembers(group) - if (numMembers.isZero()) { - const election = await this.kit.contracts.getElection() - const voteWeight = await election.getTotalVotesForGroup(group) - const { lesser, greater } = await election.findLesserAndGreaterAfterVote(group, voteWeight) - - return toTransactionObject( - this.kit, - this.contract.methods.addFirstMember(member, lesser, greater), - { from: group } - ) - } else { - return toTransactionObject(this.kit, this.contract.methods.addMember(member), { from: group }) - } - } /** * Returns the Locked Gold requirements for validators. * @returns The Locked Gold requirements for validators. @@ -106,18 +90,51 @@ export class ValidatorsWrapper extends BaseWrapper { } } - async getRegisteredValidators(): Promise { - const vgAddresses = await this.contract.methods.getRegisteredValidators().call() + async signerToAccount(signerAddress: Address) { + const accounts = await this.kit.contracts.getAccounts() + return accounts.activeValidationSignerToAccount(signerAddress) + } - return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) + /** + * Returns whether a particular account has a registered validator. + * @param account The account. + * @return Whether a particular address is a registered validator. + */ + isValidator = proxyCall(this.contract.methods.isValidator) + + /** + * Returns whether a particular account has a registered validator group. + * @param account The account. + * @return Whether a particular address is a registered validator group. + */ + isValidatorGroup = proxyCall(this.contract.methods.isValidatorGroup) + + /** + * Returns whether an account meets the requirements to register a validator. + * @param account The account. + * @return Whether an account meets the requirements to register a validator. + */ + meetsValidatorBalanceRequirements = async (address: Address) => { + const lockedGold = await this.kit.contracts.getLockedGold() + const total = await lockedGold.getAccountTotalLockedGold(address) + const reqs = await this.getValidatorLockedGoldRequirements() + return reqs.value.lte(total) } - getGroupNumMembers: (group: Address) => Promise = proxyCall( - this.contract.methods.getGroupNumMembers, - undefined, - toBigNumber - ) + /** + * Returns whether an account meets the requirements to register a group. + * @param account The account. + * @return Whether an account meets the requirements to register a group. + */ + meetsValidatorGroupBalanceRequirements = async (address: Address) => { + const lockedGold = await this.kit.contracts.getLockedGold() + const total = await lockedGold.getAccountTotalLockedGold(address) + const reqs = await this.getGroupLockedGoldRequirements() + return reqs.value.lte(total) + } + + /** Get Validator information */ async getValidator(address: Address): Promise { const res = await this.contract.methods.getValidator(address).call() return { @@ -128,20 +145,173 @@ export class ValidatorsWrapper extends BaseWrapper { } } + /** Get ValidatorGroup information */ + async getValidatorGroup(address: Address): Promise { + const res = await this.contract.methods.getValidatorGroup(address).call() + return { + address, + members: res[0], + commission: fromFixed(new BigNumber(res[1])), + } + } + /** - * Returns whether a particular account has a registered validator. - * @param account The account. - * @return Whether a particular address is a registered validator. + * Returns the Validator's group membership history + * @param validator The validator whose membership history to return. + * @return The group membership history of a validator. */ - isValidator = proxyCall(this.contract.methods.isValidator) + getValidatorMembershipHistory: (validator: Address) => Promise = proxyCall( + this.contract.methods.getMembershipHistory, + undefined, + (res) => + // tslint:disable-next-line: no-object-literal-type-assertion + zip((epoch, group) => ({ epoch: toNumber(epoch), group } as GroupMembership), res[0], res[1]) + ) + + /** Get the size (amount of members) of a ValidatorGroup */ + getValidatorGroupSize: (group: Address) => Promise = proxyCall( + this.contract.methods.getGroupNumMembers, + undefined, + toNumber + ) + + /** Get list of registered validator addresses */ + getRegisteredValidatorsAddresses: () => Promise = proxyCall( + this.contract.methods.getRegisteredValidators + ) + + /** Get list of registered validator group addresses */ + getRegisteredValidatorGroupsAddresses: () => Promise = proxyCall( + this.contract.methods.getRegisteredValidatorGroups + ) + + /** Get list of registered validators */ + async getRegisteredValidators(): Promise { + const vgAddresses = await this.getRegisteredValidatorsAddresses() + return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) + } + + /** Get list of registered validator groups */ + async getRegisteredValidatorGroups(): Promise { + const vgAddresses = await this.getRegisteredValidatorGroupsAddresses() + return Promise.all(vgAddresses.map((addr) => this.getValidatorGroup(addr))) + } /** - * Returns whether a particular account has a registered validator group. - * @param account The account. - * @return Whether a particular address is a registered validator group. + * Registers a validator unaffiliated with any validator group. + * + * Fails if the account is already a validator or validator group. + * Fails if the account does not have sufficient weight. + * + * @param publicKeysData Comprised of three tightly-packed elements: + * - publicKey - The public key that the validator is using for consensus, should match + * msg.sender. 64 bytes. + * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * - blsPoP - The BLS public key proof of possession. 96 bytes. */ - isValidatorGroup = proxyCall(this.contract.methods.isValidatorGroup) + registerValidator: (publicKeysData: string) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.registerValidator, + tupleParser(parseBytes) + ) + + /** + * De-registers a validator, removing it from the group for which it is a member. + * @param validatorAddress Address of the validator to deregister + */ + async deregisterValidator(validatorAddress: Address) { + const allValidators = await this.getRegisteredValidatorsAddresses() + const idx = allValidators.findIndex((addr) => eqAddress(validatorAddress, addr)) + + if (idx < 0) { + throw new Error(`${validatorAddress} is not a registered validator`) + } + return toTransactionObject(this.kit, this.contract.methods.deregisterValidator(idx)) + } + + /** + * Registers a validator group with no member validators. + * Fails if the account is already a validator or validator group. + * Fails if the account does not have sufficient weight. + * + * @param commission the commission this group receives on epoch payments made to its members. + */ + async registerValidatorGroup(commission: BigNumber): Promise> { + return toTransactionObject( + this.kit, + this.contract.methods.registerValidatorGroup(toFixed(commission).toFixed()) + ) + } + + /** + * De-registers a validator Group + * @param validatorGroupAddress Address of the validator group to deregister + */ + async deregisterValidatorGroup(validatorGroupAddress: Address) { + const allGroups = await this.getRegisteredValidatorGroupsAddresses() + const idx = allGroups.findIndex((addr) => eqAddress(validatorGroupAddress, addr)) + + if (idx < 0) { + throw new Error(`${validatorGroupAddress} is not a registered validator`) + } + return toTransactionObject(this.kit, this.contract.methods.deregisterValidatorGroup(idx)) + } + + /** + * Affiliates a validator with a group, allowing it to be added as a member. + * De-affiliates with the previously affiliated group if present. + * @param group The validator group with which to affiliate. + */ + affiliate: (group: Address) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.affiliate + ) + + /** + * De-affiliates a validator, removing it from the group for which it is a member. + * Fails if the account is not a validator with non-zero affiliation. + */ + + deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) + + /** + * Adds a member to the end of a validator group's list of members. + * Fails if `validator` has not set their affiliation to this account. + * @param validator The validator to add to the group + */ + async addMember(group: Address, validator: Address): Promise> { + const numMembers = await this.getValidatorGroupSize(group) + if (numMembers === 0) { + const election = await this.kit.contracts.getElection() + const voteWeight = await election.getTotalVotesForGroup(group) + const { lesser, greater } = await election.findLesserAndGreaterAfterVote(group, voteWeight) + + return toTransactionObject( + this.kit, + this.contract.methods.addFirstMember(validator, lesser, greater) + ) + } else { + return toTransactionObject(this.kit, this.contract.methods.addMember(validator)) + } + } + + /** + * Removes a member from a ValidatorGroup + * The ValidatorGroup is specified by the `from` of the tx. + * + * @param validator The Validator to remove from the group + */ + removeMember = proxySend(this.kit, this.contract.methods.removeMember) + + /** + * Reorders a member within a validator group. + * Fails if `validator` is not a member of the account's validator group. + * @param groupAddr The validator group + * @param validator The validator to reorder. + * @param newIndex New position for the validator + */ async reorderMember(groupAddr: Address, validator: Address, newIndex: number) { const group = await this.getValidatorGroup(groupAddr) @@ -167,22 +337,7 @@ export class ValidatorsWrapper extends BaseWrapper { return toTransactionObject( this.kit, - this.contract.methods.reorderMember(validator, nextMember, prevMember), - { from: groupAddr } + this.contract.methods.reorderMember(validator, nextMember, prevMember) ) } - - async getRegisteredValidatorGroups(): Promise { - const vgAddresses = await this.contract.methods.getRegisteredValidatorGroups().call() - return Promise.all(vgAddresses.map((addr) => this.getValidatorGroup(addr))) - } - - async getValidatorGroup(address: Address): Promise { - const res = await this.contract.methods.getValidatorGroup(address).call() - return { - address, - members: res[0], - commission: fromFixed(new BigNumber(res[1])), - } - } } diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index cde09896e6c..ceb3022df00 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -146,6 +146,24 @@ EXAMPLE _See code: [packages/cli/src/commands/account/isvalidator.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/isvalidator.ts)_ +### Lock + +Locks Celo Gold to be used in governance and validator elections. + +``` +USAGE + $ celocli account:lock + +OPTIONS + --from=from (required) + --value=value (required) unit amount of Celo Gold (cGLD) + +EXAMPLE + lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 +``` + +_See code: [packages/cli/src/commands/account/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/lock.ts)_ + ### New Creates a new account diff --git a/packages/docs/command-line-interface/lockedgold.md b/packages/docs/command-line-interface/lockedgold.md index 949255f3f15..49c0dabf34a 100644 --- a/packages/docs/command-line-interface/lockedgold.md +++ b/packages/docs/command-line-interface/lockedgold.md @@ -4,24 +4,6 @@ description: View and manage locked Celo Gold ## Commands -### Lock - -Locks Celo Gold to be used in governance and validator elections. - -``` -USAGE - $ celocli lockedgold:lock - -OPTIONS - --from=from (required) - --value=value (required) unit amount of Celo Gold (cGLD) - -EXAMPLE - lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 -``` - -_See code: [packages/cli/src/commands/lockedgold/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lock.ts)_ - ### Show Show Locked Gold information for a given account diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 266299fc06b..a6b0cc66a42 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -4,25 +4,59 @@ description: View and manage validators ## Commands -### Affiliation +### Affiliate -Manage affiliation to a ValidatorGroup +Affiliate to a ValidatorGroup ``` USAGE - $ celocli validator:affiliation + $ celocli validator:affiliate GROUPADDRESS + +ARGUMENTS + GROUPADDRESS ValidatorGroup's address + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Signer or Validator's address + +EXAMPLE + affiliate --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 0x97f7333c51897469e8d98e7af8653aab468050a3 +``` + +_See code: [packages/cli/src/commands/validator/affiliate.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/affiliate.ts)_ + +### Deaffiliate + +DeAffiliate to a ValidatorGroup + +``` +USAGE + $ celocli validator:deaffiliate + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Signer or Validator's address + +EXAMPLE + deaffiliate --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 +``` + +_See code: [packages/cli/src/commands/validator/deaffiliate.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/deaffiliate.ts)_ + +### Deregister + +Deregister a Validator + +``` +USAGE + $ celocli validator:deregister OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --set=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d set affiliation to given address - --unset clear affiliation field + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Signer or Validator's address -EXAMPLES - affiliation --set 0x97f7333c51897469e8d98e7af8653aab468050a3 --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 - affiliation --unset --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 +EXAMPLE + deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 ``` -_See code: [packages/cli/src/commands/validator/affiliation.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/affiliation.ts)_ +_See code: [packages/cli/src/commands/validator/deregister.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/deregister.ts)_ ### List @@ -60,6 +94,20 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ +### Requirements + +Get Requirements for Validators + +``` +USAGE + $ celocli validator:requirements + +EXAMPLE + requirements +``` + +_See code: [packages/cli/src/commands/validator/requirements.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/requirements.ts)_ + ### Show Show information about an existing Validator diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index 2611168b7a1..47dc81e167f 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -4,6 +4,23 @@ description: View and manage validator groups ## Commands +### Deregister + +Deregister a ValidatorGroup + +``` +USAGE + $ celocli validatorgroup:deregister + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Signer or ValidatorGroup's address + +EXAMPLE + deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 +``` + +_See code: [packages/cli/src/commands/validatorgroup/deregister.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/deregister.ts)_ + ### List List existing Validator Groups