diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index 37e3eabe1dc..3d22ddef327 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -1,3 +1,6 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// + import { CeloContract, CeloToken, ContractKit, newKit, newKitFromWeb3 } from '@celo/contractkit' import { TransactionResult } from '@celo/contractkit/lib/utils/tx-result' import { toFixed } from '@celo/utils/lib/fixidity' diff --git a/packages/celotool/tsconfig.json b/packages/celotool/tsconfig.json index 83264d28d8a..f1e65a88714 100644 --- a/packages/celotool/tsconfig.json +++ b/packages/celotool/tsconfig.json @@ -13,7 +13,7 @@ "@google-cloud/monitoring": ["types/monitoring"] } }, - "include": ["src/"], + "include": ["src", "../contractkit/types"], "exclude": ["node_modules/"], - "references": [{ "path": "../utils" }] + "references": [{ "path": "../utils" }, { "path": "../contractkit" }] } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 6798baeaf76..be60216c69a 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -11,13 +11,17 @@ export default class ValidatorGroupRegister extends BaseCommand { ...BaseCommand.flags, from: Flags.address({ required: true, description: "ValidatorGroup's address" }), accept: flags.boolean({ - exclusive: ['remove'], + exclusive: ['remove', 'reorder'], description: 'Accept a validator whose affiliation is already set to the group', }), remove: flags.boolean({ - exclusive: ['accept'], + exclusive: ['accept', 'reorder'], description: 'Remove a validator from the members list', }), + reorder: flags.integer({ + exclusive: ['accept', 'remove'], + description: 'Reorder a validator within the members list', + }), } static args: IArg[] = [Args.address('validatorAddress', { description: "Validator's address" })] @@ -25,13 +29,14 @@ export default class ValidatorGroupRegister extends BaseCommand { static examples = [ 'member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 ', 'member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', + 'member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', ] async run() { const res = this.parse(ValidatorGroupRegister) - if (!(res.flags.accept || res.flags.remove)) { - this.error(`Specify action: --accept or --remove`) + if (!(res.flags.accept || res.flags.remove || res.flags.reorder)) { + this.error(`Specify action: --accept, --remove or --reorder`) return } @@ -40,8 +45,20 @@ export default class ValidatorGroupRegister extends BaseCommand { if (res.flags.accept) { await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) - } else { - await displaySendTx('addMember', validators.removeMember((res.args as any).validatorAddress)) + } else if (res.flags.remove) { + await displaySendTx( + 'removeMember', + validators.removeMember((res.args as any).validatorAddress) + ) + } else if (res.flags.reorder != null) { + await displaySendTx( + 'reorderMember', + await validators.reorderMember( + res.flags.from, + (res.args as any).validatorAddress, + res.flags.reorder + ) + ) } } } diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index 4102c94ef7d..f97160513e6 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -5,7 +5,11 @@ import Table from 'cli-table' import { cli } from 'cli-ux' import { Tx } from 'web3/eth/types' -export async function displaySendTx(name: string, txObj: CeloTransactionObject, tx?: Tx) { +export async function displaySendTx( + name: string, + txObj: CeloTransactionObject, + tx?: Omit +) { cli.action.start(`Sending Transaction: ${name}`) const txResult = await txObj.send(tx) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 6b49fb45662..cfe287bd5a9 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -6,6 +6,6 @@ "esModuleInterop": true, "target": "es6" }, - "include": ["src"], - "references": [{ "path": "../utils" }] + "include": ["src", "../contractkit/types"], + "references": [{ "path": "../utils" }, { "path": "../contractkit" }] } diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 03fa7040ce9..270b53bf5ce 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -10,8 +10,8 @@ import { proxySend, toBigNumber, toNumber, + toTransactionObject, tupleParser, - wrapSend, } from './BaseWrapper' const parseSignature = SignatureUtils.parseSignature @@ -206,7 +206,7 @@ export class AttestationsWrapper extends BaseWrapper { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) const expectedSourceMessage = attestationMessageToSign(phoneHash, account) const { r, s, v } = parseSignature(expectedSourceMessage, code, issuer.toLowerCase()) - return wrapSend(this.kit, this.contract.methods.complete(phoneHash, v, r, s)) + return toTransactionObject(this.kit, this.contract.methods.complete(phoneHash, v, r, s)) } /** @@ -305,7 +305,7 @@ export class AttestationsWrapper extends BaseWrapper { async request(phoneNumber: string, attestationsRequested: number, token: CeloToken) { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) const tokenAddress = await this.kit.registry.addressFor(token) - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.request(phoneHash, attestationsRequested, tokenAddress) ) @@ -332,7 +332,7 @@ export class AttestationsWrapper extends BaseWrapper { Buffer.from(phoneNumber, 'utf8') ).toString('hex') - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.reveal( PhoneNumberUtils.getPhoneHash(phoneNumber), diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index 8da2ff80fb9..5b1f624ac78 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -22,15 +22,6 @@ export abstract class BaseWrapper { } } -export interface CeloTransactionObject { - /** web3 native TransactionObject. Normally not used */ - txo: TransactionObject - /** send the transaction to the chain */ - send(params?: Omit): Promise - /** send the transaction and waits for the receipt */ - sendAndWaitForReceipt(params?: Omit): Promise -} - /** Parse string -> BigNumber */ export function toBigNumber(input: string) { return new BigNumber(input) @@ -205,18 +196,34 @@ export function proxySend wrapSend(kit, methodFn(...preParse(...args))) + return (...args: InputArgs) => toTransactionObject(kit, methodFn(...preParse(...args))) } else { const methodFn = sendArgs[0] - return (...args: InputArgs) => wrapSend(kit, methodFn(...args)) + return (...args: InputArgs) => toTransactionObject(kit, methodFn(...args)) } } -export function wrapSend(kit: ContractKit, txo: TransactionObject): CeloTransactionObject { - return { - send: (params?: Omit) => kit.sendTransactionObject(txo, params), - txo, - sendAndWaitForReceipt: (params?: Omit) => - kit.sendTransactionObject(txo, params).then((result) => result.waitReceipt()), +export function toTransactionObject( + kit: ContractKit, + txo: TransactionObject, + defaultParams?: Omit +): CeloTransactionObject { + return new CeloTransactionObject(kit, txo, defaultParams) +} + +export class CeloTransactionObject { + constructor( + private kit: ContractKit, + readonly txo: TransactionObject, + readonly defaultParams?: Omit + ) {} + + /** send the transaction to the chain */ + send = (params?: Omit): Promise => { + return this.kit.sendTransactionObject(this.txo, { ...this.defaultParams, ...params }) } + + /** send the transaction and waits for the receipt */ + sendAndWaitForReceipt = (params?: Omit): Promise => + this.send(params).then((result) => result.waitReceipt()) } diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 87ee8c46df4..c87519a965c 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -10,7 +10,7 @@ import { proxyCall, proxySend, toBigNumber, - wrapSend, + toTransactionObject, } from '../wrappers/BaseWrapper' export interface VotingDetails { @@ -117,6 +117,21 @@ export class LockedGoldWrapper extends BaseWrapper { */ isVoting = proxyCall(this.contract.methods.isVoting) + /** + * Check if an account already exists. + * @param account The address of the account + * @return Returns `true` if account exists. Returns `false` otherwise. + * In particular it will return `false` if a delegate with given address exists. + */ + isAccount = proxyCall(this.contract.methods.isAccount) + + /** + * Check if a delegate already exists. + * @param account The address of the delegate + * @return Returns `true` if delegate exists. Returns `false` otherwise. + */ + isDelegate = proxyCall(this.contract.methods.isDelegate) + /** * Query maximum notice period. * @returns Current maximum notice period. @@ -226,7 +241,7 @@ export class LockedGoldWrapper extends BaseWrapper { role: Roles ): Promise> { const sig = await this.getParsedSignatureOfAddress(account, delegate) - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.delegateRole(role, delegate, sig.v, sig.r, sig.s) ) diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts new file mode 100644 index 00000000000..3d5c831e6e3 --- /dev/null +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -0,0 +1,139 @@ +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { testWithGanache } from '../test-utils/ganache-test' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 1 gold +const minLockedGoldNoticePeriod = 120 * 24 * 60 * 60 // 120 days + +// A random 64 byte hex string. +const publicKey = + 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' +const blsPublicKey = + '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' +const blsPoP = + '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + +const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + +testWithGanache('Validators Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let validators: ValidatorsWrapper + let lockedGold: LockedGoldWrapper + + const registerAccountWithCommitment = async (account: string) => { + // console.log('isAccount', ) + // console.log('isDelegate', await lockedGold.isDelegate(account)) + + if (!(await lockedGold.isAccount(account))) { + await lockedGold.createAccount().sendAndWaitForReceipt({ from: account }) + } + await lockedGold + .newCommitment(minLockedGoldNoticePeriod) + .sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + }) + + const setupGroup = async (groupAccount: string) => { + await registerAccountWithCommitment(groupAccount) + await validators + .registerValidatorGroup('thegroup', 'The Group', 'thegroup.com', [minLockedGoldNoticePeriod]) + .sendAndWaitForReceipt({ from: groupAccount }) + } + + const setupValidator = async (validatorAccount: string) => { + await registerAccountWithCommitment(validatorAccount) + // set account1 as the validator + await validators + .registerValidator( + 'goodoldvalidator', + 'Good old validator', + 'goodold.com', + // @ts-ignore + publicKeysData, + [minLockedGoldNoticePeriod] + ) + .sendAndWaitForReceipt({ from: validatorAccount }) + } + + test('SBAT registerValidatorGroup', async () => { + const groupAccount = accounts[0] + await setupGroup(groupAccount) + await expect(validators.isValidatorGroup(groupAccount)).resolves.toBe(true) + }) + + test('SBAT registerValidator', async () => { + const validatorAccount = accounts[1] + await setupValidator(validatorAccount) + await expect(validators.isValidator(validatorAccount)).resolves.toBe(true) + }) + + test('SBAT addMember', async () => { + const groupAccount = accounts[0] + const validatorAccount = accounts[1] + await setupGroup(groupAccount) + await setupValidator(validatorAccount) + await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) + await validators.addMember(validatorAccount).sendAndWaitForReceipt({ from: groupAccount }) + + const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) + expect(members).toContain(validatorAccount) + }) + + describe('SBAT reorderMember', () => { + let groupAccount: string, validator1: string, validator2: string + + beforeEach(async () => { + groupAccount = accounts[0] + await setupGroup(groupAccount) + + validator1 = accounts[1] + validator2 = accounts[2] + + for (const validator of [validator1, validator2]) { + await setupValidator(validator) + await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) + await validators.addMember(validator).sendAndWaitForReceipt({ from: groupAccount }) + } + + const members = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(members).toEqual([validator1, validator2]) + }) + + test('move last to first', async () => { + await validators + .reorderMember(groupAccount, validator2, 0) + .then((x) => x.sendAndWaitForReceipt()) + + const membersAfter = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(membersAfter).toEqual([validator2, validator1]) + }) + + test('move first to last', async () => { + await validators + .reorderMember(groupAccount, validator1, 1) + .then((x) => x.sendAndWaitForReceipt()) + + const membersAfter = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(membersAfter).toEqual([validator2, validator1]) + }) + }) +}) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index fe9ae06f560..2ebe2859f61 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -10,7 +10,7 @@ import { proxySend, toBigNumber, toNumber, - wrapSend, + toTransactionObject, } from './BaseWrapper' export interface Validator { @@ -53,8 +53,6 @@ export interface ValidatorConfig { export class ValidatorsWrapper extends BaseWrapper { affiliate = proxySend(this.kit, this.contract.methods.affiliate) deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) - addMember = proxySend(this.kit, this.contract.methods.addMember) - removeMember = proxySend(this.kit, this.contract.methods.removeMember) registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) /** @@ -151,6 +149,76 @@ export class ValidatorsWrapper extends BaseWrapper { } } + /** + * Returns whether a particular account is voting for a validator group. + * @param account The account. + * @return Whether a particular account is voting for a validator group. + */ + isVoting = proxyCall(this.contract.methods.isVoting) + + /** + * Returns whether a particular account is a registered validator or validator group. + * @param account The account. + * @return Whether a particular account is a registered validator or validator group. + */ + isValidating = proxyCall(this.contract.methods.isValidating) + + /** + * 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 or group. + * @param account The account. + * @param noticePeriods An array of notice periods of the Locked Gold commitments + * that cumulatively meet the requirements for validator registration. + * @return Whether an account meets the requirements to register a validator or group. + */ + meetsRegistrationRequirements = proxyCall(this.contract.methods.meetsRegistrationRequirements) + + addMember = proxySend(this.kit, this.contract.methods.addMember) + removeMember = proxySend(this.kit, this.contract.methods.removeMember) + + async reorderMember(groupAddr: Address, validator: Address, newIndex: number) { + const group = await this.getValidatorGroup(groupAddr) + + if (newIndex < 0 || newIndex >= group.members.length) { + throw new Error(`Invalid index ${newIndex}; max index is ${group.members.length - 1}`) + } + + const currentIdx = group.members.indexOf(validator) + if (currentIdx < 0) { + throw new Error(`ValidatorGroup ${groupAddr} does not inclue ${validator}`) + } else if (currentIdx === newIndex) { + throw new Error(`Validator is already in position ${newIndex}`) + } + + // remove the element + group.members.splice(currentIdx, 1) + // add it on new position + group.members.splice(newIndex, 0, validator) + + const nextMember = + newIndex === group.members.length - 1 ? NULL_ADDRESS : group.members[newIndex + 1] + const prevMember = newIndex === 0 ? NULL_ADDRESS : group.members[newIndex - 1] + + return toTransactionObject( + this.kit, + this.contract.methods.reorderMember(validator, nextMember, prevMember), + { from: groupAddr } + ) + } + async getRegisteredValidatorGroups(): Promise { const vgAddresses = await this.contract.methods.getRegisteredValidatorGroups().call() return Promise.all(vgAddresses.map((addr) => this.getValidatorGroup(addr))) @@ -191,7 +259,7 @@ export class ValidatorsWrapper extends BaseWrapper { votingDetails.weight.negated() ) - return wrapSend(this.kit, this.contract.methods.revokeVote(lesser, greater)) + return toTransactionObject(this.kit, this.contract.methods.revokeVote(lesser, greater)) } async vote(validatorGroup: Address): Promise> { @@ -207,7 +275,10 @@ export class ValidatorsWrapper extends BaseWrapper { votingDetails.weight ) - return wrapSend(this.kit, this.contract.methods.vote(validatorGroup, lesser, greater)) + return toTransactionObject( + this.kit, + this.contract.methods.vote(validatorGroup, lesser, greater) + ) } private async findLesserAndGreaterAfterVote( diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index f8e3eda5b7b..95186d3bfca 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -33,10 +33,12 @@ OPTIONS --accept Accept a validator whose affiliation is already set to the group --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address --remove Remove a validator from the members list + --reorder=reorder Reorder a validator within the members list EXAMPLES member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 + member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 ``` _See code: [packages/cli/src/commands/validatorgroup/member.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/member.ts)_