From 627dce36ae0f7baf4455bed0160cbb48d5541754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Wed, 5 Sep 2018 15:53:05 +0200 Subject: [PATCH 01/12] Use defaultValue spec --- packages/colony-js-client/src/AuthorityClient/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/colony-js-client/src/AuthorityClient/index.js b/packages/colony-js-client/src/AuthorityClient/index.js index 8dce088de..8fb3f2137 100644 --- a/packages/colony-js-client/src/AuthorityClient/index.js +++ b/packages/colony-js-client/src/AuthorityClient/index.js @@ -65,7 +65,10 @@ export default class AuthorityClient extends ContractClient { output: [['hasRole', 'boolean']], }); this.addSender('setUserRole', { - input: [user, role, ['enabled', 'boolean', true]], + input: [user, role, ['enabled', 'boolean']], + defaultValues: { + enabled: true, + }, }); } } From 581a996e5c30b539cba35b2d1efc96829dff92e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Wed, 5 Sep 2018 16:01:41 +0200 Subject: [PATCH 02/12] Update ColonyClient * Update ColonyClient to match contracts * Add support for: * `enterRecoveryMode` * `exitRecoveryMode` * `registerColonyLabel` * `setOwnerRole` * `setAdminRole` * `setRecoveryRole` * `removeAdminRole` * `removeRecoveryRole` * `setTaskManagerRole` * `setTaskWorkerRole` * `setTaskEvaluatorRole` * `getRecoveryRolesCount` * `getTotalTaskPayout` * Remove `setTaskRoleUser` * Add optional parameters to `createTask` --- .../src/ColonyClient/index.js | 215 +++++++++++++++++- .../src/ColonyClient/senders/CreateTask.js | 2 + .../src/ColonyNetworkClient/index.js | 5 +- 3 files changed, 211 insertions(+), 11 deletions(-) diff --git a/packages/colony-js-client/src/ColonyClient/index.js b/packages/colony-js-client/src/ColonyClient/index.js index f02f4d5fc..3c3dbe6e6 100644 --- a/packages/colony-js-client/src/ColonyClient/index.js +++ b/packages/colony-js-client/src/ColonyClient/index.js @@ -16,6 +16,7 @@ import GetTask from './callers/GetTask'; import CreateTask from './senders/CreateTask'; import { ROLES, + ADMIN_ROLE, WORKER_ROLE, EVALUATOR_ROLE, MANAGER_ROLE, @@ -89,6 +90,16 @@ export default class ColonyClient extends ContractClient { }, ColonyClient, >; + /* + Returns the number of recovery roles. + */ + getRecoveryRolesCount: ColonyClient.Caller< + {}, + { + count: number, // Number of users with the recovery role (excluding owner) + }, + ColonyClient, + >; /* Helper function used to generate the rating secret used in task ratings. Accepts a salt value and a value to hide, and returns the keccak256 hash of both. */ @@ -191,6 +202,19 @@ export default class ColonyClient extends ContractClient { }, ColonyClient, >; + /* + Given a specific task, and a token address, will return any payout attached to the task in the token specified (for all roles). + */ + getTotalTaskPayout: ColonyClient.Caller< + { + taskId: number, // Integer taskId. + token: TokenAddress, // Address of the token's contract. `0x0` value indicates Ether. + }, + { + amount: BigNumber, // Amount of specified tokens to payout for that task. + }, + ColonyClient, + >; /* Every task has three roles associated with it which determine permissions for editing the task, submitting work, and ratings for performance. */ @@ -300,7 +324,9 @@ export default class ColonyClient extends ContractClient { createTask: ColonyClient.Sender< { specificationHash: IPFSHash, // Hashed output of the task's work specification, stored so that it can later be referenced for task ratings or in the event of a dispute. - domainId: number, // Domain in which the task has been created (default value: `1`). + domainId?: number, // Domain in which the task has been created (default value: `1`). + skillId?: number, // The skill associated with the task (optional) + dueDate?: Date, // The due date of the task (optional) }, { TaskAdded: TaskAdded, PotAdded: PotAdded, DomainAdded: DomainAdded }, ColonyClient, @@ -316,6 +342,80 @@ export default class ColonyClient extends ContractClient { { TaskBriefChanged: TaskBriefChanged }, ColonyClient, >; + /* + Put the colony into recovery mode. Can only be called by user with a recovery role. + */ + enterRecoveryMode: ColonyClient.Sender<{}, {}, ColonyClient>; + /* + Exit recovery mode. Can be called by anyone if enough whitelist approvals are given. + */ + exitRecoveryMode: ColonyClient.Sender< + { + newVersion: number, // Resolver version to upgrade to (>= current version) + }, + {}, + ColonyClient, + >; + /* + Register the colony's ENS label. + */ + registerColonyLabel: ColonyClient.Sender< + { + subnode: string, // The keccak256 hash of the label to register + }, + {}, + ColonyClient, + >; + /* + Set a new colony owner role. There can only be one address assigned to owner role at a time. Whoever calls this function will lose their owner role. Can be called by owner role. + */ + setOwnerRole: ColonyClient.Sender< + { + user: Address, // User we want to give an owner role to + }, + {}, + ColonyClient, + >; + /* + Set a new colony admin role. Can be called by an owner or admin role. + */ + setAdminRole: ColonyClient.Sender< + { + user: Address, // User we want to give an admin role to + }, + {}, + ColonyClient, + >; + /* + Set a new colony recovery role. Can be called by an owner role. + */ + setRecoveryRole: ColonyClient.Sender< + { + user: Address, // User we want to give a recovery role to + }, + {}, + ColonyClient, + >; + /* + Remove a colony admin role. Can only be called by an owner role. + */ + removeAdminRole: ColonyClient.Sender< + { + user: Address, // User we want to remove an admin role from + }, + {}, + ColonyClient, + >; + /* + Remove a colony recovery role. Can only be called by an owner role. + */ + removeRecoveryRole: ColonyClient.Sender< + { + user: Address, // User we want to remove a recovery role from + }, + {}, + ColonyClient, + >; /* Every task must belong to a single existing Domain. This can only be called by the manager of the task. */ @@ -339,12 +439,11 @@ export default class ColonyClient extends ContractClient { ColonyClient, >; /* - Set the user for role `_role` in task `_id`. Only allowed before the task is `finalized`, meaning that the value cannot be changed after the task is complete. This can only be called by the manager of the task. + Set the manager role for the address `user` in task `taskId`. Only allowed before the task is `finalized`, meaning that the value cannot be changed after the task is complete. The current manager and the user we want to assign role to both need to sign this transaction. */ - setTaskRoleUser: ColonyClient.Sender< + setTaskManagerRole: ColonyClient.MultisigSender< { taskId: number, // Integer taskId. - role: Role, // MANAGER, EVALUATOR, or WORKER. user: Address, // address of the user. }, { TaskRoleUserChanged: TaskRoleUserChanged }, @@ -476,7 +575,7 @@ export default class ColonyClient extends ContractClient { ColonyClient, >; /* - Adds a domain to the Colony along with the new domain's respective local skill. This can only be called by owners of the colony. + Adds a domain to the Colony along with the new domain's respective local skill (with id `parentSkillId`). This can only be called by owners of the colony. Adding new domains is currently retricted to one level only, i.e. `parentSkillId` has to be the root domain (id: 1). */ addDomain: ColonyClient.Sender< { @@ -650,6 +749,11 @@ export default class ColonyClient extends ContractClient { [['role', 'role'], ['token', 'tokenAddress']], [['amount', 'bigNumber']], ); + makeTaskCaller( + 'getTotalTaskPayout', + [['token', 'tokenAddress']], + [['amount', 'bigNumber']], + ); makeTaskCaller( 'getTaskRole', [['role', 'role']], @@ -671,6 +775,10 @@ export default class ColonyClient extends ContractClient { functionName: 'authority', output: [['address', 'address']], }); + this.addCaller('getRecoveryRolesCount', { + functionName: 'numRecoveryRoles', + output: [['count', 'number']], + }); this.addCaller('generateSecret', { input: [['salt', 'string'], ['value', 'number']], output: [['secret', 'hexString']], @@ -852,8 +960,35 @@ export default class ColonyClient extends ContractClient { this.addSender('submitTaskWorkRating', { input: [['taskId', 'number'], ['role', 'role'], ['secret', 'hexString']], }); + this.addSender('registerColonyLabel', { + input: [['subnode', 'string']], + }); + this.addSender('exitRecoveryMode', { + input: [[]], + }); + this.addSender('approveExitRecovery', {}); + this.addSender('enterRecoveryMode', {}); + this.addSender('setOwnerRole', { + input: [['user', 'address']], + }); + this.addSender('setAdminRole', { + input: [['user', 'address']], + }); + this.addSender('setRecoveryRole', { + input: [['user', 'address']], + }); + this.addSender('removeAdminRole', { + input: [['user', 'address']], + }); + this.addSender('removeRecoveryRole', { + input: [['user', 'address']], + }); - // Multisig Senders + // Remove duplicate/invalid signees + const filterRequiredSignees = (signees: Array
) => + [...new Set(signees)].filter(isValidAddress); + + // Task change MultisigSenders const makeExecuteTaskChange = ( name: string, input: Array<*>, @@ -865,7 +1000,7 @@ export default class ColonyClient extends ContractClient { const taskRoles = await Promise.all( roles.map(role => this.getTaskRole.call({ taskId, role })), ); - return taskRoles.map(({ address }) => address).filter(isValidAddress); + return filterRequiredSignees(taskRoles.map(({ address }) => address)); }, multisigFunctionName: 'executeTaskChange', nonceFunctionName: 'getTaskChangeNonce', @@ -891,5 +1026,71 @@ export default class ColonyClient extends ContractClient { [['token', 'tokenAddress'], ['amount', 'bigNumber']], [MANAGER_ROLE, EVALUATOR_ROLE], ); + + // Task role change MultisigSenders + const makeExecuteTaskRoleChange = ( + name: string, + getRequiredSignees: (args: InputArgs) => Promise, + ) => + this.addMultisigSender(name, { + input: [['taskId', 'number'], ['user', 'address']], + getRequiredSignees: async (args: InputArgs) => { + const { taskId, user } = args; + + // The manager's sig is required for all role change operations + const { address: manager } = await this.getTaskRole.call({ + taskId, + role: MANAGER_ROLE, + }); + + const signees = [manager, user, ...(await getRequiredSignees(args))]; + + return filterRequiredSignees(signees); + }, + multisigFunctionName: 'executeTaskRoleChange', + nonceFunctionName: 'getTaskChangeNonce', + nonceInput: [['taskId', 'number']], + }); + makeExecuteTaskRoleChange('setTaskManagerRole', async ({ user }) => { + const isAdmin = await this.authority.hasUserRole.call({ + user, + role: ADMIN_ROLE, + }); + if (!isAdmin) + throw new Error('Unable to set task role; user must be an admin'); + return null; + }); + makeExecuteTaskRoleChange('setTaskEvaluatorRole', async ({ taskId }) => { + const { address } = await this.getTaskRole.call({ + taskId, + role: EVALUATOR_ROLE, + }); + if (isValidAddress(address)) + throw new Error('Unable to set task role; evaluator is already set'); + return null; + }); + makeExecuteTaskRoleChange('setTaskWorkerRole', async ({ taskId }) => { + const { address } = await this.getTaskRole.call({ + taskId, + role: WORKER_ROLE, + }); + if (isValidAddress(address)) + throw new Error('Unable to set task role; worker is already set'); + return null; + }); + makeExecuteTaskRoleChange('removeTaskWorkerRole', async ({ taskId }) => { + const { address } = await this.getTaskRole.call({ + taskId, + role: WORKER_ROLE, + }); + return address; + }); + makeExecuteTaskRoleChange('removeTaskEvaluatorRole', async ({ taskId }) => { + const { address } = await this.getTaskRole.call({ + taskId, + role: EVALUATOR_ROLE, + }); + return address; + }); } } diff --git a/packages/colony-js-client/src/ColonyClient/senders/CreateTask.js b/packages/colony-js-client/src/ColonyClient/senders/CreateTask.js index 381ebb2b6..117e1c48b 100644 --- a/packages/colony-js-client/src/ColonyClient/senders/CreateTask.js +++ b/packages/colony-js-client/src/ColonyClient/senders/CreateTask.js @@ -7,6 +7,8 @@ import type ColonyClient from '../index'; type InputValues = { specificationHash: string, domainId: number, + skillId: number, + dueDate: Date, }; type OutputValues = { diff --git a/packages/colony-js-client/src/ColonyNetworkClient/index.js b/packages/colony-js-client/src/ColonyNetworkClient/index.js index 95ead0983..7bae896c0 100644 --- a/packages/colony-js-client/src/ColonyNetworkClient/index.js +++ b/packages/colony-js-client/src/ColonyNetworkClient/index.js @@ -179,10 +179,7 @@ export default class ColonyNetworkClient extends ContractClient { parentSkillId: number, // The skill under which the new skill will be added globalSkill: boolean, // Whether the new skill is global }, - { - skillId: number, // Id of the added skill - parentSkillId: number, // Id of the parent skill under which the new skill is added - }, + { SkillAdded: SkillAdded }, ColonyNetworkClient, >; /* From 34d0b7dd6ecd430a4c8a87b89fc4d9ba9646bfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Wed, 5 Sep 2018 16:10:11 +0200 Subject: [PATCH 03/12] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cee703e8..5482190b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v.NEXT +**Maintenance** + +* General updates for interacting with ColonyNetwork contracts. + **Enhancements** * All events logged during method calls are now parsed according to their specifications on the `ContractClient`, without specifying event handlers (`@colony/colony-js-adapter-ethers`, `@colony/colony-js-contract-client`, `@colony/colony-js-client`) From bec3c95a29babe2e77e8a7032d4185696b9c94e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Wed, 5 Sep 2018 16:13:33 +0200 Subject: [PATCH 04/12] Update docs --- docs/_API_ColonyClient.md | 192 ++++++++++++++++++++++++++++--- docs/_API_ColonyNetworkClient.md | 5 +- 2 files changed, 177 insertions(+), 20 deletions(-) diff --git a/docs/_API_ColonyClient.md b/docs/_API_ColonyClient.md index d31d5b13c..fedc637ce 100644 --- a/docs/_API_ColonyClient.md +++ b/docs/_API_ColonyClient.md @@ -48,6 +48,19 @@ A promise which resolves to an object containing the following properties: |---|---|---| |address|Address|The colony's Authority contract address| +### `getRecoveryRolesCount.call()` + +Returns the number of recovery roles. + + +**Returns** + +A promise which resolves to an object containing the following properties: + +|Return value|Type|Description| +|---|---|---| +|count|number|Number of users with the recovery role (excluding owner)| + ### `generateSecret.call({ salt, value })` Helper function used to generate the rating secret used in task ratings. Accepts a salt value and a value to hide, and returns the keccak256 hash of both. @@ -191,6 +204,25 @@ A promise which resolves to an object containing the following properties: |---|---|---| |amount|BigNumber|Amount of specified tokens to payout for that task and a role.| +### `getTotalTaskPayout.call({ taskId, token })` + +Given a specific task, and a token address, will return any payout attached to the task in the token specified (for all roles). + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|taskId|number|Integer taskId.| +|token|Token address|Address of the token's contract. `0x0` value indicates Ether.| + +**Returns** + +A promise which resolves to an object containing the following properties: + +|Return value|Type|Description| +|---|---|---| +|amount|BigNumber|Amount of specified tokens to payout for that task.| + ### `getTaskRole.call({ taskId, role })` Every task has three roles associated with it which determine permissions for editing the task, submitting work, and ratings for performance. @@ -340,7 +372,7 @@ A promise which resolves to an object containing the following properties: ## Senders **All senders return an instance of a `ContractResponse`.** Every `send()` method takes an `options` object as the second argument. For a reference please check [here](/colonyjs/docs-contractclient/#senders). -### `createTask.send({ specificationHash, domainId }, options)` +### `createTask.send({ specificationHash, domainId, skillId, dueDate }, options)` Creates a new task by invoking `makeTask` on-chain. @@ -350,6 +382,8 @@ Creates a new task by invoking `makeTask` on-chain. |---|---|---| |specificationHash|IPFS hash|Hashed output of the task's work specification, stored so that it can later be referenced for task ratings or in the event of a dispute.| |domainId|number|Domain in which the task has been created (default value: `1`).| +|skillId|number|The skill associated with the task (optional)| +|dueDate|Date|The due date of the task (optional)| **Returns** @@ -364,38 +398,139 @@ An instance of a `ContractResponse` which will eventually receive the following |PotAdded|object|Contains the data defined in [PotAdded](#events-PotAdded)| |DomainAdded|object|Contains the data defined in [DomainAdded](#events-DomainAdded)| -### `setTaskDomain.send({ taskId, domainId }, options)` +### `enterRecoveryMode.send(options)` -Every task must belong to a single existing Domain. This can only be called by the manager of the task. +Put the colony into recovery mode. Can only be called by user with a recovery role. + + +**Returns** + +An instance of a `ContractResponse` + + + +### `exitRecoveryMode.send({ newVersion }, options)` + +Exit recovery mode. Can be called by anyone if enough whitelist approvals are given. **Arguments** |Argument|Type|Description| |---|---|---| -|taskId|number|Integer taskId.| -|domainId|number|Integer domainId.| +|newVersion|number|Resolver version to upgrade to (>= current version)| **Returns** -An instance of a `ContractResponse` which will eventually receive the following event data: +An instance of a `ContractResponse` -|Event data|Type|Description| + + +### `registerColonyLabel.send({ subnode }, options)` + +Register the colony's ENS label. + +**Arguments** + +|Argument|Type|Description| |---|---|---| -|taskId|number|The task ID.| -|domainId|number|The task's new domain ID.| -|TaskDomainChanged|object|Contains the data defined in [TaskDomainChanged](#events-TaskDomainChanged)| +|subnode|string|The keccak256 hash of the label to register| + +**Returns** + +An instance of a `ContractResponse` + + -### `setTaskRoleUser.send({ taskId, role, user }, options)` +### `setOwnerRole.send({ user }, options)` -Set the user for role `_role` in task `_id`. Only allowed before the task is `finalized`, meaning that the value cannot be changed after the task is complete. This can only be called by the manager of the task. +Set a new colony owner role. There can only be one address assigned to owner role at a time. Whoever calls this function will lose their owner role. Can be called by owner role. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|user|Address|User we want to give an owner role to| + +**Returns** + +An instance of a `ContractResponse` + + + +### `setAdminRole.send({ user }, options)` + +Set a new colony admin role. Can be called by an owner or admin role. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|user|Address|User we want to give an admin role to| + +**Returns** + +An instance of a `ContractResponse` + + + +### `setRecoveryRole.send({ user }, options)` + +Set a new colony recovery role. Can be called by an owner role. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|user|Address|User we want to give a recovery role to| + +**Returns** + +An instance of a `ContractResponse` + + + +### `removeAdminRole.send({ user }, options)` + +Remove a colony admin role. Can only be called by an owner role. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|user|Address|User we want to remove an admin role from| + +**Returns** + +An instance of a `ContractResponse` + + + +### `removeRecoveryRole.send({ user }, options)` + +Remove a colony recovery role. Can only be called by an owner role. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|user|Address|User we want to remove a recovery role from| + +**Returns** + +An instance of a `ContractResponse` + + + +### `setTaskDomain.send({ taskId, domainId }, options)` + +Every task must belong to a single existing Domain. This can only be called by the manager of the task. **Arguments** |Argument|Type|Description| |---|---|---| |taskId|number|Integer taskId.| -|role|Role|MANAGER, EVALUATOR, or WORKER.| -|user|Address|address of the user.| +|domainId|number|Integer domainId.| **Returns** @@ -404,9 +539,8 @@ An instance of a `ContractResponse` which will eventually receive the following |Event data|Type|Description| |---|---|---| |taskId|number|The task ID.| -|role|number|The role that changed for the task.| -|user|Address|The user with the role that changed for the task.| -|TaskRoleUserChanged|object|Contains the data defined in [TaskRoleUserChanged](#events-TaskRoleUserChanged)| +|domainId|number|The task's new domain ID.| +|TaskDomainChanged|object|Contains the data defined in [TaskDomainChanged](#events-TaskDomainChanged)| ### `setTaskSkill.send({ taskId, skillId }, options)` @@ -580,7 +714,7 @@ An instance of a `ContractResponse` ### `addDomain.send({ parentSkillId }, options)` -Adds a domain to the Colony along with the new domain's respective local skill. This can only be called by owners of the colony. +Adds a domain to the Colony along with the new domain's respective local skill (with id `parentSkillId`). This can only be called by owners of the colony. Adding new domains is currently retricted to one level only, i.e. `parentSkillId` has to be the root domain (id: 1). **Arguments** @@ -779,6 +913,28 @@ An instance of a `MultiSigOperation` whose sender will eventually receive the fo |dueDate|Date|The task's new due date.| |TaskDueDateChanged|object|Contains the data defined in [TaskDueDateChanged](#events-TaskDueDateChanged)| +### `setTaskManagerRole.startOperation({ taskId, user })` + +Set the manager role for the address `user` in task `taskId`. Only allowed before the task is `finalized`, meaning that the value cannot be changed after the task is complete. The current manager and the user we want to assign role to both need to sign this transaction. + +**Arguments** + +|Argument|Type|Description| +|---|---|---| +|taskId|number|Integer taskId.| +|user|Address|address of the user.| + +**Returns** + +An instance of a `MultiSigOperation` whose sender will eventually receive the following event data: + +|Event Data|Type|Description| +|---|---|---| +|taskId|number|The task ID.| +|role|number|The role that changed for the task.| +|user|Address|The user with the role that changed for the task.| +|TaskRoleUserChanged|object|Contains the data defined in [TaskRoleUserChanged](#events-TaskRoleUserChanged)| + ### `setTaskEvaluatorPayout.startOperation({ taskId, token, amount })` Sets the payout given to the EVALUATOR role when the task is finalized. diff --git a/docs/_API_ColonyNetworkClient.md b/docs/_API_ColonyNetworkClient.md index da0a0fefa..43efbc14a 100644 --- a/docs/_API_ColonyNetworkClient.md +++ b/docs/_API_ColonyNetworkClient.md @@ -311,8 +311,9 @@ An instance of a `ContractResponse` which will eventually receive the following |Event data|Type|Description| |---|---|---| -|skillId|number|Id of the added skill| -|parentSkillId|number|Id of the parent skill under which the new skill is added| +|skillId|number|| +|parentSkillId|number|| +|SkillAdded|object|Contains the data defined in [SkillAdded](#events-SkillAdded)| ### `setTokenLocking.send({ tokenLockingAddress }, options)` From cd68cc15b302f2dad33fcdf5a38482452138e1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:55:07 +0200 Subject: [PATCH 05/12] Pluralise multisig signees --- .../src/classes/MultisigOperation.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/colony-js-contract-client/src/classes/MultisigOperation.js b/packages/colony-js-contract-client/src/classes/MultisigOperation.js index 28d92790d..1425f7d79 100644 --- a/packages/colony-js-contract-client/src/classes/MultisigOperation.js +++ b/packages/colony-js-contract-client/src/classes/MultisigOperation.js @@ -229,9 +229,12 @@ export default class MultisigOperation< * this operation). */ _validateRequiredSignees() { + const missing = this.missingSignees; defaultAssert( - this.missingSignees.length === 0, - `Missing signatures (from addresses ${this.missingSignees.join(', ')})`, + missing.length === 0, + `Missing signatures (from address${ + missing.length > 1 ? 'es' : '' + } ${missing.join(', ')})`, ); return true; } From 2f751db09dddb09b1c4af8fe914930325e285288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:55:19 +0200 Subject: [PATCH 06/12] Normalise multisig addresses --- .../src/classes/MultisigOperation.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/colony-js-contract-client/src/classes/MultisigOperation.js b/packages/colony-js-contract-client/src/classes/MultisigOperation.js index 1425f7d79..c325a5e4c 100644 --- a/packages/colony-js-contract-client/src/classes/MultisigOperation.js +++ b/packages/colony-js-contract-client/src/classes/MultisigOperation.js @@ -254,10 +254,11 @@ export default class MultisigOperation< * add the address/signature to the signers. */ _addSignature(signature: Signature, address: string) { - const mode = this._findSignatureMode(signature, address); + const normalisedAddress = address.toLowerCase(); + const mode = this._findSignatureMode(signature, normalisedAddress); this._signers = Object.assign({}, this._signers, { - [address]: { + [normalisedAddress]: { mode, ...signature, }, From 5a21f905635e068b8fb9ab37c17542c4de4f3b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:55:50 +0200 Subject: [PATCH 07/12] Fix Param comment --- packages/colony-js-contract-client/src/flowtypes/params.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/colony-js-contract-client/src/flowtypes/params.js b/packages/colony-js-contract-client/src/flowtypes/params.js index ef88a4c1c..69f721bd0 100644 --- a/packages/colony-js-contract-client/src/flowtypes/params.js +++ b/packages/colony-js-contract-client/src/flowtypes/params.js @@ -11,7 +11,7 @@ export type ParamTypes = | 'tokenAddress' | 'string'; -// [param name, param type, default value (optional)] +// [param name, param type] export type Param = [string, ParamTypes]; export type Params = Array; From 7e9d6afa307d835cb226dec25c9aafc5b14674c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:56:04 +0200 Subject: [PATCH 08/12] Allow 0-value dates --- packages/colony-js-contract-client/src/modules/paramTypes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/colony-js-contract-client/src/modules/paramTypes.js b/packages/colony-js-contract-client/src/modules/paramTypes.js index 61dd8e29f..ae0b74ad2 100644 --- a/packages/colony-js-contract-client/src/modules/paramTypes.js +++ b/packages/colony-js-contract-client/src/modules/paramTypes.js @@ -57,7 +57,8 @@ const PARAM_TYPE_MAP: { }, date: { validate(value: any) { - return value instanceof Date && !!value.valueOf(); + // XXX This allows dates initialised without a value (or with `0` value) + return value instanceof Date; }, convertOutput(value: any) { const converted = parseInt( From af3f153f914ce06f5f2acee90d840a555982de2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:56:33 +0200 Subject: [PATCH 09/12] Export `TASK_STATUSES` --- packages/colony-js-client/src/constants.js | 10 ++++++++++ packages/colony-js-client/src/index.js | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/packages/colony-js-client/src/constants.js b/packages/colony-js-client/src/constants.js index f693e98f2..deefcf9d0 100644 --- a/packages/colony-js-client/src/constants.js +++ b/packages/colony-js-client/src/constants.js @@ -20,4 +20,14 @@ export const AUTHORITY_ROLES = { [ADMIN_ROLE]: 1, }; +export const ACTIVE_TASK_STATUS = 'ACTIVE'; +export const CANCELLED_TASK_STATUS = 'CANCELLED'; +export const FINALIZED_TASK_STATUS = 'FINALIZED'; + +export const TASK_STATUSES = { + [ACTIVE_TASK_STATUS]: 0, + [CANCELLED_TASK_STATUS]: 1, + [FINALIZED_TASK_STATUS]: 2, +}; + export const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/packages/colony-js-client/src/index.js b/packages/colony-js-client/src/index.js index 9542e7860..322b7c702 100644 --- a/packages/colony-js-client/src/index.js +++ b/packages/colony-js-client/src/index.js @@ -13,6 +13,10 @@ export { OWNER_ROLE, ROLES, WORKER_ROLE, + ACTIVE_TASK_STATUS, + CANCELLED_TASK_STATUS, + FINALIZED_TASK_STATUS, + TASK_STATUSES, } from './constants'; export default ColonyNetworkClient; From c22bc00d0777ad8c17fc48cd494661242612925b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 10:57:16 +0200 Subject: [PATCH 10/12] Custom dict types * Rename `roleType` to `dictType` * Add a `taskStatus` param type --- packages/colony-js-client/src/paramTypes.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/colony-js-client/src/paramTypes.js b/packages/colony-js-client/src/paramTypes.js index 378011def..d6f35175a 100644 --- a/packages/colony-js-client/src/paramTypes.js +++ b/packages/colony-js-client/src/paramTypes.js @@ -4,14 +4,14 @@ import { isBigNumber } from '@colony/colony-js-utils'; import { isHexStrict, hexToNumber } from 'web3-utils'; import { addParamType } from '@colony/colony-js-contract-client'; -import { ROLES, AUTHORITY_ROLES } from './constants'; +import { ROLES, AUTHORITY_ROLES, TASK_STATUSES } from './constants'; -const roleType = (roles: { [roleName: string]: number }) => ({ +const dictType = (dict: { [key: string]: number }) => ({ validate(value: any) { - return Object.hasOwnProperty.call(roles, value); + return Object.hasOwnProperty.call(dict, value); }, - convertInput(value: $Keys) { - return roles[value]; + convertInput(value: $Keys) { + return dict[value]; }, convertOutput(value: any) { let converted; @@ -22,10 +22,12 @@ const roleType = (roles: { [roleName: string]: number }) => ({ } else { converted = value; } - return Object.keys(roles).find(name => roles[name] === converted) || null; + return Object.keys(dict).find(name => dict[name] === converted) || null; }, }); -addParamType('role', roleType(ROLES)); +addParamType('taskStatus', dictType(TASK_STATUSES)); -addParamType('authorityRole', roleType(AUTHORITY_ROLES)); +addParamType('role', dictType(ROLES)); + +addParamType('authorityRole', dictType(AUTHORITY_ROLES)); From ab93d73df1beaa0e932e344d114cd3d65a6d382a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 11:03:03 +0200 Subject: [PATCH 11/12] Support contract updates * Update `getTask` to support the new return values * Support `taskStatus` type in `getTask` * Support the optional parameters in `createTask` with default values * Add missing events (`TaskDeliverableSubmitted`, `TaskCompleted`, `TaskWorkRatingRevealed`, `RewardPayoutCycleStarted`) * Add missing functions (`completeTask`, `getVersion`, `upgrade`, `completeTask`, `setAllTaskPayouts`, `submitTaskDeliverableAndRating`, `setStorageSlotRecovery`) * Add types for events * Normalise signees for multisig senders * Use the correct execution function for removing task roles --- .../src/ColonyClient/callers/GetTask.js | 8 +- .../src/ColonyClient/index.js | 218 +++++++++++++++--- 2 files changed, 191 insertions(+), 35 deletions(-) diff --git a/packages/colony-js-client/src/ColonyClient/callers/GetTask.js b/packages/colony-js-client/src/ColonyClient/callers/GetTask.js index db10bbe24..1ce69ee2c 100644 --- a/packages/colony-js-client/src/ColonyClient/callers/GetTask.js +++ b/packages/colony-js-client/src/ColonyClient/callers/GetTask.js @@ -13,7 +13,7 @@ type CallResult = [ string, string, boolean, - boolean, + number, number, number, number, @@ -36,13 +36,13 @@ export default class GetTask extends ContractClient.Caller< output: [ ['specificationHash', 'ipfsHash'], ['deliverableHash', 'ipfsHash'], - ['finalized', 'boolean'], - ['cancelled', 'boolean'], + ['status', 'taskStatus'], ['dueDate', 'date'], ['payoutsWeCannotMake', 'number'], ['potId', 'number'], - ['deliverableDate', 'date'], + ['completionDate', 'date'], ['domainId', 'number'], + ['skillId', 'number'], ], ...params, }); diff --git a/packages/colony-js-client/src/ColonyClient/index.js b/packages/colony-js-client/src/ColonyClient/index.js index 3c3dbe6e6..88c9c7627 100644 --- a/packages/colony-js-client/src/ColonyClient/index.js +++ b/packages/colony-js-client/src/ColonyClient/index.js @@ -21,6 +21,7 @@ import { EVALUATOR_ROLE, MANAGER_ROLE, DEFAULT_DOMAIN_ID, + TASK_STATUSES, } from '../constants'; type Address = string; @@ -28,6 +29,7 @@ type TokenAddress = string; type HexString = string; type Role = $Keys; type IPFSHash = string; +type TaskStatus = $Keys; type DomainAdded = ContractClient.Event<{ domainId: number, // The ID of the domain that was added. @@ -46,6 +48,9 @@ type TaskBriefChanged = ContractClient.Event<{ taskId: number, // The task ID. specificationHash: string, // The IPFS hash of the task's new specification. }>; +type TaskCompleted = ContractClient.Event<{ + taskId: number, // The task ID. +}>; type TaskDueDateChanged = ContractClient.Event<{ taskId: number, // The task ID. dueDate: Date, // The task's new due date. @@ -68,12 +73,24 @@ type TaskWorkerPayoutChanged = ContractClient.Event<{ token: TokenAddress, // The token address (0x indicates ether). amount: number, // The token amount. }>; +type TaskDeliverableSubmitted = ContractClient.Event<{ + taskId: number, // The task ID. + deliverableHash: IPFSHash, // The IPFS hash of the deliverable. +}>; +type TaskWorkRatingRevealed = ContractClient.Event<{ + taskId: number, // The task ID. + role: Role, // The role of the work rating. + rating: number, // The rating value. +}>; type TaskFinalized = ContractClient.Event<{ taskId: number, // The task ID of the task that was finalized. }>; type TaskCanceled = ContractClient.Event<{ taskId: number, // The task ID of the task that was canceled. }>; +type RewardPayoutCycleStarted = ContractClient.Event<{ + payoutId: number, // The payout ID logged when a new reward payout cycle has started. +}>; export default class ColonyClient extends ContractClient { networkClient: ColonyNetworkClient; @@ -90,6 +107,16 @@ export default class ColonyClient extends ContractClient { }, ColonyClient, >; + /* + Gets the Colony contract version. This starts from 1 and is incremented with every deployed contract change. + */ + getVersion: ColonyClient.Caller< + {}, + { + version: number, // The version number. + }, + ColonyClient, + >; /* Returns the number of recovery roles. */ @@ -174,17 +201,16 @@ export default class ColonyClient extends ContractClient { getTask: ColonyClient.Caller< { taskId: number }, { - cancelled: boolean, // Boolean flag denoting whether the task is cancelled. - deliverableDate: ?Date, // Date when the deliverable is due. + completionDate: ?Date, // Date when the task was completed. deliverableHash: ?IPFSHash, // Unique hash of the deliverable content. domainId: number, // Integer Domain ID the task belongs to. dueDate: ?Date, // When the task is due. - finalized: boolean, // Boolean flag denoting whether the task is finalized. id: number, // Integer task ID. payoutsWeCannotMake: ?number, // Number of payouts that cannot be completed with the current task funding. potId: ?number, // Integer ID of funding pot for the task. skillId: number, // Integer Skill ID the task is assigned to. specificationHash: IPFSHash, // Unique hash of the specification content. + status: TaskStatus, // The task status (ACTIVE, CANCELLED or FINALIZED). }, ColonyClient, >; @@ -331,6 +357,16 @@ export default class ColonyClient extends ContractClient { { TaskAdded: TaskAdded, PotAdded: PotAdded, DomainAdded: DomainAdded }, ColonyClient, >; + /* + Mark a task as complete after the due date has passed. This allows the task to be rated and finalized (and funds recovered) even in the presence of a worker who has disappeared. Note that if the due date was not set, then this function will throw. + */ + completeTask: ColonyClient.Sender< + { + taskId: number, // The task ID. + }, + { TaskCompleted: TaskCompleted }, + ColonyClient, + >; /* The task brief, or specification, is a description of the tasks work specification. The description is hashed and stored with the task for future reference in ratings or in the event of a dispute. */ @@ -460,6 +496,22 @@ export default class ColonyClient extends ContractClient { { TaskSkillChanged: TaskSkillChanged }, ColonyClient, >; + /* + Set the payouts for the task manager, evaluator and worker in one transaction, for a specific token address. This can only be called by the task manager, and only if the evaluator and worker roles are either unassigned or the same as the manager. + */ + setAllTaskPayouts: ColonyClient.Sender< + { + taskId: number, // The task ID. + token: Address, // Address of the token, `0x0` value indicates Ether. + managerAmount: BigNumber, // Payout amount for the manager. + evaluatorAmount: BigNumber, // Payout amount for the evaluator. + workerAmount: BigNumber, // Payout amount for the worker. + }, + { + TaskWorkerPayoutChanged: TaskWorkerPayoutChanged, + }, + ColonyClient, + >; /* Sets the payout given to the EVALUATOR role when the task is finalized. */ @@ -502,9 +554,12 @@ export default class ColonyClient extends ContractClient { submitTaskDeliverable: ColonyClient.Sender< { taskId: number, // Integer taskId. - deliverableHash: IPFSHash, // Hash of the work performed. + deliverableHash: IPFSHash, // IPFS hash of the work performed. + }, + { + TaskCompleted: TaskCompleted, + TaskDeliverableSubmitted: TaskDeliverableSubmitted, }, - {}, ColonyClient, >; /* @@ -519,6 +574,21 @@ export default class ColonyClient extends ContractClient { {}, ColonyClient, >; + /* + Submit the task deliverable for the worker and the rating for the manager. + */ + submitTaskDeliverableAndRating: ColonyClient.Sender< + { + taskId: number, // The task ID. + deliverableHash: IPFSHash, // IPFS hash of the work performed. + secret: HexString, // hidden work rating, generated as the output of `generateSecret(_salt, _rating)`, where `_rating` is a score from 1-3. + }, + { + TaskCompleted: TaskCompleted, + TaskDeliverableSubmitted: TaskDeliverableSubmitted, + }, + ColonyClient, + >; /* Reveals a previously submitted work rating, by proving that the `_rating` and `_salt` values result in the same `secret` submitted during the rating submission period. This is checked on-chain using the `generateSecret` function. */ @@ -529,7 +599,7 @@ export default class ColonyClient extends ContractClient { rating: number, // Rating scored (1-3). salt: string, // `_salt` value to be used in `generateSecret`. A correct value will result in the same `secret` submitted during the work rating submission period. }, - {}, + { TaskWorkRatingRevealed: TaskWorkRatingRevealed }, ColonyClient, >; /* @@ -654,7 +724,7 @@ export default class ColonyClient extends ContractClient { { token: TokenAddress, // Address of token used for reward payout (`0x0` value indicates Ether). }, - {}, + { RewardPayoutCycleStarted: RewardPayoutCycleStarted }, ColonyClient, >; /* @@ -668,7 +738,56 @@ export default class ColonyClient extends ContractClient { ColonyClient, >; - events: {}; + /* + Update the value of an arbitrary storage variable. This can only be called by a user with the recovery role. Certain critical variables are protected from editing in this function. + */ + setStorageSlotRecovery: ColonyClient.Sender< + { + slot: number, // Address of storage slot to be updated. + value: HexString, // Word of data to be set. + }, + {}, + ColonyClient, + >; + /* + Set the colony token. Secured function to authorised members. Note that if the `mint` functionality is to be controlled through the colony, control has to be transferred to the colony after this call. + */ + setToken: ColonyClient.Sender< + { + token: Address, // Address of the token contract to use. + }, + {}, + ColonyClient, + >; + /* + Upgrades the colony to a new Colony contract version. Downgrades are not allowed (i.e. `newVersion` should be higher than the currect colony version). + */ + upgrade: ColonyClient.Sender< + { + newVersion: number, + }, + {}, + ColonyClient, + >; + + events: { + DomainAdded: DomainAdded, + PotAdded: PotAdded, + RewardPayoutCycleStarted: RewardPayoutCycleStarted, + SkillAdded: SkillAdded, + TaskAdded: TaskAdded, + TaskBriefChanged: TaskBriefChanged, + TaskCanceled: TaskCanceled, + TaskCompleted: TaskCompleted, + TaskDeliverableSubmitted: TaskDeliverableSubmitted, + TaskDomainChanged: TaskDomainChanged, + TaskDueDateChanged: TaskDueDateChanged, + TaskFinalized: TaskFinalized, + TaskRoleUserChanged: TaskRoleUserChanged, + TaskSkillChanged: TaskSkillChanged, + TaskWorkerPayoutChanged: TaskWorkerPayoutChanged, + TaskWorkRatingRevealed: TaskWorkRatingRevealed, + }; static get defaultQuery() { return { @@ -775,6 +894,10 @@ export default class ColonyClient extends ContractClient { functionName: 'authority', output: [['address', 'address']], }); + this.addCaller('getVersion', { + functionName: 'version', + output: [['version', 'number']], + }); this.addCaller('getRecoveryRolesCount', { functionName: 'numRecoveryRoles', output: [['count', 'number']], @@ -833,6 +956,8 @@ export default class ColonyClient extends ContractClient { this.addEvent('DomainAdded', [['domainId', 'number']]); this.addEvent('PotAdded', [['potId', 'number']]); this.addEvent('TaskAdded', [['taskId', 'number']]); + this.addEvent('TaskCompleted', [['taskId', 'number']]); + this.addEvent('RewardPayoutCycleStarted', [['payoutId', 'number']]); this.addEvent('TaskBriefChanged', [ ['taskId', 'number'], ['specificationHash', 'ipfsHash'], @@ -852,7 +977,7 @@ export default class ColonyClient extends ContractClient { this.addEvent('TaskRoleUserChanged', [ ['taskId', 'number'], ['role', 'number'], - ['user', 'address'], + ['user', 'tokenAddress'], // XXX because 0x0 is valid ]); this.addEvent('TaskWorkerPayoutChanged', [ ['taskId', 'number'], @@ -899,11 +1024,21 @@ export default class ColonyClient extends ContractClient { client: this, name: 'createTask', functionName: 'makeTask', - input: [['specificationHash', 'ipfsHash'], ['domainId', 'number']], + input: [ + ['specificationHash', 'ipfsHash'], + ['domainId', 'number'], + ['skillId', 'number'], + ['dueDate', 'date'], + ], defaultValues: { domainId: DEFAULT_DOMAIN_ID, + skillId: 0, + dueDate: new Date(0), }, }); + this.addSender('completeTask', { + input: [['taskId', 'number']], + }); this.addSender('finalizeTask', { input: [['taskId', 'number']], }); @@ -935,9 +1070,6 @@ export default class ColonyClient extends ContractClient { this.addSender('setTaskDomain', { input: [['taskId', 'number'], ['domainId', 'number']], }); - this.addSender('setTaskRoleUser', { - input: [['taskId', 'number'], ['role', 'role'], ['user', 'address']], - }); this.addSender('setTaskManagerPayout', { input: [ ['taskId', 'number'], @@ -945,12 +1077,34 @@ export default class ColonyClient extends ContractClient { ['amount', 'bigNumber'], ], }); + this.addSender('setAllTaskPayouts', { + input: [ + ['taskId', 'number'], + ['token', 'tokenAddress'], + ['managerAmount', 'bigNumber'], + ['evaluatorAmount', 'bigNumber'], + ['workerAmount', 'bigNumber'], + ], + }); + this.addSender('setStorageSlotRecovery', { + input: [['slot', 'number'], ['value', 'hexString']], + }); + this.addSender('setToken', { + input: [['token', 'tokenAddress']], + }); this.addSender('setTaskSkill', { input: [['taskId', 'number'], ['skillId', 'number']], }); this.addSender('submitTaskDeliverable', { input: [['taskId', 'number'], ['deliverableHash', 'ipfsHash']], }); + this.addSender('submitTaskDeliverableAndRating', { + input: [ + ['taskId', 'number'], + ['deliverableHash', 'ipfsHash'], + ['secret', 'hexString'], + ], + }); this.addSender('startNextRewardPayout', { input: [['token', 'tokenAddress']], }); @@ -983,10 +1137,15 @@ export default class ColonyClient extends ContractClient { this.addSender('removeRecoveryRole', { input: [['user', 'address']], }); + this.addSender('upgrade', { + input: [['newVersion', 'number']], + }); - // Remove duplicate/invalid signees + // Remove duplicate/invalid signees and normalise lowercase const filterRequiredSignees = (signees: Array
) => - [...new Set(signees)].filter(isValidAddress); + [...new Set(signees)] + .filter(isValidAddress) + .map(signee => signee.toLowerCase()); // Task change MultisigSenders const makeExecuteTaskChange = ( @@ -1026,6 +1185,16 @@ export default class ColonyClient extends ContractClient { [['token', 'tokenAddress'], ['amount', 'bigNumber']], [MANAGER_ROLE, EVALUATOR_ROLE], ); + makeExecuteTaskChange( + 'removeTaskWorkerRole', + [], + [MANAGER_ROLE, WORKER_ROLE], + ); + makeExecuteTaskChange( + 'removeTaskEvaluatorRole', + [], + [MANAGER_ROLE, EVALUATOR_ROLE], + ); // Task role change MultisigSenders const makeExecuteTaskRoleChange = ( @@ -1043,11 +1212,12 @@ export default class ColonyClient extends ContractClient { role: MANAGER_ROLE, }); - const signees = [manager, user, ...(await getRequiredSignees(args))]; + const requiredSignees = await getRequiredSignees(args); + const signees = [manager, user].concat(requiredSignees); return filterRequiredSignees(signees); }, - multisigFunctionName: 'executeTaskRoleChange', + multisigFunctionName: 'executeTaskRoleAssignment', nonceFunctionName: 'getTaskChangeNonce', nonceInput: [['taskId', 'number']], }); @@ -1078,19 +1248,5 @@ export default class ColonyClient extends ContractClient { throw new Error('Unable to set task role; worker is already set'); return null; }); - makeExecuteTaskRoleChange('removeTaskWorkerRole', async ({ taskId }) => { - const { address } = await this.getTaskRole.call({ - taskId, - role: WORKER_ROLE, - }); - return address; - }); - makeExecuteTaskRoleChange('removeTaskEvaluatorRole', async ({ taskId }) => { - const { address } = await this.getTaskRole.call({ - taskId, - role: EVALUATOR_ROLE, - }); - return address; - }); } } From a1f1a02e43d0b108bc3efac78a3d8beec4661466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Lefr=C3=A8re?= Date: Fri, 7 Sep 2018 11:13:21 +0200 Subject: [PATCH 12/12] Update tests --- .../src/__tests__/MultisigOperation.js | 9 ++++++--- .../src/__tests__/paramTypes.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/colony-js-contract-client/src/__tests__/MultisigOperation.js b/packages/colony-js-contract-client/src/__tests__/MultisigOperation.js index e64f72252..23bfbb219 100644 --- a/packages/colony-js-contract-client/src/__tests__/MultisigOperation.js +++ b/packages/colony-js-contract-client/src/__tests__/MultisigOperation.js @@ -197,7 +197,7 @@ describe('MultisigOperation', () => { expect(assert).toHaveBeenCalledWith( true, - expect.stringContaining('Missing signatures (from addresses'), + expect.stringContaining('Missing signatures (from address'), ); expect(valid).toBe(true); @@ -217,7 +217,7 @@ describe('MultisigOperation', () => { expect(assert).toHaveBeenCalledWith( false, - `Missing signatures (from addresses ${addressThree})`, + `Missing signatures (from address ${addressThree})`, ); }); @@ -423,7 +423,10 @@ describe('MultisigOperation', () => { op._addSignature(signature, address); - expect(op._signers).toEqual({ [address]: { ...signature, ...mode } }); + expect(op._signers).toEqual({ + // The address should have been converted to lowercase + [address.toLowerCase()]: { ...signature, ...mode }, + }); expect(op._findSignatureMode).toHaveBeenCalled(); }); diff --git a/packages/colony-js-contract-client/src/__tests__/paramTypes.js b/packages/colony-js-contract-client/src/__tests__/paramTypes.js index abc8efc86..1a6ca0319 100644 --- a/packages/colony-js-contract-client/src/__tests__/paramTypes.js +++ b/packages/colony-js-contract-client/src/__tests__/paramTypes.js @@ -117,7 +117,7 @@ describe('Parameter types', () => { // Validation expect(validateValueType(date, 'date')).toBe(true); - expect(validateValueType(new Date('not a valid date'), 'date')).toBe(false); + expect(validateValueType(new Date(0), 'date')).toBe(true); expect(validateValueType(0, 'date')).toBe(false); expect(validateValueType(null, 'date')).toBe(false);