diff --git a/contracts/deploy/DelayedUpdatableInstance.sol b/contracts/deploy/DelayedUpdatableInstance.sol new file mode 100644 index 0000000..af12bca --- /dev/null +++ b/contracts/deploy/DelayedUpdatableInstance.sol @@ -0,0 +1,63 @@ +pragma solidity ^0.4.17; + +import "./DelayedUpdatableInstanceStorage.sol"; +import "./DelegatedCall.sol"; + +/** + * @title DelayedUpdatableInstance + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + * @dev Contract that can be updated by a call from itself. + */ +contract DelayedUpdatableInstance is DelayedUpdatableInstanceStorage, DelegatedCall { + + event UpdateRequested(address newKernel, uint256 activation); + event UpdateCancelled(); + event UpdateConfirmed(address oldKernel, address newKernel); + + function DelayedUpdatableInstance(address _kernel) public { + kernel = _kernel; + } + + /** + * @dev delegatecall everything (but declared functions) to `_target()` + * @notice Verify `kernel()` code to predict behavior + */ + function () external delegated { + //all goes to kernel + } + + /** + * @dev returns kernel if kernel that is configured + * @return kernel address + */ + function targetDelegatedCall() + internal + constant + returns(address) + { + return kernel; + } + + function updateRequestUpdatableInstance(address _kernel) external { + require(msg.sender == address(this)); + uint activation = block.timestamp + 30 days; + update = Update(_kernel, activation); + UpdateRequested(_kernel, activation); + } + + function updateConfirmUpdatableInstance(address _kernel) external { + require(msg.sender == address(this)); + Update memory pending = update; + require(pending.kernel == _kernel); + require(pending.activation < block.timestamp); + kernel = pending.kernel; + delete update; + UpdateConfirmed(kernel, pending.kernel); + } + + function updateCancelUpdatableInstance() external { + require(msg.sender == address(this)); + delete update; + } + +} \ No newline at end of file diff --git a/contracts/deploy/DelayedUpdatableInstanceStorage.sol b/contracts/deploy/DelayedUpdatableInstanceStorage.sol new file mode 100644 index 0000000..274da20 --- /dev/null +++ b/contracts/deploy/DelayedUpdatableInstanceStorage.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.17; + +import "./InstanceStorage.sol"; + +/** + * @title InstanceStorage + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + * @dev Defines kernel vars that Kernel contract share with Instance. + * Important to avoid overwriting wrong storage pointers is that + * InstanceStorage should be always the first contract at heritance. + */ +contract DelayedUpdatableInstanceStorage is InstanceStorage { + // protected zone start (InstanceStorage vars) + Update update; + + struct Update { + address kernel; + uint256 activation; + } + // protected zone end +} \ No newline at end of file diff --git a/contracts/identity/ERC725.sol b/contracts/identity/ERC725.sol index 3c8f5e1..17af37a 100644 --- a/contracts/identity/ERC725.sol +++ b/contracts/identity/ERC725.sol @@ -23,7 +23,7 @@ contract ERC725 { function getKeyPurpose(bytes32 _key) public constant returns(uint256[] purpose); function getKeysByPurpose(uint256 _purpose) public constant returns(bytes32[] keys); function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public returns (bool success); - function removeKey(bytes32 _key, uint256 _purpose) returns (bool success); + function removeKey(bytes32 _key, uint256 _purpose) public returns (bool success); function execute(address _to, uint256 _value, bytes _data) public returns (uint256 executionId); function approve(uint256 _id, bool _approve) public returns (bool success); } \ No newline at end of file diff --git a/contracts/identity/Identity.sol b/contracts/identity/Identity.sol index 6f06bff..86ff594 100644 --- a/contracts/identity/Identity.sol +++ b/contracts/identity/Identity.sol @@ -12,62 +12,117 @@ contract Identity is ERC725, ERC735 { mapping (uint256 => bytes32[]) claimsByType; mapping (bytes32 => uint256) indexes; mapping (uint => Transaction) txx; - mapping (uint => Action) actionCatalogByTrx; - mapping (uint => bytes32) claimCatalogByTrx; - - mapping (uint256 => uint8) minimumApprovalsByKeyType; - mapping (bytes32 => Claim) pendingClaims; + mapping (uint256 => uint256) minimumApprovalsByKeyPurpose; + bytes32[] pendingTransactions; uint nonce = 0; + address recoveryContract; + address recoveryManager; - struct Action { + struct Transaction { address to; uint value; bytes data; - } - - uint8 constant ACTION_TRANSACTION = 1; - uint8 constant CLAIM_TRANSACTION = 2; - - struct Transaction { - uint8 trxType; uint nonce; uint approverCount; - mapping(uint256 => uint8) approvalsByKeyType; mapping(bytes32 => bool) approvals; - bool executed; } modifier managerOnly { - require(isKeyType(bytes32(msg.sender), MANAGEMENT_KEY)); + require( + isKeyType(bytes32(msg.sender), MANAGEMENT_KEY) + ); _; } modifier selfOnly { - require(msg.sender == address(this)); + require( + msg.sender == address(this) + ); _; } - modifier actorOnly { - require(isKeyType(bytes32(msg.sender), ACTION_KEY)); + modifier recoveryOnly { + require( + (recoveryContract != address(0) && msg.sender == address(recoveryContract)) + ); _; } - modifier claimSignerOnly { - require(isKeyType(bytes32(msg.sender), CLAIM_SIGNER_KEY)); + modifier actorOnly(bytes32 _key) { + require(isKeyType(_key, ACTION_KEY)); _; } - modifier managerOrActor { + modifier managerOrActor(bytes32 _key) { require( isKeyType(bytes32(msg.sender), MANAGEMENT_KEY) || isKeyType(bytes32(msg.sender), ACTION_KEY) ); _; } + + modifier validECDSAKey ( + bytes32 _key, + bytes32 _signHash, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + { + require(address(_key) == ecrecover( + keccak256("\x19Ethereum Signed Message:\n32", _signHash), + _v, + _r, + _s + )); + require(keys[_key].purpose != 0); + _; + } function Identity() public { - _addKey(bytes32(msg.sender), MANAGEMENT_KEY, 1); - minimumApprovalsByKeyType[MANAGEMENT_KEY] = 1; + _constructIdentity(msg.sender); + } + + function () + public + payable + { + + } + + function managerReset(address _newKey) + public + recoveryOnly + { + recoveryManager = _newKey; + _addKey(bytes32(recoveryManager), MANAGEMENT_KEY, 0); + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = keysByPurpose[MANAGEMENT_KEY].length; + } + + function processManagerReset(uint256 limit) + public + { + require(recoveryManager != address(0)); + bytes32 newKey = bytes32(recoveryManager); + bytes32[] memory managers = keysByPurpose[MANAGEMENT_KEY]; + uint256 totalManagers = managers.length; + + if (limit == 0) { + limit = totalManagers; + } + + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = totalManagers - limit + 1; + for (uint256 i = 0; i < limit; i++) { + bytes32 manager = managers[i]; + if (manager != newKey) { + _removeKey(manager, MANAGEMENT_KEY); + totalManagers--; + } + } + + if (totalManagers == 1) { + recoveryManager = address(0); + } } function addKey( @@ -78,11 +133,26 @@ contract Identity is ERC725, ERC735 { public selfOnly returns (bool success) - { + { _addKey(_key, _purpose, _type); return true; } + function replaceKey( + bytes32 _oldKey, + bytes32 _newKey, + uint256 _newType + ) + public + selfOnly + returns (bool success) + { + uint256 purpose = keys[_oldKey].purpose; + _addKey(_newKey, purpose, _newType); + _removeKey(_oldKey, purpose); + return true; + } + function removeKey( bytes32 _key, uint256 _purpose @@ -101,118 +171,34 @@ contract Identity is ERC725, ERC735 { bytes _data ) public - managerOrActor + managerOrActor(bytes32(msg.sender)) returns (uint256 executionId) { - executionId = createTransaction(ACTION_TRANSACTION); - ExecutionRequested(executionId, _to, _value, _data); - - actionCatalogByTrx[nonce] = Action({to: _to, value: _value, data: _data}); + executionId = _execute(_to, _value, _data); approve(executionId, true); } - function approve(uint256 _id, bool _approve) + function approve(uint256 _id, bool _approval) public - managerOrActor + managerOrActor(bytes32(msg.sender)) returns (bool success) { - Transaction storage trx = txx[_id]; - require(trx.executed == false); - - bytes32 managerKeyHash = keccak256(bytes32(msg.sender), MANAGEMENT_KEY); - bytes32 actorKeyHash = keccak256(bytes32(msg.sender), ACTION_KEY); - - uint8 approvalCount; - uint256 requiredKeyType; - if (trx.trxType == ACTION_KEY) { - Action memory action = actionCatalogByTrx[_id]; - if (action.to == address(this)) { - requiredKeyType = MANAGEMENT_KEY; - if (keys[managerKeyHash].purpose == MANAGEMENT_KEY) - approvalCount = _calculateApprovals(managerKeyHash, MANAGEMENT_KEY, _approve, trx); - } else { - requiredKeyType = ACTION_KEY; - if (keys[managerKeyHash].purpose == ACTION_KEY) - approvalCount = _calculateApprovals(actorKeyHash, ACTION_KEY, _approve, trx); - } - - if (approvalCount >= minimumApprovalsByKeyType[requiredKeyType]) { - trx.executed = true; - Executed(_id, action.to, action.value, action.data); - success = action.to.call.value(action.value)(action.data); - } - } else { - // It is a claim - bytes32 claimHash = claimCatalogByTrx[_id]; - if (keys[managerKeyHash].purpose == MANAGEMENT_KEY) - approvalCount = _calculateApprovals(managerKeyHash, MANAGEMENT_KEY, _approve, trx); - - if (approvalCount >= minimumApprovalsByKeyType[MANAGEMENT_KEY]) { - Claim memory c = pendingClaims[claimHash]; - claims[claimHash] = Claim( - { - claimType: c.claimType, - scheme: c.scheme, - issuer: c.issuer, - signature: c.signature, - data: c.data, - uri: c.uri - }); - - indexes[claimHash] = claimsByType[c.claimType].length; - claimsByType[c.claimType].push(claimHash); - ClaimAdded(claimHash, c.claimType, c.scheme, c.issuer, c.signature, c.data, c.uri); - trx.executed = true; - } - - } + return _approve(bytes32(msg.sender), _id, _approval); } - function setMiminumApprovalsByKeyType( - uint256 _type, - uint8 _minimumApprovals + function setMinimumApprovalsByKeyType( + uint256 _purpose, + uint256 _minimumApprovals ) public selfOnly { - minimumApprovalsByKeyType[_type] = _minimumApprovals; - } - - function _calculateApprovals( - bytes32 _keyHash, - uint256 _keyType, - bool _approve, - Transaction storage trx - ) - private - returns (uint8 approvalCount) - { - if (trx.approvals[_keyHash] == false && _approve) { - trx.approvalsByKeyType[_keyType]++; - } else if (trx.approvals[_keyHash] == true && !_approve && trx.approverCount > 0) { - trx.approvalsByKeyType[_keyType]--; - } - trx.approvals[_keyHash] = _approve; - trx.approverCount++; - return trx.approvalsByKeyType[_keyType]; + require(_minimumApprovals > 0); + require(_minimumApprovals <= keysByPurpose[_purpose].length); + minimumApprovalsByKeyPurpose[_purpose] = _minimumApprovals; } - function createTransaction(uint8 _trxType) private returns (uint256 id){ - id = nonce; - txx[nonce] = Transaction( - { - nonce: nonce, - trxType: _trxType, - approverCount: 0, - executed: false - }); - nonce++; - } - - function getTransactionIdByClaim(bytes32 _claimHash) public view returns (uint256 id) { - return indexes[_claimHash]; - } - + function addClaim( uint256 _claimType, uint256 _scheme, @@ -222,60 +208,45 @@ contract Identity is ERC725, ERC735 { string _uri ) public - claimSignerOnly - returns (bytes32 claimRequestId) + returns (bytes32 claimHash) { - - bytes32 claimHash = keccak256(_issuer, _claimType); - - claimRequestId = claimHash; - - uint256 executionId = createTransaction(CLAIM_TRANSACTION); - claimCatalogByTrx[executionId] = claimHash; - indexes[claimHash] = executionId; - - if (claims[claimHash].claimType > 0) { - // Claim existed, needs approval - ClaimChanged( - claimRequestId, - _claimType, - _scheme, - _issuer, - _signature, - _data, - _uri); - } else { - ClaimRequested( - claimRequestId, - _claimType, - _scheme, - _issuer, - _signature, - _data, - _uri); - } - - pendingClaims[claimHash] = Claim( - { - claimType: _claimType, - scheme: _scheme, - issuer: _issuer, - signature: _signature, - data: _data, - uri: _uri + claimHash = keccak256(_issuer, _claimType); + if (msg.sender == address(this)) { + if (claims[claimHash].claimType > 0) { + _modifyClaim(claimHash, _claimType, _scheme, _issuer, _signature, _data, _uri); + } else { + _includeClaim(claimHash, _claimType, _scheme, _issuer, _signature, _data, _uri); } - ); + } else { + require(_issuer == msg.sender); + require(isKeyType(bytes32(msg.sender), CLAIM_SIGNER_KEY)); + _execute(address(this), 0, msg.data); + ClaimRequested( + claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + } } - function removeClaim(bytes32 _claimId) public returns (bool success) { + function removeClaim(bytes32 _claimId) + public + returns (bool success) + { Claim memory c = claims[_claimId]; require( msg.sender == c.issuer || - msg.sender == address(this) || - isKeyType(bytes32(msg.sender), MANAGEMENT_KEY) + msg.sender == address(this) ); + // MUST only be done by the issuer of the claim, or KEYS OF PURPOSE 1, or the identity itself. + // TODO If its the identity itself, the approval process will determine its approval. + uint claimIdTypePos = indexes[_claimId]; delete indexes[_claimId]; bytes32[] storage claimsTypeArr = claimsByType[c.claimType]; @@ -287,43 +258,6 @@ contract Identity is ERC725, ERC735 { return true; } - function _addKey(bytes32 _key, uint256 _purpose, uint256 _type) private { - bytes32 keyHash = keccak256(_key, _purpose); - - require(keys[keyHash].purpose == 0); - require( - _purpose == MANAGEMENT_KEY || - _purpose == ACTION_KEY || - _purpose == CLAIM_SIGNER_KEY || - _purpose == ENCRYPTION_KEY - ); - KeyAdded(_key, _purpose, _type); - keys[keyHash] = Key(_purpose, _type, _key); - indexes[keyHash] = keysByPurpose[_purpose].push(_key) - 1; - } - - function _removeKey(bytes32 _key, uint256 _purpose) private { - bytes32 keyHash = keccak256(_key, _purpose); - Key storage myKey = keys[keyHash]; - KeyRemoved(myKey.key, myKey.purpose, myKey.keyType); - - uint index = indexes[keyHash]; - delete indexes[keyHash]; - bytes32 replacer = keysByPurpose[_purpose][keysByPurpose[_purpose].length - 1]; - keysByPurpose[_purpose][index] = replacer; - indexes[keccak256(replacer, _purpose)] = index; - keysByPurpose[_purpose].length--; - - if (_purpose == MANAGEMENT_KEY) { - require(keysByPurpose[MANAGEMENT_KEY].length >= 1); - } - - delete keys[keyHash]; - - // MUST only be done by keys of purpose 1, or the identity itself. - // TODO If its the identity itself, the approval process will determine its approval. - } - function getKey( bytes32 _key, uint256 _purpose @@ -384,7 +318,7 @@ contract Identity is ERC725, ERC735 { function getKeysByPurpose(uint256 _purpose) public constant - returns(bytes32[] keys) + returns(bytes32[]) { return keysByPurpose[_purpose]; } @@ -392,7 +326,14 @@ contract Identity is ERC725, ERC735 { function getClaim(bytes32 _claimId) public constant - returns(uint256 claimType, uint256 scheme, address issuer, bytes signature, bytes data, string uri) + returns( + uint256 claimType, + uint256 scheme, + address issuer, + bytes signature, + bytes data, + string uri + ) { Claim memory _claim = claims[_claimId]; return (_claim.claimType, _claim.scheme, _claim.issuer, _claim.signature, _claim.data, _claim.uri); @@ -405,6 +346,276 @@ contract Identity is ERC725, ERC735 { { return claimsByType[_claimType]; } + + function approveECDSA( + uint256 _id, + bool _approval, + bytes32 _key, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + public + validECDSAKey( + _key, + keccak256( + address(this), + bytes4(keccak256("approve(uint256,bool)")), + _id, + _approval + ), + _v, + _r, + _s + ) + managerOrActor(_key) + returns (bool success) + { + return _approve(_key, _id, _approval); + } + + function executeECDSA( + address _to, + uint256 _value, + bytes _data, + uint _nonce, + bytes32 _key, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + public + validECDSAKey( + _key, + keccak256( + address(this), + bytes4( + keccak256("execute(address,uint256,bytes)")), + _to, + _value, + _data, + _nonce + ), + _v, + _r, + _s + ) + managerOrActor(_key) + returns (uint256 executionId) + { + executionId = _execute(_to, _value, _data); + _approve(_key, executionId, true); + } + + function setupRecovery(address _recoveryContract) + public + selfOnly + { + require(recoveryContract == address(0)); + recoveryContract = _recoveryContract; + } + + function _constructIdentity(address _manager) + internal + { + require(minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] == 0); + _addKey(bytes32(_manager), MANAGEMENT_KEY, 0); + + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = 1; + minimumApprovalsByKeyPurpose[ACTION_KEY] = 1; + } + + function _execute( + address _to, + uint256 _value, + bytes _data + ) + private + returns (uint256 executionId) + { + executionId = nonce; + ExecutionRequested(executionId, _to, _value, _data); + txx[executionId] = Transaction( + { + to: _to, + value: _value, + data: _data, + nonce: nonce, + approverCount: 0 + }); + nonce++; + } + + function _approve( + bytes32 _key, + uint256 _id, + bool _approval + ) + private + returns(bool success) + { + + Transaction storage trx = txx[_id]; + + uint256 approvalCount; + uint256 requiredKeyPurpose; + + Approved(_id, _approval); + + if (trx.to == address(this)) { + require(isKeyType(_key, MANAGEMENT_KEY)); + bytes32 managerKeyHash = keccak256(_key, MANAGEMENT_KEY); + requiredKeyPurpose = MANAGEMENT_KEY; + approvalCount = _calculateApprovals(managerKeyHash, _approval, trx); + } else { + require(isKeyType(_key, ACTION_KEY)); + bytes32 actorKeyHash = keccak256(_key, ACTION_KEY); + requiredKeyPurpose = ACTION_KEY; + approvalCount = _calculateApprovals(actorKeyHash, _approval, trx); + } + + if (approvalCount >= minimumApprovalsByKeyPurpose[requiredKeyPurpose]) { + Executed(_id, trx.to, trx.value, trx.data); + success = trx.to.call.value(trx.value)(trx.data); + } + } + + function _addKey( + bytes32 _key, + uint256 _purpose, + uint256 _type + ) + private + { + bytes32 keyHash = keccak256(_key, _purpose); + + require(keys[keyHash].purpose == 0); + require( + _purpose == MANAGEMENT_KEY || + _purpose == ACTION_KEY || + _purpose == CLAIM_SIGNER_KEY || + _purpose == ENCRYPTION_KEY + ); + KeyAdded(_key, _purpose, _type); + keys[keyHash] = Key(_purpose, _type, _key); + indexes[keyHash] = keysByPurpose[_purpose].push(_key) - 1; + } + + function _removeKey( + bytes32 _key, + uint256 _purpose + ) + private + { + bytes32 keyHash = keccak256(_key, _purpose); + Key storage myKey = keys[keyHash]; + KeyRemoved(myKey.key, myKey.purpose, myKey.keyType); + + uint index = indexes[keyHash]; + delete indexes[keyHash]; + bytes32 replacer = keysByPurpose[_purpose][keysByPurpose[_purpose].length - 1]; + keysByPurpose[_purpose][index] = replacer; + indexes[keccak256(replacer, _purpose)] = index; + keysByPurpose[_purpose].length--; + + if (_purpose == MANAGEMENT_KEY) { + require( + keysByPurpose[MANAGEMENT_KEY].length >= 1 && + keysByPurpose[MANAGEMENT_KEY].length >= minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] + ); + + } + + delete keys[keyHash]; + } + + function _calculateApprovals( + bytes32 _keyHash, + bool _approval, + Transaction storage trx + ) + private + returns (uint256 approvalCount) + { + require(trx.approvals[_keyHash] != _approval); + + trx.approvals[_keyHash] = _approval; + if (_approval) { + trx.approverCount++; + } else { + trx.approverCount--; + } + + return trx.approverCount; + } + + + function _includeClaim( + bytes32 _claimHash, + uint256 _claimType, + uint256 _scheme, + address _issuer, + bytes _signature, + bytes _data, + string _uri + ) + private + { + claims[_claimHash] = Claim( + { + claimType: _claimType, + scheme: _scheme, + issuer: _issuer, + signature: _signature, + data: _data, + uri: _uri + } + ); + indexes[_claimHash] = claimsByType[_claimType].length; + claimsByType[_claimType].push(_claimHash); + ClaimAdded( + _claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + } + + + function _modifyClaim( + bytes32 _claimHash, + uint256 _claimType, + uint256 _scheme, + address _issuer, + bytes _signature, + bytes _data, + string _uri + ) + private + { + require(msg.sender == _issuer); + ClaimChanged( + _claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + claims[_claimHash] = Claim({ + claimType: _claimType, + scheme: _scheme, + issuer: _issuer, + signature: _signature, + data: _data, + uri: _uri + }); + } + + } - diff --git a/contracts/identity/IdentityFactory.sol b/contracts/identity/IdentityFactory.sol new file mode 100644 index 0000000..a0dac9a --- /dev/null +++ b/contracts/identity/IdentityFactory.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.4.17; + +import "../deploy/Factory.sol"; +import "../deploy/DelayedUpdatableInstance.sol"; +import "./IdentityKernel.sol"; + + +contract IdentityFactory is Factory { + + event IdentityCreated(address instance); + + function IdentityFactory(bytes _infohash) + public + Factory(new IdentityKernel(), _infohash) + { + } + + function createIdentity() + external + { + createIdentity(msg.sender); + } + + function createIdentity(address _idOwner) + public + { + IdentityKernel instance = IdentityKernel(new DelayedUpdatableInstance(address(latestKernel))); + instance.initIdentity(_idOwner); + IdentityCreated(address(instance)); + } + +} diff --git a/contracts/identity/IdentityKernel.sol b/contracts/identity/IdentityKernel.sol new file mode 100644 index 0000000..d40056f --- /dev/null +++ b/contracts/identity/IdentityKernel.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.4.17; + +import "../deploy/DelayedUpdatableInstanceStorage.sol"; +import "./Identity.sol"; + +contract IdentityKernel is DelayedUpdatableInstanceStorage, Identity { + + function initIdentity(address _caller) external { + _constructIdentity(_caller); + } +} diff --git a/contracts/tests/TestContract.sol b/contracts/tests/TestContract.sol new file mode 100644 index 0000000..3074bda --- /dev/null +++ b/contracts/tests/TestContract.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.4.17; + + +contract TestContract { + + event TestFunctionExecuted(); + + function test() public { + TestFunctionExecuted(); + } + + + /* + Helper function to be used in unit testing due to error in web3 + web3.utils.soliditySha3([1, 2, 3]) + Error: Autodetection of array types is not supported. + at _processSoliditySha3Args (node_modules/web3-utils/src/soliditySha3.js:176:15) + */ + function hash( + address identity, + bytes32 _revealedSecret, + address _dest, + bytes _data, + bytes32 _newSecret, + bytes32[] _newFriendsHashes) + external + pure + returns(bytes32) + { + return keccak256(identity, _revealedSecret, _dest, _data, _newSecret, _newFriendsHashes); + + } + +} \ No newline at end of file diff --git a/contracts/tests/UpdatedIdentityKernel.sol b/contracts/tests/UpdatedIdentityKernel.sol new file mode 100644 index 0000000..23ad12b --- /dev/null +++ b/contracts/tests/UpdatedIdentityKernel.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.17; + +import "../identity/IdentityKernel.sol"; + + +contract UpdatedIdentityKernel is IdentityKernel { + + event TestFunctionExecuted(uint256 minApprovalsByManagementKeys); + + function test() public { + TestFunctionExecuted(minimumApprovalsByKeyPurpose[MANAGEMENT_KEY]); + } +} \ No newline at end of file diff --git a/test/factory.js b/test/factory.js new file mode 100644 index 0000000..84375c1 --- /dev/null +++ b/test/factory.js @@ -0,0 +1,102 @@ +const TestUtils = require("../utils/testUtils.js") +const idUtils = require("../utils/identityUtils.js") +const web3EthAbi = require("web3-eth-abi"); + +const Identity = artifacts.require("./identity/Identity.sol"); +const IdentityFactory = artifacts.require("./identity/IdentityFactory.sol"); +const UpdatableInstance = artifacts.require('./deploy/UpdatableInstance.sol'); +const UpdatedIdentityKernel = artifacts.require("./tests/UpdatedIdentityKernel.sol"); + +contract('IdentityFactory', function(accounts) { + + let identityFactory; + let identity; + let updatedIdentity; + let updatedIdentityKernel; + + before(async () => { + identityFactory = await IdentityFactory.new("0xaaa", {from: accounts[0]}); + }) + + describe("IdentityFactory()", () => { + it("Creates a new identity", async () => { + let tx = await identityFactory.createIdentity({from: accounts[0]}); + const logEntry = tx.logs[0]; + assert.strictEqual(logEntry.event, "IdentityCreated"); + + identity = await Identity.at(logEntry.args.instance, {from: accounts[0]}) + + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") + }); + + + it("Registers a updated identity contract", async() => { + const infoHash = "0xbbb"; + updatedIdentityKernel = await UpdatedIdentityKernel.new({from: accounts[0]}); + await identityFactory.setKernel(updatedIdentityKernel.address, infoHash); + + const newKernel = await TestUtils.listenForEvent(identityFactory.NewKernel()); + assert(newKernel.infohash, infoHash, "Infohash is not correct"); + }); + + + it("Creates a new identity using latest version", async() => { + let tx = await identityFactory.createIdentity({from: accounts[0]}); + const logEntry = tx.logs[0]; + assert.strictEqual(logEntry.event, "IdentityCreated"); + + updatedIdentity = await UpdatedIdentityKernel.at(logEntry.args.instance, {from: accounts[0]}) + tx = await updatedIdentity.test({from: accounts[0]}); + assert.strictEqual(tx.logs[0].event, "TestFunctionExecuted"); + + // Test if it still executes identity functions as expected + let baseIdentity = await Identity.at(updatedIdentity.address, {from: accounts[0]}) + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + 1, + identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") + }); + + + it("Updates an identity to the latest version", async() => { + let tx1 = await identity.execute( + identity.address, + 0, + idUtils.encode.updateRequestUpdatableInstance(updatedIdentityKernel.address), + {from: accounts[0]} + ); + + assert.strictEqual(tx1.logs[tx1.logs.length - 1].event, "Executed"); + + // Updating EVM timestamp to test delay + const plus31days = 60 * 60 * 24 * 31; + + web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [plus31days], id: 0}); + web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 0}) + + // Confirm update + let tx2 = await identity.execute( + identity.address, + 0, + idUtils.encode.updateConfirmUpdatableInstance(updatedIdentityKernel.address), + {from: accounts[0]} + ); + + assert.strictEqual(tx2.logs[tx2.logs.length - 1].event, "Executed"); + + // Calling function available in updated identity kernel + let updatedIdentity1 = await UpdatedIdentityKernel.at(identity.address, {from: accounts[0]}) + let tx3 = await updatedIdentity1.test({from: accounts[0]}); + + assert.strictEqual(tx3.logs[tx3.logs.length - 1].event, "TestFunctionExecuted"); + assert.equal( + tx3.logs[tx3.logs.length - 1].args.minApprovalsByManagementKeys.toString(10), + 1, + identity.address + " wasn't updated to last version"); + }) + }); + +}); \ No newline at end of file diff --git a/test/identity.js b/test/identity.js index 60d7c25..f240209 100644 --- a/test/identity.js +++ b/test/identity.js @@ -1,5 +1,9 @@ -const TestUtils = require("./TestUtils.js") +const TestUtils = require("../utils/testUtils.js"); +const web3EthAbi = require("web3-eth-abi"); +const idUtils = require('../utils/identityUtils.js'); + const Identity = artifacts.require("./identity/Identity.sol"); +const TestContract = artifacts.require("./test/TestContract.sol"); contract('Identity', function(accounts) { @@ -9,126 +13,205 @@ contract('Identity', function(accounts) { identity = await Identity.new({from: accounts[0]}) }) - describe("Identity()", () => { it("initialize with msg.sender as management key", async () => { assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), - 1, + idUtils.purposes.MANAGEMENT, identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") }); - }); describe("addKey(address _key, uint256 _type)", () => { - it("MANAGEMENT_KEY add a new address as ACTION_KEY", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 2, 1, {from: accounts[0]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 2, + idUtils.purposes.ACTION, identity.address+".getKeyPurpose("+accounts[1]+") is not ACTION_KEY") }); - it("should not add key by non manager", async () => { try { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 1, 1, {from: accounts[2]}) - }catch(e){ + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 0, + idUtils.purposes.NONE, identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); - it("should not add key type 1 by actor", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[2]), 2, 1, {from: accounts[0]}) + it("should not add key type 1 by actor", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[2], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + try { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 1, 1, {from: accounts[2]}) - } catch(e){ + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 0, + idUtils.purposes.NONE, identity.address+".getKeyType("+accounts[1]+") is not correct") }); - - it("fire KeyAdded(address indexed key, uint256 indexed type)", async () => { - identity.addKey(TestUtils.addressToBytes32(accounts[1]), 2, 1, {from: accounts[0]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + const keyAdded = await TestUtils.listenForEvent(identity.KeyAdded()) assert(keyAdded.key, TestUtils.addressToBytes32(accounts[1]), "Key is not correct") - assert(keyAdded.keyType, 2, "Type is not correct") + assert(keyAdded.keyType, idUtils.types.ADDRESS, "Type is not correct") }); - }); describe("removeKey(address _key, uint256 _type)", () => { + it("MANAGEMENT_KEY should remove a key", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); - it("MANAGEMENT_KEY should removes a key", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 1, 1, {from: accounts[0]}) - await identity.removeKey(TestUtils.addressToBytes32(accounts[0]), 1, {from: accounts[1]}) - assert.equal( - await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), - 0, - identity.address+".getKeyPurpose("+accounts[0]+") is not 0") - }); - - - it("other key should not removes a key", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 1, 1, {from: accounts[0]}) - try { - await identity.removeKey(TestUtils.addressToBytes32(accounts[1]), 1, {from: accounts[2]}) - }catch (e) { - - } + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.MANAGEMENT), + {from: accounts[0]} + ); + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 1, + idUtils.purposes.NONE, + identity.address+".getKeyPurpose("+accounts[1]+") is not 0") + }); + + it("other key should not remove a key", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + try { + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.MANAGEMENT), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.MANAGEMENT, identity.address+".getKeyPurpose("+accounts[1]+") is not 0") }); it("actor key should not remove key", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 2, 1, {from: accounts[0]}) - await identity.addKey(TestUtils.addressToBytes32(accounts[2]), 2, 1, {from: accounts[0]}) - try { - await identity.removeKey(TestUtils.addressToBytes32(accounts[1]), 1, {from: accounts[2]}) - }catch (e) { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[2], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + try { + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.ACTION), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 2, + idUtils.purposes.ACTION, identity.address+".getKeyType("+accounts[1]+") is not 0") }); - it("MANAGEMENT_KEY should not remove itself MANAGEMENT_KEY when there is no other MANAGEMENT_KEY", async () => { - let assertJump = (error) => { - assert.isAbove(error.message.search('revert'), -1, 'Revert should happen'); - } - try { - await identity.removeKey(TestUtils.addressToBytes32(accounts[0]), 1, {from: accounts[0]}); - assert.fail('should have reverted before'); - } catch(error) { - assertJump(error); - } - - + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[0], idUtils.purposes.MANAGEMENT), + {from: accounts[0]} + ); + + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address+".getKeyType("+accounts[0]+") is not 1") }); it("fire KeyRemoved(address indexed key, uint256 indexed type)", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 2, 1,{from: accounts[0]}) - identity.removeKey(TestUtils.addressToBytes32(accounts[1]), 2, {from: accounts[0]}) - const keyRemoved = await TestUtils.listenForEvent(identity.KeyRemoved()) - assert(keyRemoved.key, TestUtils.addressToBytes32(accounts[1]), "Key is not correct") - assert(keyRemoved.keyType, 2, "Type is not correct") - }); + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.ACTION), + {from: accounts[0]} + ); + const keyRemoved = await TestUtils.listenForEvent(identity.KeyRemoved()); + assert(keyRemoved.key, TestUtils.addressToBytes32(accounts[1]), "Key is not correct"); + assert(keyRemoved.keyType, idUtils.types.ADDRESS, "Type is not correct"); + }); }); @@ -137,34 +220,46 @@ contract('Identity', function(accounts) { it("should start only with initializer as only key", async () => { assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), - 1, + idUtils.purposes.MANAGEMENT, identity.address+".getKeyPurpose("+accounts[0]+") is not correct") assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 0, + idUtils.purposes.NONE, identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); it("should get type 2 after addKey type 2", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 2, 1, {from: accounts[0]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 2, + idUtils.purposes.ACTION, identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); - it("should get type 3 after addKey type 3", async () => { - await identity.addKey(TestUtils.addressToBytes32(accounts[1]), 3, 1, {from: accounts[0]}) + it("should get type 3 after addKey type 3", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.CLAIM_SIGNER, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), - 3, + idUtils.purposes.CLAIM_SIGNER, identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); }); - + /* describe("getKeysByType(uint256 _type)", () => { it("at initialization", async () => { @@ -178,36 +273,179 @@ contract('Identity', function(accounts) { it("after removeKey", async () => { }); - - it("after replaceKey", async () => { - - }); - }); - +*/ describe("execute(address _to, uint256 _value, bytes _data)", () => { + let testContractInstance; + let functionPayload; + + it("Identity should receive ether", async() => { + + const amountToSend = web3.toWei(0.05, "ether"); + + let idBalance0 = web3.eth.getBalance(identity.address); + + await web3.eth.sendTransaction({from:accounts[0], to:identity.address, value: amountToSend}) + + let idBalance1 = web3.eth.getBalance(identity.address); + + assert.equal(idBalance0.toNumber() + amountToSend, idBalance1.toNumber(), identity.address + " did not receive ether"); + }); it("ACTOR_KEY execute arbitrary transaction", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + testContractInstance = await TestContract.new({from: accounts[0]}); + + functionPayload = web3EthAbi.encodeFunctionCall({ + name: 'test', + type: 'function', + inputs: [] + }, []); + + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + assert.notEqual( + await TestUtils.listenForEvent(testContractInstance.TestFunctionExecuted()), + undefined, + "Test function was not executed"); }); - it("MANAGEMENT_KEY execute arbitrary transaction", async () => { - + it("MANAGEMENT_KEY cannot execute arbitrary transaction", async () => { + try { + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[0]} + ); + } catch(error) { + TestUtils.assertJump(error); + } }); it("Other keys NOT execute arbitrary transaction", async () => { + try { + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[3]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + }); + + + it("ACTION_KEY should send ether from contract", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + // Adding funds to contract + await web3.eth.sendTransaction({from:accounts[0], to:identity.address, value: web3.toWei(0.05, "ether")}) + + const amountToSend = web3.toWei(0.01, "ether"); + + let idBalance0 = web3.eth.getBalance(identity.address); + let a2Balance0 = web3.eth.getBalance(accounts[2]); + + await identity.execute( + accounts[2], + amountToSend, + '', + {from: accounts[1]} + ); + + let idBalance1 = web3.eth.getBalance(identity.address); + let a2Balance1 = web3.eth.getBalance(accounts[2]); + + assert(idBalance1.toNumber, idBalance0.toNumber - amountToSend, "Contract did not send ether"); + assert(a2Balance1.toNumber, a2Balance0.toNumber + amountToSend, accounts[2] + " did not receive ether"); + }); + + it("fire ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data)", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + + const executionRequested = await TestUtils.listenForEvent(identity.ExecutionRequested()); + assert(executionRequested.to, testContractInstance.address, "To is not correct"); + assert(executionRequested.value, 0, "Value is not correct"); + assert(executionRequested.data, functionPayload, "Data is not correct"); }); it("fire Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data)", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + + const executed = await TestUtils.listenForEvent(identity.Executed()); + assert(executed.to, testContractInstance.address, "To is not correct"); + assert(executed.value, 0, "Value is not correct"); + assert(executed.data, functionPayload, "Data is not correct"); }); }); +/* + + describe("setMinimumApprovalsByKeyPurpose(uint256 _type, uint8 _minimumApprovals)", () => { + it("MANAGEMENT_KEY should set minimum approvals for MANAGEMENT_KEYs", async () => { + + }); + + it("MANAGEMENT_KEY should set minimum approvals for ACTION_KEYs", async () => { + + }); + + it("ACTION_KEY should not be able to set minimum approvals", async () => { + + }); + + it("Other keys should not be able to set minimum approvals", async () => { + + }); + }); + describe("approve(bytes32 _id, bool _approve)", () => { it("MANAGEMENT_KEY should approve a claim", async () => { @@ -218,6 +456,10 @@ contract('Identity', function(accounts) { }); + it("2 out of 3 MANAGEMENT_KEY should approve a transaction and execute it", async () => { + + }); + it("fire Approved(uint256 indexed executionId, bool approved)", async () => { }); @@ -259,5 +501,5 @@ contract('Identity', function(accounts) { }); }); - + */ }); diff --git a/test/identityExtended.js b/test/identityExtended.js new file mode 100644 index 0000000..0e2db9a --- /dev/null +++ b/test/identityExtended.js @@ -0,0 +1,64 @@ +const TestUtils = require("../utils/testUtils.js") +var ethUtils = require('ethereumjs-util') + +const Identity = artifacts.require("./identity/Identity.sol"); + +contract('Identity - Extended Functionality', function(accounts) { + + let identity; + + beforeEach(async () => { + identity = await Identity.new({from: accounts[0]}); + }) + + + describe("Identity()", () => { + + let privateKey = new Buffer('61bffea9215f65164ad18b45aff1436c0c165d0d5dd2087ef61b4232ba6d2c1a', 'hex') + let publicKey = ethUtils.privateToPublic(privateKey); + let pkSha = web3.sha3(publicKey.toString('hex'), {encoding: 'hex'}); + + it("Add ECDSA Management Key", async () => { + + await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) + + await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); + + assert.equal( + await identity.getPublicKey(pkSha, {from: accounts[0]}), + '0x' + publicKey.toString('hex'), + identity.address+".getPublicKey("+pkSha+") is not correct"); + + }); + + + it("Test Execution", async () => { + + let to = accounts[1]; + let value = 100; + let data = ''; + + let message = ethUtils.toBuffer("SignedMessage"); + let msgHash = ethUtils.hashPersonalMessage(message); + let sig = ethUtils.ecsign(msgHash, privateKey); + + let r = '0x' + sig.r.toString('hex'); + let s = '0x' + sig.s.toString('hex'); + let v = sig.v; + + + await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) + + await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); + + let tx = await identity.executeECDSA(to, value, data, pkSha, '0x' + msgHash.toString('hex'), v, r, s, {from: accounts[0]}); + + // TODO Assert ExecutionRequested Event + console.log(tx) + + }); + }); + + + +}); diff --git a/utils/identityUtils.js b/utils/identityUtils.js new file mode 100644 index 0000000..7b52d62 --- /dev/null +++ b/utils/identityUtils.js @@ -0,0 +1,191 @@ +const web3EthAbi = require("web3-eth-abi"); + +const _types = { + ADDRESS: 0, + ECDSA: 1, + RSA: 2 +} + +const _purposes = { + MANAGEMENT: 1, + ACTION: 2, + CLAIM_SIGNER: 3, + ENCRYPTION: 4, + + NONE: 0 +} + +const hexToBytes32 = (input) => { + input = input.replace(/^0x/i,''); + const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + input; + return "0x" + stringed.substring(stringed.length - 64, stringed.length); +} + +const _addKey = function(key, purpose, type){ + if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) + throw new Error('Key "'+ key +'" is not a valid hex string'); + + if (Object.values(_purposes).indexOf(purpose) == -1) + throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); + + if (Object.values(_types).indexOf(type) == -1) + throw new Error('Type "'+ type +'" is not a valid type'); + + return web3EthAbi.encodeFunctionCall({ + name: 'addKey', + type: 'function', + inputs: [{ + type: 'bytes32', + name: '_key' + },{ + type: 'uint256', + name: '_purpose' + },{ + type: 'uint256', + name: '_type' + }] + }, [hexToBytes32(key), purpose, type]); +} + +const _removeKey = function(key, purpose){ + if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) + throw new Error('Key "'+ key +'" is not a valid hex string'); + + if (Object.values(_purposes).indexOf(purpose) == -1) + throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); + + return web3EthAbi.encodeFunctionCall({ + name: 'removeKey', + type: 'function', + inputs: [{ + type: 'bytes32', + name: '_key' + },{ + type: 'uint256', + name: '_purpose' + }] + }, [hexToBytes32(key), purpose]); +} + + +const _setMinimumApprovalsByKeyType = function(type, minimumApprovals) { + + if (Object.values(_types).indexOf(type) == -1) + throw new Error('Type "'+ type +'" is not a valid type'); + + // TODO valdate minimumApprovals + + return web3EthAbi.encodeFunctionCall({ + name: 'setMinimumApprovalsByKeyType', + type: 'function', + inputs: [{ + type: 'uint256', + name: '_type' + },{ + type: 'uint8', + name: '_minimumApprovals' + }] + }, arguments); +} + + +const _setupRecovery = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'setupRecovery', + type: 'function', + inputs: [{ + type: 'address', + name: '_recoveryContract' + }] + }, [address]); +} + +const _managerReset = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'managerReset', + type: 'function', + inputs: [{ + type: 'address', + name: '_newKey' + }] + }, [address]); +} + + +const _updateUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateRequestUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateRequestUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateConfirmUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateConfirmUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateCancelUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateCancelUpdatableInstance', + type: 'function', + inputs: [] + }, []); +} + + + + +module.exports = { + types: _types, + purposes: _purposes, + encode: { + addKey: _addKey, + removeKey: _removeKey, + setMinimumApprovalsByKeyType: _setMinimumApprovalsByKeyType, + setupRecovery: _setupRecovery, + managerReset: _managerReset, + updateUpdatableInstance: _updateUpdatableInstance, + updateRequestUpdatableInstance: _updateRequestUpdatableInstance, + updateConfirmUpdatableInstance: _updateConfirmUpdatableInstance, + updateCancelUpdatableInstance: _updateCancelUpdatableInstance + } +} \ No newline at end of file