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)_