diff --git a/.changeset/perfect-pianos-jump.md b/.changeset/perfect-pianos-jump.md new file mode 100644 index 000000000..91a167591 --- /dev/null +++ b/.changeset/perfect-pianos-jump.md @@ -0,0 +1,5 @@ +--- +'@celo/celocli': patch +--- + +Show deprecated warning on flags which will be removed after cel2 launch diff --git a/.changeset/rude-schools-help.md b/.changeset/rude-schools-help.md new file mode 100644 index 000000000..e573ff9de --- /dev/null +++ b/.changeset/rude-schools-help.md @@ -0,0 +1,5 @@ +--- +'@celo/celocli': patch +--- + +use onchain values instead of static for lock requirements diff --git a/docs/command-line-interface/validator.md b/docs/command-line-interface/validator.md index 17f64186c..2518c2152 100644 --- a/docs/command-line-interface/validator.md +++ b/docs/command-line-interface/validator.md @@ -136,7 +136,7 @@ _See code: [src/commands/validator/deaffiliate.ts](https://github.com/celo-org/d ## `celocli validator:deregister` -Deregister a Validator. Approximately 60 days after the validator is no longer part of any group, it will be possible to deregister the validator and start unlocking the CELO. If you wish to deregister your validator, you must first remove it from it's group, such as by deaffiliating it, then wait the required 60 days before running this command. +Deregister a Validator. Wait the require lock period after the validator is no longer part of any group, then it will be possible to deregister the validator and start unlocking the CELO. If you wish to deregister your validator, you must first remove it from it's group, such as by deaffiliating it, then wait the required days before running this command. ``` USAGE @@ -170,11 +170,11 @@ FLAGS Set it to use a ledger wallet DESCRIPTION - Deregister a Validator. Approximately 60 days after the validator is no longer part of - any group, it will be possible to deregister the validator and start unlocking the - CELO. If you wish to deregister your validator, you must first remove it from it's - group, such as by deaffiliating it, then wait the required 60 days before running this - command. + Deregister a Validator. Wait the require lock period after the validator is no longer + part of any group, then it will be possible to deregister the validator and start + unlocking the CELO. If you wish to deregister your validator, you must first remove it + from it's group, such as by deaffiliating it, then wait the required days before + running this command. EXAMPLES deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 diff --git a/docs/command-line-interface/validatorgroup.md b/docs/command-line-interface/validatorgroup.md index 92da134c5..aff4b8875 100644 --- a/docs/command-line-interface/validatorgroup.md +++ b/docs/command-line-interface/validatorgroup.md @@ -80,7 +80,7 @@ _See code: [src/commands/validatorgroup/commission.ts](https://github.com/celo-o ## `celocli validatorgroup:deregister` -Deregister a Validator Group. Approximately 180 days after the validator group is empty, it will be possible to deregister it start unlocking the CELO. If you wish to deregister your validator group, you must first remove all members, then wait the required 180 days before running this command. +Deregister a Validator Group. After the group lock perioid has passed it will be possible to deregister it start unlocking the CELO. If you wish to deregister your validator group, you must first remove all members, then wait the required time before running this command. ``` USAGE @@ -114,10 +114,10 @@ FLAGS Set it to use a ledger wallet DESCRIPTION - Deregister a Validator Group. Approximately 180 days after the validator group is - empty, it will be possible to deregister it start unlocking the CELO. If you wish to - deregister your validator group, you must first remove all members, then wait the - required 180 days before running this command. + Deregister a Validator Group. After the group lock perioid has passed it will be + possible to deregister it start unlocking the CELO. If you wish to deregister your + validator group, you must first remove all members, then wait the required time before + running this command. EXAMPLES deregister --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 diff --git a/packages/cli/src/base-l2.test.ts b/packages/cli/src/base-l2.test.ts index 6a6f5a267..5440d3472 100644 --- a/packages/cli/src/base-l2.test.ts +++ b/packages/cli/src/base-l2.test.ts @@ -45,9 +45,7 @@ describe('flags', () => { await help.showHelp(['transfer:celo', '--help']) expect(stripAnsiCodesFromNestedArray(writeSpy.mock.calls)).toHaveLength(3) expect(stripAnsiCodesFromNestedArray(writeSpy.mock.calls)[1][0]).toEqual( - expect.stringContaining( - `-n, --node= URL of the node to run commands against or an alias` - ) + expect.stringContaining(`-n, --node=`) ) }) }) diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index 0be84faf0..82d31f5ee 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -23,12 +23,14 @@ export default class Authorize extends BaseCommand { }), signer: CustomFlags.address({ required: true }), blsKey: CustomFlags.blsPublicKey({ + deprecated: true, description: 'The BLS public key that the validator is using for consensus, should pass proof of possession. 96 bytes.', dependsOn: ['blsPop'], required: false, }), blsPop: CustomFlags.blsProofOfPossession({ + deprecated: true, description: 'The BLS public key proof-of-possession, which consists of a signature on the account address. 48 bytes.', dependsOn: ['blsKey'], diff --git a/packages/cli/src/commands/dkg/register.ts b/packages/cli/src/commands/dkg/register.ts index 8771d37e1..6917e2a08 100644 --- a/packages/cli/src/commands/dkg/register.ts +++ b/packages/cli/src/commands/dkg/register.ts @@ -12,7 +12,7 @@ export default class DKGRegister extends BaseCommand { static flags = { ...BaseCommand.flags, - blsKey: Flags.string({ required: true }), + blsKey: Flags.string({ required: true, deprecated: true }), address: CustomFlags.address({ required: true, description: 'DKG Contract Address' }), from: CustomFlags.address({ required: true, description: 'Address of the sender' }), } diff --git a/packages/cli/src/commands/releasecelo/authorize.ts b/packages/cli/src/commands/releasecelo/authorize.ts index e30814eda..c9d98be4b 100644 --- a/packages/cli/src/commands/releasecelo/authorize.ts +++ b/packages/cli/src/commands/releasecelo/authorize.ts @@ -19,11 +19,13 @@ export default class Authorize extends ReleaseGoldBaseCommand { required: true, }), blsKey: CustomFlags.blsPublicKey({ + deprecated: true, description: 'The BLS public key that the validator is using for consensus, should pass proof of possession. 96 bytes.', dependsOn: ['blsPop'], }), blsPop: CustomFlags.blsProofOfPossession({ + deprecated: true, description: 'The BLS public key proof-of-possession, which consists of a signature on the account address. 48 bytes.', dependsOn: ['blsKey'], diff --git a/packages/cli/src/commands/validator/affiliate.ts b/packages/cli/src/commands/validator/affiliate.ts index 9ca8bb330..6338e76e4 100644 --- a/packages/cli/src/commands/validator/affiliate.ts +++ b/packages/cli/src/commands/validator/affiliate.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import prompts from 'prompts' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' +import { displaySendTx, humanizeRequirements } from '../../utils/cli' import { CustomArgs, CustomFlags } from '../../utils/command' export default class ValidatorAffiliate extends BaseCommand { @@ -39,12 +39,14 @@ export default class ValidatorAffiliate extends BaseCommand { .isValidatorGroup(groupAddress) .runChecks() + const requirements = await validators.getValidatorLockedGoldRequirements() + const { requiredCelo, requiredDays } = humanizeRequirements(requirements) if (!res.flags.yes) { const response = await prompts({ type: 'confirm', name: 'confirmation', - message: - 'Are you sure you want to affiliate with this group?\nAffiliating with a Validator Group could result in Locked Gold requirements of up to 10,000 CELO for 60 days. (y/n)', + message: `Are you sure you want to affiliate with this group? +Affiliating with a Validator Group could result in Locked Gold requirements of up to ${requiredCelo} CELO for ${requiredDays}. (y/n)`, }) if (!response.confirmation) { diff --git a/packages/cli/src/commands/validator/deregister.test.ts b/packages/cli/src/commands/validator/deregister.test.ts index 9dde3cc7b..c476c70e5 100644 --- a/packages/cli/src/commands/validator/deregister.test.ts +++ b/packages/cli/src/commands/validator/deregister.test.ts @@ -136,7 +136,7 @@ testWithAnvilL2('validator:deregister', (web3: Web3) => { " ✔ Account isn't a member of a validator group ", ], [ - " ✔ Enough time has passed since the account was removed from a validator group ", + " ✔ Enough time has passed since the account was removed from a validator group? ", ], [ "All checks passed", @@ -189,7 +189,7 @@ testWithAnvilL2('validator:deregister', (web3: Web3) => { " ✘ Account isn't a member of a validator group ", ], [ - " ✘ Enough time has passed since the account was removed from a validator group ", + " ✘ Enough time has passed since the account was removed from a validator group? ", ], ] `) diff --git a/packages/cli/src/commands/validator/deregister.ts b/packages/cli/src/commands/validator/deregister.ts index 81e55675c..b141c8746 100644 --- a/packages/cli/src/commands/validator/deregister.ts +++ b/packages/cli/src/commands/validator/deregister.ts @@ -4,9 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { CustomFlags } from '../../utils/command' export default class ValidatorDeregister extends BaseCommand { - // TODO time period to deregister might have changed for L2 consider adding a wait to show the actual static description = - "Deregister a Validator. Approximately 60 days after the validator is no longer part of any group, it will be possible to deregister the validator and start unlocking the CELO. If you wish to deregister your validator, you must first remove it from it's group, such as by deaffiliating it, then wait the required 60 days before running this command." + "Deregister a Validator. Wait the require lock period after the validator is no longer part of any group, then it will be possible to deregister the validator and start unlocking the CELO. If you wish to deregister your validator, you must first remove it from it's group, such as by deaffiliating it, then wait the required days before running this command." static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index e1884d903..b64dc898d 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -13,8 +13,8 @@ export default class ValidatorRegister extends BaseCommand { ...BaseCommand.flags, from: CustomFlags.address({ required: true, description: 'Address for the Validator' }), ecdsaKey: CustomFlags.ecdsaPublicKey({ required: true }), - blsKey: CustomFlags.blsPublicKey({ required: false }), - blsSignature: CustomFlags.blsProofOfPossession({ required: false }), + blsKey: CustomFlags.blsPublicKey({ required: false, deprecated: true }), + blsSignature: CustomFlags.blsProofOfPossession({ required: false, deprecated: true }), yes: Flags.boolean({ description: 'Answer yes to prompt' }), } diff --git a/packages/cli/src/commands/validatorgroup/deregister.test.ts b/packages/cli/src/commands/validatorgroup/deregister.test.ts index cda64f3dd..f3721596e 100644 --- a/packages/cli/src/commands/validatorgroup/deregister.test.ts +++ b/packages/cli/src/commands/validatorgroup/deregister.test.ts @@ -1,39 +1,40 @@ +import { Address } from '@celo/base' +import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/lib/anvil-test' import { ux } from '@oclif/core' import Web3 from 'web3' +import { + mockTimeForwardBy, + setupGroup, + setupValidatorAndAddToGroup, +} from '../../test-utils/chain-setup' import { stripAnsiCodesFromNestedArray, testLocallyWithWeb3Node } from '../../test-utils/cliUtils' import AccountRegister from '../account/register' -import Lock from '../lockedgold/lock' import DeRegisterValidatorGroup from './deregister' -import ValidatorGroupRegister from './register' +import ValidatorGroupMembers from './member' process.env.NO_SYNCCHECK = 'true' testWithAnvilL2('validatorgroup:deregister cmd', (web3: Web3) => { + let groupAddress: Address + let validatorAddress: Address + let kit: ContractKit beforeEach(async () => { - const accounts = await web3.eth.getAccounts() - await testLocallyWithWeb3Node(AccountRegister, ['--from', accounts[0]], web3) - await testLocallyWithWeb3Node( - Lock, - ['--from', accounts[0], '--value', '10000000000000000000000'], - web3 - ) - await testLocallyWithWeb3Node( - ValidatorGroupRegister, - ['--from', accounts[0], '--commission', '0.2', '--yes'], - web3 - ) + kit = newKitFromWeb3(web3) + const addresses = await web3.eth.getAccounts() + groupAddress = addresses[0] + validatorAddress = addresses[1] + await setupGroup(kit, groupAddress) }) afterEach(() => { jest.clearAllMocks() }) describe('when group never had members', () => { it('deregisters a group', async () => { - const logSpy = jest.spyOn(console, 'log') - const writeMock = jest.spyOn(ux.write, 'stdout') - const accounts = await web3.eth.getAccounts() + const logSpy = jest.spyOn(console, 'log').mockImplementation() + const writeMock = jest.spyOn(ux.write, 'stdout').mockImplementation() - await testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', accounts[0]], web3) + await testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', groupAddress], web3) expect(stripAnsiCodesFromNestedArray(logSpy.mock.calls)).toMatchInlineSnapshot(` [ @@ -49,6 +50,9 @@ testWithAnvilL2('validatorgroup:deregister cmd', (web3: Web3) => { [ " ✔ Signer account is ValidatorGroup ", ], + [ + " ✔ Enough time has passed since the validator group removed its last member? ", + ], [ "All checks passed", ], @@ -65,22 +69,103 @@ testWithAnvilL2('validatorgroup:deregister cmd', (web3: Web3) => { }) describe('when group has had members', () => { - beforeEach(async () => {}) - it.todo( - 'to test this need to register a validator and add it to the group then remove it then try to deregister the group' - ) + beforeEach(async () => { + await setupValidatorAndAddToGroup(kit, validatorAddress, groupAddress) + await testLocallyWithWeb3Node( + ValidatorGroupMembers, + ['--yes', '--from', groupAddress, '--remove', validatorAddress], + web3 + ) + const validators = await kit.contracts.getValidators() + await validators.deaffiliate().sendAndWaitForReceipt({ from: validatorAddress }) + }) + describe('when not enough time has passed', () => { + it('shows error that wait period is not over', async () => { + const logMock = jest.spyOn(console, 'log').mockImplementation() + logMock.mockClear() + await expect( + testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', groupAddress], web3) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Some checks didn't pass!"`) + expect(stripAnsiCodesFromNestedArray(logMock.mock.calls)).toMatchInlineSnapshot(` + [ + [ + "Running Checks:", + ], + [ + " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is Signer or registered Account ", + ], + [ + " ✔ Signer can sign Validator Txs ", + ], + [ + " ✔ Signer account is ValidatorGroup ", + ], + [ + " ✘ Enough time has passed since the validator group removed its last member? ", + ], + ] + `) + const validators = await kit.contracts.getValidators() + expect(validators.isValidatorGroup(groupAddress)).resolves.toBe(true) + }) + }) + describe('when wait duration for unlocking is over', () => { + it.only('deregisters the group', async () => { + const validators = await kit.contracts.getValidators() + const group = await validators.getValidatorGroup(groupAddress) + expect(group.members).toHaveLength(0) + expect(group.affiliates).toHaveLength(0) + const groupRequirements = await validators.getGroupLockedGoldRequirements() + const timeSpy = await mockTimeForwardBy(groupRequirements.duration.toNumber() * 2, web3) + const logMock = jest.spyOn(console, 'log').mockImplementation() + await expect( + testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', groupAddress], web3) + ).resolves.toBeUndefined() + expect(stripAnsiCodesFromNestedArray(logMock.mock.calls)).toMatchInlineSnapshot(` + [ + [ + "Running Checks:", + ], + [ + " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is Signer or registered Account ", + ], + [ + " ✔ Signer can sign Validator Txs ", + ], + [ + " ✔ Signer account is ValidatorGroup ", + ], + [ + " ✔ Enough time has passed since the validator group removed its last member? ", + ], + [ + "All checks passed", + ], + [ + "SendTransaction: deregister", + ], + [ + "txHash: 0xtxhash", + ], + ] + `) + await expect(validators.isValidatorGroup(groupAddress)).resolves.toBe(false) + timeSpy.mockClear() + }) + }) }) describe('when is not a validator group', () => { beforeEach(async () => { const accounts = await web3.eth.getAccounts() - await testLocallyWithWeb3Node(AccountRegister, ['--from', accounts[1]], web3) + await testLocallyWithWeb3Node(AccountRegister, ['--from', accounts[2]], web3) }) - it('fails', async () => { - const logSpy = jest.spyOn(console, 'log') + it('shows error message', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation() const accounts = await web3.eth.getAccounts() + logSpy.mockClear() await expect( - testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', accounts[1]], web3) + testLocallyWithWeb3Node(DeRegisterValidatorGroup, ['--from', accounts[2]], web3) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Some checks didn't pass!"`) expect(stripAnsiCodesFromNestedArray(logSpy.mock.calls)).toMatchInlineSnapshot(` [ @@ -88,91 +173,7 @@ testWithAnvilL2('validatorgroup:deregister cmd', (web3: Web3) => { "Running Checks:", ], [ - " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is not a registered Account ", - ], - [ - "All checks passed", - ], - [ - "SendTransaction: register", - ], - [ - "txHash: 0xtxhash", - ], - [ - "Running Checks:", - ], - [ - " ✔ Value [10000000000000000000000] is > 0 ", - ], - [ - "All checks passed", - ], - [ - "Running Checks:", - ], - [ - " ✔ Account has at least 10000 CELO ", - ], - [ - "All checks passed", - ], - [ - "SendTransaction: lock", - ], - [ - "txHash: 0xtxhash", - ], - [ - "Running Checks:", - ], - [ - " ✔ Commission is in range [0,1] ", - ], - [ - " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is Signer or registered Account ", - ], - [ - " ✔ Signer can sign Validator Txs ", - ], - [ - " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is not a registered Validator ", - ], - [ - " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is not a registered ValidatorGroup ", - ], - [ - " ✔ Signer's account has enough locked celo for group registration ", - ], - [ - "All checks passed", - ], - [ - "SendTransaction: registerValidatorGroup", - ], - [ - "txHash: 0xtxhash", - ], - [ - "Running Checks:", - ], - [ - " ✔ 0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb is not a registered Account ", - ], - [ - "All checks passed", - ], - [ - "SendTransaction: register", - ], - [ - "txHash: 0xtxhash", - ], - [ - "Running Checks:", - ], - [ - " ✔ 0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb is Signer or registered Account ", + " ✔ 0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84 is Signer or registered Account ", ], [ " ✔ Signer can sign Validator Txs ", diff --git a/packages/cli/src/commands/validatorgroup/deregister.ts b/packages/cli/src/commands/validatorgroup/deregister.ts index 9328a9635..ee723b549 100644 --- a/packages/cli/src/commands/validatorgroup/deregister.ts +++ b/packages/cli/src/commands/validatorgroup/deregister.ts @@ -4,9 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { CustomFlags } from '../../utils/command' export default class ValidatorGroupDeRegister extends BaseCommand { - // TODO with L2 the wait time may have changed on this. static description = - 'Deregister a Validator Group. Approximately 180 days after the validator group is empty, it will be possible to deregister it start unlocking the CELO. If you wish to deregister your validator group, you must first remove all members, then wait the required 180 days before running this command.' + 'Deregister a Validator Group. After the group lock perioid has passed it will be possible to deregister it start unlocking the CELO. If you wish to deregister your validator group, you must first remove all members, then wait the required time before running this command.' static flags = { ...BaseCommand.flags, @@ -30,7 +29,8 @@ export default class ValidatorGroupDeRegister extends BaseCommand { .isSignerOrAccount() .canSignValidatorTxs() .signerAccountIsValidatorGroup() - .runChecks() + .validatorGroupDeregisterDurationPassed() + .then((checks) => checks.runChecks()) await displaySendTx('deregister', await validators.deregisterValidatorGroup(account)) } diff --git a/packages/cli/src/commands/validatorgroup/member.test.ts b/packages/cli/src/commands/validatorgroup/member.test.ts index 27bf82951..0ca0d40df 100644 --- a/packages/cli/src/commands/validatorgroup/member.test.ts +++ b/packages/cli/src/commands/validatorgroup/member.test.ts @@ -1,19 +1,129 @@ -import { newKitFromWeb3 } from '@celo/contractkit' +import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { testWithAnvilL2, withImpersonatedAccount } from '@celo/dev-utils/lib/anvil-test' +import { ux } from '@oclif/core' import Web3 from 'web3' +import { + setupGroup, + setupValidator, + setupValidatorAndAddToGroup, +} from '../../test-utils/chain-setup' import { stripAnsiCodesFromNestedArray, testLocallyWithWeb3Node } from '../../test-utils/cliUtils' +import ValidatorAffiliate from '../validator/affiliate' import Member from './member' process.env.NO_SYNCCHECK = 'true' testWithAnvilL2('validatorgroup:member cmd', (web3: Web3) => { - beforeEach(async () => {}) afterEach(() => { jest.clearAllMocks() }) + describe('with group', () => { + let groupAddress: string + let validatorAddress: string + let kit: ContractKit + const logSpy = jest.spyOn(console, 'log').mockImplementation() + beforeEach(async () => { + kit = newKitFromWeb3(web3) + const addresses = await web3.eth.getAccounts() + groupAddress = addresses[0] + validatorAddress = addresses[1] + await setupGroup(kit, groupAddress) + }) + describe('when --accept called from the group signer', () => { + beforeEach(async () => { + await setupValidator(kit, validatorAddress) + await testLocallyWithWeb3Node( + ValidatorAffiliate, + [groupAddress, '--from', validatorAddress, '--yes'], + web3 + ) + }) + it('accepts a new member to the group', async () => { + const writeMock = jest.spyOn(ux.write, 'stdout').mockImplementation() + logSpy.mockClear() + await testLocallyWithWeb3Node( + Member, + ['--yes', '--from', groupAddress, '--accept', validatorAddress], + web3 + ) + expect(stripAnsiCodesFromNestedArray(writeMock.mock.calls)).toMatchInlineSnapshot(`[]`) + expect(stripAnsiCodesFromNestedArray(logSpy.mock.calls)).toMatchInlineSnapshot(` + [ + [ + "Running Checks:", + ], + [ + " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is Signer or registered Account ", + ], + [ + " ✔ Signer can sign Validator Txs ", + ], + [ + " ✔ Signer account is ValidatorGroup ", + ], + [ + " ✔ 0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb is Validator ", + ], + [ + "All checks passed", + ], + [ + "SendTransaction: addMember", + ], + [ + "txHash: 0xtxhash", + ], + ] + `) + }) + }) + describe('when --remove called from the group signer', () => { + beforeEach(async () => { + await setupValidatorAndAddToGroup(kit, validatorAddress, groupAddress) + }) + it('removes a member from the group', async () => { + const writeMock = jest.spyOn(ux.write, 'stdout').mockImplementation() + logSpy.mockClear() + await testLocallyWithWeb3Node( + Member, + ['--yes', '--from', groupAddress, '--remove', validatorAddress], + web3 + ) + expect(stripAnsiCodesFromNestedArray(writeMock.mock.calls)).toMatchInlineSnapshot(`[]`) + expect(stripAnsiCodesFromNestedArray(logSpy.mock.calls)).toMatchInlineSnapshot(` + [ + [ + "Running Checks:", + ], + [ + " ✔ 0x5409ED021D9299bf6814279A6A1411A7e866A631 is Signer or registered Account ", + ], + [ + " ✔ Signer can sign Validator Txs ", + ], + [ + " ✔ Signer account is ValidatorGroup ", + ], + [ + " ✔ 0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb is Validator ", + ], + [ + "All checks passed", + ], + [ + "SendTransaction: removeMember", + ], + [ + "txHash: 0xtxhash", + ], + ] + `) + }) + }) + }) describe('when --reorder called from the group signer', () => { it('orders member to new position in group rank', async () => { - const logSpy = jest.spyOn(console, 'log') + const logSpy = jest.spyOn(console, 'log').mockImplementation() const kit = newKitFromWeb3(web3) const ValidatorsWrapper = await kit.contracts.getValidators() diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index b1d556afd..9e6248fe2 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import prompts from 'prompts' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' +import { displaySendTx, humanizeRequirements } from '../../utils/cli' import { CustomArgs, CustomFlags } from '../../utils/command' export default class ValidatorGroupMembers extends BaseCommand { @@ -57,11 +57,13 @@ export default class ValidatorGroupMembers extends BaseCommand { const validatorGroup = await validators.signerToAccount(res.flags.from) if (res.flags.accept) { if (!res.flags.yes) { + const requirements = await validators.getGroupLockedGoldRequirements() + const { requiredCelo, requiredDays } = humanizeRequirements(requirements) const response = await prompts({ type: 'confirm', name: 'confirmation', - message: - 'Are you sure you want to accept this member?\nValidator Group Locked Gold requirements increase per member. Adding an additional member could result in an increase in Locked Gold requirements of up to 10,000 CELO for 180 days. (y/n)', + message: `Are you sure you want to accept this member? + Validator Group Locked Gold requirements increase per member. Adding an additional member could result in an increase in Locked Gold requirements of up to ${requiredCelo} CELO for ${requiredDays}. (y/n)`, }) if (!response.confirmation) { diff --git a/packages/cli/src/test-utils/chain-setup.ts b/packages/cli/src/test-utils/chain-setup.ts index 2e85e4d9e..57df3273c 100644 --- a/packages/cli/src/test-utils/chain-setup.ts +++ b/packages/cli/src/test-utils/chain-setup.ts @@ -7,7 +7,7 @@ import { impersonateAccount, stopImpersonatingAccount, } from '@celo/dev-utils/lib/anvil-test' -import { mineBlocks } from '@celo/dev-utils/lib/ganache-test' +import { mineBlocks, timeTravel } from '@celo/dev-utils/lib/ganache-test' import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import Web3 from 'web3' @@ -63,7 +63,7 @@ export const setupGroup = async ( }) } -const setupValidator = async (kit: ContractKit, validatorAccount: string) => { +export const setupValidator = async (kit: ContractKit, validatorAccount: string) => { await registerAccountWithLockedGold(kit, validatorAccount) const ecdsaPublicKey = await addressToPublicKey(validatorAccount, kit.connection.sign) @@ -90,17 +90,7 @@ export const setupGroupAndAffiliateValidator = async ( validatorAccount: string ) => { await setupGroup(kit, groupAccount) - await setupValidator(kit, validatorAccount) - - const validators = await kit.contracts.getValidators() - - await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) - - await ( - await validators.addMember(groupAccount, validatorAccount) - ).sendAndWaitForReceipt({ - from: groupAccount, - }) + await setupValidatorAndAddToGroup(kit, validatorAccount, groupAccount) } export const voteForGroupFrom = async ( @@ -175,3 +165,30 @@ export const changeMultiSigOwner = async (kit: ContractKit, toAccount: StrongAdd .sendAndWaitForReceipt({ from: multisig.address }) await stopImpersonatingAccount(kit.web3, multisig.address) } + +export async function setupValidatorAndAddToGroup( + kit: ContractKit, + validatorAccount: string, + groupAccount: string +) { + await setupValidator(kit, validatorAccount) + + const validators = await kit.contracts.getValidators() + + await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) + + await ( + await validators.addMember(groupAccount, validatorAccount) + ).sendAndWaitForReceipt({ + from: groupAccount, + }) +} +// you MUST call clearMock after using this function! +export async function mockTimeForwardBy(seconds: number, web3: Web3) { + const now = Date.now() + await timeTravel(seconds, web3) + const spy = jest.spyOn(global.Date, 'now').mockImplementation(() => now + seconds * 1000) + + console.warn('mockTimeForwardBy', seconds, 'seconds', 'call clearMock after using this function') + return spy +} diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 0881a5032..c94062c7c 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -142,6 +142,18 @@ class CheckBuilder { return this } + async addAsyncConditionalCheck( + name: string, + runCondition: () => Promise, + predicate: () => Promise | boolean, + errorMessage?: string + ) { + if (await runCondition()) { + return this.addCheck(name, predicate, errorMessage) + } + return this + } + isApprover = (account: Address) => this.addCheck( `${account} is approver address`, @@ -494,12 +506,41 @@ class CheckBuilder { validatorDeregisterDurationPassed = () => { return this.addCheck( - `Enough time has passed since the account was removed from a validator group`, + `Enough time has passed since the account was removed from a validator group?`, this.withValidators(async (validators, _signer, account) => { const { lastRemovedFromGroupTimestamp } = await validators.getValidatorMembershipHistoryExtraData(account) const { duration } = await validators.getValidatorLockedGoldRequirements() - return duration.toNumber() + lastRemovedFromGroupTimestamp < Date.now() / 1000 + const waitPeriodEnd = lastRemovedFromGroupTimestamp + duration.toNumber() + const isDeregisterable = waitPeriodEnd < Date.now() / 1000 + if (!isDeregisterable) { + console.warn( + `Validator will be able to be deregistered: ${new Date( + waitPeriodEnd * 1000 + ).toUTCString()}` + ) + } + return isDeregisterable + }) + ) + } + validatorGroupDeregisterDurationPassed = () => { + return this.addAsyncConditionalCheck( + 'Enough time has passed since the validator group removed its last member? ', + this.withValidators(async (validators, _signer, account) => { + return validators.isValidatorGroup(account) + }), + this.withValidators(async (validators, _signer, account) => { + const group = await validators.getValidatorGroup(account) + const { duration } = await validators.getGroupLockedGoldRequirements() + const waitPeriodEnd = group.membersUpdated + duration.toNumber() + const isDeregisterable = waitPeriodEnd < Date.now() / 1000 + if (!isDeregisterable) { + console.warn( + `Group will be able to be deregistered: ${new Date(waitPeriodEnd * 1000).toUTCString()}` + ) + } + return isDeregisterable }) ) } diff --git a/packages/cli/src/utils/cli.test.ts b/packages/cli/src/utils/cli.test.ts index eb11c7ab1..896632092 100644 --- a/packages/cli/src/utils/cli.test.ts +++ b/packages/cli/src/utils/cli.test.ts @@ -1,6 +1,7 @@ import { testWithAnvilL2 } from '@celo/dev-utils/lib/anvil-test' +import BigNumber from 'bignumber.js' import { stripAnsiCodesFromNestedArray } from '../test-utils/cliUtils' -import { printValueMapRecursive } from './cli' +import { humanizeRequirements, printValueMapRecursive } from './cli' testWithAnvilL2('printValueMapRecursive', async () => { it('should print the key-value pairs in the value map recursively', () => { const valueMap = { @@ -69,3 +70,50 @@ testWithAnvilL2('printValueMapRecursive', async () => { `) }) }) + +describe('humanizeRequirements', () => { + describe('requiredDuration', () => { + const celoINWei = new BigNumber('1000000000000000000000') + it('shows when duration is hours ', () => { + const { requiredDays } = humanizeRequirements({ + duration: new BigNumber(60 * 60), + value: celoINWei, + }) + expect(requiredDays).toEqual('1 hour') + }) + it('shows when duration is days ', () => { + const { requiredDays } = humanizeRequirements({ + duration: new BigNumber(60 * 60 * 24 * 2), + value: celoINWei, + }) + expect(requiredDays).toEqual('2 days') + }) + it('shows when duration is weeks ', () => { + const { requiredDays } = humanizeRequirements({ + duration: new BigNumber(60 * 60 * 24 * 15), + value: celoINWei, + }) + expect(requiredDays).toEqual('2 weeks, 1 day') + }) + it('shows when duration is months ', () => { + const { requiredDays } = humanizeRequirements({ + duration: new BigNumber(60 * 60 * 24 * 65), + value: celoINWei, + }) + expect(requiredDays).toEqual('2 months, 4 days, 3 hours') + }) + }) + describe('requiredCELO', () => { + const duration = new BigNumber(1000 * 60 * 60) + it('shows when value is small ', () => { + const celoINWei = new BigNumber('1e18') + const { requiredCelo } = humanizeRequirements({ duration, value: celoINWei }) + expect(requiredCelo).toEqual('1.0') + }) + it('shows when value is big ', () => { + const celoINWei = new BigNumber('1e23') + const { requiredCelo } = humanizeRequirements({ duration, value: celoINWei }) + expect(requiredCelo).toEqual('100000.0') + }) + }) +}) diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index 4d0d565ec..5fabcdc68 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -6,11 +6,14 @@ import { parseDecodedParams, TransactionResult, } from '@celo/connect' +import { LockedGoldRequirements } from '@celo/contractkit/lib/wrappers/Validators' import { Errors, ux } from '@oclif/core' import { TransactionResult as SafeTransactionResult } from '@safe-global/types-kit' import BigNumber from 'bignumber.js' import chalk from 'chalk' import { ethers } from 'ethers' +import { formatEther } from 'ethers/lib/utils' +import humanizeDuration from 'humanize-duration' import { convertEthersToCeloTx } from './mento-broker-adaptor' const CLIError = Errors.CLIError @@ -175,3 +178,12 @@ export async function binaryPrompt(promptMessage: string, defaultToNo?: boolean) export function getCurrentTimestamp() { return Math.floor(Date.now() / 1000) } + +export function humanizeRequirements(requirements: LockedGoldRequirements) { + const requiredCelo = formatEther(requirements.value.toFixed()) + const requiredDays = humanizeDuration(requirements.duration.toNumber() * 1000, { + round: true, + maxDecimalPoints: 1, + }) + return { requiredCelo, requiredDays } +}