├── .env ├── .env.example ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── contracts ├── ClaimHolder.sol ├── ClaimVerifier.sol ├── ERC725.sol ├── ERC735.sol ├── Identity.sol └── KeyHolder.sol ├── data └── .gitignore ├── dist ├── README │ ├── Digital Identity ERC-725.webm │ ├── Digital-Identity-ERC-725-Claim-Checker-Invalid.png │ ├── Digital-Identity-ERC-725-Claim-Checker-Valid.png │ ├── Digital-Identity-ERC-725-Claim-Checker.png │ ├── Digital-Identity-ERC-725-Identities-Claims-1.png │ ├── Digital-Identity-ERC-725-Identities-Claims-2.png │ ├── Digital-Identity-ERC-725-Identities-Claims-3.png │ ├── Digital-Identity-ERC-725-New-Identity.png │ └── Digital-Identity-ERC-725.png └── digital-identity.zip ├── index.js ├── issuer-services ├── _facebook.js ├── _github.js ├── _google.js ├── _linkedin.js ├── _simple.js ├── _twitter.js ├── app.js ├── config.json.eg ├── html.js ├── index.js ├── package.json ├── public │ ├── app.js │ ├── css │ │ ├── app.css │ │ ├── bootstrap.min.css │ │ └── font-awesome.min.css │ ├── dev.html │ ├── favicon.ico │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── images │ │ └── logo.png │ ├── index.html │ ├── vendor.js │ └── vendor │ │ ├── driver.min.css │ │ ├── driver.min.js │ │ └── web3.min.js └── standalone.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── bootstrap.min.css │ └── font-awesome.min.css ├── dev.html ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── images │ └── logo.png ├── index.html └── vendor │ ├── driver.min.css │ ├── driver.min.js │ └── web3.min.js ├── scripts ├── accounts.js └── deploy.js ├── src ├── Store.js ├── actions │ ├── Identity.js │ ├── Network.js │ └── Wallet.js ├── components │ ├── AccountChooser.js │ ├── DetailRow.js │ ├── Dropdown.js │ ├── FormRow.js │ ├── Loading.js │ ├── Modal.js │ ├── NavItem.js │ └── NavLink.js ├── constants │ └── Providers.js ├── contracts │ ├── ClaimHolder.js │ ├── ClaimVerifier.js │ └── Identity.js ├── index.js ├── pages │ ├── App.js │ ├── _Init.js │ ├── _Versions.js │ ├── _driver.js │ ├── console │ │ ├── _Accounts.js │ │ ├── _IPFS.js │ │ ├── _Providers.js │ │ └── index.js │ ├── identity │ │ ├── ClaimCheckerDetail.js │ │ ├── Home.js │ │ ├── IdentityDetail.js │ │ ├── _Events.js │ │ ├── _Summary.js │ │ ├── _ValidateClaim.js │ │ ├── _WalletChooser.js │ │ ├── index.js │ │ └── modals │ │ │ ├── ClaimDetail.js │ │ │ ├── KeyDetail.js │ │ │ ├── _AddClaim.js │ │ │ ├── _AddKey.js │ │ │ ├── _Approve.js │ │ │ ├── _CheckClaim.js │ │ │ ├── _NewClaimIssuer.js │ │ │ ├── _NewIdentity.js │ │ │ ├── _NewVerifier.js │ │ │ ├── _RemoveClaim.js │ │ │ ├── _RemoveIdentity.js │ │ │ └── _RemoveKey.js │ └── initIssuer.js ├── reducers │ ├── Identity.js │ ├── Network.js │ └── Wallet.js └── utils │ ├── balance.js │ ├── decodeFn.js │ ├── ipfsHash.js │ ├── keyMirror.js │ └── numberFormat.js ├── test ├── ClaimVerifier.js ├── Identity.js └── _helper.js ├── webpack.config.js ├── webpack.prod.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | LOCAL=true 2 | RESET=true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LOCAL=true 2 | RESET=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | public/app*.js 4 | public/vendor*.js 5 | public/css/app.css 6 | data/db 7 | data/ipfs 8 | data/OfficialIdentities.js 9 | NOTES 10 | src/contracts/deployed.js 11 | issuer-services/package-lock.json 12 | issuer-services/up.json 13 | issuer-services/config.json 14 | # VSCode 15 | .vscode 16 | 17 | # Environment file 18 | #.env -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 O2O Protocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/ClaimHolder.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | import './ERC735.sol'; 4 | import './KeyHolder.sol'; 5 | 6 | contract ClaimHolder is KeyHolder, ERC735 { 7 | 8 | mapping (bytes32 => Claim) claims; 9 | mapping (uint256 => bytes32[]) claimsByType; 10 | 11 | function addClaim( 12 | uint256 _claimType, 13 | uint256 _scheme, 14 | address _issuer, 15 | bytes _signature, 16 | bytes _data, 17 | string _uri 18 | ) 19 | public 20 | returns (bytes32 claimRequestId) 21 | { 22 | bytes32 claimId = keccak256(_issuer, _claimType); 23 | 24 | if (msg.sender != address(this)) { 25 | require(keyHasPurpose(keccak256(msg.sender), 3), "Sender does not have claim signer key"); 26 | } 27 | 28 | if (claims[claimId].issuer != _issuer) { 29 | claimsByType[_claimType].push(claimId); 30 | } 31 | 32 | claims[claimId].claimType = _claimType; 33 | claims[claimId].scheme = _scheme; 34 | claims[claimId].issuer = _issuer; 35 | claims[claimId].signature = _signature; 36 | claims[claimId].data = _data; 37 | claims[claimId].uri = _uri; 38 | 39 | emit ClaimAdded( 40 | claimId, 41 | _claimType, 42 | _scheme, 43 | _issuer, 44 | _signature, 45 | _data, 46 | _uri 47 | ); 48 | 49 | return claimId; 50 | } 51 | 52 | function removeClaim(bytes32 _claimId) public returns (bool success) { 53 | if (msg.sender != address(this)) { 54 | require(keyHasPurpose(keccak256(msg.sender), 1), "Sender does not have management key"); 55 | } 56 | 57 | /* uint index; */ 58 | /* (index, ) = claimsByType[claims[_claimId].claimType].indexOf(_claimId); 59 | claimsByType[claims[_claimId].claimType].removeByIndex(index); */ 60 | 61 | emit ClaimRemoved( 62 | _claimId, 63 | claims[_claimId].claimType, 64 | claims[_claimId].scheme, 65 | claims[_claimId].issuer, 66 | claims[_claimId].signature, 67 | claims[_claimId].data, 68 | claims[_claimId].uri 69 | ); 70 | 71 | delete claims[_claimId]; 72 | return true; 73 | } 74 | 75 | function getClaim(bytes32 _claimId) 76 | public 77 | constant 78 | returns( 79 | uint256 claimType, 80 | uint256 scheme, 81 | address issuer, 82 | bytes signature, 83 | bytes data, 84 | string uri 85 | ) 86 | { 87 | return ( 88 | claims[_claimId].claimType, 89 | claims[_claimId].scheme, 90 | claims[_claimId].issuer, 91 | claims[_claimId].signature, 92 | claims[_claimId].data, 93 | claims[_claimId].uri 94 | ); 95 | } 96 | 97 | function getClaimIdsByType(uint256 _claimType) 98 | public 99 | constant 100 | returns(bytes32[] claimIds) 101 | { 102 | return claimsByType[_claimType]; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /contracts/ClaimVerifier.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | import './ClaimHolder.sol'; 4 | 5 | contract ClaimVerifier { 6 | 7 | event ClaimValid(ClaimHolder _identity, uint256 claimType); 8 | event ClaimInvalid(ClaimHolder _identity, uint256 claimType); 9 | 10 | ClaimHolder public trustedClaimHolder; 11 | 12 | function ClaimVerifier(address _trustedClaimHolder) public { 13 | trustedClaimHolder = ClaimHolder(_trustedClaimHolder); 14 | } 15 | 16 | function checkClaim(ClaimHolder _identity, uint256 claimType) 17 | public 18 | returns (bool claimValid) 19 | { 20 | if (claimIsValid(_identity, claimType)) { 21 | emit ClaimValid(_identity, claimType); 22 | return true; 23 | } else { 24 | emit ClaimInvalid(_identity, claimType); 25 | return false; 26 | } 27 | } 28 | 29 | function claimIsValid(ClaimHolder _identity, uint256 claimType) 30 | public 31 | constant 32 | returns (bool claimValid) 33 | { 34 | uint256 foundClaimType; 35 | uint256 scheme; 36 | address issuer; 37 | bytes memory sig; 38 | bytes memory data; 39 | 40 | // Construct claimId (identifier + claim type) 41 | bytes32 claimId = keccak256(trustedClaimHolder, claimType); 42 | 43 | // Fetch claim from user 44 | ( foundClaimType, scheme, issuer, sig, data, ) = _identity.getClaim(claimId); 45 | 46 | bytes32 dataHash = keccak256(_identity, claimType, data); 47 | bytes32 prefixedHash = keccak256("\x19Ethereum Signed Message:\n32", dataHash); 48 | 49 | // Recover address of data signer 50 | address recovered = getRecoveredAddress(sig, prefixedHash); 51 | 52 | // Take hash of recovered address 53 | bytes32 hashedAddr = keccak256(recovered); 54 | 55 | // Does the trusted identifier have they key which signed the user's claim? 56 | return trustedClaimHolder.keyHasPurpose(hashedAddr, 3); 57 | } 58 | 59 | function getRecoveredAddress(bytes sig, bytes32 dataHash) 60 | public 61 | view 62 | returns (address addr) 63 | { 64 | bytes32 ra; 65 | bytes32 sa; 66 | uint8 va; 67 | 68 | // Check the signature length 69 | if (sig.length != 65) { 70 | return (0); 71 | } 72 | 73 | // Divide the signature in r, s and v variables 74 | assembly { 75 | ra := mload(add(sig, 32)) 76 | sa := mload(add(sig, 64)) 77 | va := byte(0, mload(add(sig, 96))) 78 | } 79 | 80 | if (va < 27) { 81 | va += 27; 82 | } 83 | 84 | address recoveredAddress = ecrecover(dataHash, va, ra, sa); 85 | 86 | return (recoveredAddress); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /contracts/ERC725.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | contract ERC725 { 4 | 5 | uint256 constant MANAGEMENT_KEY = 1; 6 | uint256 constant ACTION_KEY = 2; 7 | uint256 constant CLAIM_SIGNER_KEY = 3; 8 | uint256 constant ENCRYPTION_KEY = 4; 9 | 10 | event KeyAdded(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 11 | event KeyRemoved(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 12 | event ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 13 | event Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 14 | event Approved(uint256 indexed executionId, bool approved); 15 | 16 | struct Key { 17 | uint256 purpose; //e.g., MANAGEMENT_KEY = 1, ACTION_KEY = 2, etc. 18 | uint256 keyType; // e.g. 1 = ECDSA, 2 = RSA, etc. 19 | bytes32 key; 20 | } 21 | 22 | function getKey(bytes32 _key) public constant returns(uint256 purpose, uint256 keyType, bytes32 key); 23 | function getKeyPurpose(bytes32 _key) public constant returns(uint256 purpose); 24 | function getKeysByPurpose(uint256 _purpose) public constant returns(bytes32[] keys); 25 | function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public returns (bool success); 26 | function execute(address _to, uint256 _value, bytes _data) public returns (uint256 executionId); 27 | function approve(uint256 _id, bool _approve) public returns (bool success); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/ERC735.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | contract ERC735 { 4 | 5 | event ClaimRequested(uint256 indexed claimRequestId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); event ClaimAdded(bytes32 indexed claimId, uint256 indexed claimType, address indexed issuer, uint256 signatureType, bytes32 signature, bytes claim, string uri); 6 | event ClaimAdded(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 7 | event ClaimRemoved(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 8 | event ClaimChanged(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 9 | 10 | struct Claim { 11 | uint256 claimType; 12 | uint256 scheme; 13 | address issuer; // msg.sender 14 | bytes signature; // this.address + claimType + data 15 | bytes data; 16 | string uri; 17 | } 18 | 19 | function getClaim(bytes32 _claimId) public constant returns(uint256 claimType, uint256 scheme, address issuer, bytes signature, bytes data, string uri); 20 | function getClaimIdsByType(uint256 _claimType) public constant returns(bytes32[] claimIds); 21 | function addClaim(uint256 _claimType, uint256 _scheme, address issuer, bytes _signature, bytes _data, string _uri) public returns (bytes32 claimRequestId); 22 | function removeClaim(bytes32 _claimId) public returns (bool success); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/Identity.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | import './ClaimHolder.sol'; 4 | 5 | /** 6 | * NOTE: This contract exists as a convenience for deploying an identity with 7 | * some 'pre-signed' claims. If you don't care about that, just use ClaimHolder 8 | * instead. 9 | */ 10 | 11 | contract Identity is ClaimHolder { 12 | 13 | function Identity( 14 | uint256[] _claimType, 15 | uint256[] _scheme, 16 | address[] _issuer, 17 | bytes _signature, 18 | bytes _data, 19 | string _uri, 20 | uint256[] _sigSizes, 21 | uint256[] dataSizes, 22 | uint256[] uriSizes 23 | ) 24 | public 25 | { 26 | bytes32 claimId; 27 | uint offset = 0; 28 | uint uoffset = 0; 29 | uint doffset = 0; 30 | 31 | for (uint i = 0; i < _claimType.length; i++) { 32 | 33 | claimId = keccak256(_issuer[i], _claimType[i]); 34 | 35 | claims[claimId] = Claim( 36 | _claimType[i], 37 | _scheme[i], 38 | _issuer[i], 39 | getBytes(_signature, offset, _sigSizes[i]), 40 | getBytes(_data, doffset, dataSizes[i]), 41 | getString(_uri, uoffset, uriSizes[i]) 42 | ); 43 | 44 | offset += _sigSizes[i]; 45 | uoffset += uriSizes[i]; 46 | doffset += dataSizes[i]; 47 | 48 | emit ClaimAdded( 49 | claimId, 50 | claims[claimId].claimType, 51 | claims[claimId].scheme, 52 | claims[claimId].issuer, 53 | claims[claimId].signature, 54 | claims[claimId].data, 55 | claims[claimId].uri 56 | ); 57 | } 58 | } 59 | 60 | function getBytes(bytes _str, uint256 _offset, uint256 _length) constant returns (bytes) { 61 | bytes memory sig = new bytes(_length); 62 | uint256 j = 0; 63 | for (uint256 k = _offset; k< _offset + _length; k++) { 64 | sig[j] = _str[k]; 65 | j++; 66 | } 67 | return sig; 68 | } 69 | 70 | function getString(string _str, uint256 _offset, uint256 _length) constant returns (string) { 71 | bytes memory strBytes = bytes(_str); 72 | bytes memory sig = new bytes(_length); 73 | uint256 j = 0; 74 | for (uint256 k = _offset; k< _offset + _length; k++) { 75 | sig[j] = strBytes[k]; 76 | j++; 77 | } 78 | return string(sig); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/KeyHolder.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | import './ERC725.sol'; 4 | 5 | contract KeyHolder is ERC725 { 6 | 7 | uint256 executionNonce; 8 | 9 | struct Execution { 10 | address to; 11 | uint256 value; 12 | bytes data; 13 | bool approved; 14 | bool executed; 15 | } 16 | 17 | mapping (bytes32 => Key) keys; 18 | mapping (uint256 => bytes32[]) keysByPurpose; 19 | mapping (uint256 => Execution) executions; 20 | 21 | event ExecutionFailed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 22 | 23 | function KeyHolder() public { 24 | bytes32 _key = keccak256(msg.sender); 25 | keys[_key].key = _key; 26 | keys[_key].purpose = 1; 27 | keys[_key].keyType = 1; 28 | keysByPurpose[1].push(_key); 29 | emit KeyAdded(_key, keys[_key].purpose, 1); 30 | } 31 | 32 | function getKey(bytes32 _key) 33 | public 34 | view 35 | returns(uint256 purpose, uint256 keyType, bytes32 key) 36 | { 37 | return (keys[_key].purpose, keys[_key].keyType, keys[_key].key); 38 | } 39 | 40 | function getKeyPurpose(bytes32 _key) 41 | public 42 | view 43 | returns(uint256 purpose) 44 | { 45 | return (keys[_key].purpose); 46 | } 47 | 48 | function getKeysByPurpose(uint256 _purpose) 49 | public 50 | view 51 | returns(bytes32[] _keys) 52 | { 53 | return keysByPurpose[_purpose]; 54 | } 55 | 56 | function addKey(bytes32 _key, uint256 _purpose, uint256 _type) 57 | public 58 | returns (bool success) 59 | { 60 | require(keys[_key].key != _key, "Key already exists"); // Key should not already exist 61 | if (msg.sender != address(this)) { 62 | require(keyHasPurpose(keccak256(msg.sender), 1), "Sender does not have management key"); // Sender has MANAGEMENT_KEY 63 | } 64 | 65 | keys[_key].key = _key; 66 | keys[_key].purpose = _purpose; 67 | keys[_key].keyType = _type; 68 | 69 | keysByPurpose[_purpose].push(_key); 70 | 71 | emit KeyAdded(_key, _purpose, _type); 72 | 73 | return true; 74 | } 75 | 76 | function approve(uint256 _id, bool _approve) 77 | public 78 | returns (bool success) 79 | { 80 | require(keyHasPurpose(keccak256(msg.sender), 2), "Sender does not have action key"); 81 | 82 | emit Approved(_id, _approve); 83 | 84 | if (_approve == true) { 85 | executions[_id].approved = true; 86 | success = executions[_id].to.call(executions[_id].data, 0); 87 | if (success) { 88 | executions[_id].executed = true; 89 | emit Executed( 90 | _id, 91 | executions[_id].to, 92 | executions[_id].value, 93 | executions[_id].data 94 | ); 95 | return; 96 | } else { 97 | emit ExecutionFailed( 98 | _id, 99 | executions[_id].to, 100 | executions[_id].value, 101 | executions[_id].data 102 | ); 103 | return; 104 | } 105 | } else { 106 | executions[_id].approved = false; 107 | } 108 | return true; 109 | } 110 | 111 | function execute(address _to, uint256 _value, bytes _data) 112 | public 113 | returns (uint256 executionId) 114 | { 115 | require(!executions[executionNonce].executed, "Already executed"); 116 | executions[executionNonce].to = _to; 117 | executions[executionNonce].value = _value; 118 | executions[executionNonce].data = _data; 119 | 120 | emit ExecutionRequested(executionNonce, _to, _value, _data); 121 | 122 | if (keyHasPurpose(keccak256(msg.sender),1) || keyHasPurpose(keccak256(msg.sender),2)) { 123 | approve(executionNonce, true); 124 | } 125 | 126 | executionNonce++; 127 | return executionNonce-1; 128 | } 129 | 130 | function removeKey(bytes32 _key) 131 | public 132 | returns (bool success) 133 | { 134 | require(keys[_key].key == _key, "No such key"); 135 | emit KeyRemoved(keys[_key].key, keys[_key].purpose, keys[_key].keyType); 136 | 137 | /* uint index; 138 | (index,) = keysByPurpose[keys[_key].purpose.indexOf(_key); 139 | keysByPurpose[keys[_key].purpose.removeByIndex(index); */ 140 | 141 | delete keys[_key]; 142 | 143 | return true; 144 | } 145 | 146 | function keyHasPurpose(bytes32 _key, uint256 _purpose) 147 | public 148 | view 149 | returns(bool result) 150 | { 151 | bool isThere; 152 | if (keys[_key].key == 0) return false; 153 | isThere = keys[_key].purpose <= _purpose; 154 | return isThere; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /dist/README/Digital Identity ERC-725.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital Identity ERC-725.webm -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Claim-Checker-Invalid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Claim-Checker-Invalid.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Claim-Checker-Valid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Claim-Checker-Valid.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Claim-Checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Claim-Checker.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Identities-Claims-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Identities-Claims-1.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Identities-Claims-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Identities-Claims-2.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-Identities-Claims-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-Identities-Claims-3.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725-New-Identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725-New-Identity.png -------------------------------------------------------------------------------- /dist/README/Digital-Identity-ERC-725.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/README/Digital-Identity-ERC-725.png -------------------------------------------------------------------------------- /dist/digital-identity.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/dist/digital-identity.zip -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import serveStatic from 'serve-static' 3 | import spawn from "cross-spawn" 4 | import Ganache from 'ganache-core' 5 | import opener from 'opener' 6 | import fs from 'fs' 7 | import Web3 from 'web3' 8 | import path from "path" 9 | import dotenv from "dotenv" 10 | import shelljs from "shelljs" 11 | 12 | import simpleIssuer from './issuer-services/_simple' 13 | import { spawnSync } from 'child_process'; 14 | import syncReq from "sync-request" 15 | 16 | dotenv.config() 17 | const LOCAL = process.env.LOCAL === "true" 18 | const RESET= process.env.RESET === "true" 19 | 20 | let HOST = "localhost" 21 | if(!LOCAL){ 22 | try{ 23 | HOST = syncReq("GET", "http://ipinfo.io/ip").getBody('utf8').trim() 24 | }catch(e){ 25 | /* Ignore */ 26 | } 27 | } 28 | 29 | const app = express() 30 | 31 | app.get('/', (req, res) => { 32 | var html = fs.readFileSync(__dirname + '/public/dev.html').toString() 33 | res.send(html.replace(/\{HOST\}/g, `http://${HOST}:8082/`)) 34 | }) 35 | app.use(serveStatic('public')) 36 | 37 | try { 38 | var { simpleApp } = require('./issuer-services/config.json') 39 | simpleIssuer(app, { web3: new Web3(), simpleApp }) 40 | } catch(e) { 41 | /* Ignore */ 42 | } 43 | 44 | const startGanache = () => 45 | new Promise((resolve, reject) => { 46 | const dataDir = path.join(__dirname, "data", "db") 47 | if(RESET){ 48 | console.log("Ganache > Reset") 49 | shelljs.rm("-rf", dataDir) 50 | } 51 | shelljs.mkdir("-p", dataDir) 52 | var server = Ganache.server({ 53 | total_accounts: 5, 54 | default_balance_ether: 100, 55 | db_path: 'data/db', 56 | network_id: 999, 57 | seed: 123, 58 | mnemonic: "logic cradle area quality lumber pitch radar sense dove fault capital observe" 59 | // blocktime: 3 60 | }) 61 | server.listen(8545, err => { 62 | if (err) { 63 | return reject(err) 64 | } 65 | console.log('Ganache listening. Starting webpack...') 66 | resolve() 67 | }) 68 | }) 69 | 70 | async function start() { 71 | await startGanache() 72 | const cmd = path.join(__dirname, "node_modules", ".bin", "webpack-dev-server") 73 | const webpackDevServer = spawn(cmd, [ 74 | '--info=true', 75 | '--port=8082', 76 | '--host=0.0.0.0' 77 | ]) 78 | webpackDevServer.stdout.pipe(process.stdout) 79 | webpackDevServer.stderr.pipe(process.stderr) 80 | process.on('exit', () => webpackDevServer.kill()) 81 | 82 | const PORT = process.env.PORT || 3333 83 | app.listen(PORT, () => { 84 | console.log(`\nListening on host ${HOST}, port ${PORT}\n`) 85 | setTimeout(() => { 86 | try{ 87 | const url = `http://${HOST}:${PORT}` 88 | console.log(`Opening Browser at ${url}`) 89 | const browser = opener(url) 90 | browser.unref(); 91 | }catch(err){ 92 | console.log("open browser", err.message); 93 | } 94 | }, 4500) 95 | }) 96 | } 97 | 98 | start() 99 | -------------------------------------------------------------------------------- /issuer-services/_facebook.js: -------------------------------------------------------------------------------- 1 | // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow 2 | 3 | var superagent = require('superagent') 4 | var HTML = require('./html') 5 | 6 | const ClaimType = 3 // Has Facebook 7 | 8 | module.exports = function facebook(app, { web3, facebookApp, baseUrl }) { 9 | const redirect_uri = `${baseUrl}/fb-auth-response` 10 | app.get('/fb-auth', (req, res) => { 11 | if (!req.query.target) { 12 | res.send('No target identity contract provided') 13 | return 14 | } 15 | if (!req.query.issuer) { 16 | res.send('No issuer identity contract provided') 17 | return 18 | } 19 | 20 | req.session.targetIdentity = req.query.target 21 | req.session.issuer = req.query.issuer 22 | req.session.state = web3.utils.randomHex(8) 23 | 24 | var query = [ 25 | `client_id=${facebookApp.client_id}`, 26 | `redirect_uri=${redirect_uri}`, 27 | `state=${req.session.state}` 28 | ] 29 | res.redirect( 30 | `https://www.facebook.com/v2.12/dialog/oauth?${query.join('&')}` 31 | ) 32 | }) 33 | 34 | app.get( 35 | '/fb-auth-response', 36 | (req, res, next) => { 37 | if (!req.query.code) { 38 | return res.send('No Code specified') 39 | } 40 | if (req.query.state !== req.session.state) { 41 | return res.send('State param does not match') 42 | } 43 | if (!req.session.targetIdentity) { 44 | return res.send('No target identity found') 45 | } 46 | if (!req.session.issuer) { 47 | return res.send('No issuer found') 48 | } 49 | 50 | superagent 51 | .get(`https://graph.facebook.com/v2.12/oauth/access_token`) 52 | .query({ 53 | client_id: facebookApp.client_id, 54 | client_secret: facebookApp.secret, 55 | redirect_uri, 56 | code: req.query.code 57 | }) 58 | .then(response => { 59 | req.userToken = response.body 60 | next() 61 | }) 62 | .catch(() => { 63 | res.send('Error fetching token') 64 | }) 65 | }, 66 | (req, res, next) => { 67 | superagent 68 | .get(`https://graph.facebook.com/debug_token`) 69 | .query({ 70 | input_token: req.userToken.access_token, 71 | access_token: `${facebookApp.client_id}|${facebookApp.secret}` 72 | }) 73 | .then(response => { 74 | req.tokenDebug = JSON.parse(response.text).data 75 | 76 | if (req.tokenDebug.app_id !== facebookApp.client_id) { 77 | return res.send("Token's App does not match") 78 | } 79 | if (!req.tokenDebug.is_valid) { 80 | return res.send('Token is invalid') 81 | } 82 | next() 83 | }) 84 | .catch((e) => { 85 | console.log(e) 86 | res.send('Error validating token') 87 | }) 88 | }, 89 | async (req, res) => { 90 | // var data = JSON.stringify({ user_id: req.tokenDebug.user_id }) 91 | var rawData = 'Verified OK' 92 | var hexData = web3.utils.asciiToHex(rawData) 93 | var hashed = web3.utils.soliditySha3(req.session.targetIdentity, ClaimType, hexData) 94 | req.signedData = await web3.eth.accounts.sign(hashed, facebookApp.claimSignerKey) 95 | 96 | res.send(HTML(` 97 |
Successfully signed claim:
98 |
Issuer: ${req.session.issuer}
99 |
Target: ${req.session.targetIdentity}
100 |
Data: ${rawData}
101 |
Signature: ${req.signedData.signature}
102 |
Hash: ${req.signedData.messageHash}
103 |
104 | ` 111 | )) 112 | } 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /issuer-services/_github.js: -------------------------------------------------------------------------------- 1 | // https://github.com/settings/developers 2 | 3 | var OAuth = require('oauth').OAuth2 4 | var HTML = require('./html') 5 | var superagent = require('superagent') 6 | 7 | const ClaimType = 5 // Has GitHub 8 | 9 | module.exports = function github(app, { web3, githubApp, baseUrl }) { 10 | const redirect_uri = `${baseUrl}/github-auth-response` 11 | 12 | var githubOAuth = new OAuth( 13 | githubApp.client_id, 14 | githubApp.secret, 15 | 'https://github.com', 16 | '/login/oauth/authorize', 17 | '/login/oauth/access_token', 18 | null 19 | ) 20 | 21 | app.get('/github-auth', (req, res) => { 22 | if (!req.query.target) { 23 | res.send('No target identity contract provided') 24 | return 25 | } 26 | if (!req.query.issuer) { 27 | res.send('No issuer identity contract provided') 28 | return 29 | } 30 | 31 | req.session.targetIdentity = req.query.target 32 | req.session.issuer = req.query.issuer 33 | req.session.state = web3.utils.randomHex(8) 34 | 35 | var authURL = githubOAuth.getAuthorizeUrl({ 36 | redirect_uri, 37 | scope: ['user'], 38 | state: req.session.state 39 | }) 40 | 41 | res.redirect(authURL) 42 | }) 43 | 44 | app.get( 45 | '/github-auth-response', 46 | (req, res, next) => { 47 | githubOAuth.getOAuthAccessToken( 48 | req.query.code, 49 | { redirect_uri }, 50 | function(e, access_token, refresh_token, results) { 51 | if (e) { 52 | next(e) 53 | } else if (results.error) { 54 | next(results.error) 55 | } else { 56 | req.access_token = access_token 57 | next() 58 | } 59 | } 60 | ) 61 | }, 62 | (req, res, next) => { 63 | superagent 64 | .get('https://api.github.com/user') 65 | .set('Authorization', `token ${req.access_token}`) 66 | .accept('json') 67 | .then(response => { 68 | req.githubUser = response.body 69 | next() 70 | }) 71 | }, 72 | async (req, res) => { 73 | // var data = JSON.stringify({ user_id: req.githubUser.id }) 74 | var rawData = 'Verified OK' 75 | var hexData = web3.utils.asciiToHex(rawData) 76 | var hashed = web3.utils.soliditySha3(req.session.targetIdentity, ClaimType, hexData) 77 | req.signedData = await web3.eth.accounts.sign(hashed, githubApp.claimSignerKey) 78 | 79 | res.send( 80 | HTML(` 81 |
Successfully signed claim:
82 |
Issuer: ${req.session.issuer}
83 |
Target: ${req.session.targetIdentity}
84 |
Data: ${rawData}
85 |
Signature: ${req.signedData.signature}
86 |
Hash: ${req.signedData.messageHash}
87 |
88 | `) 95 | ) 96 | } 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /issuer-services/_google.js: -------------------------------------------------------------------------------- 1 | // https://console.developers.google.com/apis/credentials 2 | 3 | var OAuth = require('oauth').OAuth2 4 | var HTML = require('./html') 5 | var superagent = require('superagent') 6 | 7 | const ClaimType = 6 // Has Google 8 | 9 | module.exports = function google(app, { web3, googleApp, baseUrl }) { 10 | const redirect_uri = `${baseUrl}/google-auth-response` 11 | 12 | var googleOAuth = new OAuth( 13 | googleApp.client_id, 14 | googleApp.secret, 15 | 'https://accounts.google.com', 16 | '/o/oauth2/auth', 17 | '/o/oauth2/token' 18 | ) 19 | 20 | app.get('/google-auth', (req, res) => { 21 | if (!req.query.target) { 22 | res.send('No target identity contract provided') 23 | return 24 | } 25 | if (!req.query.issuer) { 26 | res.send('No issuer identity contract provided') 27 | return 28 | } 29 | 30 | req.session.targetIdentity = req.query.target 31 | req.session.issuer = req.query.issuer 32 | req.session.state = web3.utils.randomHex(8) 33 | 34 | var authURL = googleOAuth.getAuthorizeUrl({ 35 | redirect_uri, 36 | scope: 'https://www.googleapis.com/auth/userinfo.profile', 37 | state: req.session.state, 38 | response_type: 'code' 39 | }) 40 | 41 | res.redirect(authURL) 42 | }) 43 | 44 | app.get( 45 | '/google-auth-response', 46 | (req, res, next) => { 47 | googleOAuth.getOAuthAccessToken( 48 | req.query.code, 49 | { 50 | redirect_uri, 51 | grant_type: 'authorization_code' 52 | }, 53 | function(e, access_token, refresh_token, results) { 54 | if (e) { 55 | next(e) 56 | } else if (results.error) { 57 | next(results.error) 58 | } else { 59 | req.access_token = access_token 60 | next() 61 | } 62 | } 63 | ) 64 | }, 65 | (req, res, next) => { 66 | superagent 67 | .get('https://www.googleapis.com/oauth2/v1/userinfo') 68 | .query({ 69 | alt: 'json', 70 | access_token: req.access_token 71 | }) 72 | .then(response => { 73 | req.googleUser = response.body 74 | next() 75 | }) 76 | }, 77 | async (req, res) => { 78 | // var data = JSON.stringify({ user_id: req.googleUser.id }) 79 | 80 | var rawData = 'Verified OK' 81 | var hexData = web3.utils.asciiToHex(rawData) 82 | var hashed = web3.utils.soliditySha3(req.session.targetIdentity, ClaimType, hexData) 83 | req.signedData = await web3.eth.accounts.sign(hashed, googleApp.claimSignerKey) 84 | 85 | res.send( 86 | HTML(` 87 |
Successfully signed claim:
88 |
Issuer: ${req.session.issuer}
89 |
Target: ${req.session.targetIdentity}
90 |
Data: ${rawData}
91 |
Signature: ${req.signedData.signature}
92 |
Hash: ${req.signedData.messageHash}
93 |
94 | `) 101 | ) 102 | } 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /issuer-services/_linkedin.js: -------------------------------------------------------------------------------- 1 | // https://developer.linkedin.com/docs/oauth2 2 | 3 | var OAuth = require('oauth').OAuth2 4 | var HTML = require('./html') 5 | var superagent = require('superagent') 6 | 7 | const ClaimType = 9 // Has LinkedIn 8 | 9 | module.exports = function linkedin(app, { web3, linkedInApp, baseUrl }) { 10 | const redirect_uri = `${baseUrl}/linkedin-auth-response` 11 | 12 | var linkedInOAuth = new OAuth( 13 | linkedInApp.client_id, 14 | linkedInApp.secret, 15 | 'https://www.linkedin.com', 16 | '/oauth/v2/authorization', 17 | '/oauth/v2/accessToken', 18 | null 19 | ) 20 | 21 | app.get('/linkedin-auth', (req, res) => { 22 | if (!req.query.target) { 23 | res.send('No target identity contract provided') 24 | return 25 | } 26 | if (!req.query.issuer) { 27 | res.send('No issuer identity contract provided') 28 | return 29 | } 30 | 31 | req.session.targetIdentity = req.query.target 32 | req.session.issuer = req.query.issuer 33 | req.session.state = web3.utils.randomHex(8) 34 | 35 | var authURL = linkedInOAuth.getAuthorizeUrl({ 36 | redirect_uri, 37 | scope: ['r_basicprofile', 'r_emailaddress'], 38 | state: req.session.state, 39 | response_type: 'code' 40 | }) 41 | 42 | res.redirect(authURL) 43 | }) 44 | 45 | app.get( 46 | '/linkedin-auth-response', 47 | (req, res, next) => { 48 | linkedInOAuth.getOAuthAccessToken( 49 | req.query.code, 50 | { 51 | redirect_uri, 52 | grant_type: 'authorization_code' 53 | }, 54 | function(e, access_token, refresh_token, results) { 55 | if (e) { 56 | next(e) 57 | } else if (results.error) { 58 | next(results.error) 59 | } else { 60 | req.access_token = access_token 61 | next() 62 | } 63 | } 64 | ) 65 | }, 66 | (req, res, next) => { 67 | superagent 68 | .get('https://api.linkedin.com/v1/people/~') 69 | .set('Authorization', `Bearer ${req.access_token}`) 70 | .query({ format: 'json' }) 71 | .then(response => { 72 | req.linkedInUser = response.body 73 | next() 74 | }) 75 | }, 76 | async (req, res) => { 77 | // var data = JSON.stringify({ user_id: req.githubUser.id }) 78 | var rawData = 'Verified OK' 79 | var hexData = web3.utils.asciiToHex(rawData) 80 | var hashed = web3.utils.soliditySha3(req.session.targetIdentity, ClaimType, hexData) 81 | req.signedData = await web3.eth.accounts.sign( 82 | hashed, 83 | linkedInApp.claimSignerKey 84 | ) 85 | 86 | res.send( 87 | HTML(` 88 |
Successfully signed claim:
89 |
Issuer: ${req.session.issuer}
90 |
Target: ${req.session.targetIdentity}
91 |
Data: ${rawData}
92 |
Signature: ${req.signedData.signature}
93 |
Hash: ${req.signedData.messageHash}
94 |
95 | `) 102 | ) 103 | } 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /issuer-services/_simple.js: -------------------------------------------------------------------------------- 1 | var HTML = require('./html') 2 | 3 | const ClaimType = '7'; 4 | 5 | module.exports = function dummyService(app, { web3, simpleApp }) { 6 | 7 | app.get('/simple-auth', async (req, res) => { 8 | var issuer = req.query.issuer, 9 | target = req.query.target 10 | 11 | if (!target) { 12 | res.send(HTML('No target identity contract provided')) 13 | return 14 | } 15 | if (!issuer) { 16 | res.send(HTML('No issuer identity contract provided')) 17 | return 18 | } 19 | if (!simpleApp.claimSignerKey) { 20 | res.send(HTML('No private key specified.')) 21 | return 22 | } 23 | 24 | var rawData = 'Verified OK' 25 | var hexData = web3.utils.asciiToHex(rawData) 26 | var hashed = web3.utils.soliditySha3(target, ClaimType, hexData) 27 | var signedData = await web3.eth.accounts.sign(hashed, simpleApp.claimSignerKey) 28 | 29 | res.send( 30 | HTML( 31 | `
This example authentication service returns some signed data which can be added to a claim
32 |
Issuer: ${issuer}
33 |
Target: ${target}
34 |
Data: ${rawData}
35 |
Signature: ${signedData.signature}
36 |
Hash: ${signedData.messageHash}
37 |
38 | ` 45 | ) 46 | ) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /issuer-services/_twitter.js: -------------------------------------------------------------------------------- 1 | // https://apps.twitter.com/ 2 | 3 | var OAuth = require('oauth').OAuth 4 | var HTML = require('./html') 5 | 6 | const ClaimType = 4 // Has Twitter 7 | 8 | module.exports = function twitter(app, { web3, twitterApp, baseUrl }) { 9 | var twitterOAuth = new OAuth( 10 | 'https://api.twitter.com/oauth/request_token', 11 | 'https://api.twitter.com/oauth/access_token', 12 | twitterApp.client_id, 13 | twitterApp.secret, 14 | '1.0', 15 | `${baseUrl}/twitter-auth-response`, 16 | 'HMAC-SHA1' 17 | ) 18 | 19 | app.get('/twitter-auth', (req, res) => { 20 | if (!req.query.target) { 21 | res.send('No target identity contract provided') 22 | return 23 | } 24 | if (!req.query.issuer) { 25 | res.send('No issuer identity contract provided') 26 | return 27 | } 28 | 29 | req.session.targetIdentity = req.query.target 30 | req.session.issuer = req.query.issuer 31 | 32 | twitterOAuth.getOAuthRequestToken(function( 33 | error, 34 | oAuthToken, 35 | oAuthTokenSecret 36 | ) { 37 | req.session.oAuthTokenSecret = oAuthTokenSecret 38 | res.redirect( 39 | `https://twitter.com/oauth/authenticate?oauth_token=${oAuthToken}` 40 | ) 41 | }) 42 | }) 43 | 44 | app.get( 45 | '/twitter-auth-response', 46 | (req, res, next) => { 47 | twitterOAuth.getOAuthAccessToken( 48 | req.query.oauth_token, 49 | req.session.oAuthTokenSecret, 50 | req.query.oauth_verifier, 51 | function(error, oAuthAccessToken, oAuthAccessTokenSecret) { 52 | if (error) { 53 | console.log(error) 54 | res.send('Error') 55 | return 56 | } 57 | 58 | req.oAuthAccessToken = oAuthAccessToken 59 | req.oAuthAccessTokenSecret = oAuthAccessTokenSecret 60 | 61 | next() 62 | } 63 | ) 64 | }, 65 | (req, res, next) => { 66 | twitterOAuth.get( 67 | 'https://api.twitter.com/1.1/account/verify_credentials.json', 68 | req.oAuthAccessToken, 69 | req.oAuthAccessTokenSecret, 70 | function(error, twitterResponseData) { 71 | if (error) { 72 | res.send('Error') 73 | return 74 | } 75 | try { 76 | req.twitterUser = JSON.parse(twitterResponseData) 77 | next() 78 | } catch (parseError) { 79 | res.send("Error parsing response") 80 | } 81 | } 82 | ) 83 | }, 84 | async (req, res) => { 85 | // var data = JSON.stringify({ user_id: req.twitterUser.id }) 86 | 87 | var rawData = 'Verified OK' 88 | var hexData = web3.utils.asciiToHex(rawData) 89 | var hashed = web3.utils.soliditySha3(req.session.targetIdentity, ClaimType, hexData) 90 | req.signedData = await web3.eth.accounts.sign(hashed, twitterApp.claimSignerKey) 91 | 92 | res.send( 93 | HTML(` 94 |
Successfully signed claim:
95 |
Issuer: ${req.session.issuer}
96 |
Target: ${req.session.targetIdentity}
97 |
Data: ${rawData}
98 |
Signature: ${req.signedData.signature}
99 |
Hash: ${req.signedData.messageHash}
100 |
101 | `) 108 | ) 109 | } 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /issuer-services/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var session = require('express-session') 3 | var Web3 = require('web3') 4 | 5 | var simple = require('./_simple') 6 | var facebook = require('./_facebook') 7 | var twitter = require('./_twitter') 8 | var github = require('./_github') 9 | var google = require('./_google') 10 | var linkedin = require('./_linkedin') 11 | 12 | try { 13 | var Config = require('./config.json') 14 | } catch (e) { 15 | console.log('Please copy config.json.eg to config.json and update it.') 16 | process.exit() 17 | } 18 | 19 | Config.web3 = new Web3(Config.provider) 20 | 21 | const app = express() 22 | app.use( 23 | session({ 24 | secret: 'top secret string', 25 | resave: false, 26 | saveUninitialized: true 27 | }) 28 | ) 29 | 30 | app.get('/', (req, res) => { 31 | res.send('Issuer Services') 32 | }) 33 | 34 | simple(app, Config) 35 | facebook(app, Config) 36 | twitter(app, Config) 37 | github(app, Config) 38 | google(app, Config) 39 | linkedin(app, Config) 40 | 41 | const PORT = process.env.PORT || 3001 42 | app.listen(PORT, () => { 43 | console.log(`\nListening on port ${PORT}\n`) 44 | }) 45 | -------------------------------------------------------------------------------- /issuer-services/config.json.eg: -------------------------------------------------------------------------------- 1 | { 2 | "provider": "ws://localhost:8545", 3 | "baseUrl": "http://localhost:3333", 4 | "facebookApp": { 5 | "client_id": "000000000000000", 6 | "secret": "00000000000000000000000000000000", 7 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 8 | }, 9 | "twitterApp": { 10 | "client_id": "0000000000000000000000000", 11 | "secret": "00000000000000000000000000000000000000000000000000", 12 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 13 | }, 14 | "githubApp": { 15 | "client_id": "00000000000000000000", 16 | "secret": "0000000000000000000000000000000000000000", 17 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 18 | }, 19 | "googleApp": { 20 | "client_id": "0000000000000000000000000.apps.googleusercontent.com", 21 | "secret": "000000000000000000000000", 22 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 23 | }, 24 | "linkedInApp": { 25 | "client_id": "00000000000000", 26 | "secret": "0000000000000000", 27 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 28 | }, 29 | "simpleApp": { 30 | "claimSignerKey": "0x0000000000000000000000000000000000000000000000000000000000000000" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /issuer-services/html.js: -------------------------------------------------------------------------------- 1 | const HTML = content => ` 2 | 3 | 4 | ERC-725 Authentication 5 | 6 | 7 | 8 | 9 | 10 | 24 | 25 |
${content}
26 | ` 27 | 28 | module.exports = HTML 29 | -------------------------------------------------------------------------------- /issuer-services/index.js: -------------------------------------------------------------------------------- 1 | var Web3 = require('web3') 2 | 3 | var Config = require('./config.json') 4 | var facebook = require('./_facebook') 5 | var google = require('./_google') 6 | var github = require('./_github') 7 | var simple = require('./_simple') 8 | var twitter = require('./_twitter') 9 | var linkedin = require('./_linkedin') 10 | 11 | module.exports = function (app) { 12 | Config.web3 = new Web3() 13 | facebook(app, Config) 14 | google(app, Config) 15 | github(app, Config) 16 | simple(app, Config) 17 | twitter(app, Config) 18 | linkedin(app, Config) 19 | } 20 | -------------------------------------------------------------------------------- /issuer-services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "issuer-services", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "engines": { 7 | "node": ">=8.11.1" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "node standalone.js" 12 | }, 13 | "author": "O2OProtocol", 14 | "license": "MIT", 15 | "dependencies": { 16 | "express": "^4.16.3", 17 | "express-session": "^1.15.6", 18 | "oauth": "^0.9.15", 19 | "superagent": "^3.8.2", 20 | "ws": "^5.1.1", 21 | "xhr2": "^0.1.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /issuer-services/public/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Digital Identity ERC-725 Dev 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Please wait...
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /issuer-services/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/favicon.ico -------------------------------------------------------------------------------- /issuer-services/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /issuer-services/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /issuer-services/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /issuer-services/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /issuer-services/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /issuer-services/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/issuer-services/public/images/logo.png -------------------------------------------------------------------------------- /issuer-services/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Digital Identity Blockchain ERC-725 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /issuer-services/public/vendor/driver.min.css: -------------------------------------------------------------------------------- 1 | div#driver-popover-item{display:none;position:absolute;background:#fff;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px rgba(0,0,0,.4);z-index:1000000000}div#driver-popover-item .driver-popover-tip{border:5px solid #fff;content:"";position:absolute}div#driver-popover-item .driver-popover-tip.bottom{bottom:-10px;border-top-color:#fff;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}div#driver-popover-item .driver-popover-tip.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:#fff;border-bottom-color:transparent;border-left-color:transparent}div#driver-popover-item .driver-popover-tip.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:#fff}div#driver-popover-item .driver-popover-tip.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:#fff;border-left-color:transparent}div#driver-popover-item .driver-popover-footer{display:block;clear:both;margin-top:5px}div#driver-popover-item .driver-popover-footer button{display:inline-block;padding:3px 10px;border:1px solid #d4d4d4;text-decoration:none;text-shadow:1px 1px 0 #fff;color:#2d2d2d;font:11px/normal sans-serif;cursor:pointer;outline:0;background-color:#f1f1f1;border-radius:2px;zoom:1;margin:10px 0 0;line-height:1.3}div#driver-popover-item .driver-popover-footer button.driver-disabled{color:gray;cursor:default;pointer-events:none}div#driver-popover-item .driver-popover-footer .driver-close-btn{float:left}div#driver-popover-item .driver-popover-footer .driver-btn-group{float:right}div#driver-popover-item .driver-popover-title{font:19px/normal sans-serif;margin:0 0 5px;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1}div#driver-popover-item .driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;color:#2d2d2d;font-weight:400;zoom:1}.driver-no-animation{-webkit-transition:none!important;-moz-transition:none!important;-ms-transition:none!important;-o-transition:none!important;transition:none!important}div#driver-page-overlay{background:#000;position:fixed;bottom:0;right:0;display:block;width:100%;height:100%;zoom:1;filter:alpha(opacity=75);opacity:.75;z-index:100002!important}div#driver-highlighted-element-stage,div#driver-page-overlay{top:0;left:0;-webkit-transition:all .4s;-moz-transition:all .4s;-ms-transition:all .4s;-o-transition:all .4s;transition:all .4s}div#driver-highlighted-element-stage{position:absolute;height:50px;width:300px;background:#fff;z-index:100003!important;display:none}.driver-highlighted-element{z-index:100004!important}.driver-position-relative{position:relative!important}.driver-fix-stacking{z-index:auto!important;opacity:1!important;-webkit-transform:none!important;-moz-transform:none!important;-ms-transform:none!important;-o-transform:none!important;transform:none!important;-webkit-filter:none!important;-moz-filter:none!important;-ms-filter:none!important;-o-filter:none!important;filter:none!important;-webkit-perspective:none!important;-moz-perspective:none!important;-ms-perspective:none!important;-o-perspective:none!important;perspective:none!important} 2 | /*# sourceMappingURL=driver.min.css.map*/ -------------------------------------------------------------------------------- /issuer-services/standalone.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const session = require('express-session') 3 | const crypto = require('crypto') 4 | const WebSocket = require('ws') 5 | const serveStatic = require('serve-static') 6 | 7 | global.XMLHttpRequest = require('xhr2') 8 | global.window = { Promise, WebSocket, crypto } 9 | 10 | var simple = require('./_simple') 11 | var facebook = require('./_facebook') 12 | var google = require('./_google') 13 | var github = require('./_github') 14 | var twitter = require('./_twitter') 15 | var linkedin = require('./_linkedin') 16 | 17 | var Web3 = require('./public/vendor/web3.min') 18 | 19 | let Config = require('./config.json') 20 | Config.web3 = new Web3(Config.provider) 21 | 22 | const app = express() 23 | if (process.env.NODE_ENV === 'production') { 24 | app.use((req, res, next) => { 25 | if (req.header('x-forwarded-proto') !== 'https') { 26 | res.redirect(`https://${req.header('host')}${req.url}`) 27 | } else { 28 | next() 29 | } 30 | }) 31 | } 32 | app.use(serveStatic('public')) 33 | app.use( 34 | session({ 35 | secret: 'top secret string', 36 | resave: false, 37 | saveUninitialized: true 38 | }) 39 | ) 40 | 41 | app.get('/', (req, res) => { 42 | res.sendFile(__dirname + '/public/index.html') 43 | }) 44 | 45 | simple(app, Config) 46 | facebook(app, Config) 47 | google(app, Config) 48 | github(app, Config) 49 | twitter(app, Config) 50 | linkedin(app, Config) 51 | 52 | const PORT = process.env.PORT || 3001 53 | app.listen(PORT, () => { 54 | console.log(`\nListening on port ${PORT}\n`) 55 | }) 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "O2OProtocol", 3 | "name": "digital-identity", 4 | "version": "0.2.0", 5 | "license": "MIT", 6 | "description": "An implementation of ERC-725 & ERC-735 for Digital Identity Blockchain. We uses the Truffle framework and Ganache CLI for testing.", 7 | "main": "index.js", 8 | "engines": { 9 | "node": ">=8.11.1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/o2oprotocol/digital-identity.git" 14 | }, 15 | "scripts": { 16 | "test": "mocha -r @babel/register test -t 10000", 17 | "test:watch": "mocha -r @babel/register -w --watch-extensions sol -t 5000 test", 18 | "start": "node -r @babel/register index", 19 | "build:contracts": "BUILD=1 mocha -r @babel/register -t 10000 test", 20 | "build:js": "webpack --config webpack.prod.js --progress", 21 | "build:css": "node -r @babel/register -r ./src/pages/App -e \"console.log(require('react-styl').getCss())\" > public/css/app.css", 22 | "build": "npm run build:js && npm run build:css", 23 | "lint": "eslint .", 24 | "prettier": "find ./src -iname '*.js' | xargs ./node_modules/.bin/prettier --write", 25 | "clean": "rm -rf data/db" 26 | }, 27 | "dependencies": { 28 | "@babel/core": "^7.0.0-beta.44", 29 | "@babel/preset-react": "^7.0.0-beta.44", 30 | "@babel/register": "^7.0.0-beta.44", 31 | "ajv": "^6.4.0", 32 | "babel-plugin-module-resolver": "^3.1.1", 33 | "body-parser": "^1.18.2", 34 | "bs58": "^4.0.1", 35 | "cross-spawn": "^6.0.5", 36 | "date-fns": "^1.29.0", 37 | "dotenv": "^5.0.1", 38 | "express": "^4.16.3", 39 | "express-session": "^1.15.6", 40 | "ganache-core": "2.1.0", 41 | "ipfs": "^0.28.2", 42 | "ipfs-api": "^20.0.1", 43 | "oauth": "^0.9.15", 44 | "prettier": "^1.12.1", 45 | "react": "^16.3.2", 46 | "react-dom": "^16.3.2", 47 | "react-redux": "^5.0.7", 48 | "react-styl": "^0.0.3", 49 | "redux": "^3.7.2", 50 | "redux-logger": "^3.0.6", 51 | "redux-thunk": "^2.2.0", 52 | "serve-static": "^1.13.2", 53 | "shelljs": "^0.8.2", 54 | "solc": "^0.4.22", 55 | "superagent": "^3.8.2", 56 | "sync-request": "^6.0.0", 57 | "web3": "^1.0.0-beta.34" 58 | }, 59 | "prettier": { 60 | "semi": false, 61 | "singleQuote": true, 62 | "bracketSpacing": true 63 | }, 64 | "babel": { 65 | "presets": [ 66 | "@babel/preset-env", 67 | "@babel/preset-react" 68 | ], 69 | "plugins": [ 70 | [ 71 | "module-resolver", 72 | { 73 | "alias": { 74 | "actions": "./src/actions", 75 | "components": "./src/components", 76 | "constants": "./src/constants", 77 | "contracts": "./src/contracts", 78 | "pages": "./src/pages", 79 | "reducers": "./src/reducers", 80 | "utils": "./src/utils" 81 | } 82 | } 83 | ], 84 | "@babel/plugin-transform-runtime", 85 | "@babel/plugin-transform-destructuring", 86 | "@babel/plugin-transform-object-assign", 87 | "@babel/plugin-proposal-object-rest-spread" 88 | ] 89 | }, 90 | "devDependencies": { 91 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.44", 92 | "@babel/plugin-syntax-object-rest-spread": "^7.0.0-beta.44", 93 | "@babel/plugin-transform-destructuring": "^7.0.0-beta.44", 94 | "@babel/plugin-transform-object-assign": "^7.0.0-beta.44", 95 | "@babel/plugin-transform-runtime": "^7.0.0-beta.44", 96 | "@babel/preset-env": "^7.0.0-beta.44", 97 | "@babel/runtime": "^7.0.0-beta.44", 98 | "babel-eslint": "^8.2.3", 99 | "babel-loader": "^8.0.0-beta.0", 100 | "clean-webpack-plugin": "^0.1.19", 101 | "eslint": "^4.19.1", 102 | "eslint-plugin-babel": "^5.0.0", 103 | "eslint-plugin-react": "^7.7.0", 104 | "opener": "^1.4.3", 105 | "react-router": "^4.2.0", 106 | "react-router-dom": "^4.2.2", 107 | "webpack": "^4.5.0", 108 | "webpack-cli": "^2.0.14", 109 | "webpack-dev-middleware": "^3.1.2", 110 | "webpack-dev-server": "^v3.1.3" 111 | }, 112 | "eslintConfig": { 113 | "parser": "babel-eslint", 114 | "parserOptions": { 115 | "ecmaVersion": 6, 116 | "sourceType": "module", 117 | "ecmaFeatures": { 118 | "jsx": true, 119 | "impliedStrict": true 120 | } 121 | }, 122 | "globals": { 123 | "Web3": true, 124 | "web3": true, 125 | "OfficialIdentities": true 126 | }, 127 | "env": { 128 | "browser": true, 129 | "node": true, 130 | "es6": true, 131 | "mocha": true 132 | }, 133 | "plugins": [ 134 | "react" 135 | ], 136 | "extends": [ 137 | "eslint:recommended", 138 | "plugin:react/recommended" 139 | ], 140 | "rules": { 141 | "react/prop-types": "off", 142 | "react/no-children-prop": "off", 143 | "no-console": "off" 144 | } 145 | }, 146 | "eslintIgnore": [ 147 | "node_modules", 148 | "public" 149 | ], 150 | "bugs": { 151 | "url": "https://github.com/o2oprotocol/digital-identity/issues" 152 | }, 153 | "homepage": "https://github.com/o2oprotocol/digital-identity#readme", 154 | "keywords": [ 155 | "Digital", 156 | "Identity", 157 | "Identity", 158 | "Blockchain", 159 | "ERC-725", 160 | "ERC-735" 161 | ] 162 | } 163 | -------------------------------------------------------------------------------- /public/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Digital Identity ERC-725 Dev 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Please wait...
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2oprotocol/digital-identity/fe5e45d041d225d4438982dbc81891ae6c7e097e/public/images/logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Digital Identity Blockchain ERC-725 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/vendor/driver.min.css: -------------------------------------------------------------------------------- 1 | div#driver-popover-item{display:none;position:absolute;background:#fff;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px rgba(0,0,0,.4);z-index:1000000000}div#driver-popover-item .driver-popover-tip{border:5px solid #fff;content:"";position:absolute}div#driver-popover-item .driver-popover-tip.bottom{bottom:-10px;border-top-color:#fff;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}div#driver-popover-item .driver-popover-tip.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:#fff;border-bottom-color:transparent;border-left-color:transparent}div#driver-popover-item .driver-popover-tip.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:#fff}div#driver-popover-item .driver-popover-tip.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:#fff;border-left-color:transparent}div#driver-popover-item .driver-popover-footer{display:block;clear:both;margin-top:5px}div#driver-popover-item .driver-popover-footer button{display:inline-block;padding:3px 10px;border:1px solid #d4d4d4;text-decoration:none;text-shadow:1px 1px 0 #fff;color:#2d2d2d;font:11px/normal sans-serif;cursor:pointer;outline:0;background-color:#f1f1f1;border-radius:2px;zoom:1;margin:10px 0 0;line-height:1.3}div#driver-popover-item .driver-popover-footer button.driver-disabled{color:gray;cursor:default;pointer-events:none}div#driver-popover-item .driver-popover-footer .driver-close-btn{float:left}div#driver-popover-item .driver-popover-footer .driver-btn-group{float:right}div#driver-popover-item .driver-popover-title{font:19px/normal sans-serif;margin:0 0 5px;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1}div#driver-popover-item .driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;color:#2d2d2d;font-weight:400;zoom:1}.driver-no-animation{-webkit-transition:none!important;-moz-transition:none!important;-ms-transition:none!important;-o-transition:none!important;transition:none!important}div#driver-page-overlay{background:#000;position:fixed;bottom:0;right:0;display:block;width:100%;height:100%;zoom:1;filter:alpha(opacity=75);opacity:.75;z-index:100002!important}div#driver-highlighted-element-stage,div#driver-page-overlay{top:0;left:0;-webkit-transition:all .4s;-moz-transition:all .4s;-ms-transition:all .4s;-o-transition:all .4s;transition:all .4s}div#driver-highlighted-element-stage{position:absolute;height:50px;width:300px;background:#fff;z-index:100003!important;display:none}.driver-highlighted-element{z-index:100004!important}.driver-position-relative{position:relative!important}.driver-fix-stacking{z-index:auto!important;opacity:1!important;-webkit-transform:none!important;-moz-transform:none!important;-ms-transform:none!important;-o-transform:none!important;transform:none!important;-webkit-filter:none!important;-moz-filter:none!important;-ms-filter:none!important;-o-filter:none!important;filter:none!important;-webkit-perspective:none!important;-moz-perspective:none!important;-ms-perspective:none!important;-o-perspective:none!important;perspective:none!important} 2 | /*# sourceMappingURL=driver.min.css.map*/ -------------------------------------------------------------------------------- /scripts/accounts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a list of accounts and private keys given a mnemonic 3 | */ 4 | 5 | var bip39 = require('bip39') 6 | var HDKey = require('hdkey') 7 | var Web3 = require('web3') 8 | var web3 = new Web3() 9 | 10 | const mnemonic = process.argv.slice(2).join(' ') 11 | 12 | if (!mnemonic) { 13 | console.log("\nUsage: node accounts.js [mnemonic]") 14 | // node scripts/accounts.js "logic cradle area quality lumber pitch radar sense dove fault capital observe" 15 | console.log("eg node scripts/accounts.js logic cradle area quality lumber pitch radar sense dove fault capital observe") 16 | process.exit() 17 | } 18 | 19 | console.log(`\nMnemonic: ${mnemonic}\n`) 20 | 21 | for (var offset = 0; offset < 10; offset++) { 22 | var seed = bip39.mnemonicToSeed(mnemonic) 23 | var acct = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/" + offset) 24 | var account = web3.eth.accounts.privateKeyToAccount( 25 | `0x${acct.privateKey.toString('hex')}` 26 | ) 27 | console.log(`${account.address} - ${account.privateKey}`) 28 | } 29 | 30 | console.log() 31 | process.exit() 32 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | import helper from '../test/_helper' 2 | 3 | ;(async () => { 4 | var { accounts, web3, deploy, server } = await helper( 5 | `${__dirname}/identity/`, 6 | 'https://rinkeby.infura.io' 7 | // 'http://localhost:8545' 8 | ) 9 | 10 | var account = web3.eth.accounts.wallet.add( 11 | '0xPRIV_KEY' 12 | ) 13 | 14 | await deploy('ClaimHolder', { from: account, log: true }) 15 | 16 | if (server) { 17 | server.close() 18 | } 19 | })() 20 | -------------------------------------------------------------------------------- /src/Store.js: -------------------------------------------------------------------------------- 1 | import thunkMiddleware from 'redux-thunk' 2 | import { createStore, applyMiddleware, combineReducers } from 'redux' 3 | 4 | import network from './reducers/Network' 5 | import wallet from './reducers/Wallet' 6 | import identity from './reducers/Identity' 7 | 8 | let middlewares = [thunkMiddleware] 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | const { logger } = require(`redux-logger`) 12 | middlewares.push(logger) 13 | } 14 | 15 | export default createStore( 16 | combineReducers({ 17 | wallet, 18 | network, 19 | identity 20 | }), 21 | applyMiddleware(...middlewares) 22 | ) 23 | -------------------------------------------------------------------------------- /src/actions/Network.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'utils/keyMirror' 2 | import balance from 'utils/balance' 3 | 4 | import { loadWallet } from './Wallet' 5 | 6 | export const NetworkConstants = keyMirror( 7 | { 8 | CHANGE: null, 9 | CHANGE_SUCCESS: null, 10 | CHANGE_ERROR: null, 11 | 12 | UPDATE_BALANCE: null, 13 | UPDATE_BALANCE_SUCCESS: null, 14 | UPDATE_BALANCE_ERROR: null, 15 | 16 | FETCH_ACCOUNTS: null, 17 | FETCH_ACCOUNTS_SUCCESS: null, 18 | 19 | FETCH_LOCAL_WALLET: null, 20 | FETCH_LOCAL_WALLET_SUCCESS: null, 21 | 22 | SELECT_ACCOUNT: null, 23 | SET_PROVIDER: null, 24 | SET_IPFS: null, 25 | 26 | SEND_FROM_NODE: null, 27 | SEND_FROM_NODE_SUCCESS: null, 28 | SEND_FROM_NODE_ERROR: null, 29 | 30 | SEND_FROM_ACCOUNT: null, 31 | SEND_FROM_ACCOUNT_SUCCESS: null, 32 | SEND_FROM_ACCOUNT_ERROR: null 33 | }, 34 | 'NETWORK' 35 | ) 36 | 37 | export function init() { 38 | return async function(dispatch, getState) { 39 | var state = getState() 40 | dispatch({ type: NetworkConstants.CHANGE }) 41 | dispatch(loadWallet()) 42 | 43 | var accounts = [], 44 | balanceWei 45 | var id = await web3.eth.net.getId().catch(() => { 46 | dispatch({ 47 | type: NetworkConstants.CHANGE_ERROR, 48 | error: 'Network unavailable' 49 | }) 50 | return 51 | }) 52 | if (!id) { 53 | return 54 | } 55 | 56 | var accountsRaw = await web3.eth.getAccounts() 57 | 58 | for (let hash of accountsRaw) { 59 | balanceWei = await web3.eth.getBalance(hash) 60 | accounts.push({ 61 | hash, 62 | balanceWei, 63 | balance: balance(balanceWei, state.wallet.exchangeRates) 64 | }) 65 | } 66 | 67 | dispatch({ 68 | type: NetworkConstants.CHANGE_SUCCESS, 69 | id, 70 | accounts 71 | }) 72 | } 73 | } 74 | 75 | export function fetchAccounts() { 76 | return async function(dispatch) { 77 | dispatch({ type: NetworkConstants.FETCH_ACCOUNTS }) 78 | 79 | var accounts = [] 80 | var accountsRaw = await web3.eth.getAccounts() 81 | for (let hash of accountsRaw) { 82 | var balanceWei = await web3.eth.getBalance(hash) 83 | accounts.push({ 84 | hash, 85 | balanceWei, 86 | balance: web3.utils.fromWei(balanceWei, 'ether') 87 | }) 88 | } 89 | 90 | dispatch({ type: NetworkConstants.FETCH_ACCOUNTS_SUCCESS, accounts }) 91 | } 92 | } 93 | 94 | export function updateBalance(account) { 95 | return async function(dispatch, getState) { 96 | var state = getState() 97 | dispatch({ type: NetworkConstants.UPDATE_BALANCE }) 98 | 99 | var balanceWei = await web3.eth.getBalance(account) 100 | 101 | dispatch({ 102 | type: NetworkConstants.UPDATE_BALANCE_SUCCESS, 103 | account, 104 | balance: balance(balanceWei, state.wallet.exchangeRates) 105 | }) 106 | } 107 | } 108 | 109 | export function sendFromNode(from, to, value) { 110 | return function(dispatch) { 111 | dispatch({ type: NetworkConstants.SEND_FROM_NODE, from, to, value }) 112 | 113 | web3.eth 114 | .sendTransaction({ 115 | from, 116 | to, 117 | value: web3.utils.toWei(value, 'ether'), 118 | gas: 4612388 119 | }) 120 | .on('transactionHash', hash => { 121 | dispatch({ type: 'LOG', message: 'transactionHash', hash }) 122 | }) 123 | .on('receipt', receipt => { 124 | dispatch({ type: 'LOG', message: 'receipt', receipt }) 125 | }) 126 | .on('confirmation', function(num, receipt) { 127 | if (num === 1) { 128 | dispatch({ type: NetworkConstants.SEND_FROM_NODE_SUCCESS, receipt }) 129 | dispatch(updateBalance(from)) 130 | dispatch(updateBalance(to)) 131 | } 132 | }) 133 | .on('error', error => { 134 | dispatch({ type: NetworkConstants.SEND_FROM_NODE_ERROR, error }) 135 | }) 136 | } 137 | } 138 | 139 | export function sendFromAccount(from, to, value) { 140 | return async function(dispatch, getState) { 141 | var state = getState() 142 | var chainId = state.network.id 143 | var account = state.wallet.raw[from] 144 | var valEth = value 145 | if (state.wallet.currency !== 'eth') { 146 | valEth = String( 147 | Number(value) / state.wallet.exchangeRates[state.wallet.currency] 148 | ) 149 | } 150 | 151 | dispatch({ type: NetworkConstants.SEND_FROM_ACCOUNT, from, to, value }) 152 | 153 | var signedTx = await account.signTransaction({ 154 | from: account.address, 155 | to, 156 | gas: 4612388, 157 | value: web3.utils.toWei(valEth, 'ether'), 158 | chainId: chainId > 10 ? 1 : chainId 159 | }) 160 | 161 | web3.eth 162 | .sendSignedTransaction(signedTx.rawTransaction) 163 | .on('error', error => { 164 | dispatch({ 165 | type: NetworkConstants.SEND_FROM_ACCOUNT, 166 | message: error.message 167 | }) 168 | }) 169 | .on('transactionHash', hash => { 170 | dispatch({ type: 'LOG', message: 'transactionHash', hash }) 171 | }) 172 | .on('receipt', receipt => { 173 | dispatch({ type: 'LOG', message: 'receipt', receipt }) 174 | }) 175 | .on('confirmation', num => { 176 | if (num === 1) { 177 | dispatch({ type: NetworkConstants.SEND_FROM_ACCOUNT_SUCCESS }) 178 | dispatch(updateBalance(from)) 179 | dispatch(updateBalance(to)) 180 | } 181 | }) 182 | } 183 | } 184 | 185 | export function setProvider(provider) { 186 | return async function(dispatch, getState) { 187 | var state = getState() 188 | if (state.network.provider === provider) { 189 | return 190 | } 191 | web3.eth.setProvider(provider) 192 | dispatch({ type: NetworkConstants.SET_PROVIDER, provider }) 193 | dispatch(init()) 194 | } 195 | } 196 | 197 | export function selectAccount(hash) { 198 | return { type: NetworkConstants.SELECT_ACCOUNT, hash } 199 | } 200 | export function setIpfs(gateway, api) { 201 | return { type: NetworkConstants.SET_IPFS, gateway, api } 202 | } 203 | -------------------------------------------------------------------------------- /src/actions/Wallet.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'utils/keyMirror' 2 | import balance from 'utils/balance' 3 | 4 | export const WalletConstants = keyMirror( 5 | { 6 | LOAD: null, 7 | LOAD_SUCCESS: null, 8 | LOAD_ERROR: null, 9 | 10 | LOAD_EXTERNAL: null, 11 | LOAD_EXTERNAL_SUCCESS: null, 12 | LOAD_EXTERNAL_ERROR: null, 13 | 14 | ADD_ACCOUNT: null, 15 | ADD_ACCOUNT_SUCCESS: null, 16 | ADD_ACCOUNT_ERROR: null, 17 | 18 | SELECT_ACCOUNT: null, 19 | SELECT_ACCOUNT_SUCCESS: null, 20 | SELECT_ACCOUNT_ERROR: null, 21 | 22 | IMPORT_ACCOUNT: null, 23 | IMPORT_ACCOUNT_SUCCESS: null, 24 | IMPORT_ACCOUNT_ERROR: null, 25 | 26 | SAVE: null, 27 | SAVE_SUCCESS: null, 28 | SAVE_ERROR: null, 29 | 30 | REMOVE_ACCOUNT: null, 31 | REMOVE_ACCOUNT_SUCCESS: null, 32 | REMOVE_ACCOUNT_ERROR: null, 33 | 34 | UPDATE_BALANCE: null, 35 | UPDATE_BALANCE_SUCCESS: null, 36 | UPDATE_BALANCE_ERROR: null, 37 | 38 | SET_CURRENCY: null, 39 | LOCK_WALLET: null, 40 | UNLOCK_WALLET: null, 41 | UNLOCKED_WALLET: null 42 | }, 43 | 'WALLET' 44 | ) 45 | 46 | var watchMetaMaskInterval 47 | function watchMetaMask(dispatch, currentAccount) { 48 | watchMetaMaskInterval = setInterval(async function() { 49 | var accounts = await web3.eth.getAccounts() 50 | if (currentAccount !== accounts[0]) { 51 | dispatch(loadWallet(true)) 52 | } 53 | }, 1000) 54 | } 55 | 56 | export function loadWallet(external) { 57 | return async function(dispatch, getState) { 58 | var state = getState() 59 | 60 | dispatch({ type: WalletConstants.LOAD, external }) 61 | var wallet = web3.eth.accounts.wallet, 62 | accounts = [], 63 | balanceWei, 64 | balances = {} 65 | 66 | clearInterval(watchMetaMaskInterval) 67 | 68 | try { 69 | if (external) { 70 | web3.setProvider(state.network.browserProvider) 71 | accounts = await web3.eth.getAccounts() 72 | 73 | balanceWei = await web3.eth.getBalance(accounts[0]) 74 | balances[accounts[0]] = balance(balanceWei, state.wallet.exchangeRates) 75 | 76 | web3.eth.accounts.wallet.clear() 77 | web3.eth.defaultAccount = accounts[0] 78 | dispatch({ 79 | type: WalletConstants.LOAD_EXTERNAL_SUCCESS, 80 | activeAddress: accounts[0], 81 | balances 82 | }) 83 | watchMetaMask(dispatch, accounts[0]) 84 | return 85 | } 86 | 87 | web3.setProvider(state.network.provider) 88 | 89 | // wallet.load is expensive, so cache private keys in sessionStorage 90 | if (window.sessionStorage.privateKeys) { 91 | JSON.parse(window.sessionStorage.privateKeys).forEach(key => 92 | web3.eth.accounts.wallet.add(key) 93 | ) 94 | } else { 95 | wallet = web3.eth.accounts.wallet.load('', 'o2oprotocol') 96 | 97 | var accountKeys = [] 98 | for (var k = 0; k < wallet.length; k++) { 99 | accountKeys.push(wallet[k].privateKey) 100 | } 101 | if (accountKeys.length) { 102 | window.sessionStorage.privateKeys = JSON.stringify(accountKeys) 103 | } 104 | } 105 | 106 | for (var i = 0; i < wallet.length; i++) { 107 | accounts.push(wallet[i].address) 108 | } 109 | 110 | for (let hash of accounts) { 111 | balanceWei = await web3.eth.getBalance(hash) 112 | balances[hash] = balance(balanceWei, state.wallet.exchangeRates) 113 | } 114 | 115 | web3.eth.defaultAccount = accounts[0] 116 | 117 | dispatch({ 118 | type: WalletConstants.LOAD_SUCCESS, 119 | wallet, 120 | accounts, 121 | balances 122 | }) 123 | } catch (error) { 124 | dispatch({ type: WalletConstants.LOAD_ERROR, error }) 125 | } 126 | } 127 | } 128 | 129 | export function selectAccount(address) { 130 | return async function(dispatch) { 131 | dispatch({ type: WalletConstants.SELECT_ACCOUNT, address }) 132 | 133 | try { 134 | var account = web3.eth.accounts.wallet[address] 135 | web3.eth.defaultAccount = address 136 | 137 | dispatch({ 138 | type: WalletConstants.SELECT_ACCOUNT_SUCCESS, 139 | account, 140 | activeAddress: address 141 | }) 142 | } catch (error) { 143 | dispatch({ type: WalletConstants.SELECT_ACCOUNT_ERROR, error }) 144 | } 145 | } 146 | } 147 | 148 | export function addAccount() { 149 | return async function(dispatch) { 150 | dispatch({ type: WalletConstants.ADD_ACCOUNT }) 151 | 152 | try { 153 | var wallet = web3.eth.accounts.wallet.create(1), 154 | account = wallet[wallet.length - 1] 155 | 156 | dispatch({ 157 | type: WalletConstants.ADD_ACCOUNT_SUCCESS, 158 | wallet, 159 | account 160 | }) 161 | } catch (error) { 162 | dispatch({ type: WalletConstants.ADD_ACCOUNT_ERROR, error }) 163 | } 164 | } 165 | } 166 | 167 | export function importAccountFromKey(privateKey) { 168 | return async function(dispatch, getState) { 169 | var state = getState() 170 | dispatch({ type: WalletConstants.IMPORT_ACCOUNT }) 171 | 172 | try { 173 | var account = web3.eth.accounts.wallet.add(privateKey) 174 | var wallet = web3.eth.accounts.wallet 175 | 176 | var balanceWei = await web3.eth.getBalance(account.address) 177 | 178 | dispatch({ 179 | type: WalletConstants.IMPORT_ACCOUNT_SUCCESS, 180 | account: wallet[wallet.length - 1], 181 | wallet, 182 | balance: balance(balanceWei, state.wallet.exchangeRates) 183 | }) 184 | } catch (error) { 185 | dispatch({ type: WalletConstants.IMPORT_ACCOUNT_ERROR, error }) 186 | } 187 | } 188 | } 189 | 190 | export function removeAccount(hash) { 191 | return async function(dispatch) { 192 | dispatch({ type: WalletConstants.REMOVE_ACCOUNT }) 193 | 194 | try { 195 | var wallet = web3.eth.accounts.wallet.remove(hash) 196 | 197 | dispatch({ 198 | type: WalletConstants.REMOVE_ACCOUNT_SUCCESS, 199 | hash, 200 | wallet 201 | }) 202 | } catch (error) { 203 | dispatch({ type: WalletConstants.REMOVE_ACCOUNT_ERROR, error }) 204 | } 205 | } 206 | } 207 | 208 | export function saveWallet() { 209 | return async function(dispatch) { 210 | dispatch({ type: WalletConstants.SAVE }) 211 | 212 | try { 213 | web3.eth.accounts.wallet.save('', 'o2oprotocol') 214 | dispatch({ type: WalletConstants.SAVE_SUCCESS }) 215 | } catch (error) { 216 | dispatch({ type: WalletConstants.SAVE_ERROR, error }) 217 | } 218 | } 219 | } 220 | 221 | export function updateBalance() { 222 | return async function(dispatch, getState) { 223 | dispatch({ type: WalletConstants.UPDATE_BALANCE }) 224 | 225 | var state = getState() 226 | var account = state.wallet.activeAddress 227 | 228 | var wei = await web3.eth.getBalance(account) 229 | 230 | dispatch({ 231 | type: WalletConstants.UPDATE_BALANCE_SUCCESS, 232 | account, 233 | balance: balance(wei, state.wallet.exchangeRates) 234 | }) 235 | } 236 | } 237 | 238 | export function setCurrency(currency) { 239 | return { 240 | type: WalletConstants.SET_CURRENCY, 241 | currency 242 | } 243 | } 244 | 245 | export function lockWallet() { 246 | return { 247 | type: WalletConstants.LOCK_WALLET 248 | } 249 | } 250 | 251 | export function unlockWallet() { 252 | return { 253 | type: WalletConstants.UNLOCK_WALLET 254 | } 255 | } 256 | 257 | export function unlockedWallet() { 258 | return { 259 | type: WalletConstants.UNLOCKED_WALLET 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/components/AccountChooser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Dropdown from './Dropdown' 4 | import Modal from './Modal' 5 | 6 | export default class AccountChooser extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = {} 10 | } 11 | 12 | render() { 13 | const { wallet, balance, account } = this.props 14 | const currency = `${wallet.currency}Str` 15 | return ( 16 |
17 | 22 | {`Wallet ${account.substr(0, 6)}: ${balance[currency]}`} 23 | 24 | 25 | } 26 | > 27 | {wallet.accounts.map((a, idx) => ( 28 | { 35 | e.preventDefault() 36 | this.props.selectAccount(a) 37 | }} 38 | > 39 | {`${this.props.wallet.balances[a][currency]}`} 40 | {`${a.substr(0, 8)}`} 41 | 42 | ))} 43 |
44 | 45 | 46 | Network / Wallet Settings 47 | 48 |
49 |
50 |
51 | Prices in: 52 |
73 |
74 | 75 | 76 | {this.props.wallet.tryUnlock && ( 77 | { 80 | this.props.lockWallet() 81 | this.setState({ unlock: false, correct: false }) 82 | }} 83 | onOpen={() => { 84 | this.pw.focus() 85 | this.setState({ correct: false }) 86 | }} 87 | shouldClose={this.state.unlock} 88 | submitted={this.state.unlock} 89 | className="p-3" 90 | > 91 | {this.state.correct ? ( 92 |
93 | 97 |
98 | ) : ( 99 |
100 | (this.pw = r)} 105 | onKeyDown={e => { 106 | if (e.keyCode === 13) { 107 | this.setState({ correct: true }) 108 | setTimeout(() => { 109 | this.setState({ unlock: true }) 110 | }, 500) 111 | } 112 | }} 113 | /> 114 |
115 | 116 | 117 | 118 |
119 |
120 | )} 121 |
122 | )} 123 |
124 | ) 125 | } 126 | } 127 | 128 | require('react-styl')(` 129 | .wallet-unlocked 130 | color: green 131 | `) 132 | -------------------------------------------------------------------------------- /src/components/DetailRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class DetailRow extends Component { 4 | constructor() { 5 | super() 6 | this.state = { expanded: false } 7 | } 8 | render() { 9 | var { label, className } = this.props 10 | return ( 11 | this.setState({ mouseDown: +new Date() })} 14 | onMouseUp={() => { 15 | if (this.state.mode === 'info') { 16 | this.setState({ mode: null }) 17 | } else if ( 18 | this.state.expanded && 19 | +new Date() - this.state.mouseDown < 250 20 | ) { 21 | this.setState({ expanded: false }) 22 | } else if (!this.state.expanded) { 23 | this.setState({ expanded: true }) 24 | } 25 | }} 26 | > 27 | {label} 28 | 29 | {this.state.mode === 'info' ? ( 30 |
{this.props.info}
31 | ) : ( 32 |
33 | {this.props.children} 34 |
35 | )} 36 | 37 | 38 | {!this.props.info ? null : ( 39 | e.preventDefault()} 43 | onMouseUp={e => { 44 | e.stopPropagation() 45 | if (this.state.mode === 'info') { 46 | this.setState({ mode: null, expanded: false }) 47 | } else { 48 | this.setState({ mode: 'info', expanded: false }) 49 | } 50 | }} 51 | > 52 | 53 | 54 | )} 55 | 56 | 57 | ) 58 | } 59 | } 60 | 61 | export default DetailRow 62 | -------------------------------------------------------------------------------- /src/components/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class Dropdown extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = { open: false } 7 | this.onBlur = this.onBlur.bind(this) 8 | this.onClick = this.onClick.bind(this) 9 | this.onToggle = this.onToggle.bind(this) 10 | } 11 | 12 | componentWillUnmount() { 13 | document.removeEventListener('click', this.onBlur) 14 | } 15 | 16 | render() { 17 | var size = this.props.size, 18 | active = this.state.open ? ' active' : '', 19 | disabled = this.props.disabled ? ' disabled' : '' 20 | 21 | return ( 22 |
this.setState({ mouseOver: true })} 26 | onMouseOut={() => this.setState({ mouseOver: false })} 27 | onMouseDown={null} 28 | onClick={this.props.disabled ? null : this.onClick} 29 | > 30 | { 34 | if (this.props.onMouseDown) { 35 | this.props.onMouseDown(e) 36 | } 37 | }} 38 | onMouseOver={e => { 39 | if (this.props.onMouseOver && !this.state.open) { 40 | this.props.onMouseOver(e) 41 | } 42 | }} 43 | onMouseOut={e => { 44 | if (this.props.onMouseOut) { 45 | this.props.onMouseOut(e) 46 | } 47 | }} 48 | className={`${size + active + disabled + this.props.linkClass}`} 49 | > 50 | {this.props.label} 51 | {this.props.caret && } 52 | 53 | {!this.state.open ? null : ( 54 |
59 | )} 60 |
61 | ) 62 | } 63 | 64 | onToggle(e) { 65 | e.preventDefault() 66 | if (this.state.open) { 67 | document.removeEventListener('click', this.onBlur) 68 | } else { 69 | document.addEventListener('click', this.onBlur) 70 | } 71 | this.setState({ open: !this.state.open }) 72 | } 73 | 74 | onClick() { 75 | if (this.state.open) { 76 | this.setState({ mouseOver: true, open: false }) 77 | document.removeEventListener('click', this.onBlur) 78 | } 79 | } 80 | 81 | onBlur() { 82 | if (!this.state.mouseOver) { 83 | this.setState({ open: false }) 84 | } 85 | } 86 | } 87 | 88 | Dropdown.defaultProps = { 89 | size: '', 90 | style: { position: 'relative', display: 'inline-block' } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/FormRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FormRow = props => ( 4 | 5 | {props.label} 6 | 7 | {props.children} 8 | 9 | 10 | ) 11 | 12 | export default FormRow 13 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = props => { 4 | if (props.show !== true) return null 5 | return ( 6 |
7 |
8 | Loading... 9 |
10 |
11 | ) 12 | } 13 | 14 | export default Loading 15 | 16 | require('react-styl')(` 17 | .loading-spinner 18 | z-index: 5 19 | position: absolute; 20 | background: rgba(255,255,255,0.9); 21 | top: 0; 22 | left: 0; 23 | right: 0; 24 | bottom: 0; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | font-size: 1.5rem; 29 | color: #333; 30 | `) 31 | -------------------------------------------------------------------------------- /src/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | function freezeVp(e) { 5 | e.preventDefault() 6 | } 7 | 8 | export default class Modal extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | x: 0, 13 | y: 0, 14 | o: 1, 15 | co: 1, 16 | scale: 1, 17 | rotate: 0, 18 | anim: 'is-entering' 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | this.portal = document.createElement('div') 24 | document.body.appendChild(this.portal) 25 | document.body.className += ' pl-modal-open' 26 | document.body.addEventListener('touchmove', freezeVp, false) 27 | this.renderContent(this.props) 28 | 29 | this.onKeyDown = this.onKeyDown.bind(this) 30 | this.onClose = this.onClose.bind(this) 31 | this.doClose = this.doClose.bind(this) 32 | 33 | window.addEventListener('keydown', this.onKeyDown) 34 | setTimeout(() => { 35 | if (this.props.onOpen) { 36 | this.props.onOpen() 37 | } 38 | this.setState({ active: true }) 39 | }, 10) 40 | } 41 | 42 | componentWillUnmount() { 43 | document.body.className = document.body.className.replace( 44 | ' pl-modal-open', 45 | '' 46 | ) 47 | document.body.removeEventListener('touchmove', freezeVp, false) 48 | window.removeEventListener('keydown', this.onKeyDown) 49 | ReactDOM.unmountComponentAtNode(this.portal) 50 | document.body.removeChild(this.portal) 51 | } 52 | 53 | render() { 54 | return null 55 | } 56 | 57 | componentWillReceiveProps(nextProps) { 58 | if (!this.props.shouldClose && nextProps.shouldClose) { 59 | this.doClose() 60 | } 61 | } 62 | 63 | componentDidUpdate() { 64 | this.renderContent(this.props) 65 | } 66 | 67 | renderContent() { 68 | var content = ( 69 | <> 70 |
75 |
this.onClose(e)}> 76 |
77 |
82 |
86 | {!this.props.closeBtn ? null : ( 87 | { 91 | e.preventDefault() 92 | this.doClose() 93 | }} 94 | > 95 | × 96 | 97 | )} 98 | {this.props.children} 99 |
100 |
101 |
102 |
103 | 104 | ) 105 | 106 | ReactDOM.render(content, this.portal) 107 | } 108 | 109 | onClose(e) { 110 | if ( 111 | this.props.onClose && 112 | String(e.target.className).indexOf('pl-modal-cell') >= 0 113 | ) { 114 | this.doClose() 115 | } 116 | } 117 | 118 | doClose() { 119 | this.setState({ anim: 'is-leaving' }) 120 | setTimeout(() => { 121 | this.setState({ 122 | anim: `is-leaving is-${this.props.submitted ? 'submitted' : 'closed'}` 123 | }) 124 | }, 10) 125 | this.onCloseTimeout = setTimeout(() => this.props.onClose(), 500) 126 | } 127 | 128 | onKeyDown(e) { 129 | if (e.keyCode === 27) { 130 | // Esc 131 | this.doClose() 132 | } 133 | if (e.keyCode === 13 && this.props.onPressEnter) { 134 | // Enter 135 | this.props.onPressEnter() 136 | } 137 | } 138 | } 139 | 140 | require('react-styl')(` 141 | .pl-modal-open 142 | overflow: hidden 143 | touch-action: none 144 | .pl-modal 145 | position: fixed 146 | z-index: 2000 147 | top: 0 148 | right: 0 149 | bottom: 0 150 | left: 0 151 | overflow-y: auto 152 | -webkit-transform: translate3d(0, 0, 0) 153 | .pl-modal-table 154 | display: table; 155 | table-layout: fixed; 156 | height: 100%; 157 | width: 100%; 158 | .pl-modal-cell 159 | display: table-cell; 160 | height: 100%; 161 | width: 100%; 162 | vertical-align: middle; 163 | padding: 50px; 164 | .pl-modal-content 165 | position: relative; 166 | overflow: hidden; 167 | border-radius: 2px; 168 | margin-left: auto; 169 | margin-right: auto; 170 | max-width: 520px; 171 | background-color: #fff 172 | 173 | .pl-modal-cell 174 | position: relative; 175 | -webkit-transition-property: opacity,-webkit-transform; 176 | transition-property: opacity,-webkit-transform; 177 | transition-property: opacity,transform; 178 | transition-property: opacity,transform,-webkit-transform 179 | 180 | .pl-modal-cell.is-entering>.pl-modal-content 181 | opacity: 0; 182 | -webkit-transform: translateY(50px) scale(.95); 183 | transform: translateY(50px) scale(.95) 184 | 185 | .pl-modal-cell.is-active.is-entering>.pl-modal-content 186 | opacity: 1; 187 | -webkit-transform: translateY(0) scale(1); 188 | transform: translateY(0) scale(1); 189 | -webkit-transition-timing-function: cubic-bezier(.15,1.45,.55,1); 190 | transition-timing-function: cubic-bezier(.15,1.45,.55,1); 191 | -webkit-transition-duration: .4s; 192 | transition-duration: .4s 193 | 194 | .pl-modal-cell.is-leaving.is-closed>.pl-modal-content 195 | opacity: 0; 196 | -webkit-transform: translateY(50px) scale(.95); 197 | transform: translateY(50px) scale(.95); 198 | -webkit-transition-timing-function: ease-in-out; 199 | transition-timing-function: ease-in-out; 200 | -webkit-transition-duration: .2s; 201 | transition-duration: .2s 202 | 203 | .pl-modal-cell.is-leaving.is-submitted>.pl-modal-content 204 | opacity: 0; 205 | -webkit-transform: translateY(-300px) translateZ(-70px) rotateX(10deg); 206 | transform: translateY(-300px) translateZ(-70px) rotateX(10deg); 207 | -webkit-transition-property: opacity,-webkit-transform; 208 | transition-property: opacity,-webkit-transform; 209 | transition-property: opacity,transform; 210 | transition-property: opacity,transform,-webkit-transform; 211 | -webkit-transition-timing-function: cubic-bezier(.5,-.33,1,1); 212 | transition-timing-function: cubic-bezier(.5,-.33,1,1); 213 | -webkit-transition-duration: .2s; 214 | transition-duration: .2s 215 | 216 | .pl-modal 217 | -webkit-transition-property: opacity; 218 | transition-property: opacity 219 | 220 | .pl-modal-bg 221 | position: fixed; 222 | top: 0; 223 | right: 0; 224 | bottom: 0; 225 | left: 0 226 | touch-action: none 227 | 228 | .pl-modal-bg 229 | background: rgba(0,0,0,.6); 230 | z-index: 1 231 | 232 | .pl-modal-bg.is-entering 233 | opacity: 0; 234 | -webkit-transition-duration: .2s; 235 | transition-duration: .2s; 236 | -webkit-transition-timing-function: ease; 237 | transition-timing-function: ease 238 | 239 | .pl-modal-bg.is-active.is-entering 240 | opacity: 1 241 | 242 | .pl-modal-bg.is-leaving 243 | opacity: 1; 244 | -webkit-transition-delay: .2s; 245 | transition-delay: .2s; 246 | -webkit-transition-duration: .2s; 247 | transition-duration: .2s; 248 | -webkit-transition-timing-function: ease-in-out; 249 | transition-timing-function: ease-in-out 250 | 251 | .pl-modal-bg.is-active.is-leaving 252 | opacity: 0 253 | 254 | .pl-modal-content 255 | border-radius: 6px; 256 | background-color: #f5f5f7; 257 | box-shadow: 0 12px 30px 0 rgba(0,0,0,.5),inset 0 1px 0 0 hsla(0,0%,100%,.65); 258 | -webkit-backface-visibility: hidden; 259 | backface-visibility: hidden 260 | 261 | `) 262 | -------------------------------------------------------------------------------- /src/components/NavItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NavItem = props => ( 4 |
  • 5 | { 9 | e.preventDefault() 10 | props.onClick(props.id) 11 | }} 12 | > 13 | {props.label} 14 | 15 |
  • 16 | ) 17 | 18 | export default NavItem 19 | -------------------------------------------------------------------------------- /src/components/NavLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, Route } from 'react-router-dom' 3 | 4 | const NavLink = ({ to, exact, ...rest }) => ( 5 | ( 6 |
  • 7 | 8 |
  • 9 | )}/> 10 | ) 11 | 12 | export default NavLink 13 | -------------------------------------------------------------------------------- /src/constants/Providers.js: -------------------------------------------------------------------------------- 1 | const HOST = process.env.HOST || 'localhost' 2 | 3 | export default [ 4 | { 5 | name: 'Localhost', 6 | endpoints: [`http://${HOST}:8545`, `ws://${HOST}:8545`] 7 | }, 8 | { 9 | name: 'Ropsten', 10 | endpoints: ['https://ropsten.infura.io', 'wss://ropsten.infura.io/ws'], 11 | faucet: 'http://faucet.ropsten.be:3001' 12 | }, 13 | { 14 | name: 'Rinkeby', 15 | endpoints: ['https://rinkeby.infura.io', 'wss://rinkeby.infura.io/ws'], 16 | faucet: 'https://faucet.rinkeby.io' 17 | }, 18 | { 19 | name: 'Mainnet', 20 | endpoints: ['https://mainnet.infura.io', 'wss://mainnet.infura.io/ws'] 21 | }, 22 | { 23 | name: 'Kovan', 24 | endpoints: ['https://kovan.infura.io'], 25 | website: 'https://kovan-testnet.github.io/website', 26 | faucet: 'https://github.com/kovan-testnet/faucet' 27 | } 28 | // { 29 | // name: 'Custom' 30 | // } 31 | ] 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { Route, HashRouter } from 'react-router-dom' 5 | 6 | import Store from './Store' 7 | import App from './pages/App' 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('app') 16 | ) 17 | 18 | require('react-styl').addStylesheet() 19 | -------------------------------------------------------------------------------- /src/pages/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Switch, Route, Link } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | 5 | import Console from './console' 6 | import Identity from './identity' 7 | import Versions from './_Versions' 8 | import Init from './_Init' 9 | 10 | import { init } from 'actions/Network' 11 | import AccountChooser from 'components/AccountChooser' 12 | 13 | import { selectAccount, setCurrency, loadWallet } from 'actions/Wallet' 14 | 15 | class App extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.state = {} 19 | } 20 | 21 | componentDidMount() { 22 | this.props.initNetwork() 23 | } 24 | 25 | componentWillUnmount() { 26 | clearTimeout(this.hideNotice) 27 | } 28 | 29 | componentDidUpdate(prevProps) { 30 | if ( 31 | window.innerWidth <= 575 && 32 | this.props.location !== prevProps.location 33 | ) { 34 | window.scrollTo(0, 0) 35 | } 36 | } 37 | 38 | componentWillReceiveProps(nextProps) { 39 | // If no accounts are present, pre-populate for an easier demo experience. 40 | if ( 41 | !this.props.wallet.loaded && 42 | nextProps.wallet.loaded && 43 | !nextProps.wallet.activeAddress 44 | ) { 45 | window.sessionStorage.privateKeys = JSON.stringify([ 46 | // "0x24f3c3b01a0783948380fb683a9712f079e7d249c0461e1f40054b10b1bb0b23", // accounts[0] ClaimSignerKey 47 | "0xd6079ba5123c57b9a8cb3e1fbde9f879c7a18eeca23fa2a965e8181d3ff59f0c", // accounts[1] Identity 48 | "0x20ea25d6c8d99bea5e81918d805b4268d950559b36c5e1cfcbb1cda0197faa08", // accounts[2] Certifier 49 | "0x25acb0da38f5364588f78b4e1f33c4a3981354c9b044d64bf201aad8f38f50ae", // accounts[3] ClaimChecker 50 | ]) 51 | this.props.loadWallet() 52 | this.setState({ preloaded: true }) 53 | this.hideNotice = setTimeout( 54 | () => this.setState({ preloaded: false }), 55 | 3333 56 | ) 57 | } 58 | } 59 | 60 | render() { 61 | return ( 62 |
    63 | this.props.history.push('/')} /> 64 | 107 | 108 |
    109 | {!this.state.preloaded ? null : ( 110 | 123 | )} 124 | 125 | 126 | 127 | 128 | 129 | 130 |
    131 |
    132 | 133 | 134 | 135 |
    136 |
    137 | ©{' 2018 '} 138 | 139 | O2OProtocol Blockchain 140 | 141 |
    142 |
    143 | 144 | 145 | 146 | 147 |
    148 |
    149 |
    150 |
    151 | ) 152 | } 153 | } 154 | 155 | const mapStateToProps = state => ({ 156 | account: state.wallet.activeAddress, 157 | balance: state.wallet.balances[state.wallet.activeAddress], 158 | wallet: state.wallet, 159 | nodeAccounts: state.network.accounts 160 | }) 161 | 162 | const mapDispatchToProps = dispatch => ({ 163 | initNetwork: () => { 164 | dispatch(init()) 165 | }, 166 | loadWallet: () => { 167 | dispatch(loadWallet()) 168 | }, 169 | selectAccount: hash => dispatch(selectAccount(hash)), 170 | setCurrency: currency => dispatch(setCurrency(currency)) 171 | }) 172 | 173 | export default connect(mapStateToProps, mapDispatchToProps)(App) 174 | 175 | require('react-styl')(` 176 | table.table 177 | thead tr th 178 | border-top: 0 179 | .btn-sm 180 | padding: 0.125rem 0.375rem 181 | .navbar 182 | border-bottom: 1px solid #E5E9EF; 183 | .navbar-light .navbar-text .dropdown-item.active, 184 | .navbar-light .navbar-text .dropdown-item:active 185 | color: #fff; 186 | .pointer 187 | cursor: pointer 188 | .no-wrap 189 | white-space: nowrap 190 | .footer 191 | display: flex 192 | align-items: center; 193 | color: #999; 194 | margin: 1rem 0; 195 | padding-top: 1rem; 196 | border-top: 1px solid #eee; 197 | font-size: 14px; 198 | a 199 | color: #999; 200 | .middle 201 | flex: 1 202 | text-align: center 203 | .right 204 | flex: 1 205 | text-align: right 206 | .powered-by 207 | flex: 1 208 | font-size: 14px; 209 | letter-spacing: -0.01rem; 210 | img 211 | opacity: .4; 212 | height: 12px; 213 | margin-top: -2px 214 | margin-right: 0.25rem 215 | `) 216 | -------------------------------------------------------------------------------- /src/pages/_Init.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import Modal from 'components/Modal' 5 | 6 | import { sendFromNode } from 'actions/Network' 7 | import { reset, deployIdentityContract, addKey } from 'actions/Identity' 8 | import { selectAccount } from 'actions/Wallet' 9 | import { callDeploy, callAddKey } from 'pages/initIssuer' 10 | import { setTimeout } from 'timers' 11 | 12 | class Event extends Component { 13 | constructor(props) { 14 | super(props) 15 | this.state = { modal: false, logs: [] } 16 | this.stage = 0 17 | } 18 | 19 | async componentDidMount(){ 20 | const blockNo = await web3.eth.getBlockNumber() 21 | console.log("[blockNo]", blockNo) 22 | this.shouldInit = blockNo <= 0 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | var nodeAccounts = nextProps.network.accounts, 27 | walletAccounts = nextProps.wallet.accounts, 28 | balances = nextProps.wallet.balances; 29 | 30 | const RESET = this.shouldInit 31 | const LOCAL = process.env.LOCAL === "true" 32 | 33 | const broker = LOCAL ? "LocalBroker" : "SellerBuyerBroker" 34 | const uri = LOCAL ? "http://localhost:3001" : "https://digital-identity.o2oprotocol.com" 35 | 36 | if ( 37 | this.stage === 0 && 38 | nextProps.network.status === 'connected' && 39 | nodeAccounts.length > 2 && 40 | walletAccounts.length > 2 && 41 | balances[walletAccounts[0]] && 42 | RESET 43 | ) { 44 | window.localStorage.clear() 45 | this.props.reset() 46 | this.next('✔ Init Identity Account 1...') 47 | setTimeout(() => { 48 | this.props.sendFromNode(nodeAccounts[0].hash, walletAccounts[0], '5') 49 | }, 500) 50 | } else if (this.stage === 1 && RESET) { 51 | this.next('✔ Init ClaimIssuer Account 2...') 52 | setTimeout(() => { 53 | this.props.sendFromNode(nodeAccounts[0].hash, walletAccounts[1], '15') 54 | }, 500) 55 | } else if (this.stage === 2 && RESET) { 56 | this.next('✔ Init ClaimChecker Account 3...') 57 | setTimeout(() => { 58 | this.props.sendFromNode(nodeAccounts[0].hash, walletAccounts[2], '25') 59 | }, 500) 60 | } else if (this.stage === 3 && RESET) { 61 | this.next(`✔ Add ${broker} certifier...`) 62 | setTimeout(() => { 63 | this.props.selectAccount(walletAccounts[1]) 64 | callDeploy(this.props.deployIdentityContract, broker, uri) 65 | }, 500) 66 | } else if ( 67 | this.stage === 4 && 68 | this.props.createIdentityResponse !== 'success' && 69 | nextProps.createIdentityResponse === 'success' 70 | ) { 71 | this.next('✔ Add Claim Signer key...') 72 | setTimeout(() => { 73 | const identity = this.props.identity.identities.find(i => i.name === broker) 74 | const key = 75 | '0x2d1da9eb632a0a8052bb8c23aac7b482afbc7bd06ef97b8a0a54a943fa68cdfd' 76 | callAddKey(this.props.addKey, key, identity.address) 77 | }, 500) 78 | } else if ( 79 | this.stage === 5 && 80 | this.props.addKeyResponse !== 'success' && 81 | nextProps.addKeyResponse === 'success' 82 | ) { 83 | this.next('Done!') 84 | this.props.selectAccount(walletAccounts[0]) 85 | setTimeout(() => { 86 | this.setState({ shouldClose: true }) 87 | }, 1500) 88 | } 89 | } 90 | 91 | next(msg) { 92 | this.stage += 1 93 | this.setState({ modal: true, logs: [...this.state.logs, msg] }) 94 | } 95 | 96 | render() { 97 | if (!this.state.modal) { 98 | return null 99 | } 100 | return ( 101 | { 103 | this.setState({ modal: false }) 104 | if (this.props.onClose) { 105 | this.props.onClose() 106 | } 107 | }} 108 | shouldClose={this.state.shouldClose} 109 | style={{ maxWidth: 375 }} 110 | > 111 |
    112 |

    Initialize

    113 | {this.state.logs.map((log, idx) =>
    {log}
    )} 114 |
    115 |
    116 | ) 117 | } 118 | } 119 | 120 | const mapStateToProps = state => ({ 121 | wallet: state.wallet, 122 | event: state.event, 123 | network: state.network, 124 | identity: state.identity, 125 | createIdentityResponse: state.identity.createIdentityResponse, 126 | addKeyResponse: state.identity.addKeyResponse 127 | }) 128 | 129 | const mapDispatchToProps = dispatch => ({ 130 | sendFromNode: (...args) => dispatch(sendFromNode(...args)), 131 | deployIdentityContract: (...args) => 132 | dispatch(deployIdentityContract(...args)), 133 | addKey: (...args) => dispatch(addKey(...args)), 134 | selectAccount: (...args) => dispatch(selectAccount(...args)), 135 | reset: () => dispatch(reset()) 136 | }) 137 | 138 | export default connect(mapStateToProps, mapDispatchToProps)(Event) 139 | -------------------------------------------------------------------------------- /src/pages/_Versions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Dropdown from 'components/Dropdown' 4 | 5 | const latestVersion = '0.2.0' 6 | const oldVersions = [ 7 | // { version: '0.2.0', hash: '' } 8 | ] 9 | 10 | const Versions = () => ( 11 | 12 | 13 | {latestVersion} 14 | 15 | 16 | {oldVersions.map(({ version, hash }) => ( 17 | {}} 22 | > 23 | {version} 24 | 25 | 26 | ))} 27 | 28 | ) 29 | 30 | export default Versions 31 | -------------------------------------------------------------------------------- /src/pages/_driver.js: -------------------------------------------------------------------------------- 1 | 2 | var driver = new Driver(); 3 | window.driver = driver; 4 | // Define the steps for introduction 5 | driver.defineSteps([ 6 | { 7 | element: '.wallet-chooser', 8 | popover: { 9 | title: 'Wallet Chooser', 10 | description: 'The wallet chooser lets you quickly switch between wallets. Use different wallets for Identities, Certifiers and Protected Contracts.', 11 | position: 'bottom' 12 | } 13 | }, 14 | { 15 | element: '.identities-list', 16 | popover: { 17 | title: 'Identities', 18 | description: 'Identities have Claims and', 19 | position: 'bottom' 20 | } 21 | }, 22 | { 23 | element: '.certifiers-list', 24 | popover: { 25 | title: 'Title on Popover', 26 | description: 'Body of the popover', 27 | position: 'top' 28 | } 29 | }, 30 | { 31 | element: '.protected-list', 32 | popover: { 33 | title: 'Title on Popover', 34 | description: 'Body of the popover', 35 | position: 'right' 36 | } 37 | }, 38 | ]); 39 | -------------------------------------------------------------------------------- /src/pages/console/_IPFS.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { setIpfs } from 'actions/Network' 5 | 6 | const HOST = process.env.HOST || 'localhost' 7 | 8 | class IPFS extends Component { 9 | render() { 10 | return ( 11 |
    12 | IPFS: 13 |
    14 | 22 | 30 | 31 | 32 |
    33 |
    34 | ) 35 | } 36 | } 37 | 38 | // const mapStateToProps = state => ({ 39 | // }) 40 | 41 | const mapDispatchToProps = dispatch => ({ 42 | setIpfs: (gateway, api) => dispatch(setIpfs(gateway, api)) 43 | }) 44 | 45 | export default connect(null, mapDispatchToProps)(IPFS) 46 | -------------------------------------------------------------------------------- /src/pages/console/_Providers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { 5 | fetchAccounts, 6 | init, 7 | setProvider, 8 | sendFromNode 9 | } from 'actions/Network' 10 | 11 | class Console extends Component { 12 | constructor(props) { 13 | super(props) 14 | this.state = {} 15 | } 16 | 17 | render() { 18 | var activeProvider = 19 | this.props.providers.find( 20 | p => (p.endpoints || []).indexOf(this.props.provider) >= 0 21 | ) || this.props.providers[this.props.providers.length - 1] 22 | 23 | return ( 24 |
    25 |
    26 |
    27 | {this.props.providers.map((p, idx) => ( 28 | 37 | ))} 38 |
    39 |
    40 | {(activeProvider.endpoints || []).map(e => ( 41 | 50 | ))} 51 |
    52 |
    53 |
    54 | {this.renderStatus()} 55 | {`to ${this.props.provider}`} 56 |
    57 | 58 | {/*
    59 |
    Net ID: {this.props.netId}
    60 |
    61 | {this.props.accounts.length > 0 && 62 |
    63 |
    {`~989 ETH in ${ 64 | this.props.accounts.length 65 | } node managed accounts`}
    66 |
    67 | Transfer
    68 | {'From: '} 69 | 76 |
    77 | To: 78 | 85 |
    86 | Amount: 87 | this.amount = ref} type="text" defaultValue="5" /> 88 |
    89 | 92 |
    93 |
    94 | } */} 95 |
    96 | ) 97 | } 98 | 99 | setProvider(idx) { 100 | var endpoints = this.props.providers[idx].endpoints || [] 101 | var endpoint = 102 | endpoints.find( 103 | e => e.split(':')[0] === this.props.provider.split(':')[0] 104 | ) || endpoints[0] 105 | 106 | if (endpoint) { 107 | this.setState({ customEndpoint: false }) 108 | this.props.setProvider(endpoint) 109 | } else { 110 | this.setState({ customEndpoint: true }) 111 | } 112 | } 113 | 114 | renderStatus() { 115 | var color = 'green', 116 | text = 'Connected' 117 | if (this.props.status === 'disconnected') { 118 | text = 'Disconnected' 119 | color = 'red' 120 | } else if (this.props.status === 'connecting') { 121 | text = 'Connecting' 122 | color = 'orange' 123 | } else if (this.props.status === 'error') { 124 | text = 'Error' 125 | color = 'red' 126 | } 127 | return ( 128 | 129 | {text} 130 | 131 | ) 132 | } 133 | } 134 | 135 | const mapStateToProps = state => ({ 136 | accounts: state.network.accounts, 137 | account: state.network.account, 138 | providers: state.network.providers, 139 | provider: state.network.provider, 140 | netId: state.network.id, 141 | status: state.network.status, 142 | wallet: state.wallet 143 | }) 144 | 145 | const mapDispatchToProps = dispatch => ({ 146 | init: () => dispatch(init()), 147 | fetchAccounts: () => dispatch(fetchAccounts()), 148 | setProvider: provider => dispatch(setProvider(provider)), 149 | sendFromNode: (from, to, value) => dispatch(sendFromNode(from, to, value)) 150 | }) 151 | 152 | export default connect(mapStateToProps, mapDispatchToProps)(Console) 153 | -------------------------------------------------------------------------------- /src/pages/console/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { loadWallet } from 'actions/Wallet' 5 | 6 | import Providers from './_Providers' 7 | import Accounts from './_Accounts' 8 | 9 | class Console extends Component { 10 | constructor() { 11 | super() 12 | this.state = {} 13 | } 14 | render() { 15 | return ( 16 |
    17 |
    18 |
    19 |
    20 |
    21 | Wallet Manager: 22 |
    23 | 31 | 45 |
    46 |
    47 | {this.props.externalProvider ? null : ( 48 | <> 49 | 50 | 51 | 52 | )} 53 |
    54 |
    55 |
    56 | ) 57 | } 58 | } 59 | 60 | const mapStateToProps = state => ({ 61 | externalProvider: state.wallet.externalProvider, 62 | browserHasProvider: state.network.browserProvider ? true : false 63 | }) 64 | 65 | const mapDispatchToProps = dispatch => ({ 66 | loadWallet: external => { 67 | dispatch(loadWallet(external)) 68 | } 69 | }) 70 | 71 | export default connect(mapStateToProps, mapDispatchToProps)(Console) 72 | -------------------------------------------------------------------------------- /src/pages/identity/ClaimCheckerDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Switch, Route, NavLink } from 'react-router-dom' 4 | 5 | import { 6 | getEvents, 7 | checkClaim, 8 | removeVerifier, 9 | claimType 10 | } from 'actions/Identity' 11 | 12 | import { selectAccount } from 'actions/Wallet' 13 | 14 | import Events from './_Events' 15 | import CheckClaim from './modals/_CheckClaim' 16 | 17 | class ProtectedDetail extends Component { 18 | constructor(props) { 19 | super(props) 20 | this.state = { mode: 'summary' } 21 | this.props.getEvents('ClaimVerifier', props.match.params.id) 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (this.props.match.params.id !== nextProps.match.params.id) { 26 | this.props.getEvents('ClaimVerifier', nextProps.match.params.id) 27 | } 28 | } 29 | 30 | render() { 31 | var identities = this.props.identities.filter(i => i.type !== 'certifier') 32 | var address = this.props.match.params.id 33 | 34 | var verifier = this.props.verifiers.find(i => i.address === address) 35 | if (!verifier) { 36 | return null 37 | } 38 | var isOwner = verifier.owner === this.props.wallet.activeAddress 39 | 40 | return ( 41 | <> 42 |
    43 | 78 |
    79 | 80 | ( 83 | <> 84 |
    {`Address: ${verifier.address}`}
    85 |
    {`Owner: ${verifier.owner}`}
    86 | 87 |
    88 |
    89 | 102 |
    103 | 104 | )} 105 | /> 106 | ( 109 | 113 | )} 114 | /> 115 | ( 119 | <> 120 |
    121 | 127 |
    128 | {this.renderAttempts(this.props.events, verifier)} 129 | 130 | )} 131 | /> 132 |
    133 | 134 | {this.state.checkClaim && ( 135 | this.setState({ checkClaim: false })} 137 | verifier={verifier} 138 | identities={identities} 139 | checkClaim={this.props.checkClaim} 140 | response={this.props.checkClaimResponse} 141 | /> 142 | )} 143 | 144 | ) 145 | } 146 | 147 | renderAttempts(events, verifier) { 148 | if (!events || !events.length) { 149 | return null 150 | } 151 | 152 | return ( 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | {events.map((evt, idx) => ( 165 | 166 | 167 | 171 | 172 | 176 | 183 | 184 | ))} 185 | 186 |
    BlockIdentityClaim TypeIssuerResult
    {evt.blockNumber} 168 | {this.props.identity.names[evt.returnValues._identity] || 169 | String(evt.returnValues._identity).substr(0, 8)} 170 | {claimType(verifier.claimType)} 173 | {this.props.identity.names[verifier.trustedIdentity] || 174 | String(verifier.trustedIdentity).substr(0, 8)} 175 | 177 | {evt.event === 'ClaimValid' ? ( 178 | Valid 179 | ) : ( 180 | Invalid 181 | )} 182 |
    187 | ) 188 | } 189 | } 190 | 191 | const mapStateToProps = (state, ownProps) => ({ 192 | identity: state.identity, 193 | identities: [ 194 | ...state.identity.identities, 195 | ...state.identity.officialIdentities 196 | ], 197 | verifiers: state.identity.claimVerifiers, 198 | events: state.identity.events, 199 | eventsResponse: state.identity.eventsResponse, 200 | checkClaimResponse: state.identity.checkClaimResponse, 201 | wallet: state.wallet, 202 | activeIdentity: ownProps.match.params.id 203 | }) 204 | 205 | const mapDispatchToProps = dispatch => ({ 206 | getEvents: (type, address) => dispatch(getEvents(type, address)), 207 | checkClaim: (...args) => dispatch(checkClaim(...args)), 208 | removeVerifier: (...args) => dispatch(removeVerifier(...args)), 209 | selectAccount: hash => dispatch(selectAccount(hash)) 210 | }) 211 | 212 | export default connect(mapStateToProps, mapDispatchToProps)(ProtectedDetail) 213 | -------------------------------------------------------------------------------- /src/pages/identity/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Home extends Component { 4 | 5 | render() { 6 | return ( 7 |
    8 |
    9 | Select a contract for more 10 | information 11 |
    12 |
    13 |
    14 |
    Identity
    15 | Controlled by Keys. Has Claims, can add Claims to other identities. 16 |
    17 |
    18 |
    Claim Issuer
    19 | Also an Identity. Trusted by Claim Checkers to issue valid claims. 20 |
    21 |
    22 |
    Claim Checker
    23 | A contract only allowing interactions from Identites holding Claims 24 | from a Trusted Issuer. 25 |
    26 |
    27 |
    Claim
    28 | Some data on one Identity that provably came from another Identity. 29 |
    30 |
    31 | ) 32 | } 33 | } 34 | 35 | export default Home 36 | -------------------------------------------------------------------------------- /src/pages/identity/_Events.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ClaimHolder from '../../contracts/ClaimHolder' 4 | import decodeFn from 'utils/decodeFn' 5 | 6 | const Events = props => { 7 | if (props.eventsResponse === null) { 8 | return
    Loading...
    9 | } 10 | return ( 11 |
    12 | {props.events.length ? null :
    No Events
    } 13 | {props.events.map((event, idx) => { 14 | var data = null 15 | if (String(event.event).match(/(ExecutionRequested|Executed)/)) { 16 | var decoded = decodeFn(ClaimHolder, event.returnValues.data) 17 | data = ( 18 |
    19 |               {`${decoded.name}\n`}
    20 |               {displayEvent(decoded.params)}
    21 |             
    22 | ) 23 | } 24 | 25 | return ( 26 |
    27 |
    28 | {event.event} 29 | Block {event.blockNumber} 30 |
    31 |
    {displayEvent(event.returnValues)}
    32 | {data} 33 |
    34 | ) 35 | })} 36 |
    37 | ) 38 | } 39 | 40 | export function displayEvent(obj) { 41 | if (typeof obj !== 'object') { 42 | return '' 43 | } 44 | var ret = [] 45 | Object.keys(obj).forEach(key => { 46 | if (!key.match(/^([0-9]+|__.*)$/)) { 47 | var val = String(obj[key]) 48 | val = val.length > 32 ? `${val.substr(0, 32)}...` : val 49 | ret.push(`${key}: ${val}`) 50 | } 51 | }) 52 | return ret.join('\n') 53 | } 54 | 55 | export default Events 56 | -------------------------------------------------------------------------------- /src/pages/identity/_ValidateClaim.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | fromRpcSig, 4 | ecrecover, 5 | toBuffer, 6 | bufferToHex, 7 | pubToAddress 8 | } from 'ethereumjs-util' 9 | 10 | import ClaimHolder from '../../contracts/ClaimHolder' 11 | 12 | class ValidateClaim extends Component { 13 | constructor() { 14 | super() 15 | this.state = { valid: false, icon: 'fa-spinner fa-spin' } 16 | } 17 | 18 | componentDidMount() { 19 | this.validate() 20 | } 21 | 22 | async validate() { 23 | var claim = this.props.claim, 24 | hasKey 25 | 26 | var hashedSignature = web3.utils.soliditySha3( 27 | this.props.subject, 28 | claim.claimType, 29 | claim.data 30 | ) 31 | const prefixedMsg = web3.eth.accounts.hashMessage(hashedSignature) 32 | 33 | if (claim.scheme === '4') { 34 | hasKey = true 35 | } else { 36 | var dataBuf = toBuffer(prefixedMsg) 37 | var sig = fromRpcSig(claim.signature) 38 | var recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) 39 | var recoveredKeyBuf = pubToAddress(recovered) 40 | var recoveredKey = bufferToHex(recoveredKeyBuf) 41 | var hashedRecovered = web3.utils.soliditySha3(recoveredKey) 42 | 43 | var issuer = new web3.eth.Contract(ClaimHolder.abi, claim.issuer) 44 | try { 45 | hasKey = await issuer.methods.keyHasPurpose(hashedRecovered, 3).call() 46 | } catch (e) { 47 | /* Ignore */ 48 | } 49 | } 50 | 51 | this.setState({ 52 | icon: hasKey ? 'fa-check' : 'fa-times', 53 | className: hasKey ? 'text-success' : 'text-danger', 54 | text: hasKey ? 'Valid' : 'Invalid' 55 | }) 56 | } 57 | 58 | render() { 59 | return ( 60 | 61 | {this.state.text} 62 | 63 | 64 | ) 65 | } 66 | } 67 | 68 | export default ValidateClaim 69 | -------------------------------------------------------------------------------- /src/pages/identity/_WalletChooser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class WalletChooser extends Component { 4 | render() { 5 | return ( 6 |
    7 | Active wallet: 8 |
    9 | {this.props.wallet.accounts.map((a, idx) => ( 10 | 19 | ))} 20 |
    21 |
    22 | ) 23 | } 24 | } 25 | 26 | export default WalletChooser 27 | -------------------------------------------------------------------------------- /src/pages/identity/modals/ClaimDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | fromRpcSig, 4 | ecrecover, 5 | toBuffer, 6 | bufferToHex, 7 | pubToAddress 8 | } from 'ethereumjs-util' 9 | 10 | import { claimType, scheme } from 'actions/Identity' 11 | 12 | import Identity from 'contracts/ClaimHolder' 13 | import Modal from 'components/Modal' 14 | import Row from 'components/DetailRow' 15 | 16 | class ClaimDetail extends Component { 17 | constructor(props) { 18 | super(props) 19 | this.state = {} 20 | } 21 | 22 | async componentDidMount() { 23 | var { address } = this.props.identity 24 | var identity = new web3.eth.Contract(Identity.abi, address) 25 | var claim = await identity.methods.getClaim(this.props.claimId).call() 26 | if (claim) { 27 | if (claim.scheme === '4') { 28 | this.setState({ claim, hasKey: true }) 29 | return 30 | } 31 | var expectedData = web3.utils.soliditySha3('Verified OK') 32 | var hashedSignature = web3.utils.soliditySha3( 33 | address, 34 | claim.claimType, 35 | claim.data 36 | ) 37 | const prefixedMsg = web3.eth.accounts.hashMessage(hashedSignature) 38 | 39 | var dataBuf = toBuffer(prefixedMsg) 40 | var sig = fromRpcSig(claim.signature) 41 | var recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) 42 | var recoveredKeyBuf = pubToAddress(recovered) 43 | var recoveredKey = bufferToHex(recoveredKeyBuf) 44 | var hashedRecovered = web3.utils.soliditySha3(recoveredKey) 45 | 46 | var issuer = new web3.eth.Contract(Identity.abi, claim.issuer) 47 | try { 48 | var hasKey = await issuer.methods 49 | .keyHasPurpose(hashedRecovered, 3) 50 | .call() 51 | } catch (e) { 52 | /*Ignore*/ 53 | } 54 | 55 | this.setState({ 56 | claim, 57 | recoveredKey, 58 | hasKey, 59 | hashedSignature, 60 | hashedRecovered, 61 | expectedData 62 | }) 63 | } 64 | } 65 | 66 | render() { 67 | const { claim, hasKey } = this.state 68 | if (!claim) { 69 | return null 70 | } 71 | 72 | return ( 73 | this.props.onClose()} 77 | > 78 | Claim Detail 79 | 80 | 81 | 85 | {this.props.identity.address} 86 | 87 | 91 | {claim.issuer} 92 | 93 | 97 | {this.props.claimId} 98 | 99 | {`${claimType(claim.claimType)} (${claim.claimType})`} 103 | {`${scheme(claim.scheme)} (${claim.scheme})`} 107 | 111 | {web3.utils.hexToAscii(claim.data)} 112 | 113 | 117 | {claim.uri} 118 | 119 | 123 | {this.state.hashedSignature} 124 | 125 | 129 | {claim.signature} 130 | 131 | 136 | {this.state.recoveredKey} 137 | 138 | 142 | {this.state.hashedRecovered} 143 | 144 | 148 | 153 | {hasKey ? 'Yes' : 'No'} 154 | 155 | 156 | 157 |
    158 |
    159 | ) 160 | } 161 | } 162 | 163 | export default ClaimDetail 164 | 165 | require('react-styl')(` 166 | th.label 167 | white-space: no-wrap 168 | td.info a 169 | color: #999 170 | &:hover 171 | color: #000 172 | &.active 173 | color: green 174 | .no-wrap 175 | white-space: nowrap 176 | .bt td, .bt th 177 | border-top-width: 3px 178 | .wb 179 | word-break: break-all 180 | .ellipsis 181 | position: absolute 182 | width: 100% 183 | overflow: hidden 184 | white-space: nowrap 185 | text-overflow: ellipsis 186 | `) 187 | -------------------------------------------------------------------------------- /src/pages/identity/modals/KeyDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { keyPurpose, keyType } from 'actions/Identity' 4 | 5 | import Identity from 'contracts/ClaimHolder' 6 | import Modal from 'components/Modal' 7 | import Row from 'components/DetailRow' 8 | 9 | class KeyDetail extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = {} 13 | } 14 | 15 | async componentDidMount() { 16 | var identity = new web3.eth.Contract( 17 | Identity.abi, 18 | this.props.identity.address 19 | ) 20 | var key = await identity.methods.getKey(this.props.keyId).call() 21 | if (key) { 22 | this.setState({ key }) 23 | } 24 | } 25 | 26 | render() { 27 | const { key } = this.state 28 | if (!key) { 29 | return null 30 | } 31 | 32 | return ( 33 | this.props.onClose()} 37 | > 38 | Key Detail 39 | 40 | 41 | 45 | {this.props.identity.address} 46 | 47 | 48 | {`${keyPurpose(key.purpose)} (${key.purpose})`} 49 | 50 | 51 | {`${keyType(key.keyType)} (${key.keyType})`} 52 | 53 | 54 | {this.props.keyId} 55 | 56 | 57 |
    58 |
    59 | ) 60 | } 61 | } 62 | 63 | export default KeyDetail 64 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_AddKey.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | import FormRow from 'components/FormRow' 6 | 7 | import { KeyPurpose, KeyTypes } from 'actions/Identity' 8 | 9 | class AddKey extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = { 13 | keyPurpose: '1', 14 | keyType: '1', 15 | key: '', 16 | publicKey: '', 17 | privateKey: '' 18 | } 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (this.props.response !== 'success' && nextProps.response === 'success') { 23 | this.setState({ shouldClose: true, submitted: true }) 24 | } 25 | if ( 26 | this.props.response !== 'submitted' && 27 | nextProps.response === 'submitted' 28 | ) { 29 | this.setState({ loading: true }) 30 | } 31 | if (this.props.response !== 'error' && nextProps.response === 'error') { 32 | this.setState({ error: true }) 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 | this.props.onClose()} 44 | > 45 | 46 |
    Add a Key to an Identity:
    47 | {this.props.wrongOwner && ( 48 |
    49 | {`Active wallet does not own this identity`} 50 |
    51 | )} 52 | {this.state.error && ( 53 |
    54 | Error! Please try again. 55 |
    56 | )} 57 | 58 | 59 | 60 | 75 | 76 | 77 | 92 | 93 | 94 |
    95 | this.setState({ key: e.currentTarget.value })} 100 | /> 101 |
    102 | 108 |
    109 |
    110 |
    111 | {!this.state.privateKey ? null : ( 112 | <> 113 | 114 | 120 | 121 | 122 | 128 | 129 | 130 | )} 131 | 132 |
    133 |
    134 | 147 |
    148 |
    149 | ) 150 | } 151 | 152 | generateKeys() { 153 | var privateKey = web3.utils.randomHex(32) 154 | for (var i = privateKey.length; i < 66; i++) { 155 | privateKey += '0' 156 | } 157 | var publicKey = web3.eth.accounts.privateKeyToAccount(privateKey).address 158 | var key = web3.utils.sha3(publicKey) 159 | 160 | this.setState({ privateKey, publicKey, key }) 161 | } 162 | } 163 | 164 | export default AddKey 165 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_Approve.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | 6 | class NewIdentity extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = {} 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (this.props.response !== 'success' && nextProps.response === 'success') { 14 | this.setState({ shouldClose: true, submitted: true }) 15 | } 16 | if (this.props.response !== 'submitted' && nextProps.response === 'submitted') { 17 | this.setState({ loading: true }) 18 | } 19 | if (this.props.response !== 'error' && nextProps.response === 'error') { 20 | this.setState({ error: true }) 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 | this.props.onClose()} 32 | > 33 | 34 |
    Are you sure?
    35 |
    36 | 47 |
    48 |
    49 | ) 50 | } 51 | } 52 | 53 | export default NewIdentity 54 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_CheckClaim.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | import FormRow from 'components/FormRow' 6 | 7 | class CheckClaim extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | identity: props.identities[0] ? props.identities[0].address : '', 12 | verifier: props.verifier.address, 13 | submitted: false 14 | } 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (this.props.response !== 'success' && nextProps.response === 'success') { 19 | this.setState({ shouldClose: true, submitted: true }) 20 | } 21 | if ( 22 | this.props.response !== 'submitted' && 23 | nextProps.response === 'submitted' 24 | ) { 25 | this.setState({ loading: true }) 26 | } 27 | } 28 | 29 | render() { 30 | return ( 31 | this.props.onClose()} 36 | > 37 | 38 |
    39 |
    Check for valid claim:
    40 | 41 | 42 | 43 | 58 | 59 | 60 |
    61 |
    62 | 75 |
    76 |
    77 |
    78 | ) 79 | } 80 | } 81 | 82 | export default CheckClaim 83 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_NewClaimIssuer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | import FormRow from 'components/FormRow' 6 | 7 | import { ClaimTypes, claimType } from 'actions/Identity' 8 | 9 | class NewClaimIssuer extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = { 13 | name: '', 14 | uri: '', 15 | preAdd: false, 16 | icon: 'key', 17 | claimType: '3', 18 | stage: 'main', 19 | signerServices: [] 20 | } 21 | } 22 | 23 | componentWillReceiveProps(nextProps) { 24 | if (this.props.response !== 'success' && nextProps.response === 'success') { 25 | this.setState({ shouldClose: true, submitted: true }) 26 | } 27 | if ( 28 | this.props.response !== 'submitted' && 29 | nextProps.response === 'submitted' 30 | ) { 31 | this.setState({ loading: true }) 32 | } 33 | } 34 | 35 | render() { 36 | return ( 37 | this.props.onClose()} 44 | onOpen={() => this.nameInput.focus()} 45 | onPressEnter={() => this.onDeploy()} 46 | > 47 | 48 | {this.state.stage === 'main' 49 | ? this.renderMain() 50 | : this.renderSignerService()} 51 | 52 | ) 53 | } 54 | 55 | renderMain() { 56 | var { identityType, activeAddress, identities } = this.props 57 | var otherTypeSameOwner = identities.find( 58 | i => i.type !== identityType && i.owner === activeAddress 59 | ) 60 | return ( 61 | <> 62 |
    63 | Deploy a new Claim Issuer contract: 64 |
    65 | {otherTypeSameOwner && ( 66 |
    67 | {`You may want to use a different wallet`} 68 |
    69 | )} 70 | 71 | 72 | 73 | (this.nameInput = r)} 78 | onChange={e => this.setState({ name: e.currentTarget.value })} 79 | /> 80 | 81 | 82 |
    83 | {this.state.signerServices.length === 0 ? null : ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {this.state.signerServices.map((ss, idx) => ( 94 | 95 | 96 | 99 | 100 | 101 | ))} 102 | 103 |
    Claim TypeIconService URL
    {claimType(ss.claimType)} 97 | 98 | {ss.uri}
    104 | )} 105 | { 108 | e.preventDefault() 109 | this.setState({ 110 | stage: 'add-service', 111 | uri: '', 112 | icon: 'key', 113 | claimType: '7' 114 | }) 115 | }} 116 | > 117 | Add Claim Signer Service 118 | 119 |
    120 | 126 |
    127 | 128 | ) 129 | } 130 | 131 | renderSignerService() { 132 | const Btn = props => ( 133 | 145 | ) 146 | 147 | return ( 148 | <> 149 |
    150 | Add Signer Service to Claim Issuer: 151 |
    152 | 153 | 154 | 155 | 170 | 171 | 172 | this.setState({ uri: e.currentTarget.value })} 177 | /> 178 | 179 | 180 |
    181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
    191 |
    192 | 193 |
    194 |
    195 | 203 | 221 |
    222 | 223 | ) 224 | } 225 | 226 | onDeploy() { 227 | this.props.deployIdentityContract( 228 | this.state.name, 229 | this.props.identityType, 230 | this.state.uri, 231 | null, 232 | this.state.icon, 233 | this.state.signerServices 234 | ) 235 | } 236 | } 237 | 238 | export default NewClaimIssuer 239 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_NewVerifier.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import FormRow from 'components/FormRow' 5 | import Loading from 'components/Loading' 6 | 7 | import { ClaimTypes } from 'actions/Identity' 8 | 9 | class NewVerifier extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = { 13 | name: '', 14 | claimType: '10', 15 | trustedIdentity: props.identities[0] ? props.identities[0].address : '', 16 | methodName: '' 17 | } 18 | } 19 | 20 | componentWillReceiveProps(nextProps) { 21 | if (this.props.response !== 'success' && nextProps.response === 'success') { 22 | this.setState({ shouldClose: true, submitted: true, loading: false }) 23 | } 24 | if (!this.props.response && nextProps.response === 'submitted') { 25 | this.setState({ loading: true }) 26 | } 27 | } 28 | 29 | render() { 30 | return ( 31 | this.props.onClose()} 37 | onOpen={() => this.nameInput.focus()} 38 | onPressEnter={() => this.onDeploy()} 39 | > 40 | 41 |
    42 | Deploy a new Claim Checker Contract: 43 |
    44 | {!this.props.identities.length && ( 45 |
    46 | {`Try deploying a Certifier first`} 47 |
    48 | )} 49 | 50 | 51 | 52 | 53 | (this.nameInput = r)} 58 | onChange={e => this.setState({ name: e.currentTarget.value })} 59 | /> 60 | 61 | 62 | 77 | 78 | 79 | 94 | 95 | 96 | 101 | this.setState({ methodName: e.currentTarget.value }) 102 | } 103 | /> 104 | 105 | 106 |
    107 |
    108 | 111 |
    112 |
    113 | ) 114 | } 115 | 116 | onDeploy() { 117 | const { name, trustedIdentity, methodName, claimType } = this.state 118 | this.props.deployClaimVerifier({ 119 | name, 120 | trustedIdentity, 121 | methodName, 122 | claimType 123 | }) 124 | } 125 | } 126 | 127 | export default NewVerifier 128 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_RemoveClaim.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | 6 | class RemoveClaim extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = {} 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (this.props.response !== 'success' && nextProps.response === 'success') { 14 | this.setState({ shouldClose: true, submitted: true }) 15 | } 16 | if (this.props.response !== 'submitted' && nextProps.response === 'submitted') { 17 | this.setState({ loading: true }) 18 | } 19 | if (this.props.response !== 'error' && nextProps.response === 'error') { 20 | this.setState({ error: true }) 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 | this.props.onClose()} 31 | > 32 | 33 | 34 |
    35 |
    Are you sure?
    36 | {this.props.wrongOwner && ( 37 |
    38 | {`Active wallet does not own this identity`} 39 |
    40 | )} 41 | {this.state.error && ( 42 |
    Error! Please try again.
    43 | )} 44 |
    45 | 53 |
    54 |
    55 |
    56 | ) 57 | } 58 | } 59 | 60 | export default RemoveClaim 61 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_RemoveIdentity.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | 5 | class RemoveIdentity extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = {} 9 | } 10 | 11 | render() { 12 | return ( 13 | this.props.onClose()} 16 | > 17 |
    18 |

    Are you sure you wish to remove this identity?

    19 |
    20 | 28 |
    29 |
    30 |
    31 | ) 32 | } 33 | } 34 | 35 | export default RemoveIdentity 36 | -------------------------------------------------------------------------------- /src/pages/identity/modals/_RemoveKey.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Modal from 'components/Modal' 4 | import Loading from 'components/Loading' 5 | 6 | class RemoveKey extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = {} 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (this.props.response !== 'success' && nextProps.response === 'success') { 14 | this.setState({ shouldClose: true, submitted: true }) 15 | } 16 | if (this.props.response !== 'submitted' && nextProps.response === 'submitted') { 17 | this.setState({ loading: true }) 18 | } 19 | if (this.props.response !== 'error' && nextProps.response === 'error') { 20 | this.setState({ error: true }) 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 | this.props.onClose()} 31 | > 32 | 33 | 34 |
    35 |
    Are you sure?
    36 | {this.props.wrongOwner && ( 37 |
    38 | {`Active wallet does not own this identity`} 39 |
    40 | )} 41 | {this.state.error && ( 42 |
    Error! Please try again.
    43 | )} 44 |
    45 | 53 |
    54 |
    55 |
    56 | ) 57 | } 58 | } 59 | 60 | export default RemoveKey 61 | -------------------------------------------------------------------------------- /src/pages/initIssuer.js: -------------------------------------------------------------------------------- 1 | let waitLastDeploy = Promise.resolve() 2 | let waitLastAddKey = Promise.resolve() 3 | 4 | export const callDeploy = (deployIdentityContract, issuerName, oauthLink) => { 5 | waitLastDeploy = waitLastDeploy.then( 6 | () => 7 | new Promise(resolve => { 8 | const liveServices = [ 9 | { 10 | uri: `${oauthLink}/fb-auth`, 11 | icon: 'facebook', 12 | claimType: '3' 13 | }, 14 | { 15 | uri: `${oauthLink}/twitter-auth`, 16 | icon: 'twitter', 17 | claimType: '4' 18 | }, 19 | { 20 | uri: `${oauthLink}/github-auth`, 21 | icon: 'github', 22 | claimType: '5' 23 | }, 24 | { 25 | uri: `${oauthLink}/google-auth`, 26 | icon: 'google', 27 | claimType: '6' 28 | }, 29 | { 30 | uri: `${oauthLink}/linkedin-auth`, 31 | icon: 'linkedin', 32 | claimType: '9' 33 | } 34 | ] 35 | 36 | const localServices = [ 37 | { 38 | uri: `${oauthLink}/twitter-auth`, 39 | icon: 'twitter', 40 | claimType: '4' 41 | }, 42 | { 43 | uri: `${oauthLink}/google-auth`, 44 | icon: 'google', 45 | claimType: '6' 46 | }, 47 | { 48 | uri: `${oauthLink}/linkedin-auth`, 49 | icon: 'linkedin', 50 | claimType: '9' 51 | } 52 | ] 53 | 54 | const services = process.env.LOCAL === "true" ? localServices : liveServices 55 | 56 | setTimeout(() => { 57 | deployIdentityContract( 58 | issuerName, 59 | 'certifier', 60 | `${oauthLink}/github-auth`, 61 | false, 62 | 'issuer-icon', 63 | services 64 | ) 65 | resolve() 66 | }, 500) 67 | }) 68 | ) 69 | } 70 | 71 | export const callAddKey = (addKey, key, address) => { 72 | waitLastAddKey = waitLastAddKey.then( 73 | () => 74 | new Promise(resolve => { 75 | setTimeout(() => { 76 | addKey({ 77 | key, 78 | purpose: '3', 79 | keyType: '1', 80 | identity: address 81 | }) 82 | resolve() 83 | }, 500) 84 | }) 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /src/reducers/Identity.js: -------------------------------------------------------------------------------- 1 | import { IdentityConstants } from 'actions/Identity' 2 | import { NetworkConstants } from 'actions/Network' 3 | 4 | const officialIdentities = 5 | typeof OfficialIdentities !== 'undefined' ? OfficialIdentities : [] 6 | 7 | const initialState = { 8 | contract: null, 9 | officialIdentities: [], 10 | identities: [], 11 | claimVerifiers: [], 12 | keys: [], 13 | claims: [], 14 | createIdentityResponse: undefined, 15 | createVerifierResponse: undefined, 16 | createKeyResponse: undefined, 17 | createClaimResponse: undefined, 18 | removeClaimResponse: undefined, 19 | checkClaimResponse: undefined, 20 | approveExecutionResponse: undefined, 21 | eventsResponse: undefined, 22 | lastDeployedIdentity: undefined, 23 | events: [] 24 | } 25 | 26 | function getInitialState() { 27 | var state = { 28 | ...initialState 29 | } 30 | try { 31 | state.identities = JSON.parse(localStorage.identities) 32 | } catch (e) { 33 | /* Ignore */ 34 | } 35 | try { 36 | state.claimVerifiers = JSON.parse(localStorage.claimVerifiers) 37 | } catch (e) { 38 | /* Ignore */ 39 | } 40 | return applyContractNames(state) 41 | } 42 | 43 | function applyContractNames(state) { 44 | var names = {} 45 | state.identities.forEach(i => (names[i.address] = i.name)) 46 | state.claimVerifiers.forEach(i => (names[i.address] = i.name)) 47 | return { ...state, names } 48 | } 49 | 50 | export const Purposes = [ 51 | { id: '1', name: 'Management' }, 52 | { id: '2', name: 'Action' }, 53 | { id: '3', name: 'Claim Signer' }, 54 | { id: '4', name: 'Encryption' } 55 | ] 56 | 57 | export const KeyTypes = [{ id: '1', name: 'ECDSA' }, { id: '2', name: 'RSA' }] 58 | 59 | export default function Identity(state = getInitialState(), action = {}) { 60 | switch (action.type) { 61 | case NetworkConstants.CHANGE_SUCCESS: 62 | return { 63 | ...state, 64 | officialIdentities: officialIdentities[action.id] || [] 65 | } 66 | 67 | case IdentityConstants.RESET: 68 | return getInitialState() 69 | 70 | case IdentityConstants.CHECK_CLAIM: 71 | return { ...state, checkClaimResponse: 'submitted' } 72 | 73 | case IdentityConstants.CHECK_CLAIM_HASH: 74 | return { ...state, checkClaimResponse: 'in-pool' } 75 | 76 | case IdentityConstants.CHECK_CLAIM_SUCCESS: 77 | return { ...state, checkClaimResponse: 'success' } 78 | 79 | case IdentityConstants.CHECK_CLAIM_ERROR: 80 | return { ...state, checkClaimResponse: 'error' } 81 | 82 | case IdentityConstants.ADD_CLAIM: 83 | return { ...state, addClaimResponse: 'submitted' } 84 | 85 | case IdentityConstants.ADD_CLAIM_HASH: 86 | return { ...state, addClaimResponse: 'in-pool' } 87 | 88 | case IdentityConstants.ADD_CLAIM_SUCCESS: 89 | return { ...state, addClaimResponse: 'success' } 90 | 91 | case IdentityConstants.ADD_CLAIM_ERROR: 92 | return { ...state, addClaimResponse: 'error' } 93 | 94 | case IdentityConstants.REMOVE_CLAIM: 95 | return { ...state, removeClaimResponse: 'submitted' } 96 | 97 | case IdentityConstants.REMOVE_CLAIM_HASH: 98 | return { ...state, removeClaimResponse: 'in-pool' } 99 | 100 | case IdentityConstants.REMOVE_CLAIM_SUCCESS: 101 | return { ...state, removeClaimResponse: 'success' } 102 | 103 | case IdentityConstants.REMOVE_CLAIM_ERROR: 104 | return { ...state, removeClaimResponse: 'error' } 105 | 106 | case IdentityConstants.ADD_KEY: 107 | return { ...state, addKeyResponse: 'submitted' } 108 | 109 | case IdentityConstants.ADD_KEY_HASH: 110 | return { ...state, addKeyResponse: 'in-pool' } 111 | 112 | case IdentityConstants.ADD_KEY_SUCCESS: 113 | return { ...state, addKeyResponse: 'success' } 114 | 115 | case IdentityConstants.ADD_KEY_ERROR: 116 | return { ...state, addKeyResponse: 'error' } 117 | 118 | case IdentityConstants.REMOVE_KEY: 119 | return { ...state, removeKeyResponse: 'submitted' } 120 | 121 | case IdentityConstants.REMOVE_KEY_HASH: 122 | return { ...state, removeKeyResponse: 'in-pool' } 123 | 124 | case IdentityConstants.REMOVE_KEY_SUCCESS: 125 | return { ...state, removeKeyResponse: 'success' } 126 | 127 | case IdentityConstants.REMOVE_KEY_ERROR: 128 | return { ...state, removeKeyResponse: 'error' } 129 | 130 | case IdentityConstants.APPROVE_EXECUTION: 131 | return { ...state, approveExecutionResponse: 'submitted' } 132 | 133 | case IdentityConstants.APPROVE_EXECUTION_HASH: 134 | return { ...state, approveExecutionResponse: 'in-pool' } 135 | 136 | case IdentityConstants.APPROVE_EXECUTION_SUCCESS: 137 | return { ...state, approveExecutionResponse: 'success' } 138 | 139 | case IdentityConstants.APPROVE_EXECUTION_ERROR: 140 | return { ...state, approveExecutionResponse: 'error' } 141 | 142 | case IdentityConstants.DEPLOY: 143 | return { ...state, createIdentityResponse: 'submitted' } 144 | 145 | case IdentityConstants.DEPLOY_HASH: 146 | return { ...state, createIdentityResponse: 'in-pool' } 147 | 148 | case IdentityConstants.DEPLOY_RECEIPT: { 149 | let newState = { 150 | ...state, 151 | createIdentityResponse: 'success', 152 | identities: [ 153 | ...state.identities, 154 | { 155 | name: action.name, 156 | address: action.receipt.contractAddress, 157 | owner: action.owner, 158 | type: action.identityType, 159 | signerServices: action.signerServices 160 | } 161 | ], 162 | lastDeployedIdentity: action.receipt.contractAddress 163 | } 164 | localStorage.identities = JSON.stringify(newState.identities) 165 | return applyContractNames(newState) 166 | } 167 | 168 | case IdentityConstants.IMPORT: { 169 | let newState = { 170 | ...state, 171 | identities: [ 172 | ...state.identities, 173 | { 174 | name: action.name, 175 | address: action.address, 176 | owner: action.owner, 177 | type: action.identityType 178 | } 179 | ] 180 | } 181 | localStorage.identities = JSON.stringify(newState.identities) 182 | return applyContractNames(newState) 183 | } 184 | 185 | case IdentityConstants.DEPLOY_VERIFIER: 186 | return { ...state, createVerifierResponse: 'submitted' } 187 | 188 | case IdentityConstants.DEPLOY_VERIFIER_HASH: 189 | return { 190 | ...state, 191 | contract: action.contract, 192 | createVerifierResponse: 'in-pool' 193 | } 194 | 195 | case IdentityConstants.DEPLOY_VERIFIER_RECEIPT: { 196 | let newState = { 197 | ...state, 198 | createVerifierResponse: 'success', 199 | claimVerifiers: [ 200 | ...state.claimVerifiers, 201 | { 202 | name: action.name, 203 | address: action.receipt.contractAddress, 204 | owner: action.owner, 205 | trustedIdentity: action.trustedIdentity, 206 | methodName: action.methodName, 207 | claimType: action.claimType 208 | } 209 | ] 210 | } 211 | localStorage.claimVerifiers = JSON.stringify(newState.claimVerifiers) 212 | return applyContractNames(newState) 213 | } 214 | 215 | case IdentityConstants.GET_EVENTS: 216 | return { ...state, eventsResponse: null } 217 | 218 | case IdentityConstants.GET_EVENTS_SUCCESS: 219 | return { ...state, events: action.events, eventsResponse: 200 } 220 | 221 | case IdentityConstants.REMOVE: { 222 | let newState = { 223 | ...state, 224 | identities: state.identities.filter(i => i.address !== action.address) 225 | } 226 | localStorage.identities = JSON.stringify(newState.identities) 227 | return applyContractNames(newState) 228 | } 229 | 230 | case IdentityConstants.REMOVE_VERIFIER: { 231 | let newState = { 232 | ...state, 233 | claimVerifiers: state.claimVerifiers.filter( 234 | i => i.address !== action.address 235 | ) 236 | } 237 | localStorage.claimVerifiers = JSON.stringify(newState.claimVerifiers) 238 | return applyContractNames(newState) 239 | } 240 | } 241 | 242 | return state 243 | } 244 | -------------------------------------------------------------------------------- /src/reducers/Network.js: -------------------------------------------------------------------------------- 1 | import { NetworkConstants } from 'actions/Network' 2 | 3 | import Providers from 'constants/Providers' 4 | 5 | const HOST = process.env.HOST || 'localhost' 6 | let ipfsGateway = 'https://ipfs.o2oprotocol.com', 7 | ipfsRPC = 'https://ipfs.o2oprotocol.com', 8 | provider = 'https://rinkeby.infura.io', 9 | browserProvider = false 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | ipfsGateway = `http://${HOST}:8081` 13 | ipfsRPC = `http://${HOST}:5001` 14 | provider = `http://${HOST}:8545` 15 | } 16 | 17 | if (typeof window !== 'undefined') { 18 | provider = window.sessionStorage.provider || provider 19 | if (window.web3) { 20 | browserProvider = web3.currentProvider 21 | } 22 | window.web3 = new Web3(provider) 23 | } 24 | 25 | const initialState = { 26 | id: null, 27 | contract: null, 28 | accounts: [], 29 | account: null, 30 | status: 'disconnected', 31 | 32 | browserProvider, 33 | providers: Providers, 34 | provider, 35 | 36 | ipfsGateway, 37 | ipfsRPC 38 | } 39 | 40 | export default function Network(state = initialState, action = {}) { 41 | switch (action.type) { 42 | case NetworkConstants.GET_CONTRACT_SUCCESS: 43 | return { 44 | ...state, 45 | contract: action.contract 46 | } 47 | 48 | case NetworkConstants.CHANGE_SUCCESS: 49 | return { 50 | ...state, 51 | status: 'connected', 52 | id: action.id, 53 | contract: action.contract, 54 | accounts: action.accounts, 55 | account: action.accounts[0] 56 | } 57 | 58 | case NetworkConstants.CHANGE_ERROR: 59 | return { 60 | ...state, 61 | status: 'error' 62 | } 63 | 64 | case NetworkConstants.SELECT_ACCOUNT: 65 | return { 66 | ...state, 67 | account: state.accounts.find(a => a.hash === action.hash) 68 | } 69 | 70 | case NetworkConstants.UPDATE_BALANCE_SUCCESS: 71 | var acctIdx = state.accounts.findIndex(a => a.hash === action.account) 72 | if (acctIdx < 0) { 73 | return state 74 | } 75 | var accounts = [...state.accounts], 76 | account = { 77 | hash: action.account, 78 | balance: action.balance 79 | } 80 | accounts[acctIdx] = account 81 | return { 82 | ...state, 83 | accounts, 84 | account 85 | } 86 | 87 | case NetworkConstants.SET_PROVIDER: 88 | window.sessionStorage.provider = action.provider 89 | return { 90 | ...state, 91 | status: 'connecting', 92 | provider: action.provider 93 | } 94 | 95 | case NetworkConstants.SET_IPFS: 96 | return { 97 | ...state, 98 | ipfsGateway: action.gateway, 99 | ipfsRPC: action.api 100 | } 101 | } 102 | 103 | return state 104 | } 105 | -------------------------------------------------------------------------------- /src/reducers/Wallet.js: -------------------------------------------------------------------------------- 1 | import { WalletConstants } from 'actions/Wallet' 2 | import { NetworkConstants } from 'actions/Network' 3 | 4 | import balance from 'utils/balance' 5 | 6 | const initialState = { 7 | externalProvider: false, 8 | 9 | activeAddress: null, 10 | balances: {}, 11 | currency: 'eth', 12 | currencyStr: '$', 13 | exchangeRates: { 14 | usd: 400 15 | }, 16 | 17 | unsaved: false, 18 | loaded: false, 19 | raw: {}, 20 | accounts: [], 21 | active: null, 22 | locked: false, 23 | tryUnlock: false 24 | } 25 | 26 | export default function Wallet(state = initialState, action = {}) { 27 | switch (action.type) { 28 | case WalletConstants.SELECT_ACCOUNT_SUCCESS: 29 | return { 30 | ...state, 31 | activeAddress: action.activeAddress 32 | } 33 | 34 | case WalletConstants.LOAD_EXTERNAL_SUCCESS: 35 | return { 36 | ...state, 37 | activeAddress: action.activeAddress, 38 | accounts: [], 39 | balances: action.balances, 40 | active: null, 41 | loaded: false 42 | } 43 | 44 | case WalletConstants.LOAD: 45 | return { ...state, externalProvider: action.external } 46 | 47 | case WalletConstants.LOAD_SUCCESS: 48 | return { 49 | ...state, 50 | raw: action.wallet, 51 | accounts: action.accounts, 52 | balances: action.balances, 53 | active: action.wallet[0] ? action.wallet[0] : null, 54 | activeAddress: action.wallet[0] ? action.wallet[0].address : null, 55 | loaded: true 56 | } 57 | 58 | case WalletConstants.ADD_ACCOUNT_SUCCESS: 59 | return { 60 | ...state, 61 | raw: action.wallet, 62 | accounts: [...state.accounts, action.account.address], 63 | balances: { 64 | ...state.balances, 65 | [action.account.address]: balance('0', state.exchangeRates) 66 | }, 67 | unsaved: true 68 | } 69 | 70 | case WalletConstants.IMPORT_ACCOUNT_SUCCESS: 71 | return { 72 | ...state, 73 | raw: action.wallet, 74 | accounts: [...state.accounts, action.account.address], 75 | balances: { 76 | ...state.balances, 77 | [action.account.address]: action.balance 78 | }, 79 | unsaved: true 80 | } 81 | 82 | case WalletConstants.REMOVE_ACCOUNT_SUCCESS: 83 | return { 84 | ...state, 85 | raw: action.wallet, 86 | accounts: state.accounts.filter(h => h !== action.hash), 87 | unsaved: true 88 | } 89 | 90 | case WalletConstants.SAVE_SUCCESS: 91 | return { ...state, unsaved: false } 92 | 93 | case WalletConstants.SET_CURRENCY: 94 | return { 95 | ...state, 96 | currency: action.currency, 97 | currencyStr: action.currency === 'usd' ? '$' : 'ETH' 98 | } 99 | 100 | case WalletConstants.UPDATE_BALANCE_SUCCESS: 101 | return { 102 | ...state, 103 | balances: { 104 | ...state.balances, 105 | [action.account]: action.balance 106 | } 107 | } 108 | 109 | case NetworkConstants.UPDATE_BALANCE_SUCCESS: 110 | return { 111 | ...state, 112 | balances: { 113 | ...state.balances, 114 | [action.account]: action.balance 115 | } 116 | } 117 | 118 | case WalletConstants.LOCK_WALLET: 119 | return { 120 | ...state, 121 | locked: true, 122 | tryUnlock: false 123 | } 124 | 125 | case WalletConstants.UNLOCK_WALLET: 126 | return { 127 | ...state, 128 | tryUnlock: true 129 | } 130 | 131 | case WalletConstants.UNLOCKED_WALLET: 132 | return { 133 | ...state, 134 | tryUnlock: false, 135 | locked: false 136 | } 137 | } 138 | 139 | return state 140 | } 141 | -------------------------------------------------------------------------------- /src/utils/balance.js: -------------------------------------------------------------------------------- 1 | import numberFormat from './numberFormat' 2 | 3 | export default function(wei, exchangeRates) { 4 | var eth = web3.utils.fromWei(wei, 'ether') 5 | var balance = { 6 | wei, 7 | eth, 8 | ethStr: `${numberFormat(Number(eth), 4)} ETH` 9 | } 10 | Object.keys(exchangeRates).forEach(currency => { 11 | balance[currency] = 12 | Math.round(exchangeRates[currency] * Number(eth) * 100) / 100 13 | balance[currency + 'Str'] = '$' + numberFormat(balance[currency], 2) 14 | }) 15 | return balance 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/decodeFn.js: -------------------------------------------------------------------------------- 1 | import web3 from 'Web3' 2 | 3 | export default function decodeFn(Contract, data) { 4 | var methodSigs = Contract.abi.filter(a => a.type === 'function').reduce((m, fn) => { 5 | m[web3.eth.abi.encodeFunctionSignature(fn)] = fn 6 | return m 7 | }, {}) 8 | 9 | var methodAbi = methodSigs[data.slice(0, 10)] 10 | return { name: methodAbi.name, params: web3.eth.abi.decodeParameters(methodAbi.inputs, data.slice(10)) } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/ipfsHash.js: -------------------------------------------------------------------------------- 1 | // export function getBytes32FromIpfsHash(hash) { 2 | // return hash; 3 | // } 4 | // export function getIpfsHashFromBytes32(bytes32Hex) { 5 | // return bytes32Hex; 6 | // } 7 | import bs58 from 'bs58' 8 | 9 | export function getBytes32FromIpfsHash(hash) { 10 | return `0x${bs58 11 | .decode(hash) 12 | .slice(2) 13 | .toString('hex')}` 14 | } 15 | 16 | // Return base58 encoded ipfs hash from bytes32 hex string, 17 | // E.g. "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231" 18 | // --> "QmNSUYVKDSvPUnRLKmuxk9diJ6yS96r1TrAXzjTiBcCLAL" 19 | export function getIpfsHashFromBytes32(bytes32Hex) { 20 | // Add our default ipfs values for first 2 bytes: 21 | // function:0x12=sha2, size:0x20=256 bits 22 | // and cut off leading "0x" 23 | const hashHex = '1220' + bytes32Hex.slice(2) 24 | const hashBytes = Buffer.from(hashHex, 'hex') 25 | const hashStr = bs58.encode(hashBytes) 26 | return hashStr 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/keyMirror.js: -------------------------------------------------------------------------------- 1 | export default function(obj, namespace) { 2 | var ret = {}, 3 | key 4 | 5 | for (key in obj) { 6 | if (namespace) { 7 | ret[key] = namespace + '_' + key 8 | } else { 9 | ret[key] = key 10 | } 11 | } 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/numberFormat.js: -------------------------------------------------------------------------------- 1 | export default function numberFormat(number, dec, dsep, tsep) { 2 | if (isNaN(number) || number == null) return '' 3 | 4 | number = number.toFixed(~~dec) 5 | tsep = typeof tsep == 'string' ? tsep : ',' 6 | 7 | var parts = number.split('.'), 8 | fnums = parts[0], 9 | decimals = parts[1] ? (dsep || '.') + parts[1] : '' 10 | 11 | return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + tsep) + decimals 12 | } 13 | -------------------------------------------------------------------------------- /test/ClaimVerifier.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import helper from './_helper' 3 | 4 | describe('ClaimVerifier.sol', async function() { 5 | var web3, accounts, deploy, prvSigner, pubSigner 6 | var UserIdentity, ClaimIssuer, ClaimVerifier 7 | 8 | before(async function() { 9 | ({ deploy, accounts, web3 } = await helper( 10 | `${__dirname}/../contracts/` 11 | )) 12 | 13 | prvSigner = web3.utils.randomHex(32) 14 | pubSigner = web3.eth.accounts.privateKeyToAccount(prvSigner).address 15 | 16 | UserIdentity = await deploy('ClaimHolder', { from: accounts[0] }) 17 | ClaimIssuer = await deploy('ClaimHolder', { from: accounts[1] }) 18 | ClaimVerifier = await deploy('ClaimVerifier', { from: accounts[2], args: [ 19 | ClaimIssuer._address 20 | ] }) 21 | }) 22 | 23 | it('should allow verifier owner to addKey', async function() { 24 | var key = web3.utils.sha3(pubSigner) 25 | var result = await ClaimIssuer.methods.addKey(key, 3, 1).send({ from: accounts[1] }) 26 | 27 | assert(result) 28 | }) 29 | 30 | it('should not allow new listing without identity claim', async function() { 31 | var res = await ClaimVerifier.methods 32 | .checkClaim(UserIdentity._address, 3) 33 | .send({ from: accounts[0] }) 34 | assert(res.events.ClaimInvalid) 35 | }) 36 | 37 | it('should allow identity owner to addClaim', async function() { 38 | var data = web3.utils.asciiToHex('Verified OK') 39 | var claimType = 3 40 | var hashed = web3.utils.soliditySha3(UserIdentity._address, claimType, data) 41 | var signed = await web3.eth.accounts.sign(hashed, prvSigner) 42 | 43 | var claimRes = await UserIdentity.methods 44 | .addClaim( 45 | claimType, 46 | 2, 47 | ClaimIssuer._address, 48 | signed.signature, 49 | data, 50 | 'abc.com' 51 | ).send({ from: accounts[0] }) 52 | 53 | assert(claimRes.events.ClaimAdded) 54 | }) 55 | 56 | it('should not allow new listing without identity claim', async function() { 57 | var res = await ClaimVerifier.methods 58 | .checkClaim(UserIdentity._address, 3) 59 | .send({ from: accounts[0] }) 60 | assert(res.events.ClaimValid) 61 | }) 62 | 63 | }) 64 | -------------------------------------------------------------------------------- /test/Identity.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import helper from './_helper' 3 | 4 | describe('Identity', async function() { 5 | var web3, accounts, deploy, acctSha3, randomHex 6 | var UserIdentity 7 | 8 | before(async function() { 9 | ({ web3, deploy, accounts, web3: { utils: { randomHex } } } = await helper( 10 | `${__dirname}/../contracts/` 11 | )) 12 | 13 | UserIdentity = await deploy('ClaimHolder', { from: accounts[0] }) 14 | acctSha3 = web3.utils.keccak256(accounts[0]) 15 | }) 16 | 17 | describe('Pre-Auth Identity', async function() { 18 | it('should deploy successfully', async function() { 19 | var sig = randomHex(10) 20 | var data = randomHex(10) 21 | var url = "1234567890" 22 | await deploy('Identity', { from: accounts[0], args: [ 23 | // [1], [3], [accounts[0]], sig, data, url, [sig.length-2], [data.length-2], [url.length] 24 | [1], [3], [accounts[0]], sig, data, url, [10], [10], [10] 25 | ] }) 26 | }) 27 | }) 28 | 29 | describe('Keys', async function() { 30 | it('should set a default MANAGEMENT_KEY', async function() { 31 | var res = await UserIdentity.methods.getKey(acctSha3).call() 32 | assert.equal(res.purpose, '1') 33 | assert.equal(res.keyType, '1') 34 | assert.equal(res.key, acctSha3) 35 | }) 36 | 37 | it('should respond to getKeyPurpose', async function() { 38 | var res = await UserIdentity.methods.getKeyPurpose(acctSha3).call() 39 | assert.equal(res, '1') 40 | }) 41 | 42 | it('should respond to getKeysByPurpose', async function() { 43 | var res = await UserIdentity.methods.getKeysByPurpose(1).call() 44 | assert.deepEqual(res, [acctSha3]) 45 | }) 46 | 47 | it('should implement addKey', async function() { 48 | var newKey = web3.utils.randomHex(32) 49 | var res = await UserIdentity.methods.addKey(newKey, 1, 1).send() 50 | assert(res.events.KeyAdded) 51 | 52 | var getKey = await UserIdentity.methods.getKey(newKey).call() 53 | assert.equal(getKey.key, newKey) 54 | }) 55 | 56 | it('should not allow an existing key to be added', async function() { 57 | try { 58 | await UserIdentity.methods.addKey(acctSha3, 1, 1).send() 59 | assert(false) 60 | } catch (e) { 61 | assert(e.message.match(/revert/)) 62 | } 63 | }) 64 | 65 | it('should not allow sender without MANAGEMENT_KEY to addKey', async function() { 66 | try { 67 | await UserIdentity.methods.addKey(web3.utils.randomHex(32), 1, 1).send({ 68 | from: accounts[1] 69 | }) 70 | assert(false) 71 | } catch (e) { 72 | assert(e.message.match(/revert/)) 73 | } 74 | }) 75 | }) 76 | 77 | describe('Claims', async function() { 78 | 79 | it('should allow a claim to be added by management account', async function() { 80 | var response = await UserIdentity.methods 81 | .addClaim(1, 2, accounts[0], randomHex(32), randomHex(32), 'abc.com') 82 | .send() 83 | assert(response.events.ClaimAdded) 84 | }) 85 | 86 | it('should disallow new claims from unrecognized accounts', async function() { 87 | try { 88 | await UserIdentity.methods 89 | .addClaim(1, 2, accounts[0], randomHex(32), randomHex(32), 'abc.com') 90 | .send({ from: accounts[2] }) 91 | assert(false) 92 | } catch (e) { 93 | assert(e.message.match(/revert/)) 94 | } 95 | }) 96 | 97 | it('should have 1 claim by type', async function() { 98 | var byTypeRes = await UserIdentity.methods.getClaimIdsByType(1).call() 99 | assert.equal(byTypeRes.length, 1) 100 | }) 101 | 102 | it('should respond to getClaim', async function() { 103 | var claimId = web3.utils.soliditySha3(accounts[0], 1) 104 | var claim = await UserIdentity.methods.getClaim(claimId).call() 105 | assert.equal(claim.claimType, "1") 106 | }) 107 | 108 | // it('should respond to isClaimValid', async function() { 109 | // var claimId = web3.utils.soliditySha3(accounts[0], 1) 110 | // var valid = await UserIdentity.methods.isClaimValid(claimId).call() 111 | // assert(valid) 112 | // }) 113 | 114 | it('should allow claim to be removed', async function() { 115 | var claimId = web3.utils.soliditySha3(accounts[0], 1) 116 | var response = await UserIdentity.methods 117 | .removeClaim(claimId) 118 | .send({ from: accounts[0] }) 119 | assert(response.events.ClaimRemoved) 120 | 121 | var claim = await UserIdentity.methods.getClaim(claimId).call() 122 | assert.equal(claim.claimType, "0") 123 | }) 124 | }) 125 | 126 | describe('Executions', async function() { 127 | it('should allow any account to execute actions', async function() { 128 | var addClaimAbi = await UserIdentity.methods 129 | .addClaim(1, 2, accounts[0], randomHex(32), randomHex(32), 'abc.com') 130 | .encodeABI() 131 | 132 | var response = await UserIdentity.methods 133 | .execute(UserIdentity.options.address, 0, addClaimAbi) 134 | .send({ 135 | from: accounts[2] 136 | }) 137 | 138 | assert(response.events.ExecutionRequested) 139 | assert(!response.events.Approved) 140 | assert(!response.events.Executed) 141 | }) 142 | 143 | it('should auto-approve executions from MANAGEMENT_KEYs', async function() { 144 | var addClaimAbi = await UserIdentity.methods 145 | .addClaim(1, 2, accounts[0], randomHex(32), randomHex(32), 'abc.com') 146 | .encodeABI() 147 | 148 | var response = await UserIdentity.methods 149 | .execute(UserIdentity.options.address, 0, addClaimAbi) 150 | .send({ 151 | from: accounts[0] 152 | }) 153 | 154 | assert(response.events.ExecutionRequested) 155 | assert(response.events.Approved) 156 | assert(response.events.ClaimAdded) 157 | assert(response.events.Executed) 158 | }) 159 | }) 160 | 161 | describe('Approvals', async function() { 162 | it('should allow MANAGEMENT_KEYs to approve executions', async function() { 163 | var addClaimAbi = await UserIdentity.methods 164 | .addClaim(1, 2, accounts[2], randomHex(32), randomHex(32), 'abc.com') 165 | .encodeABI() 166 | 167 | var response = await UserIdentity.methods 168 | .execute(UserIdentity.options.address, 0, addClaimAbi) 169 | .send({ from: accounts[2] }) 170 | 171 | assert(response.events.ExecutionRequested) 172 | assert(!response.events.Approved) 173 | 174 | var id = response.events.ExecutionRequested.returnValues.executionId; 175 | 176 | var approval = await UserIdentity.methods.approve(id, true) 177 | .send({ from: accounts[0] }) 178 | 179 | assert(approval.events.Approved) 180 | assert(approval.events.ClaimAdded) 181 | assert(approval.events.Executed) 182 | }) 183 | 184 | it('should allow ACTION_KEYs to approve executions') 185 | it('should not allow CLAIM_SIGNER_KEYs to approve executions') 186 | it('should not be able to approve an already executed execution') 187 | it('should not be able to approve a non-existant execution') 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /test/_helper.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import solc from 'solc' 3 | import linker from 'solc/linker' 4 | import Ganache from 'ganache-core' 5 | import Web3 from 'web3' 6 | 7 | var solcOpts = { 8 | language: 'Solidity', 9 | settings: { 10 | metadata: { useLiteralContent: true }, 11 | outputSelection: { 12 | '*': { 13 | '*': ['abi', 'evm.bytecode.object'] 14 | } 15 | } 16 | } 17 | } 18 | 19 | // Instantiate a web3 instance. Start a node if one is not already running. 20 | export async function web3Helper(provider = 'ws://localhost:7545') { 21 | var web3 = new Web3(provider) 22 | var instance = await server(web3, provider) 23 | return { web3, server: instance } 24 | } 25 | 26 | function findImportsPath(prefix) { 27 | return function findImports(path) { 28 | try { 29 | return { 30 | contents: fs.readFileSync(prefix + path).toString() 31 | } 32 | } catch (e) { 33 | return { error: 'File not found' } 34 | } 35 | } 36 | } 37 | 38 | export default async function testHelper(contracts, provider) { 39 | const { web3, server } = await web3Helper(provider) 40 | const accounts = await web3.eth.getAccounts() 41 | 42 | async function deploy(contractName, { from, args, log }) { 43 | var sources = { 44 | [contractName]: { 45 | content: fs.readFileSync(`${contracts}/${contractName}.sol`).toString() 46 | } 47 | } 48 | var compileOpts = JSON.stringify({ ...solcOpts, sources }) 49 | 50 | // Compile the contract using solc 51 | var rawOutput = solc.compileStandardWrapper( 52 | compileOpts, 53 | findImportsPath(contracts) 54 | ) 55 | var output = JSON.parse(rawOutput) 56 | 57 | // If there were any compilation errors, throw them 58 | if (output.errors) { 59 | output.errors.forEach(err => { 60 | if (!err.formattedMessage.match(/Warning:/)) { 61 | throw new SyntaxError(err.formattedMessage) 62 | } 63 | }) 64 | } 65 | 66 | var { abi, evm: { bytecode } } = output.contracts[contractName][ 67 | contractName 68 | ] 69 | 70 | // Deploy linked libraries 71 | for (let linkedFile in bytecode.linkReferences) { 72 | for (let linkedLib in bytecode.linkReferences[linkedFile]) { 73 | let libObj = output.contracts[linkedFile][linkedLib] 74 | let LibContract = new web3.eth.Contract(libObj.abi) 75 | var libContract = await LibContract.deploy({ 76 | data: libObj.evm.bytecode.object 77 | }).send({ 78 | from, 79 | gas: 3000000 80 | }) 81 | 82 | let libs = { [`${linkedFile}:${linkedLib}`]: libContract._address } 83 | 84 | bytecode.object = linker.linkBytecode(bytecode.object, libs) 85 | } 86 | } 87 | 88 | if (!bytecode.object) { 89 | throw new Error( 90 | 'No Bytecode. Do the method signatures match the interface?' 91 | ) 92 | } 93 | 94 | if (process.env.BUILD) { 95 | fs.writeFileSync( 96 | __dirname + '/../src/contracts/' + contractName + '.js', 97 | 'module.exports = ' + 98 | JSON.stringify( 99 | { 100 | abi, 101 | data: bytecode.object 102 | }, 103 | null, 104 | 4 105 | ) 106 | ) 107 | } 108 | 109 | // Instantiate the web3 contract using the abi and bytecode output from solc 110 | var Contract = new web3.eth.Contract(abi) 111 | var contract 112 | 113 | await new Promise(async resolve => { 114 | var chainId = web3.eth.net.getId() 115 | 116 | var data = await Contract.deploy({ 117 | data: '0x' + bytecode.object, 118 | arguments: args 119 | }).encodeABI() 120 | 121 | web3.eth 122 | .sendTransaction({ 123 | data, 124 | from, 125 | value: 0, 126 | gas: 4612388, 127 | chainId 128 | }) 129 | .once('transactionHash', hash => { 130 | if (log) { 131 | console.log('Transaction Hash', hash) 132 | } 133 | }) 134 | .once('receipt', receipt => { 135 | if (log) { 136 | console.log( 137 | `Deployed ${contractName} to ${receipt.contractAddress} (${ 138 | receipt.cumulativeGasUsed 139 | } gas used)` 140 | ) 141 | } 142 | }) 143 | .catch('error', err => { 144 | console.log(err) 145 | resolve() 146 | }) 147 | .then(instance => { 148 | contract = new web3.eth.Contract(abi, instance.contractAddress) 149 | resolve() 150 | }) 151 | }) 152 | 153 | if (contract) { 154 | // Set some default options on the contract 155 | contract.options.gas = 1500000 156 | contract.options.from = from 157 | } 158 | 159 | return contract 160 | } 161 | 162 | return { web3, accounts, deploy, server } 163 | } 164 | 165 | // Start the server if it hasn't been already... 166 | async function server(web3, provider) { 167 | try { 168 | // Hack to prevent "connection not open on send" error when using websockets 169 | web3.setProvider(provider.replace(/^ws/, 'http')) 170 | await web3.eth.net.getId() 171 | web3.setProvider(provider) 172 | return 173 | } catch (e) { 174 | /* Ignore */ 175 | } 176 | 177 | var port = '7545' 178 | if (String(provider).match(/:([0-9]+)$/)) { 179 | port = provider.match(/:([0-9]+)$/)[1] 180 | } 181 | var server = Ganache.server() 182 | await server.listen(port) 183 | return server 184 | } 185 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const dotenv = require( "dotenv") 4 | const syncReq = require("sync-request") 5 | 6 | dotenv.config() 7 | const publicIp = syncReq("GET", "http://ipinfo.io/ip").getBody('utf8').trim() 8 | const LOCAL = process.env.LOCAL === "true" 9 | if(!LOCAL){ 10 | process.env.HOST = publicIp 11 | } 12 | 13 | var OfficialIdentities = [] 14 | try { 15 | OfficialIdentities = require('./data/OfficialIdentities') 16 | } catch (e) { 17 | /* Ignore */ 18 | } 19 | 20 | var config = { 21 | entry: { 22 | app: './src/index.js' 23 | }, 24 | devtool: 'cheap-module-source-map', 25 | output: { 26 | filename: '[name].js', 27 | path: path.resolve(__dirname, 'public'), 28 | hashDigestLength: 8 29 | }, 30 | externals: { 31 | Web3: 'web3' 32 | }, 33 | module: { 34 | noParse: [/^react$/], 35 | rules: [ 36 | { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | loader: 'babel-loader' 40 | } 41 | ] 42 | }, 43 | resolve: { 44 | extensions: ['.js', '.json'] 45 | }, 46 | node: { 47 | fs: 'empty' 48 | }, 49 | devServer: { 50 | port: 8082, 51 | headers: { 52 | 'Access-Control-Allow-Origin': '*' 53 | } 54 | }, 55 | mode: 'development', 56 | plugins: [ 57 | new webpack.EnvironmentPlugin({ 58 | HOST: 'localhost', 59 | RESET: true, 60 | LOCAL: true 61 | }), 62 | new webpack.DefinePlugin({ 63 | OfficialIdentities: JSON.stringify(OfficialIdentities) 64 | }) 65 | ], 66 | 67 | optimization: { 68 | splitChunks: { 69 | cacheGroups: { 70 | vendor: { 71 | chunks: 'initial', 72 | test: path.resolve(__dirname, 'node_modules'), 73 | name: 'vendor', 74 | enforce: true 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | module.exports = config 82 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var devConfig = require('./webpack.config.js') 3 | var path = require('path') 4 | 5 | var prodConfig = { 6 | ...devConfig, 7 | mode: 'production', 8 | devtool: false 9 | } 10 | 11 | prodConfig.plugins.push(new webpack.IgnorePlugin(/redux-logger/)) 12 | prodConfig.resolve.alias = { 13 | react: 'react/umd/react.production.min.js', 14 | 'react-dom': 'react-dom/umd/react-dom.production.min.js', 15 | 'react-styl': 'react-styl/prod.js', 16 | web3: path.resolve(__dirname, 'public/web3.min'), 17 | redux: 'redux/dist/redux.min.js', 18 | 'react-redux': 'react-redux/dist/react-redux.min.js', 19 | 'react-router-dom': 'react-router-dom/umd/react-router-dom.min.js' 20 | } 21 | prodConfig.module.noParse = [ 22 | /^(react|react-dom|react-styl|redux|react-redux|react-router-dom)$/, 23 | /web3/ 24 | ] 25 | 26 | module.exports = prodConfig 27 | --------------------------------------------------------------------------------