├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── LICENSE ├── README.md ├── contracts ├── Migrations.sol ├── StowHub.sol ├── StowPermissions.sol ├── StowRecords.sol ├── StowUsers.sol ├── StowWhitelistI.sol ├── interfaces │ ├── IrisScoreProviderI.sol │ └── PermissionPolicyI.sol └── mock │ ├── ERC20Mock.sol │ ├── IrisScoreProviderMock.sol │ ├── PermissionPolicyMock.sol │ └── WhitelistMock.sol ├── images ├── getting-started-with-stow.png └── linnia_architecture_chart.png ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── test ├── 1_hub_test.js ├── 2_users_test.js ├── 3_records_test.js ├── 4_permissions_test.js ├── 5_records-token_test.js └── 6_whitelist_test.js ├── testData ├── config.js ├── index.js ├── setupData.js ├── setupMetadata.js ├── setupRoles.js ├── synthetic_patients_data │ ├── Angel Marvin.json │ ├── Annemarie Nicolas.json │ ├── Arden Barton.json │ ├── Arron Trantow.json │ ├── Cherish Weissnat.json │ ├── Chris Littel.json │ ├── Claude Satterfield.json │ ├── Delicia Schowalter.json │ ├── Domingo Schaden.json │ ├── Edith Batz.json │ ├── Estela Rangel.json │ ├── Ewa Macejkovic.json │ ├── Gregory Schmitt.json │ ├── Guadalupe Tamayo.json │ ├── Hassan Renner.json │ ├── Ilse Jacobi.json │ ├── Jenna Haag.json │ ├── Jeremy Cremin.json │ ├── Kristin Stark.json │ ├── Lacy Fritsch.json │ ├── Laverne Prohaska.json │ ├── Lou Kuhn.json │ ├── Lucien Boyer.json │ ├── Luisa Vega.json │ ├── Lyndsey Zieme.json │ ├── Lynelle Schimmel.json │ ├── Mac Okuneva.json │ ├── Manual Wolff.json │ ├── Maricarmen Sotelo.json │ ├── Martín Navarro.json │ ├── Mirian Wilkinson.json │ ├── Orville O'Keefe.json │ ├── Ramon Lubowitz.json │ ├── Renato Murphy.json │ ├── Son Turcotte.json │ ├── Tegan Howe.json │ ├── Teisha Romaguera.json │ ├── Theo Kshlerin.json │ ├── Trent Goldner.json │ └── Wilson Brown.json ├── test-encryption-keys.json └── utils.js └── truffle-config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "stage-3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.5 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | - run: node -v 29 | - run: npm --version 30 | 31 | # start testrpc so that prepare runs without errors 32 | - run: 33 | command: npm start 34 | background: true 35 | - run: sleep 5 36 | 37 | - run: npm ci 38 | 39 | - save_cache: 40 | paths: 41 | - node_modules 42 | key: v1-dependencies-{{ checksum "package.json" }} 43 | 44 | - run: 45 | name: "JS linter" 46 | command: npm run lint:tests 47 | - store_test_results: 48 | path: reports/junit 49 | - store_artifacts: 50 | path: reports/junit 51 | 52 | # run tests! 53 | - run: npm run coverage 54 | - run: bash <(curl -s https://codecov.io/bash) 55 | # Teardown 56 | # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each 57 | # Save test results 58 | - store_test_results: 59 | path: coverage 60 | # Save artifacts 61 | - store_artifacts: 62 | path: coverage 63 | - store_artifacts: 64 | path: build 65 | 66 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{ts,tsx,js,jsx}] 10 | indent_size = 2 11 | indent_style = space 12 | 13 | [*.json] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.yml] 18 | indent_size = 2 19 | indent_style = space 20 | 21 | [*.sol] 22 | indent_size = 4 23 | indent_style = space 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /dist 3 | /node_modules 4 | /src/contractse 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "commonjs": true 6 | }, 7 | "extends": ["airbnb","prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": 2017, 10 | "sourceType": "module" 11 | }, 12 | "globals": { 13 | "assert": true, 14 | "web3": true, 15 | "artifacts": true, 16 | "contract": true 17 | }, 18 | "rules": { 19 | "no-underscore-dangle": "off", 20 | "comma-dangle": 0, 21 | "indent": [ 22 | "error", 23 | 2 24 | ], 25 | "linebreak-style": [ 26 | "error", 27 | "unix" 28 | ], 29 | "quotes": [ 30 | "error", 31 | "single" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ], 37 | "no-unused-expressions": [ 38 | "error", 39 | {"allowTernary": true} 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea 3 | 4 | .DS_Store 5 | 6 | */.DS_Store 7 | */*.DS_Store 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | coverageEnv 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Typescript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | #private-keys.json 70 | test-private-keys.json 71 | 72 | # next.js build output 73 | .next 74 | 75 | # Build 76 | /build/ 77 | 78 | # Solidity-coverage 79 | coverage.json 80 | 81 | reports 82 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 8555, 3 | copyPackages: ['openzeppelin-solidity'], 4 | skipFiles: ['Migrations.sol'], 5 | compileCommand: '../node_modules/.bin/truffle compile', 6 | testCommand: '../node_modules/.bin/truffle test --network coverage', 7 | }; 8 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ], 15 | "arg-overflow": [ 16 | "off" 17 | ], 18 | "no-constant": [ 19 | "error" 20 | ], 21 | "security/enforce-explicit-visibility": [ 22 | "error" 23 | ], 24 | "security/no-block-members": [ 25 | "warning" 26 | ], 27 | "security/no-inline-assembly": [ 28 | "warning" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ConsenSys 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stow Smart Contracts [![NPM Package](https://img.shields.io/npm/v/@stowprotocol/stow-smart-contracts.svg?style=flat-square)](https://www.npmjs.com/package/@stowprotocol/stow-smart-contracts) ![Build Status](https://circleci.com/gh/ConsenSys/Stow-Smart-Contracts.png?circle-token=:circle-token&style=shield) ![Coverage Status](https://codecov.io/gh/ConsenSys/Stow-Smart-Contracts/branch/master/graph/badge.svg) 2 | --- 3 | > :warning: WIP 4 | 5 | Smart Contracts for Stow 6 | 7 | # DEPLOYED CONTACTS 8 | 9 | - Ropsten: [0xfae15fe388a0c0c04d9614f6a8b4f81142bfc87b](https://ropsten.etherscan.io/address/0xfae15fe388a0c0c04d9614f6a8b4f81142bfc87b) 10 | 11 | - All addresses for protocol located: [Here](https://github.com/ConsenSys/stow-addresses) 12 | 13 | # Overview 14 | ## Stow Users 15 | A contract that keeps a registry of registered users and their provenance. 16 | 17 | ## Stow Records 18 | A contract that keeps a registry of metadata of uploaded medical records, as well as the IRIS score of those records. The metadata makes records easily searchable. 19 | 20 | # Recieving Tokens when adding Stow records 21 | 22 | When a person uploads data, 1 Finney STOW token is transferred from the Stow admin pool/acct to the user’s address. 23 | Currently no data validation needed. 24 | 25 | 1 Finney of STOW tokens per upload for now. 26 | 27 | ## Stow Permissions 28 | A contract that keeps a registry of permissions. Permissions include who can view what data, and where the permissioned copy is stored on IPFS. 29 | 30 | ## Stow Overall Architecture 31 | ![Stow architecture](images/stow_architecture_chart.png) 32 | 33 | # Getting started 34 | 35 | ### Prerequisites 36 | * Node.js 37 | * Node Package Manager 38 | 39 | Clone the repository 40 | ``` 41 | $ git clone https://github.com/ConsenSys/Stow-Smart-Contracts.git 42 | ``` 43 | 44 | Install the dependencies 45 | ``` 46 | $ npm install 47 | ``` 48 | 49 | ## Deploying 50 | ``` 51 | npm run migrate 52 | ``` 53 | 54 | ## Testing 55 | To run tests with coverage 56 | ``` 57 | npm run coverage 58 | ``` 59 | 60 | To run tests without coverage 61 | - First start testrpc with `npm start` 62 | - Alternatively you can run Ganache GUI at port 7545 with network id 5777 63 | - Run `npm test` 64 | 65 | ## Video to Help You Get Started 66 | 67 | [![Video to Get You Started with Stow Smart Contract](images/getting-started-with-stow.png)](https://www.youtube.com/watch?v=9RzCvB_Gvvo&t) 68 | 69 | # Contributing 70 | 71 | Please read [CONTRIBUTING.md](https://github.com/ConsenSys/stow-resources/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 72 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public lastCompletedMigration; 7 | 8 | modifier restricted() { 9 | require(msg.sender == owner); 10 | _; 11 | } 12 | 13 | constructor() public { 14 | owner = msg.sender; 15 | } 16 | 17 | function setCompleted(uint completed) public restricted { 18 | lastCompletedMigration = completed; 19 | } 20 | 21 | function upgrade(address newAddress) public restricted { 22 | Migrations upgraded = Migrations(newAddress); 23 | upgraded.setCompleted(lastCompletedMigration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/StowHub.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | 4 | import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | import "./StowUsers.sol"; 7 | import "./StowRecords.sol"; 8 | import "./StowPermissions.sol"; 9 | 10 | 11 | contract StowHub is Ownable, Destructible { 12 | StowUsers public usersContract; 13 | StowRecords public recordsContract; 14 | StowPermissions public permissionsContract; 15 | 16 | event StowUsersContractSet(address from, address to); 17 | event StowRecordsContractSet(address from, address to); 18 | event StowPermissionsContractSet(address from, address to); 19 | 20 | constructor() public { } 21 | 22 | function () public { } 23 | 24 | function setUsersContract(StowUsers _usersContract) 25 | onlyOwner 26 | external 27 | returns (bool) 28 | { 29 | address prev = address(usersContract); 30 | usersContract = _usersContract; 31 | emit StowUsersContractSet(prev, _usersContract); 32 | return true; 33 | } 34 | 35 | function setRecordsContract(StowRecords _recordsContract) 36 | onlyOwner 37 | external 38 | returns (bool) 39 | { 40 | address prev = address(recordsContract); 41 | recordsContract = _recordsContract; 42 | emit StowRecordsContractSet(prev, _recordsContract); 43 | return true; 44 | } 45 | 46 | function setPermissionsContract(StowPermissions _permissionsContract) 47 | onlyOwner 48 | external 49 | returns (bool) 50 | { 51 | address prev = address(permissionsContract); 52 | permissionsContract = _permissionsContract; 53 | emit StowPermissionsContractSet(prev, _permissionsContract); 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/StowPermissions.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | 7 | import "./StowHub.sol"; 8 | import "./StowRecords.sol"; 9 | import "./StowUsers.sol"; 10 | import "./interfaces/PermissionPolicyI.sol"; 11 | 12 | 13 | contract StowPermissions is Ownable, Pausable, Destructible { 14 | struct Permission { 15 | bool canAccess; 16 | // data path of the data, encrypted to the viewer 17 | string dataUri; 18 | } 19 | 20 | event StowAccessGranted(bytes32 indexed dataHash, address indexed owner, 21 | address indexed viewer, address sender 22 | ); 23 | event StowAccessRevoked(bytes32 indexed dataHash, address indexed owner, 24 | address indexed viewer, address sender 25 | ); 26 | event StowPermissionDelegateAdded(address indexed user, address indexed delegate); 27 | 28 | event StowPolicyChecked( 29 | bytes32 indexed dataHash, 30 | string dataUri, 31 | address indexed viewer, 32 | address indexed policy, 33 | bool isOk, 34 | address sender 35 | ); 36 | 37 | StowHub public hub; 38 | // dataHash => viewer => permission mapping 39 | mapping(bytes32 => mapping(address => Permission)) public permissions; 40 | // user => delegate => bool mapping 41 | mapping(address => mapping(address => bool)) public delegates; 42 | 43 | /* Modifiers */ 44 | modifier onlyUser() { 45 | require(hub.usersContract().isUser(msg.sender)); 46 | _; 47 | } 48 | 49 | modifier onlyRecordOwnerOf(bytes32 dataHash, address owner) { 50 | require(hub.recordsContract().recordOwnerOf(dataHash) == owner); 51 | _; 52 | } 53 | 54 | modifier onlyWhenSenderIsDelegate(address owner) { 55 | require(delegates[owner][msg.sender]); 56 | _; 57 | } 58 | 59 | /* Constructor */ 60 | constructor(StowHub _hub) public { 61 | hub = _hub; 62 | } 63 | 64 | /* Fallback function */ 65 | function () public { } 66 | 67 | /* External functions */ 68 | 69 | /// Check if a viewer has access to a record 70 | /// @param dataHash the hash of the unencrypted data 71 | /// @param viewer the address being allowed to view the data 72 | 73 | function checkAccess(bytes32 dataHash, address viewer) 74 | view 75 | external 76 | returns (bool) 77 | { 78 | return permissions[dataHash][viewer].canAccess; 79 | } 80 | 81 | /// Add a delegate for a user's permissions 82 | /// @param delegate the address of the delegate being added by user 83 | function addDelegate(address delegate) 84 | onlyUser 85 | whenNotPaused 86 | external 87 | returns (bool) 88 | { 89 | require(delegate != address(0)); 90 | require(delegate != msg.sender); 91 | delegates[msg.sender][delegate] = true; 92 | emit StowPermissionDelegateAdded(msg.sender, delegate); 93 | return true; 94 | } 95 | 96 | /// Give a viewer access to a stow record 97 | /// Called by owner of the record. 98 | /// @param dataHash the data hash of the stow record 99 | /// @param viewer the user being granted permission to view the data 100 | /// @param dataUri the path of the re-encrypted data 101 | function grantAccess( 102 | bytes32 dataHash, address viewer, string dataUri) 103 | onlyUser 104 | onlyRecordOwnerOf(dataHash, msg.sender) 105 | external 106 | returns (bool) 107 | { 108 | require( 109 | _grantAccess(dataHash, viewer, msg.sender, dataUri) 110 | ); 111 | return true; 112 | } 113 | 114 | /// Give a viewer access to a stow record 115 | /// Called by delegate to the owner of the record. 116 | /// @param dataHash the data hash of the stow record 117 | /// @param viewer the user being permissioned to view the data 118 | /// @param owner the owner of the stow record 119 | /// @param dataUri the path of the re-encrypted data 120 | function grantAccessbyDelegate( 121 | bytes32 dataHash, address viewer, address owner, string dataUri) 122 | onlyWhenSenderIsDelegate(owner) 123 | onlyRecordOwnerOf(dataHash, owner) 124 | external 125 | returns (bool) 126 | { 127 | require( 128 | _grantAccess(dataHash, viewer, owner, dataUri) 129 | ); 130 | return true; 131 | } 132 | 133 | /// Give a viewer access to a stow record 134 | /// Called by owner of the record. 135 | /// @param dataHash the data hash of the stow record 136 | /// @param viewer the user being granted permission to view the data 137 | /// @param dataUri the path of the re-encrypted data 138 | function grantPolicyBasedAccess( 139 | bytes32 dataHash, 140 | address viewer, 141 | string dataUri, 142 | address[] policies) 143 | onlyUser 144 | onlyRecordOwnerOf(dataHash, msg.sender) 145 | external 146 | returns (bool) 147 | { 148 | require(dataHash != 0); 149 | 150 | // check policies and fail on first one that is not ok 151 | for (uint i = 0; i < policies.length; i++) { 152 | address curPolicy = policies[i]; 153 | require(curPolicy != address(0)); 154 | PermissionPolicyI currPolicy = PermissionPolicyI(curPolicy); 155 | bool isOk = currPolicy.checkPolicy(dataHash, viewer, dataUri); 156 | emit StowPolicyChecked(dataHash, dataUri, viewer, curPolicy, isOk, msg.sender); 157 | require(isOk); 158 | } 159 | 160 | require(_grantAccess(dataHash, viewer, msg.sender, dataUri)); 161 | return true; 162 | } 163 | 164 | /// Revoke a viewer access to a stow record 165 | /// Note that this does not necessarily remove the data from storage 166 | /// Called by owner of the record. 167 | /// @param dataHash the data hash of the stow record 168 | /// @param viewer the user that has permission to view the data 169 | function revokeAccess( 170 | bytes32 dataHash, address viewer) 171 | onlyUser 172 | onlyRecordOwnerOf(dataHash, msg.sender) 173 | external 174 | returns (bool) 175 | { 176 | require( 177 | _revokeAccess(dataHash, viewer, msg.sender) 178 | ); 179 | return true; 180 | } 181 | 182 | /// Revoke a viewer access to a stow record 183 | /// Note that this does not necessarily remove the data from storage 184 | /// Called by delegate to the owner of the record. 185 | /// @param dataHash the data hash of the stow record 186 | /// @param viewer the user that has permission to view the data 187 | /// @param owner the owner of the stow record 188 | function revokeAccessbyDelegate( 189 | bytes32 dataHash, address viewer, address owner) 190 | onlyWhenSenderIsDelegate(owner) 191 | onlyRecordOwnerOf(dataHash, owner) 192 | external 193 | returns (bool) 194 | { 195 | require(_revokeAccess(dataHash, viewer, owner)); 196 | return true; 197 | } 198 | 199 | /// Internal function to give a viewer access to a stow record 200 | /// Called by external functions 201 | /// @param dataHash the data hash of the stow record 202 | /// @param viewer the user being permissioned to view the data 203 | /// @param dataUri the data path of the re-encrypted data 204 | function _grantAccess(bytes32 dataHash, address viewer, address owner, string dataUri) 205 | whenNotPaused 206 | internal 207 | returns (bool) 208 | { 209 | // validate input 210 | require(owner != address(0)); 211 | require(viewer != address(0)); 212 | require(bytes(dataUri).length != 0); 213 | 214 | // TODO, Uncomment this to prevent grant access twice, It is commented for testing purposes 215 | // access must not have already been granted 216 | // require(!permissions[dataHash][viewer].canAccess); 217 | permissions[dataHash][viewer] = Permission({ 218 | canAccess: true, 219 | dataUri: dataUri 220 | }); 221 | emit StowAccessGranted(dataHash, owner, viewer, msg.sender); 222 | return true; 223 | } 224 | 225 | /// Internal function to revoke a viewer access to a stow record 226 | /// Called by external functions 227 | /// Note that this does not necessarily remove the data from storage 228 | /// @param dataHash the data hash of the stow record 229 | /// @param viewer the user that has permission to view the data 230 | /// @param owner the owner of the stow record 231 | function _revokeAccess(bytes32 dataHash, address viewer, address owner) 232 | whenNotPaused 233 | internal 234 | returns (bool) 235 | { 236 | require(owner != address(0)); 237 | // access must have already been grated 238 | require(permissions[dataHash][viewer].canAccess); 239 | permissions[dataHash][viewer] = Permission({ 240 | canAccess: false, 241 | dataUri: "" 242 | }); 243 | emit StowAccessRevoked(dataHash, owner, viewer, msg.sender); 244 | return true; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /contracts/StowRecords.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 7 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 8 | import "./StowHub.sol"; 9 | import "./StowUsers.sol"; 10 | import "./interfaces/IrisScoreProviderI.sol"; 11 | 12 | 13 | 14 | contract StowRecords is Ownable, Pausable, Destructible { 15 | using SafeMath for uint; 16 | 17 | // Struct of a stow record 18 | // A stow record is identified by its data hash, which is 19 | // keccak256(data + optional nonce) 20 | struct Record { 21 | // owner of the record 22 | address owner; 23 | // hash of the plaintext metadata 24 | bytes32 metadataHash; 25 | // attester signatures 26 | mapping (address => bool) sigs; 27 | // count of attester sigs 28 | uint sigCount; 29 | // calculated iris score 30 | uint irisScore; 31 | // ipfs path of the encrypted data 32 | string dataUri; 33 | // timestamp of the block when the record is added 34 | uint timestamp; 35 | // non zero score returned from the specific IRIS provider oracles 36 | mapping (address => uint256) irisProvidersReports; 37 | } 38 | 39 | event StowUpdateRecordsIris( 40 | bytes32 indexed dataHash, address indexed irisProvidersAddress, uint256 val, address indexed sender) 41 | ; 42 | 43 | event StowRecordAdded( 44 | bytes32 indexed dataHash, address indexed owner, string metadata 45 | ); 46 | 47 | event StowRecordSigAdded( 48 | bytes32 indexed dataHash, address indexed attester, uint irisScore 49 | ); 50 | 51 | event StowReward(bytes32 indexed dataHash, address indexed owner, uint256 value, address tokenContract); 52 | 53 | StowHub public hub; 54 | // all stow records 55 | // dataHash => record mapping 56 | mapping(bytes32 => Record) public records; 57 | 58 | /* Modifiers */ 59 | 60 | modifier onlyUser() { 61 | require(hub.usersContract().isUser(msg.sender) == true); 62 | _; 63 | } 64 | 65 | modifier hasProvenance(address user) { 66 | require(hub.usersContract().provenanceOf(user) > 0); 67 | _; 68 | } 69 | 70 | /* Constructor */ 71 | constructor(StowHub _hub) public { 72 | hub = _hub; 73 | } 74 | 75 | /* Fallback function */ 76 | function () public { } 77 | 78 | /* External functions */ 79 | 80 | function addRecordByAdmin( 81 | bytes32 dataHash, address owner, address attester, 82 | string metadata, string dataUri) 83 | onlyOwner 84 | whenNotPaused 85 | external 86 | returns (bool) 87 | { 88 | require(_addRecord(dataHash, owner, metadata, dataUri) == true); 89 | if (attester != address(0)) { 90 | require(_addSig(dataHash, attester)); 91 | } 92 | return true; 93 | } 94 | 95 | /// @param dataHash the datahash of the record to be scored 96 | /// @param irisProvidersAddress address of the oracle contract 97 | function updateIris(bytes32 dataHash, address irisProvidersAddress) 98 | external 99 | onlyOwner 100 | whenNotPaused 101 | returns (uint256) 102 | { 103 | require(irisProvidersAddress != address(0)); 104 | require(dataHash != 0); 105 | 106 | Record storage record = records[dataHash]; 107 | require(records[dataHash].timestamp != 0); 108 | 109 | // make sure the irisProviders is only called once 110 | require(record.irisProvidersReports[irisProvidersAddress] == 0); 111 | 112 | IrisScoreProviderI currOracle = IrisScoreProviderI(irisProvidersAddress); 113 | uint256 val = currOracle.report(dataHash); 114 | // zero and less values are reverted 115 | require(val > 0); 116 | record.irisScore = record.irisScore.add(val); 117 | 118 | // keep a record of iris score breakdown 119 | record.irisProvidersReports[irisProvidersAddress] = val; 120 | emit StowUpdateRecordsIris(dataHash, irisProvidersAddress, val, msg.sender); 121 | return val; 122 | } 123 | 124 | function getIrisProvidersReport(bytes32 dataHash, address irisProvider) 125 | external 126 | view 127 | returns (uint256) 128 | { 129 | return records[dataHash].irisProvidersReports[irisProvider]; 130 | } 131 | 132 | /* Public functions */ 133 | 134 | /// Add a record by user without any provider's signatures. 135 | /// @param dataHash the hash of the data 136 | /// @param metadata plaintext metadata for the record 137 | /// @param dataUri the ipfs path of the encrypted data 138 | function addRecord( 139 | bytes32 dataHash, string metadata, string dataUri) 140 | onlyUser 141 | whenNotPaused 142 | public 143 | returns (bool) 144 | { 145 | require( 146 | _addRecord(dataHash, msg.sender, metadata, dataUri) == true 147 | ); 148 | return true; 149 | } 150 | 151 | /// Add a record by user without any provider's signatures and get a reward. 152 | /// 153 | /// @param dataHash the hash of the data 154 | /// @param metadata plaintext metadata for the record 155 | /// @param dataUri the data uri path of the encrypted data 156 | /// @param token the ERC20 token address for the rewarding token 157 | function addRecordwithReward ( 158 | bytes32 dataHash, string metadata, string dataUri, address token) 159 | onlyUser 160 | whenNotPaused 161 | public 162 | returns (bool) 163 | { 164 | // the amount of tokens to be transferred 165 | uint256 reward = 1 finney; 166 | require (token != address (0)); 167 | require (token != address (this)); 168 | ERC20 tokenInstance = ERC20 (token); 169 | require ( 170 | _addRecord (dataHash, msg.sender, metadata, dataUri) == true 171 | ); 172 | // tokens are provided by the contracts balance 173 | require(tokenInstance.transfer (msg.sender, reward)); 174 | emit StowReward (dataHash, msg.sender, reward, token); 175 | return true; 176 | } 177 | 178 | /// Add a record by a data provider. 179 | /// @param dataHash the hash of the data 180 | /// @param owner owner of the record 181 | /// @param metadata plaintext metadata for the record 182 | /// @param dataUri the ipfs path of the encrypted data 183 | function addRecordByProvider( 184 | bytes32 dataHash, address owner, string metadata, string dataUri) 185 | onlyUser 186 | hasProvenance(msg.sender) 187 | whenNotPaused 188 | public 189 | returns (bool) 190 | { 191 | // add the file first 192 | require(_addRecord(dataHash, owner, metadata, dataUri) == true); 193 | // add provider's sig to the file 194 | require(_addSig(dataHash, msg.sender)); 195 | return true; 196 | } 197 | 198 | /// Add a provider's signature to a stow record, 199 | /// i.e. adding an attestation 200 | /// This function is only callable by a provider 201 | /// @param dataHash the data hash of the stow record 202 | function addSigByProvider(bytes32 dataHash) 203 | hasProvenance(msg.sender) 204 | whenNotPaused 205 | public 206 | returns (bool) 207 | { 208 | require(_addSig(dataHash, msg.sender)); 209 | return true; 210 | } 211 | 212 | /// Add a provider's signature to a stow record 213 | /// i.e. adding an attestation 214 | /// This function can be called by anyone. As long as the signatures are 215 | /// indeed from a provider, the sig will be added to the record. 216 | /// The signature should cover the root hash, which is 217 | /// hash(hash(data), hash(metadata)) 218 | /// @param dataHash the data hash of a stow record 219 | /// @param r signature: R 220 | /// @param s signature: S 221 | /// @param v signature: V 222 | function addSig(bytes32 dataHash, bytes32 r, bytes32 s, uint8 v) 223 | public 224 | whenNotPaused 225 | returns (bool) 226 | { 227 | // find the root hash of the record 228 | bytes32 rootHash = rootHashOf(dataHash); 229 | // recover the provider's address from signature 230 | address provider = recover(rootHash, r, s, v); 231 | // add sig 232 | require(_addSig(dataHash, provider)); 233 | return true; 234 | } 235 | 236 | function recover(bytes32 message, bytes32 r, bytes32 s, uint8 v) 237 | public pure returns (address) 238 | { 239 | bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 240 | bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, message)); 241 | return ecrecover(prefixedHash, v, r, s); 242 | } 243 | 244 | function recordOwnerOf(bytes32 dataHash) 245 | public view returns (address) 246 | { 247 | return records[dataHash].owner; 248 | } 249 | 250 | function rootHashOf(bytes32 dataHash) 251 | public view returns (bytes32) 252 | { 253 | return keccak256(abi.encodePacked(dataHash, records[dataHash].metadataHash)); 254 | } 255 | 256 | function sigExists(bytes32 dataHash, address provider) 257 | public view returns (bool) 258 | { 259 | return records[dataHash].sigs[provider]; 260 | } 261 | 262 | /* Internal functions */ 263 | 264 | function _addRecord( 265 | bytes32 dataHash, address owner, string metadata, string dataUri) 266 | internal 267 | returns (bool) 268 | { 269 | // validate input 270 | require(dataHash != 0); 271 | require(bytes(dataUri).length != 0); 272 | bytes32 metadataHash = keccak256(abi.encodePacked(metadata)); 273 | 274 | // the file must be new 275 | require(records[dataHash].timestamp == 0); 276 | // verify owner 277 | require(hub.usersContract().isUser(owner) == true); 278 | // add record 279 | records[dataHash] = Record({ 280 | owner: owner, 281 | metadataHash: metadataHash, 282 | sigCount: 0, 283 | irisScore: 0, 284 | dataUri: dataUri, 285 | // solium-disable-next-line security/no-block-members 286 | timestamp: block.timestamp 287 | }); 288 | // emit event 289 | emit StowRecordAdded(dataHash, owner, metadata); 290 | return true; 291 | } 292 | 293 | function _addSig(bytes32 dataHash, address provider) 294 | hasProvenance(provider) 295 | internal 296 | returns (bool) 297 | { 298 | Record storage record = records[dataHash]; 299 | // the file must exist 300 | require(record.timestamp != 0); 301 | // the provider must not have signed the file already 302 | require(!record.sigs[provider]); 303 | uint provenanceScore = hub.usersContract().provenanceOf(provider); 304 | // add signature 305 | record.sigCount = record.sigCount.add(1); 306 | record.sigs[provider] = true; 307 | // update iris score 308 | record.irisScore = record.irisScore.add(provenanceScore); 309 | // emit event 310 | emit StowRecordSigAdded(dataHash, provider, record.irisScore); 311 | return true; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /contracts/StowUsers.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | import "./StowWhitelistI.sol"; 7 | import "./StowHub.sol"; 8 | 9 | 10 | contract StowUsers is Ownable, Pausable, Destructible { 11 | struct User { 12 | bool exists; 13 | uint registerBlocktime; 14 | uint provenance; 15 | } 16 | 17 | event StowUserRegistered(address indexed user); 18 | event StowProvenanceChanged(address indexed user, uint provenance); 19 | event StowWhitelistScoreAdded(address indexed whitelist); 20 | 21 | StowHub public hub; 22 | mapping(address => User) public users; 23 | 24 | constructor(StowHub _hub) public { 25 | hub = _hub; 26 | } 27 | 28 | /* Fallback function */ 29 | function () public { } 30 | 31 | /* External functions */ 32 | 33 | // register allows any user to self register on Stow 34 | function register() 35 | whenNotPaused 36 | external 37 | returns (bool) 38 | { 39 | require(!isUser(msg.sender)); 40 | users[msg.sender] = User({ 41 | exists: true, 42 | registerBlocktime: block.number, 43 | provenance: 0 44 | }); 45 | emit StowUserRegistered(msg.sender); 46 | return true; 47 | } 48 | 49 | // setExpertScore allows admin to set the expert score of a user from a trusted third party 50 | function setExpertScore(StowWhitelistI whitelist, address user) 51 | onlyOwner 52 | external 53 | returns (bool) 54 | { 55 | uint score = whitelist.expertScoreOf(user); 56 | setProvenance(user, score); 57 | emit StowWhitelistScoreAdded(whitelist); 58 | return true; 59 | } 60 | 61 | /* Public functions */ 62 | 63 | // setProvenance allows admin to set the provenance of a user 64 | function setProvenance(address user, uint provenance) 65 | onlyOwner 66 | public 67 | returns (bool) 68 | { 69 | require(isUser(user)); 70 | users[user].provenance = provenance; 71 | emit StowProvenanceChanged(user, provenance); 72 | return true; 73 | } 74 | 75 | function isUser(address user) 76 | public 77 | view 78 | returns (bool) 79 | { 80 | return users[user].exists; 81 | } 82 | 83 | function provenanceOf(address user) 84 | public 85 | view 86 | returns (uint) 87 | { 88 | if (users[user].exists) { 89 | return users[user].provenance; 90 | } else { 91 | return 0; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/StowWhitelistI.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | 4 | contract StowWhitelistI { 5 | 6 | event LogExpertScoreUpdated(address indexed user, uint indexed score); 7 | 8 | mapping(address => uint) public expertScores; 9 | 10 | function expertScoreOf(address user) public view returns (uint); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/IrisScoreProviderI.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | 4 | contract /* interface */ IrisScoreProviderI { 5 | 6 | /// report the IRIS score for the dasaHash records 7 | /// @param dataHash the hash of the data to be scored 8 | function report(bytes32 dataHash) 9 | public 10 | view 11 | returns (uint256); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/PermissionPolicyI.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | 4 | contract /* interface */ PermissionPolicyI { 5 | 6 | /// check permission policy then return true if condition are met 7 | /// @param viewer the user being granted permission to view the data 8 | /// @param dataUri the path of the re-encrypted data 9 | /// @param dataHash the hash of the data to be scored 10 | function checkPolicy(bytes32 dataHash, address viewer, string dataUri) 11 | view 12 | external 13 | returns (bool); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/mock/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 5 | 6 | /** 7 | * @title Mock Test Token Contract 8 | * @dev Based on ERC20 standard 9 | * @dev Following https://consensys.github.io/smart-contract-best-practices/tokens/ 10 | */ 11 | 12 | 13 | contract ERC20Mock is PausableToken, DetailedERC20 { 14 | 15 | string public name = "Test TOKEN"; 16 | string public symbol = "TEST"; 17 | uint8 public decimals = 18; 18 | /*------------------------------------constructor------------------------------------*/ 19 | /** 20 | * @dev constructor for mock token 21 | */ 22 | constructor() 23 | DetailedERC20(name, symbol, decimals) 24 | public { 25 | paused = true; 26 | totalSupply_ = 10 ** 27; 27 | balances[msg.sender] = totalSupply_; 28 | emit Transfer(address(0), msg.sender, balances[msg.sender]); 29 | } 30 | 31 | /** 32 | * @dev override Transfer token for a specified address 33 | * @dev Prevent transferring tokens to the contract address 34 | * @param to The address to transfer to. 35 | * @param value The amount to be transferred. 36 | */ 37 | function transfer(address to, uint256 value) public returns (bool) { 38 | super.transfer(to, value); 39 | return true; 40 | } 41 | 42 | /** 43 | * @dev override Transfer tokens from one address to another 44 | * @dev Prevent transferring tokens to the contract address 45 | * @param from address The address which you want to send tokens from 46 | * @param to address The address which you want to transfer to 47 | * @param value uint256 the amount of tokens to be transferred 48 | */ 49 | function transferFrom( 50 | address from, 51 | address to, 52 | uint256 value 53 | ) 54 | public returns (bool) 55 | { 56 | require(to != address(this), "attempt to send to contact address."); 57 | super.transferFrom(from, to, value); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/mock/IrisScoreProviderMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "../interfaces/IrisScoreProviderI.sol"; 4 | 5 | 6 | contract IrisScoreProviderMock is IrisScoreProviderI{ 7 | 8 | uint256 public val = 42; 9 | 10 | function report(bytes32 dataHash) public view returns (uint256) { 11 | require(dataHash != 0); 12 | return val; 13 | } 14 | 15 | function setVal(uint256 newVale) public { 16 | val = newVale; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/mock/PermissionPolicyMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | import "../interfaces/PermissionPolicyI.sol"; 4 | 5 | 6 | contract /* interface */ PermissionPolicyMock is PermissionPolicyI { 7 | 8 | bool public result = true; 9 | 10 | /// @param viewer the user being granted permission to view the data 11 | /// @param dataUri the path of the re-encrypted data 12 | /// @param dataHash the hash of the data to be scored 13 | function checkPolicy(bytes32 dataHash, address viewer, string dataUri) 14 | view 15 | external 16 | returns (bool) 17 | { 18 | return result; 19 | } 20 | 21 | function setVal(bool newResult) public { 22 | result = newResult; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/mock/WhitelistMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../StowWhitelistI.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | 7 | 8 | contract WhitelistMock is StowWhitelistI, Ownable, Destructible { 9 | 10 | function updateScore(address user, uint score) 11 | public 12 | onlyOwner 13 | returns(bool) 14 | { 15 | expertScores[user] = score; 16 | emit LogExpertScoreUpdated(user, score); 17 | return true; 18 | } 19 | 20 | function expertScoreOf(address user) 21 | public 22 | view 23 | returns(uint) 24 | { 25 | return expertScores[user]; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /images/getting-started-with-stow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConsenSysMesh/Stow-Smart-Contracts/865da5fa2eee8b881dbd88bd47d1f6de70b75efd/images/getting-started-with-stow.png -------------------------------------------------------------------------------- /images/linnia_architecture_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConsenSysMesh/Stow-Smart-Contracts/865da5fa2eee8b881dbd88bd47d1f6de70b75efd/images/linnia_architecture_chart.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("./Migrations.sol") 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const StowHub = artifacts.require("./StowHub.sol"); 2 | const StowUsers = artifacts.require("./StowUsers.sol"); 3 | const StowRecords = artifacts.require("./StowRecords.sol"); 4 | const StowPermissions = artifacts.require("./StowPermissions.sol"); 5 | 6 | module.exports = (deployer, network, accounts) => { 7 | const adminAddress = accounts[0]; 8 | let hubInstance; 9 | // deploy the hub 10 | deployer.deploy(StowHub).then(() => { 11 | return StowHub.deployed() 12 | }).then((_hubInstace) => { 13 | hubInstance = _hubInstace 14 | // deploy Users 15 | return deployer.deploy(StowUsers, hubInstance.address) 16 | }).then(() => { 17 | // deploy Records 18 | return deployer.deploy(StowRecords, hubInstance.address) 19 | }).then(() => { 20 | // deploy Permissions 21 | return deployer.deploy(StowPermissions, hubInstance.address) 22 | }).then(() => { 23 | // set all the addresses in the hub 24 | return hubInstance.setUsersContract(StowUsers.address) 25 | }).then(() => { 26 | return hubInstance.setRecordsContract(StowRecords.address) 27 | }).then(() => { 28 | return hubInstance.setPermissionsContract(StowPermissions.address) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stowprotocol/stow-smart-contracts", 3 | "version": "0.1.9", 4 | "description": "Stow Smart Contracts", 5 | "main": "truffle-config.js", 6 | "files": [ 7 | "build", 8 | "contracts" 9 | ], 10 | "scripts": { 11 | "start": "npm run start:testrpc", 12 | "start:testrpc": "ganache-cli -p 7545 -i 5777 -a 61 -m 'drastic outer student holiday hour ordinary reduce absurd clever govern ensure merry keen sound mountain'", 13 | "migrate": "truffle migrate", 14 | "migrate:dev": "truffle migrate --reset && node testData/index.js", 15 | "migrate:ropsten": "truffle migrate --network ropsten && node testData/index.js", 16 | "test": "truffle test", 17 | "lint:sol": "solium -d contracts", 18 | "coverage": "solidity-coverage", 19 | "lint:tests": "eslint test --format junit -o reports/junit/js-lint-results.xml" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/ConsenSys/Stow-Smart-Contracts.git" 24 | }, 25 | "keywords": [ 26 | "Stow", 27 | "Ethereum", 28 | "Smart Contracts", 29 | "Solidity" 30 | ], 31 | "license": "Apache-2.0", 32 | "bugs": { 33 | "url": "https://github.com/ConsenSys/Stow-Smart-Contracts/issues" 34 | }, 35 | "homepage": "https://stow-protocol.com", 36 | "devDependencies": { 37 | "babel-polyfill": "^6.26.0", 38 | "babel-preset-es2015": "^6.24.1", 39 | "babel-preset-stage-2": "^6.24.1", 40 | "babel-preset-stage-3": "^6.24.1", 41 | "babel-register": "^6.26.0", 42 | "eslint": "^5.0.1", 43 | "eslint-config-airbnb": "^17.1.0", 44 | "eslint-config-prettier": "^2.10.0", 45 | "eslint-loader": "^2.1.1", 46 | "eslint-plugin-import": "^2.14.0", 47 | "eslint-plugin-jsx-a11y": "^6.1.1", 48 | "eslint-plugin-react": "^7.11.0", 49 | "ethereumjs-util": "5.2.0", 50 | "ganache-cli": "^6.0.3", 51 | "multihashes": "^0.4.13", 52 | "solidity-coverage": "0.5.2", 53 | "solium": "1.1.8", 54 | "truffle": "4.1.11" 55 | }, 56 | "dependencies": { 57 | "@linniaprotocol/linnia-js": "^0.3.0", 58 | "dotenv": "^6.0.0", 59 | "ipfs-mini": "^1.1.2", 60 | "openzeppelin-solidity": "1.10.0", 61 | "truffle-hdwallet-provider": "git+https://github.com/vrotmanh/truffle-hdwallet-provider", 62 | "truffle-hdwallet-provider-privkey": "^0.2.0", 63 | "web3": "^1.0.0-beta.34" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/1_hub_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const StowHub = artifacts.require('./StowHub.sol'); 4 | 5 | contract('StowHub', accounts => { 6 | let instance; 7 | beforeEach('deploy a new StowHub contract', async () => { 8 | instance = await StowHub.new(); 9 | }); 10 | 11 | describe('constructor', () => { 12 | it('should set admin correctly', async () => { 13 | const newInstance = await StowHub.new(); 14 | assert.equal(await newInstance.owner(), accounts[0]); 15 | }); 16 | it('should initialize users, records addresss to zero', async () => { 17 | assert.equal(await instance.usersContract(), 0); 18 | assert.equal(await instance.recordsContract(), 0); 19 | }); 20 | }); 21 | describe('set users contract', () => { 22 | it('should allow admin to set Users address', async () => { 23 | const tx = await instance.setUsersContract(42); 24 | assert.equal(tx.logs[0].event, 'StowUsersContractSet'); 25 | assert.equal(tx.logs[0].args.from, 0); 26 | assert.equal(tx.logs[0].args.to, 42); 27 | assert.equal(await instance.usersContract(), 42); 28 | }); 29 | it('should not allow non-admin to set Users address', async () => { 30 | await assertRevert(instance.setUsersContract(42, { from: accounts[1] })); 31 | }); 32 | }); 33 | describe('set Records contract', () => { 34 | it('should allow admin to set Records address', async () => { 35 | const tx = await instance.setRecordsContract(42); 36 | assert.equal(tx.logs[0].event, 'StowRecordsContractSet'); 37 | assert.equal(tx.logs[0].args.from, 0); 38 | assert.equal(tx.logs[0].args.to, 42); 39 | assert.equal(await instance.recordsContract(), 42); 40 | }); 41 | it('should not allow non-admin to set Records address', async () => { 42 | await assertRevert( 43 | instance.setRecordsContract(42, { from: accounts[1] }) 44 | ); 45 | }); 46 | }); 47 | describe('set Permissions contract', () => { 48 | it('should allow admin to set Permissions address', async () => { 49 | const tx = await instance.setPermissionsContract(42); 50 | assert.equal(tx.logs[0].event, 'StowPermissionsContractSet'); 51 | assert.equal(tx.logs[0].args.from, 0); 52 | assert.equal(tx.logs[0].args.to, 42); 53 | assert.equal(await instance.permissionsContract(), 42); 54 | }); 55 | it('should not allow non-admin to set Permissions address', async () => { 56 | await assertRevert( 57 | instance.setPermissionsContract(42, { from: accounts[1] }) 58 | ); 59 | }); 60 | }); 61 | // copy paste from records contract 62 | describe('destructible', () => { 63 | it('should not allow non-admin to destroy', async () => { 64 | await assertRevert(instance.destroy({ from: accounts[1] })); 65 | }); 66 | it('should allow admin to destroy', async () => { 67 | const admin = accounts[0]; 68 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 69 | const tx = await instance.destroy({ from: admin }); 70 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 71 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 72 | }); 73 | it('should allow admin to destroyAndSend', async () => { 74 | const admin = accounts[0]; 75 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 76 | const tx = await instance.destroyAndSend(admin, { from: admin }); 77 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 78 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 79 | assert.equal(web3.eth.getBalance(instance.address).toNumber(), 0); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/2_users_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const StowHub = artifacts.require('./StowHub.sol'); 4 | const StowUsers = artifacts.require('./StowUsers.sol'); 5 | 6 | contract('StowUsers', accounts => { 7 | let hub; 8 | let instance; 9 | before('set up a StowHub contract', async () => { 10 | hub = await StowHub.new(); 11 | }); 12 | beforeEach('deploy a new StowUsers contract', async () => { 13 | instance = await StowUsers.new(hub.address); 14 | await hub.setUsersContract(instance.address); 15 | }); 16 | describe('constructor', () => { 17 | it('should set the deployer as admin', async () => { 18 | const newInstance = await StowUsers.new(hub.address); 19 | assert.equal(await newInstance.owner(), accounts[0]); 20 | }); 21 | it('should set hub address correctly', async () => { 22 | const newInstance = await StowUsers.new(hub.address); 23 | assert.equal(await newInstance.hub(), hub.address); 24 | }); 25 | }); 26 | describe('change admin', () => { 27 | it('should allow admin to change admin', async () => { 28 | const newInstance = await StowUsers.new(hub.address); 29 | await newInstance.transferOwnership(accounts[1], { from: accounts[0] }); 30 | assert.equal(await newInstance.owner(), accounts[1]); 31 | }); 32 | it('should not allow non admin to change admin', async () => { 33 | const newInstance = await StowUsers.new(hub.address); 34 | await assertRevert( 35 | newInstance.transferOwnership(accounts[1], { 36 | from: accounts[1] 37 | }) 38 | ); 39 | }); 40 | }); 41 | describe('register', () => { 42 | it('should allow user to self register', async () => { 43 | const tx = await instance.register({ from: accounts[1] }); 44 | assert.equal(tx.logs[0].event, 'StowUserRegistered'); 45 | assert.equal(tx.logs[0].args.user, accounts[1]); 46 | 47 | const storedUser = await instance.users(accounts[1]); 48 | assert.equal(storedUser[0], true); 49 | assert.equal(storedUser[1], tx.receipt.blockNumber); 50 | assert.equal(storedUser[2], 0); 51 | }); 52 | it('should not allow a user to self register as twice', async () => { 53 | const tx = await instance.register({ from: accounts[1] }); 54 | assert.equal(tx.logs[0].args.user, accounts[1]); 55 | await assertRevert(instance.register({ from: accounts[1] })); 56 | }); 57 | }); 58 | describe('set provenance', () => { 59 | it('should allow admin to set provenance of a user', async () => { 60 | // register a user first 61 | await instance.register({ from: accounts[1] }); 62 | // set provenance 63 | const tx = await instance.setProvenance(accounts[1], 42, { 64 | from: accounts[0] 65 | }); 66 | // check logs 67 | assert.equal(tx.logs[0].event, 'StowProvenanceChanged'); 68 | assert.equal(tx.logs[0].args.user, accounts[1]); 69 | assert.equal(tx.logs[0].args.provenance, 42); 70 | // check state 71 | assert.equal((await instance.users(accounts[1]))[2], 42); 72 | }); 73 | it('should not allow non admin to change provenance', async () => { 74 | await instance.register({ from: accounts[1] }); 75 | await assertRevert( 76 | instance.setProvenance(accounts[1], 42, { from: accounts[1] }) 77 | ); 78 | }); 79 | it( 80 | 'should not allow admin to change provenance of nonexistent provider', 81 | async () => { 82 | await assertRevert(instance.setProvenance(accounts[1], 42)); 83 | } 84 | ); 85 | }); 86 | describe('is user', () => { 87 | it('should return true if user is registered', async () => { 88 | await instance.register({ from: accounts[1] }); 89 | assert.equal(await instance.isUser(accounts[1]), true); 90 | }); 91 | it('should return false if user is not registered', async () => { 92 | assert.equal(await instance.isUser(accounts[1]), false); 93 | }); 94 | }); 95 | describe('provenance score', () => { 96 | it('should return the provenance score of a user', async () => { 97 | await instance.register({ from: accounts[1] }); 98 | await instance.setProvenance(accounts[1], 42, { from: accounts[0] }); 99 | assert.equal((await instance.provenanceOf(accounts[1])).toString(), '42'); 100 | }); 101 | it('should return 0 if user isn\'t registered', async () => { 102 | assert.equal(await instance.provenanceOf(accounts[1]), 0); 103 | }); 104 | }); 105 | describe('pausable', () => { 106 | it('should not allow non-admin to pause or unpause', async () => { 107 | await assertRevert(instance.pause({ from: accounts[1] })); 108 | await assertRevert(instance.unpause({ from: accounts[1] })); 109 | }); 110 | it('should not allow register of users when paused by admin', async () => { 111 | const tx = await instance.pause(); 112 | assert.equal(await instance.isUser(accounts[1]), false); 113 | assert.equal(tx.logs[0].event, 'Pause'); 114 | await assertRevert(instance.register({ from: accounts[1] })); 115 | const tx2 = await instance.unpause(); 116 | assert.equal(tx2.logs[0].event, 'Unpause'); 117 | const tx3 = await instance.register({ from: accounts[1] }); 118 | assert.equal(tx3.logs[0].event, 'StowUserRegistered'); 119 | }); 120 | }); 121 | // copy paste from records contract 122 | describe('destructible', () => { 123 | it('should not allow non-admin to destroy', async () => { 124 | await assertRevert(instance.destroy({ from: accounts[1] })); 125 | }); 126 | it('should allow admin to destroy', async () => { 127 | const admin = accounts[0]; 128 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 129 | const tx = await instance.destroy({ from: admin }); 130 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 131 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 132 | }); 133 | it('should allow admin to destroyAndSend', async () => { 134 | const admin = accounts[0]; 135 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 136 | const tx = await instance.destroyAndSend(admin, { from: admin }); 137 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 138 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 139 | assert.equal(web3.eth.getBalance(instance.address).toNumber(), 0); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/3_records_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const StowHub = artifacts.require('./StowHub.sol'); 4 | const StowUsers = artifacts.require('./StowUsers.sol'); 5 | const StowRecords = artifacts.require('./StowRecords.sol'); 6 | const irisScoreProvider = artifacts.require('./mock/IrisScoreProviderMock.sol'); 7 | 8 | const crypto = require('crypto'); 9 | const eutil = require('ethereumjs-util'); 10 | 11 | let irisScoreProviderContractAddress; 12 | let irisScoreProviderInstance; 13 | 14 | const testDataContent = '{"foo":"bar","baz":42}'; 15 | const testDataHash = eutil.bufferToHex(eutil.sha3(testDataContent)); 16 | const testDataUri = 'QmUMqi1rr4Ad1eZ3ctsRUEmqK2U3CyZqpetUe51LB9GiAM'; 17 | const testMetadata = 'KEYWORDS'; 18 | const testMetaHash = eutil.bufferToHex(eutil.sha3(testMetadata)); 19 | const testRootHash = eutil.bufferToHex( 20 | eutil.sha3( 21 | Buffer.concat([eutil.sha3(testDataContent), eutil.sha3(testMetadata)]) 22 | ) 23 | ); 24 | 25 | contract('StowRecords', accounts => { 26 | const admin = accounts[0]; 27 | const user = accounts[1]; 28 | const provider1 = accounts[2]; 29 | const provider2 = accounts[3]; 30 | const nonUser = accounts[4]; 31 | let hub; 32 | let instance; 33 | 34 | before('set up a StowHub contract', async () => { 35 | hub = await StowHub.new(); 36 | }); 37 | before('set up a StowUsers contract', async () => { 38 | const usersInstance = await StowUsers.new(hub.address); 39 | await hub.setUsersContract(usersInstance.address); 40 | usersInstance.register({ from: user }); 41 | usersInstance.register({ from: provider1 }); 42 | usersInstance.register({ from: provider2 }); 43 | usersInstance.setProvenance(provider1, 1); 44 | usersInstance.setProvenance(provider2, 2); 45 | }); 46 | beforeEach('deploy a new StowRecords contract', async () => { 47 | instance = await StowRecords.new(hub.address); 48 | await hub.setRecordsContract(instance.address); 49 | }); 50 | describe('constructor', () => { 51 | it('should set hub address correctly', async () => { 52 | const newInstance = await StowRecords.new(hub.address); 53 | assert.equal(await newInstance.hub(), hub.address); 54 | }); 55 | }); 56 | describe('recover', () => { 57 | it('should recover the signer address if sig is valid', async () => { 58 | const msgHash = eutil.bufferToHex(eutil.sha3(crypto.randomBytes(2000))); 59 | const rsv = eutil.fromRpcSig(web3.eth.sign(provider1, msgHash)); 60 | const recoveredAddr = await instance.recover( 61 | msgHash, 62 | eutil.bufferToHex(rsv.r), 63 | eutil.bufferToHex(rsv.s), 64 | rsv.v 65 | ); 66 | assert.equal(recoveredAddr, provider1); 67 | }); 68 | it('should recover zero address if sig is bad', async () => { 69 | const msgHash = eutil.bufferToHex(eutil.sha3(crypto.randomBytes(2000))); 70 | const recoveredAddr = await instance.recover( 71 | msgHash, 72 | eutil.bufferToHex(Buffer.from(new Uint32Array(64))), 73 | eutil.bufferToHex(Buffer.from(new Uint32Array(64))), 74 | 27 75 | ); 76 | assert.equal(recoveredAddr, 0); 77 | }); 78 | }); 79 | describe('add record by user', () => { 80 | it('should allow a user to add a record', async () => { 81 | const tx = await instance.addRecord( 82 | testDataHash, 83 | testMetadata, 84 | testDataUri, 85 | { from: user } 86 | ); 87 | assert.equal(tx.logs.length, 1); 88 | assert.equal(tx.logs[0].event, 'StowRecordAdded'); 89 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 90 | assert.equal(tx.logs[0].args.owner, user); 91 | assert.equal(tx.logs[0].args.metadata, testMetadata); 92 | const { timestamp } = web3.eth.getBlock(tx.receipt.blockNumber); 93 | // check state 94 | const storedRecord = await instance.records(testDataHash); 95 | assert.equal(storedRecord[0], user); 96 | assert.equal(storedRecord[1], testMetaHash); 97 | assert.equal(storedRecord[2], 0); // sig count 98 | assert.equal(storedRecord[3], 0); // iris score 99 | assert.equal(storedRecord[4], testDataUri); 100 | assert.equal(storedRecord[5], timestamp); 101 | }); 102 | it('should not allow user to add same record twice', async () => { 103 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 104 | from: user 105 | }); 106 | // try submitting the file again 107 | await assertRevert( 108 | instance.addRecord(testDataHash, testMetadata, testDataUri, { 109 | from: user 110 | }) 111 | ); 112 | }); 113 | it('should not allow non-users to call', async () => { 114 | await assertRevert( 115 | instance.addRecord(testDataHash, testMetadata, testDataUri, { 116 | from: nonUser 117 | }) 118 | ); 119 | }); 120 | it('should reject if data hash or data uri is zero', async () => { 121 | // try zero data hash 122 | await assertRevert( 123 | instance.addRecord(0, testMetadata, testDataUri, { 124 | from: user 125 | }) 126 | ); 127 | // try zero data uri 128 | await assertRevert( 129 | instance.addRecord(testDataHash, testMetadata, 0, { 130 | from: user 131 | }) 132 | ); 133 | }); 134 | it('should allow a long dataUri', async () => { 135 | const testLongDataUri = eutil.bufferToHex( 136 | 'https://www.centralService.com/cloud/storage/v1/b/example-bucket/o/foo%2f%3fbar' 137 | ); 138 | const tx = await instance.addRecord( 139 | testDataHash, 140 | testMetadata, 141 | testLongDataUri, 142 | { from: user } 143 | ); 144 | assert.equal(tx.logs.length, 1); 145 | // check state 146 | const storedRecord = await instance.records(testDataHash); 147 | assert.equal(storedRecord[4], testLongDataUri); 148 | }); 149 | }); 150 | describe('add record by provider', () => { 151 | it('should allow a provider to add a record', async () => { 152 | const tx = await instance.addRecordByProvider( 153 | testDataHash, 154 | user, 155 | testMetadata, 156 | testDataUri, 157 | { from: provider1 } 158 | ); 159 | assert.equal(tx.logs.length, 2); 160 | assert.equal(tx.logs[0].event, 'StowRecordAdded'); 161 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 162 | assert.equal(tx.logs[0].args.owner, user); 163 | assert.equal(tx.logs[0].args.metadata, testMetadata); 164 | assert.equal(tx.logs[1].event, 'StowRecordSigAdded'); 165 | assert.equal(tx.logs[1].args.dataHash, testDataHash); 166 | assert.equal(tx.logs[1].args.attester, provider1); 167 | assert.equal(tx.logs[1].args.irisScore, 1); 168 | const { timestamp } = web3.eth.getBlock(tx.receipt.blockNumber); 169 | // check state 170 | const storedRecord = await instance.records(testDataHash); 171 | assert.equal(storedRecord[0], user); 172 | assert.equal(storedRecord[1], testMetaHash); 173 | assert.equal(storedRecord[2], 1); // sig count 174 | assert.equal(storedRecord[3], 1); // iris score 175 | assert.equal(storedRecord[4], testDataUri); 176 | assert.equal(storedRecord[5], timestamp); 177 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 178 | }); 179 | it('should not allow provider to add a record twice', async () => { 180 | await instance.addRecordByProvider( 181 | testDataHash, 182 | user, 183 | testMetadata, 184 | testDataUri, 185 | { from: provider1 } 186 | ); 187 | await assertRevert( 188 | instance.addRecordByProvider( 189 | testDataHash, 190 | user, 191 | testMetadata, 192 | testDataUri, 193 | { from: provider1 } 194 | ) 195 | ); 196 | }); 197 | it('should not allow provider to add a record for non-user', async () => { 198 | await assertRevert( 199 | instance.addRecordByProvider( 200 | testDataHash, 201 | nonUser, 202 | testMetadata, 203 | testDataUri, 204 | { from: provider1 } 205 | ) 206 | ); 207 | }); 208 | it('should not allow non-provider to call', async () => { 209 | await assertRevert( 210 | instance.addRecordByProvider( 211 | testDataHash, 212 | user, 213 | testMetadata, 214 | testDataUri, 215 | { from: user } 216 | ) 217 | ); 218 | }); 219 | }); 220 | describe('add signature', () => { 221 | it('should allow adding valid signature', async () => { 222 | // add a file without any sig, by user 223 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 224 | from: user 225 | }); 226 | // have provider1 sign the root hash 227 | const rsv = eutil.fromRpcSig(web3.eth.sign(provider1, testRootHash)); 228 | // anyone should be able to submit the signature 229 | const tx = await instance.addSig( 230 | testDataHash, 231 | eutil.bufferToHex(rsv.r), 232 | eutil.bufferToHex(rsv.s), 233 | rsv.v, 234 | { from: nonUser } 235 | ); 236 | assert.equal(tx.logs.length, 1); 237 | assert.equal(tx.logs[0].event, 'StowRecordSigAdded'); 238 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 239 | assert.equal(tx.logs[0].args.attester, provider1); 240 | assert.equal(tx.logs[0].args.irisScore, 1); 241 | // check state 242 | const storedRecord = await instance.records(testDataHash); 243 | assert.equal(storedRecord[2], 1); // sig count 244 | assert.equal(storedRecord[3], 1); // iris score 245 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 246 | }); 247 | it('should allow adding valid signature by provider', async () => { 248 | // add a file without any sig, by user 249 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 250 | from: user 251 | }); 252 | // have provider1 sign it 253 | const tx = await instance.addSigByProvider(testDataHash, { 254 | from: provider1 255 | }); 256 | assert.equal(tx.logs.length, 1); 257 | assert.equal(tx.logs[0].event, 'StowRecordSigAdded'); 258 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 259 | assert.equal(tx.logs[0].args.attester, provider1); 260 | assert.equal(tx.logs[0].args.irisScore, 1); 261 | // check state 262 | const storedRecord = await instance.records(testDataHash); 263 | assert.equal(storedRecord[2], 1); // sig count 264 | assert.equal(storedRecord[3], 1); // iris score 265 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 266 | }); 267 | it('should not allow adding the same sig twice', async () => { 268 | // add a file without any sig 269 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 270 | from: user 271 | }); 272 | // have provider1 sign it 273 | const rsv = eutil.fromRpcSig(web3.eth.sign(provider1, testRootHash)); 274 | await instance.addSig( 275 | testDataHash, 276 | eutil.bufferToHex(rsv.r), 277 | eutil.bufferToHex(rsv.s), 278 | rsv.v, 279 | { from: nonUser } 280 | ); 281 | await assertRevert( 282 | instance.addSig( 283 | testDataHash, 284 | eutil.bufferToHex(rsv.r), 285 | eutil.bufferToHex(rsv.s), 286 | rsv.v, 287 | { from: nonUser } 288 | ) 289 | ); 290 | }); 291 | it('should allow adding sigs from different providers', async () => { 292 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 293 | from: user 294 | }); 295 | // have provider1 sign it 296 | const rsv1 = eutil.fromRpcSig(web3.eth.sign(provider1, testRootHash)); 297 | const tx1 = await instance.addSig( 298 | testDataHash, 299 | eutil.bufferToHex(rsv1.r), 300 | eutil.bufferToHex(rsv1.s), 301 | rsv1.v, 302 | { from: nonUser } 303 | ); 304 | // check log 305 | assert.equal(tx1.logs.length, 1); 306 | assert.equal(tx1.logs[0].event, 'StowRecordSigAdded'); 307 | assert.equal(tx1.logs[0].args.dataHash, testDataHash); 308 | assert.equal(tx1.logs[0].args.attester, provider1); 309 | assert.equal(tx1.logs[0].args.irisScore, 1); 310 | // have provider2 sign it 311 | const rsv2 = eutil.fromRpcSig(web3.eth.sign(provider2, testRootHash)); 312 | const tx2 = await instance.addSig( 313 | testDataHash, 314 | eutil.bufferToHex(rsv2.r), 315 | eutil.bufferToHex(rsv2.s), 316 | rsv2.v, 317 | { from: nonUser } 318 | ); 319 | // check log 320 | assert.equal(tx2.logs.length, 1); 321 | assert.equal(tx2.logs[0].event, 'StowRecordSigAdded'); 322 | assert.equal(tx2.logs[0].args.dataHash, testDataHash); 323 | assert.equal(tx2.logs[0].args.attester, provider2); 324 | assert.equal(tx2.logs[0].args.irisScore, 3); // iris should increment 325 | // check state 326 | const storedRecord = await instance.records(testDataHash); 327 | assert.equal(storedRecord[2], 2); // sig count 328 | assert.equal(storedRecord[3], 3); // iris score 329 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 330 | assert.equal(await instance.sigExists(testDataHash, provider2), true); 331 | }); 332 | it( 333 | 'should allow adding another sig after provider added file', 334 | async () => { 335 | await instance.addRecordByProvider( 336 | testDataHash, 337 | user, 338 | testMetadata, 339 | testDataUri, 340 | { from: provider1 } 341 | ); 342 | // now have provider2 sign it 343 | const rsv2 = eutil.fromRpcSig(web3.eth.sign(provider2, testRootHash)); 344 | const tx = await instance.addSig( 345 | testDataHash, 346 | eutil.bufferToHex(rsv2.r), 347 | eutil.bufferToHex(rsv2.s), 348 | rsv2.v, 349 | { from: nonUser } 350 | ); 351 | assert.equal(tx.logs[0].event, 'StowRecordSigAdded'); 352 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 353 | assert.equal(tx.logs[0].args.attester, provider2); 354 | assert.equal(tx.logs[0].args.irisScore, 3); 355 | // check state 356 | const storedRecord = await instance.records(testDataHash); 357 | assert.equal(storedRecord[2], 2); // sig count 358 | assert.equal(storedRecord[3], 3); // iris score 359 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 360 | assert.equal(await instance.sigExists(testDataHash, provider2), true); 361 | } 362 | ); 363 | it('should reject bad signatures', async () => { 364 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 365 | from: user 366 | }); 367 | const rsv = eutil.fromRpcSig(web3.eth.sign(provider1, testRootHash)); 368 | // flip S and V 369 | await assertRevert( 370 | instance.addSig( 371 | testDataHash, 372 | eutil.bufferToHex(rsv.s), 373 | eutil.bufferToHex(rsv.v), 374 | rsv.v, 375 | { from: user } 376 | ) 377 | ); 378 | }); 379 | it('should reject sig that doesn\'t cover metadata hash', async () => { 380 | await instance.addRecord(testDataHash, testMetadata, testDataUri, { 381 | from: user 382 | }); 383 | // sign the data hash instead of root hash 384 | const rsv = eutil.fromRpcSig(web3.eth.sign(provider1, testDataHash)); 385 | await assertRevert( 386 | instance.addSig( 387 | testDataHash, 388 | eutil.bufferToHex(rsv.r), 389 | eutil.bufferToHex(rsv.s), 390 | rsv.v, 391 | { from: user } 392 | ) 393 | ); 394 | }); 395 | }); 396 | describe('add record by admin', () => { 397 | it('should allow admin to add a record without attestation', async () => { 398 | const tx = await instance.addRecordByAdmin( 399 | testDataHash, 400 | user, 401 | 0, 402 | testMetadata, 403 | testDataUri, 404 | { from: admin } 405 | ); 406 | assert.equal(tx.logs.length, 1); 407 | assert.equal(tx.logs[0].event, 'StowRecordAdded'); 408 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 409 | assert.equal(tx.logs[0].args.owner, user); 410 | assert.equal(tx.logs[0].args.metadata, testMetadata); 411 | // check state 412 | const { timestamp } = web3.eth.getBlock(tx.receipt.blockNumber); 413 | const storedRecord = await instance.records(testDataHash); 414 | assert.equal(storedRecord[0], user); 415 | assert.equal(storedRecord[1], testMetaHash); 416 | assert.equal(storedRecord[2], 0); // sig count 417 | assert.equal(storedRecord[3], 0); // iris score 418 | assert.equal(storedRecord[4], testDataUri); 419 | assert.equal(storedRecord[5], timestamp); 420 | }); 421 | it('should allow admin to add a record with attestation', async () => { 422 | const tx = await instance.addRecordByAdmin( 423 | testDataHash, 424 | user, 425 | provider1, 426 | testMetadata, 427 | testDataUri, 428 | { from: admin } 429 | ); 430 | assert.equal(tx.logs.length, 2); 431 | assert.equal(tx.logs[0].event, 'StowRecordAdded'); 432 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 433 | assert.equal(tx.logs[0].args.owner, user); 434 | assert.equal(tx.logs[0].args.metadata, testMetadata); 435 | assert.equal(tx.logs[1].event, 'StowRecordSigAdded'); 436 | assert.equal(tx.logs[1].args.dataHash, testDataHash); 437 | assert.equal(tx.logs[1].args.attester, provider1); 438 | assert.equal(tx.logs[1].args.irisScore, 1); 439 | // check state 440 | const { timestamp } = web3.eth.getBlock(tx.receipt.blockNumber); 441 | const storedRecord = await instance.records(testDataHash); 442 | assert.equal(storedRecord[0], user); 443 | assert.equal(storedRecord[1], testMetaHash); 444 | assert.equal(storedRecord[2], 1); // sig count 445 | assert.equal(storedRecord[3], 1); // iris score 446 | assert.equal(storedRecord[4], testDataUri); 447 | assert.equal(storedRecord[5], timestamp); 448 | assert.equal(await instance.sigExists(testDataHash, provider1), true); 449 | }); 450 | it('should not allow non admin to call', async () => { 451 | await assertRevert( 452 | instance.addRecordByAdmin( 453 | testDataHash, 454 | user, 455 | provider1, 456 | testMetadata, 457 | testDataUri, 458 | { from: provider1 } 459 | ) 460 | ); 461 | await assertRevert( 462 | instance.addRecordByAdmin( 463 | testDataHash, 464 | user, 465 | 0, 466 | testMetadata, 467 | testDataUri, 468 | { from: provider1 } 469 | ) 470 | ); 471 | }); 472 | }); 473 | describe('updateIris', () => { 474 | before('set up a irisScoreProvider mock contract', async () => { 475 | irisScoreProviderInstance = await irisScoreProvider.new({from: admin}); 476 | irisScoreProviderContractAddress = irisScoreProviderInstance.address; 477 | }); 478 | it('update record is score with irisScoreProvider', async () => { 479 | await instance.addRecord(testDataHash, testMetadata, testDataUri, {from: user}); 480 | const resultValue = await instance.updateIris.call(testDataHash, irisScoreProviderContractAddress); 481 | assert.equal(resultValue, 42); 482 | }); 483 | it('should not allow datahash of zero', async () => { 484 | await assertRevert(instance.updateIris.call(0, irisScoreProviderContractAddress)); 485 | await assertRevert(instance.updateIris(0, irisScoreProviderContractAddress)); 486 | }); 487 | it('should not allow address of zero', async () => { 488 | await assertRevert(instance.updateIris.call(testDataHash, 0)); 489 | await assertRevert(instance.updateIris(testDataHash, 0)); 490 | }); 491 | it('should not allow irisScoreProvider to return zero or less', async () => { 492 | await irisScoreProviderInstance.setVal(0); 493 | await assertRevert(instance.updateIris(testDataHash, irisScoreProviderContractAddress)); 494 | await irisScoreProviderInstance.setVal(42); 495 | }); 496 | it('should not allow updating more than once with the same irisScoreProvider', async () => { 497 | const tx0 = await instance.addRecord(testDataHash, testMetadata, testDataUri, { 498 | from: user 499 | }); 500 | // sometimes retuns 0x01 -- not sure why 501 | assert.equal(parseInt(tx0.receipt.status, 16), 1); 502 | const record0 = await instance.records(testDataHash); 503 | assert.equal(record0[2], '0'); 504 | const score0 = await instance.getIrisProvidersReport.call(testDataHash, irisScoreProviderContractAddress); 505 | assert.equal(score0.toString(), '0'); 506 | 507 | const tx = await instance.updateIris(testDataHash, irisScoreProviderContractAddress, {from: admin}); 508 | assert.equal(tx.logs[0].event, 'StowUpdateRecordsIris'); 509 | assert.equal(JSON.stringify(tx.logs[0].args), 510 | JSON.stringify({ 511 | 'dataHash':testDataHash, 512 | 'irisProvidersAddress':irisScoreProviderContractAddress, 513 | 'val':'42', 514 | 'sender':admin 515 | })); 516 | const record = await instance.records(testDataHash); 517 | assert.equal(record[3], '42'); 518 | 519 | const score = await instance.getIrisProvidersReport.call(testDataHash, irisScoreProviderContractAddress); 520 | assert.equal(score.toString(), '42'); 521 | 522 | await assertRevert(instance.updateIris(testDataHash, irisScoreProviderContractAddress)); 523 | }); 524 | }); 525 | describe('pausable', () => { 526 | it('should not allow non-admin to pause or unpause', async () => { 527 | await assertRevert(instance.pause({ from: accounts[1] })); 528 | await assertRevert(instance.unpause({ from: accounts[1] })); 529 | }); 530 | it('should not allow adding records when paused by admin', async () => { 531 | const tx = await instance.pause(); 532 | assert.equal(tx.logs[0].event, 'Pause'); 533 | await assertRevert( 534 | instance.addRecord(testDataHash, testMetadata, testDataUri, { 535 | from: user 536 | }) 537 | ); 538 | const tx2 = await instance.unpause(); 539 | assert.equal(tx2.logs[0].event, 'Unpause'); 540 | const tx3 = await instance.addRecord( 541 | testDataHash, 542 | testMetadata, 543 | testDataUri, 544 | { from: user } 545 | ); 546 | assert.equal(tx3.logs[0].event, 'StowRecordAdded'); 547 | }); 548 | }); 549 | // copy paste from records contract 550 | describe('destructible', () => { 551 | it('should not allow non-admin to destroy', async () => { 552 | await assertRevert(instance.destroy({ from: accounts[1] })); 553 | }); 554 | it('should allow admin to destroy', async () => { 555 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 556 | const tx = await instance.destroy({ from: admin }); 557 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 558 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 559 | }); 560 | it('should allow admin to destroyAndSend', async () => { 561 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 562 | const tx = await instance.destroyAndSend(admin, { from: admin }); 563 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 564 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 565 | assert.equal(web3.eth.getBalance(instance.address).toNumber(), 0); 566 | }); 567 | }); 568 | }); 569 | -------------------------------------------------------------------------------- /test/4_permissions_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const StowHub = artifacts.require('./StowHub.sol'); 4 | const StowUsers = artifacts.require('./StowUsers.sol'); 5 | const StowRecords = artifacts.require('./StowRecords.sol'); 6 | const StowPermissions = artifacts.require('./StowPermissions.sol'); 7 | const permissionPolicy = artifacts.require('./mock/PermissionPolicyMock.sol'); 8 | 9 | let permissionPolicyContractAddress; 10 | let permissionPolicyInstance; 11 | 12 | const crypto = require('crypto'); 13 | const eutil = require('ethereumjs-util'); 14 | 15 | const testDataContent1 = '{"foo":"bar","baz":42}'; 16 | const testDataContent2 = '{"asdf":42}'; 17 | const testDataHash1 = eutil.bufferToHex(eutil.sha3(testDataContent1)); 18 | const testDataHash2 = eutil.bufferToHex(eutil.sha3(testDataContent2)); 19 | const testDataUri1 = 'QmUMqi1rr4Ad1eZ3ctsRUEmqK2U3CyZqpetUe51LB9GiAM'; 20 | const testDataUri2 = 'QmUoCHEZqSuYhr9fV1c2b4gLASG2hPpC2moQXQ6qzy697d'; 21 | const testMetadata = 'KEYWORDS'; 22 | 23 | contract('StowPermissions', accounts => { 24 | const admin = accounts[0]; 25 | const user1 = accounts[1]; 26 | const user2 = accounts[2]; 27 | const delegate = accounts[5]; 28 | const nonUser = accounts[6]; 29 | const provider1 = accounts[3]; 30 | const provider2 = accounts[4]; 31 | let hub; 32 | let instance; 33 | 34 | before('set up a StowHub contract', async () => { 35 | hub = await StowHub.new(); 36 | }); 37 | before('set up a StowUsers contract', async () => { 38 | const usersInstance = await StowUsers.new(hub.address); 39 | await hub.setUsersContract(usersInstance.address); 40 | usersInstance.register({ from: user1 }); 41 | usersInstance.register({ from: user2 }); 42 | usersInstance.register({ from: provider1 }); 43 | usersInstance.register({ from: provider2 }); 44 | usersInstance.setProvenance(provider1, 1, { from: admin }); 45 | usersInstance.setProvenance(provider2, 1, { from: admin }); 46 | }); 47 | before('set up a StowRecords contract', async () => { 48 | const recordsInstance = await StowRecords.new(hub.address); 49 | await hub.setRecordsContract(recordsInstance.address); 50 | // upload 2 records, one for user1 and one for user2 51 | // 1st one is not attested, 2nd one is attested by provider1 52 | await recordsInstance.addRecord(testDataHash1, testMetadata, testDataUri1, { 53 | from: user1 54 | }); 55 | await recordsInstance.addRecordByProvider( 56 | testDataHash2, 57 | user2, 58 | testMetadata, 59 | testDataUri2, 60 | { from: provider1 } 61 | ); 62 | }); 63 | beforeEach('deploy a new StowPermissions contract', async () => { 64 | instance = await StowPermissions.new(hub.address, { from: accounts[0] }); 65 | await hub.setPermissionsContract(instance.address); 66 | }); 67 | 68 | describe('constructor', () => { 69 | it('should set hub address correctly', async () => { 70 | const newInstance = await StowRecords.new(hub.address, { 71 | from: accounts[0] 72 | }); 73 | assert.equal(await newInstance.hub(), hub.address); 74 | }); 75 | }); 76 | describe('delegate', () => { 77 | it('should not let non-user set a delegate', async () => { 78 | await assertRevert( 79 | instance.addDelegate(delegate, {from: nonUser}) 80 | ); 81 | }); 82 | it('should set a delegate', async () => { 83 | const tx = await instance.addDelegate(delegate, {from: user1}); 84 | assert.equal(tx.logs[0].event, 'StowPermissionDelegateAdded'); 85 | assert.equal(tx.logs[0].args.user, user1); 86 | assert.equal(tx.logs[0].args.delegate, delegate); 87 | assert.equal(await instance.delegates.call(user1, delegate), true); 88 | }); 89 | }); 90 | describe('grant access', () => { 91 | it('should allow user to grant access to their data', async () => { 92 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 93 | const tx = await instance.grantAccess( 94 | testDataHash1, 95 | provider2, 96 | fakeIpfsHash, 97 | { from: user1 } 98 | ); 99 | assert.equal(tx.logs[0].event, 'StowAccessGranted'); 100 | assert.equal(tx.logs[0].args.dataHash, testDataHash1); 101 | assert.equal(tx.logs[0].args.owner, user1); 102 | assert.equal(tx.logs[0].args.viewer, provider2); 103 | const perm = await instance.permissions(testDataHash1, provider2); 104 | assert.equal(perm[0], true); 105 | assert.equal(perm[1], fakeIpfsHash); 106 | }); 107 | it('should not allow non-owner to grant access', async () => { 108 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 109 | await assertRevert( 110 | instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 111 | from: provider1 112 | }) 113 | ); 114 | await assertRevert( 115 | instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 116 | from: provider2 117 | }) 118 | ); 119 | await assertRevert( 120 | instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 121 | from: user2 122 | }) 123 | ); 124 | }); 125 | it('should not allow non-delegate to grant access', async () => { 126 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 127 | await assertRevert( 128 | instance.grantAccessbyDelegate(testDataHash1, provider2, user1, fakeIpfsHash, { 129 | from: user2 130 | }) 131 | ); 132 | await assertRevert( 133 | instance.grantAccessbyDelegate(testDataHash2, provider1, provider2, fakeIpfsHash, { 134 | from: user1 135 | }) 136 | ); 137 | }); 138 | it('should allow delegate to grant access', async () => { 139 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 140 | await instance.addDelegate(delegate, {from: user1}); 141 | const tx = await instance.grantAccessbyDelegate( 142 | testDataHash1, 143 | user2, 144 | user1, 145 | fakeIpfsHash, 146 | { from: delegate } 147 | ); 148 | assert.equal(tx.logs[0].event, 'StowAccessGranted'); 149 | assert.equal(tx.logs[0].args.dataHash, testDataHash1); 150 | assert.equal(tx.logs[0].args.owner, user1); 151 | assert.equal(tx.logs[0].args.viewer, user2); 152 | assert.equal(tx.logs[0].args.sender, delegate); 153 | const perm = await instance.permissions(testDataHash1, user2); 154 | assert.equal(perm[0], true); 155 | assert.equal(perm[1], fakeIpfsHash); 156 | }); 157 | it('should reject if viewer or data uri is zero', async () => { 158 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 159 | await assertRevert( 160 | instance.grantAccess(testDataHash1, 0, fakeIpfsHash, { from: user1 }) 161 | ); 162 | await assertRevert( 163 | instance.grantAccess(testDataHash1, provider2, 0, { from: user1 }) 164 | ); 165 | }); 166 | // TODO, This is disabled for testing purposes 167 | // it( 168 | // 'should not allow sharing same record twice with same user', 169 | // async () => { 170 | // const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 171 | // await instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 172 | // from: user1 173 | // }); 174 | // await assertRevert( 175 | // instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 176 | // from: user1 177 | // }) 178 | // ); 179 | // } 180 | // ); 181 | }); 182 | describe('grant policy based access', () => { 183 | before('set up a permissionPolicy mock contract', async () => { 184 | permissionPolicyInstance = await permissionPolicy.new({from: admin}); 185 | permissionPolicyContractAddress = permissionPolicyInstance.address; 186 | }); 187 | it('should allow user to grant policy based access to their data', async () => { 188 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 189 | const tx = await instance.grantPolicyBasedAccess( 190 | testDataHash1, 191 | provider2, 192 | fakeIpfsHash, 193 | [permissionPolicyContractAddress], 194 | { from: user1 } 195 | ); 196 | 197 | const expectedArgs = { 198 | 'dataHash': testDataHash1, 199 | 'dataUri': fakeIpfsHash, 200 | 'viewer': provider2, 201 | 'policy': permissionPolicyContractAddress, 202 | 'isOk': true, 203 | 'sender': user1 204 | }; 205 | 206 | assert.equal(tx.logs[0].event, 'StowPolicyChecked'); 207 | assert.equal(JSON.stringify(tx.logs[0].args), JSON.stringify(expectedArgs)); 208 | 209 | assert.equal(tx.logs[1].event, 'StowAccessGranted'); 210 | assert.equal(tx.logs[1].args.dataHash, testDataHash1); 211 | assert.equal(tx.logs[1].args.owner, user1); 212 | assert.equal(tx.logs[1].args.viewer, provider2); 213 | const perm = await instance.permissions(testDataHash1, provider2); 214 | assert.equal(perm[0], true); 215 | assert.equal(perm[1], fakeIpfsHash); 216 | }); 217 | it('should allow not access when check fails', async () => { 218 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 219 | await permissionPolicyInstance.setVal(false); 220 | 221 | await assertRevert( instance.grantPolicyBasedAccess( 222 | testDataHash1, 223 | provider2, 224 | fakeIpfsHash, 225 | [permissionPolicyContractAddress], 226 | { from: user1 } 227 | )); 228 | 229 | await permissionPolicyInstance.setVal(true); 230 | }); 231 | }); 232 | describe('revoke access', () => { 233 | beforeEach('grant provider2 to access user1\'s record1', async () => { 234 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 235 | await instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 236 | from: user1 237 | }); 238 | }); 239 | it('should allow owner to revoke access to their data', async () => { 240 | const tx = await instance.revokeAccess(testDataHash1, provider2, { 241 | from: user1 242 | }); 243 | assert.equal(tx.logs[0].event, 'StowAccessRevoked'); 244 | assert.equal(tx.logs[0].args.dataHash, testDataHash1); 245 | assert.equal(tx.logs[0].args.owner, user1); 246 | assert.equal(tx.logs[0].args.viewer, provider2); 247 | const perm = await instance.permissions(testDataHash1, provider2); 248 | assert.equal(perm[0], false); 249 | assert.equal(perm[1], ''); 250 | }); 251 | it('should not allow non-owner to revoke access to data', async () => { 252 | await assertRevert( 253 | instance.revokeAccess(testDataHash1, provider2, { from: provider1 }) 254 | ); 255 | await assertRevert( 256 | instance.revokeAccess(testDataHash1, provider2, { from: provider2 }) 257 | ); 258 | await assertRevert( 259 | instance.revokeAccess(testDataHash1, provider2, { from: user2 }) 260 | ); 261 | }); 262 | it('should allow delegate to revoke access to data', async () => { 263 | await instance.addDelegate(delegate, {from: user1}); 264 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 265 | await instance.grantAccessbyDelegate(testDataHash1, provider2, user1, fakeIpfsHash, { 266 | from: delegate 267 | }); 268 | const tx = await instance.revokeAccessbyDelegate(testDataHash1, provider2, user1, { 269 | from: delegate 270 | }); 271 | assert.equal(tx.logs[0].event, 'StowAccessRevoked'); 272 | assert.equal(tx.logs[0].args.dataHash, testDataHash1); 273 | assert.equal(tx.logs[0].args.owner, user1); 274 | assert.equal(tx.logs[0].args.viewer, provider2); 275 | assert.equal(tx.logs[0].args.sender, delegate); 276 | const perm = await instance.permissions(testDataHash1, provider2); 277 | assert.equal(perm[0], false); 278 | assert.equal(perm[1], ''); 279 | }); 280 | it('should not allow non-delegate to revoke access to data', async () => { 281 | await assertRevert( 282 | instance.revokeAccessbyDelegate(testDataHash1, provider2, user1, { from: user2 }) 283 | ); 284 | await assertRevert( 285 | instance.revokeAccessbyDelegate(testDataHash1, provider2, user1, { from: provider2 }) 286 | ); 287 | await assertRevert( 288 | instance.revokeAccessbyDelegate(testDataHash1, provider2, user1, { from: user1 }) 289 | ); 290 | }); 291 | }); 292 | describe('check access', () => { 293 | it('should check access to data', async () => { 294 | assert.equal(await instance.checkAccess(testDataHash1, provider2, { 295 | from: user1 296 | }), false); 297 | // grant provider2 to access user1\'s record1 298 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 299 | await instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 300 | from: user1 301 | }); 302 | assert.equal(await instance.checkAccess(testDataHash1, provider2, { 303 | from: user1 304 | }), true); 305 | }); 306 | }); 307 | describe('pausable', () => { 308 | it('should not allow non-admin to pause or unpause', async () => { 309 | await assertRevert(instance.pause({ from: accounts[1] })); 310 | await assertRevert(instance.unpause({ from: accounts[1] })); 311 | }); 312 | it('should not allow sharing records when paused by admin', async () => { 313 | const fakeIpfsHash = eutil.bufferToHex(crypto.randomBytes(32)); 314 | const tx = await instance.pause(); 315 | assert.equal(tx.logs[0].event, 'Pause'); 316 | await assertRevert( 317 | instance.grantAccess(testDataHash1, provider2, fakeIpfsHash, { 318 | from: user1 319 | }) 320 | ); 321 | const tx2 = await instance.unpause(); 322 | assert.equal(tx2.logs[0].event, 'Unpause'); 323 | const tx3 = await instance.grantAccess( 324 | testDataHash1, 325 | provider2, 326 | fakeIpfsHash, 327 | { from: user1 } 328 | ); 329 | assert.equal(tx3.logs[0].event, 'StowAccessGranted'); 330 | }); 331 | }); 332 | // copy paste from records contract 333 | describe('destructible', () => { 334 | it('should not allow non-admin to destroy', async () => { 335 | await assertRevert(instance.destroy({ from: accounts[1] })); 336 | }); 337 | it('should allow admin to destroy', async () => { 338 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 339 | const tx = await instance.destroy({ from: admin }); 340 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 341 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 342 | }); 343 | it('should allow admin to destroyAndSend', async () => { 344 | assert.notEqual(web3.eth.getCode(instance.address), '0x0'); 345 | const tx = await instance.destroyAndSend(admin, { from: admin }); 346 | assert.equal(tx.logs.length, 0, `did not expect logs but got ${tx.logs}`); 347 | assert.equal(web3.eth.getCode(instance.address), '0x0'); 348 | assert.equal(web3.eth.getBalance(instance.address).toNumber(), 0); 349 | }); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /test/5_records-token_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const ERC20 = artifacts.require('./ERC20Mock.sol'); 4 | const StowHub = artifacts.require('./StowHub.sol'); 5 | const StowUsers = artifacts.require('./StowUsers.sol'); 6 | const StowRecords = artifacts.require('./StowRecords.sol'); 7 | 8 | const eutil = require('ethereumjs-util'); 9 | 10 | const testDataContent = '{"foo":"bar","baz":42}'; 11 | const testDataHash = eutil.bufferToHex(eutil.sha3(testDataContent)); 12 | const testDataUri = 'QmUMqi1rr4Ad1eZ3ctsRUEmqK2U3CyZqpetUe51LB9GiAM'; 13 | const testMetadata = 'KEYWORDS'; 14 | const testMetaHash = eutil.bufferToHex(eutil.sha3(testMetadata)); 15 | 16 | 17 | contract('StowRecords with Reward', accounts => { 18 | const admin = accounts[0]; 19 | const user = accounts[1]; 20 | const provider1 = accounts[2]; 21 | const provider2 = accounts[3]; 22 | const nonUser = accounts[4]; 23 | let tokenContractAddress; 24 | let hub; 25 | let instance; 26 | let token; 27 | 28 | before('set up a token contract', async () => { 29 | token = await ERC20.new({from: admin}); 30 | tokenContractAddress = token.address; 31 | token.unpause({from: admin}); 32 | 33 | }); 34 | before('set up a StowHub contract', async () => { 35 | hub = await StowHub.new({from: admin}); 36 | }); 37 | before('set up a StowUsers contract', async () => { 38 | const usersInstance = await StowUsers.new(hub.address); 39 | await hub.setUsersContract(usersInstance.address); 40 | usersInstance.register({from: user}); 41 | usersInstance.register({from: provider1}); 42 | usersInstance.register({from: provider2}); 43 | usersInstance.setProvenance(provider1, 1); 44 | usersInstance.setProvenance(provider2, 2); 45 | }); 46 | beforeEach('deploy a new StowRecords contract', async () => { 47 | instance = await StowRecords.new(hub.address); 48 | await hub.setRecordsContract(instance.address); 49 | }); 50 | describe('add record by user and receive STOW tokens', () => { 51 | it('should allow a user to add a new record and receive STOW tokens', async () => { 52 | await token.transfer(instance.address, web3.toWei(1000, 'finney'), {from: admin}); 53 | let userBalance = await token.balanceOf(user); 54 | assert.equal(userBalance.toNumber(), 0); 55 | const tx = await instance.addRecordwithReward( 56 | testDataHash, 57 | testMetadata, 58 | testDataUri, 59 | tokenContractAddress, 60 | {from: user} 61 | ); 62 | userBalance = await token.balanceOf(user); 63 | assert.equal(userBalance.toNumber(), web3.toWei(1, 'finney')); 64 | assert.equal(tx.logs.length, 2); 65 | assert.equal(tx.logs[1].event, 'StowReward'); 66 | 67 | assert.equal(tx.logs[0].event, 'StowRecordAdded'); 68 | assert.equal(tx.logs[0].args.dataHash, testDataHash); 69 | assert.equal(tx.logs[0].args.owner, user); 70 | assert.equal(tx.logs[0].args.metadata, testMetadata); 71 | const {timestamp} = web3.eth.getBlock(tx.receipt.blockNumber); 72 | // check state 73 | const storedRecord = await instance.records(testDataHash); 74 | assert.equal(storedRecord[0], user); 75 | assert.equal(storedRecord[1], testMetaHash); 76 | assert.equal(storedRecord[2], 0); // sig count 77 | assert.equal(storedRecord[3], 0); // iris score 78 | assert.equal(storedRecord[4], testDataUri); 79 | assert.equal(storedRecord[5], timestamp); 80 | }); 81 | it('should not allow user to add same record twice', async () => { 82 | await instance.addRecord( 83 | testDataHash, 84 | testMetadata, 85 | testDataUri, { 86 | from: user 87 | }); 88 | // try submitting the file again 89 | await assertRevert( 90 | instance.addRecordwithReward( 91 | testDataHash, 92 | testMetadata, 93 | testDataUri, 94 | tokenContractAddress, { 95 | from: user 96 | }) 97 | ); 98 | }); 99 | it('should not allow non-users to call', async () => { 100 | await assertRevert( 101 | instance.addRecordwithReward( 102 | testDataHash, 103 | testMetadata, 104 | testDataUri, 105 | tokenContractAddress, { 106 | from: nonUser 107 | }) 108 | ); 109 | }); 110 | it('should reject if data hash or data uri is zero', async () => { 111 | // try zero data hash 112 | await assertRevert( 113 | instance.addRecordwithReward( 114 | 0, 115 | testMetadata, 116 | testDataUri, 117 | tokenContractAddress, { 118 | from: user 119 | }) 120 | ); 121 | // try zero data uri 122 | await assertRevert( 123 | instance.addRecordwithReward( 124 | testDataHash, 125 | testMetadata, 126 | 0, 127 | tokenContractAddress, { 128 | from: user 129 | }) 130 | ); 131 | }); 132 | }); 133 | describe('pausable', () => { 134 | it('should not allow adding records for reward when paused by admin', async () => { 135 | const tx = await instance.pause(); 136 | assert.equal(tx.logs[0].event, 'Pause'); 137 | await assertRevert( 138 | instance.addRecordwithReward( 139 | testDataHash, 140 | testMetadata, 141 | testDataUri, 142 | tokenContractAddress, { 143 | from: user 144 | }) 145 | ); 146 | const tx2 = await instance.unpause(); 147 | assert.equal(tx2.logs[0].event, 'Unpause'); 148 | 149 | await token.transfer(instance.address, web3.toWei(1000 ,'finney'), {from: admin}); 150 | const tx3 = await instance.addRecordwithReward( 151 | testDataHash, 152 | testMetadata, 153 | testDataUri, 154 | tokenContractAddress, 155 | {from: user} 156 | ); 157 | assert.equal(tx3.logs[0].event, 'StowRecordAdded'); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/6_whitelist_test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from 'openzeppelin-solidity/test/helpers/assertRevert'; 2 | 3 | const StowHub = artifacts.require('./StowHub.sol'); 4 | const StowUsers = artifacts.require('./StowUsers.sol'); 5 | const Whitelist = artifacts.require('./WhitelistMock.sol'); 6 | 7 | /* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */ 8 | 9 | contract('MockWhitelist', accounts => { 10 | let hub; 11 | let users; 12 | let instance; 13 | const expertScore = 3; 14 | const nonOwner = accounts[5]; 15 | const expertUser1 = accounts[1]; 16 | 17 | before('set up a StowHub contract', async () => { 18 | hub = await StowHub.new(); 19 | }); 20 | beforeEach('deploy a new StowUsers contract', async () => { 21 | users = await StowUsers.new(hub.address); 22 | await hub.setUsersContract(users.address); 23 | }); 24 | beforeEach('deploy a new Whitelist contract', async () => { 25 | instance = await Whitelist.new(); 26 | }); 27 | describe('adding and getting', () => { 28 | it('should not allow non owner to add expert', async () => { 29 | await assertRevert( 30 | instance.updateScore(expertUser1, expertScore, { from: nonOwner }) 31 | ); 32 | }); 33 | it('owner should set score of expert', async () => { 34 | const tx = await instance.updateScore(expertUser1, expertScore); 35 | const score = await instance.expertScores.call(expertUser1); 36 | // check state 37 | assert.equal(score, expertScore); 38 | // check logs 39 | assert.equal(tx.logs[0].event, 'LogExpertScoreUpdated'); 40 | assert.equal(tx.logs[0].args.user, expertUser1); 41 | assert.equal(tx.logs[0].args.score, expertScore); 42 | }); 43 | it('should get the scores from whitelist function', async () => { 44 | await instance.updateScore(expertUser1, expertScore); 45 | const score = await instance.expertScoreOf.call(expertUser1); 46 | assert.equal(score, expertScore); 47 | }); 48 | }); 49 | describe('set expert score function', () => { 50 | it('should not allow non owner to add expert score', async () => { 51 | await instance.updateScore(expertUser1, expertScore); 52 | await users.register({ from: expertUser1 }); 53 | await assertRevert(users.setExpertScore(instance.address, expertUser1, { from: nonOwner })); 54 | }); 55 | it('should not allow score of non user to be updated', async () => { 56 | await instance.updateScore(expertUser1, expertScore); 57 | await assertRevert(users.setExpertScore(instance.address, expertUser1)); 58 | }); 59 | it('should allow score of user to be updated', async () => { 60 | await instance.updateScore(expertUser1, expertScore); 61 | await users.register({ from: expertUser1 }); 62 | const tx = await users.setExpertScore(instance.address, expertUser1); 63 | // check state 64 | assert.equal((await users.users(expertUser1))[2], expertScore); 65 | // check logs 66 | assert.equal(tx.logs[1].event, 'StowWhitelistScoreAdded'); 67 | assert.equal(tx.logs[1].args.whitelist, instance.address); 68 | assert.equal(tx.logs[0].event, 'StowProvenanceChanged'); 69 | assert.equal(tx.logs[0].args.user, expertUser1); 70 | assert.equal(tx.logs[0].args.provenance, expertScore); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /testData/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | const IPFS = require('ipfs-mini'); 3 | const Web3 = require('web3'); 4 | 5 | const providerHost = process.env.STOW_ETH_PROVIDER; 6 | const ipfsProvider = process.env.STOW_IPFS_HOST; 7 | const ipfsPort = process.env.STOW_IPFS_PORT; 8 | const protocol = process.env.STOW_IPFS_PROTOCOL; 9 | 10 | const HDWalletProvider = require('truffle-hdwallet-provider'); 11 | 12 | let privKeys; 13 | 14 | // If ropsten, set the owner private key 15 | if (providerHost === 'ropsten') { 16 | privKeys = require('../test-private-keys').private_keys; 17 | privKeys[0] = process.env.STOW_ETH_INFURA_ROPSTEN_HUB_OWNER_PRIVATE_KEY; 18 | } 19 | 20 | // If rinkeby, set the owner private key 21 | if (providerHost === 'rinkeby') { 22 | privKeys = require('../test-private-keys').private_keys; 23 | privKeys[0] = process.env.STOW_ETH_INFURA_RINKEBY_HUB_OWNER_PRIVATE_KEY; 24 | } 25 | 26 | const networks = { 27 | ropsten: { 28 | provider() { 29 | return new HDWalletProvider( 30 | privKeys, 31 | `https://ropsten.infura.io/${process.env.STOW_ETH_INFURA_KEY}`, 32 | ); 33 | }, 34 | network_id: 3, 35 | }, 36 | rinkeby: { 37 | provider() { 38 | return new HDWalletProvider( 39 | privKeys, 40 | `https://rinkeby.infura.io/${process.env.STOW_ETH_INFURA_KEY}`, 41 | ); 42 | }, 43 | network_id: 4, 44 | }, 45 | }; 46 | 47 | let web3; 48 | 49 | // If ropsten or rinkeby 50 | if (providerHost === 'ropsten' || providerHost === 'rinkeby') { 51 | const provider = networks[providerHost].provider(); 52 | web3 = new Web3(provider.engine); 53 | } 54 | else{ 55 | web3 = new Web3('http://localhost:7545'); 56 | } 57 | 58 | const ipfs = new IPFS({ host: ipfsProvider, port: ipfsPort, protocol }); 59 | 60 | module.exports = { 61 | web3, 62 | ipfs 63 | }; 64 | -------------------------------------------------------------------------------- /testData/index.js: -------------------------------------------------------------------------------- 1 | // const Stow = require('@stowprotocol/stow-js'); 2 | throw 'will not work until @linniaprotocol/linnia-js is move to stow '; 3 | const Stow = require('@linniaprotocol/linnia-js'); 4 | 5 | const {setupRoles} = require('./setupRoles'); 6 | const {setupData} = require('./setupData'); 7 | const {web3} = require('./config'); 8 | 9 | const setup = async () => { 10 | const networkId = await web3.eth.net.getId(); 11 | if(networkId === 3 || networkId === 4 || networkId === 5777) { 12 | const StowHub = require('../build/contracts/StowHub.json'); 13 | const stowContractUpgradeHubAddress = StowHub.networks[networkId].address; 14 | const stow = new Stow(web3, { stowContractUpgradeHubAddress }); 15 | await setupRoles(stow); 16 | await setupData(stow); 17 | } 18 | 19 | }; 20 | 21 | setup(); 22 | -------------------------------------------------------------------------------- /testData/setupData.js: -------------------------------------------------------------------------------- 1 | const {web3} = require('./config'); 2 | const {setupMetadata} = require('./setupMetadata'); 3 | const {encrypt, ipfsPush, getFiles} = require('./utils'); 4 | 5 | 6 | const setupData = async (stow) => { 7 | const files = await getFiles(); 8 | const { records } = await stow.getContractInstances(); 9 | for(let i=0; i { 7 | const accounts = await getAccounts(); 8 | const nonce = crypto.randomBytes(256); 9 | const account = accounts[accountIndex]; 10 | const providerIndex = Math.floor(Math.random() * 19) + 41; 11 | const provider = accounts[providerIndex]; 12 | const publicKey = userPubKeys[keyIndex]; 13 | const year = 2018 - keyIndex; 14 | const metadata = JSON.stringify({ 15 | dataFormat: 'json', 16 | domain: 'medical data', 17 | storage: 'IPFS', 18 | encryptionScheme: 'x25519-xsalsa20-poly1305', 19 | encryptionPublicKey: publicKey, 20 | stowjsVersion: '0.2.1', 21 | providerName: 'Stow Test Provider', 22 | providerEthereumAddress: provider, 23 | keywords: [ 'medical', 'diabetes', 'patient', 'test', 'data' ], 24 | timeframe: `${year}-06-07T10:30,${year}-06-08T10:30`, 25 | }); 26 | return {nonce, metadata, publicKey, provider, account}; 27 | }; 28 | 29 | module.exports = {setupMetadata}; 30 | -------------------------------------------------------------------------------- /testData/setupRoles.js: -------------------------------------------------------------------------------- 1 | const {getAccounts} = require('./utils'); 2 | 3 | const setupRoles = async (stow) => { 4 | const accounts = await getAccounts(); 5 | const { users } = await stow.getContractInstances(); 6 | const userPromises = accounts.map((account,i) => { 7 | if(i>0 && i<41){ 8 | // adding 2/3 of users to smart contracts as plain users 9 | return users.register({ from: accounts[i].toLowerCase(), gas: 500000 }); 10 | } 11 | else{ 12 | // add user to smart contracts and then add provenance so they are providers 13 | return users.register({ from: accounts[i].toLowerCase(), gas: 500000 }).then(() => { 14 | return users.setProvenance(accounts[i], 1, { 15 | from: accounts[0].toLowerCase(), 16 | gas: 500000, 17 | }); 18 | }); 19 | } 20 | }); 21 | 22 | await Promise.all(userPromises); 23 | 24 | console.log('done setting up accounts!'); 25 | }; 26 | 27 | module.exports ={setupRoles}; 28 | -------------------------------------------------------------------------------- /testData/synthetic_patients_data/Tegan Howe.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [ 3 | { 4 | "fullUrl": "urn:uuid:e033f805-576e-4e1b-bcc4-a526138efad5", 5 | "resource": { 6 | "resourceType": "Basic", 7 | "id": "e033f805-576e-4e1b-bcc4-a526138efad5", 8 | "meta": { 9 | "profile": [ 10 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-entity-Person" 11 | ] 12 | }, 13 | "code": { 14 | "coding": [ 15 | { 16 | "system": "http://standardhealthrecord.org/fhir/basic-resource-type", 17 | "code": "shr-entity-Person", 18 | "display": "shr-entity-Person" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | { 25 | "fullUrl": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c", 26 | "resource": { 27 | "resourceType": "Patient", 28 | "id": "02b639cb-83f2-4574-9bcc-a1544bf2b57c", 29 | "meta": { 30 | "profile": [ 31 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-entity-Patient" 32 | ] 33 | }, 34 | "text": { 35 | "status": "generated", 36 | "div": "
Generated by Synthea.Version identifier: v1.3.1-559-g6d667119\n . Person seed: -4590349729781517936 Population seed: 1528486983749
" 37 | }, 38 | "extension": [ 39 | { 40 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", 41 | "extension": [ 42 | { 43 | "url": "ombCategory", 44 | "valueCoding": { 45 | "system": "urn:oid:2.16.840.1.113883.6.238", 46 | "code": "2106-3", 47 | "display": "White" 48 | } 49 | }, 50 | { 51 | "url": "text", 52 | "valueString": "White" 53 | } 54 | ] 55 | }, 56 | { 57 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", 58 | "extension": [ 59 | { 60 | "url": "ombCategory", 61 | "valueCoding": { 62 | "system": "urn:oid:2.16.840.1.113883.6.238", 63 | "code": "2186-5", 64 | "display": "Not Hispanic or Latino" 65 | } 66 | }, 67 | { 68 | "url": "text", 69 | "valueString": "Not Hispanic or Latino" 70 | } 71 | ] 72 | }, 73 | { 74 | "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", 75 | "valueString": "Willow678 Feil794" 76 | }, 77 | { 78 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", 79 | "valueCode": "F" 80 | }, 81 | { 82 | "url": "http://hl7.org/fhir/StructureDefinition/birthPlace", 83 | "valueAddress": { 84 | "city": "Weymouth Town", 85 | "state": "Massachusetts", 86 | "country": "US" 87 | } 88 | }, 89 | { 90 | "url": "http://standardhealthrecord.org/fhir/StructureDefinition/shr-actor-FictionalPerson-extension", 91 | "valueBoolean": true 92 | }, 93 | { 94 | "url": "http://standardhealthrecord.org/fhir/StructureDefinition/shr-entity-FathersName-extension", 95 | "valueHumanName": { 96 | "text": "Erasmo545 VonRueden376" 97 | } 98 | }, 99 | { 100 | "url": "http://standardhealthrecord.org/fhir/StructureDefinition/shr-demographics-SocialSecurityNumber-extension", 101 | "valueString": "999-30-1144" 102 | }, 103 | { 104 | "url": "http://standardhealthrecord.org/fhir/StructureDefinition/shr-entity-Person-extension", 105 | "valueReference": { 106 | "reference": "urn:uuid:e033f805-576e-4e1b-bcc4-a526138efad5" 107 | } 108 | }, 109 | { 110 | "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", 111 | "valueDecimal": 3.597124727852909 112 | }, 113 | { 114 | "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", 115 | "valueDecimal": 68.40287527214709 116 | } 117 | ], 118 | "identifier": [ 119 | { 120 | "system": "https://github.com/synthetichealth/synthea", 121 | "value": "ecb99ada-89c8-480a-9ec6-fd3624681fd4" 122 | }, 123 | { 124 | "type": { 125 | "coding": [ 126 | { 127 | "system": "http://hl7.org/fhir/v2/0203", 128 | "code": "MR", 129 | "display": "Medical Record Number" 130 | } 131 | ], 132 | "text": "Medical Record Number" 133 | }, 134 | "system": "http://hospital.smarthealthit.org", 135 | "value": "ecb99ada-89c8-480a-9ec6-fd3624681fd4" 136 | }, 137 | { 138 | "type": { 139 | "coding": [ 140 | { 141 | "system": "http://hl7.org/fhir/identifier-type", 142 | "code": "SB", 143 | "display": "Social Security Number" 144 | } 145 | ], 146 | "text": "Social Security Number" 147 | }, 148 | "system": "http://hl7.org/fhir/sid/us-ssn", 149 | "value": "999-30-1144" 150 | }, 151 | { 152 | "type": { 153 | "coding": [ 154 | { 155 | "system": "http://hl7.org/fhir/v2/0203", 156 | "code": "DL", 157 | "display": "Driver's License" 158 | } 159 | ], 160 | "text": "Driver's License" 161 | }, 162 | "system": "urn:oid:2.16.840.1.113883.4.3.25", 163 | "value": "S99912098" 164 | }, 165 | { 166 | "type": { 167 | "coding": [ 168 | { 169 | "system": "http://hl7.org/fhir/v2/0203", 170 | "code": "PPN", 171 | "display": "Passport Number" 172 | } 173 | ], 174 | "text": "Passport Number" 175 | }, 176 | "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", 177 | "value": "X44083640X" 178 | } 179 | ], 180 | "name": [ 181 | { 182 | "use": "official", 183 | "family": "Howe", 184 | "given": [ 185 | "Tegan" 186 | ], 187 | "prefix": [ 188 | "Mrs." 189 | ] 190 | }, 191 | { 192 | "use": "maiden", 193 | "family": "VonRueden376", 194 | "given": [ 195 | "Tegan755" 196 | ], 197 | "prefix": [ 198 | "Mrs." 199 | ] 200 | } 201 | ], 202 | "telecom": [ 203 | { 204 | "system": "phone", 205 | "value": "555-455-7811", 206 | "use": "home" 207 | } 208 | ], 209 | "gender": "female", 210 | "birthDate": "1945-10-08", 211 | "address": [ 212 | { 213 | "extension": [ 214 | { 215 | "url": "http://hl7.org/fhir/StructureDefinition/geolocation", 216 | "extension": [ 217 | { 218 | "url": "latitude", 219 | "valueDecimal": -71.094162 220 | }, 221 | { 222 | "url": "longitude", 223 | "valueDecimal": 41.725351 224 | } 225 | ] 226 | } 227 | ], 228 | "line": [ 229 | "900 Stanton Village Apt 12" 230 | ], 231 | "city": "Fall River", 232 | "state": "Massachusetts", 233 | "postalCode": "02720", 234 | "country": "US" 235 | } 236 | ], 237 | "maritalStatus": { 238 | "coding": [ 239 | { 240 | "system": "http://hl7.org/fhir/v3/MaritalStatus", 241 | "code": "M", 242 | "display": "M" 243 | } 244 | ], 245 | "text": "M" 246 | }, 247 | "multipleBirthBoolean": false, 248 | "communication": [ 249 | { 250 | "language": { 251 | "coding": [ 252 | { 253 | "system": "urn:ietf:bcp:47", 254 | "code": "en-US", 255 | "display": "English" 256 | } 257 | ], 258 | "text": "English" 259 | } 260 | } 261 | ] 262 | } 263 | }, 264 | { 265 | "fullUrl": "urn:uuid:d612c851-07e3-46cb-bf96-f835a68760d7", 266 | "resource": { 267 | "resourceType": "Observation", 268 | "id": "d612c851-07e3-46cb-bf96-f835a68760d7", 269 | "meta": { 270 | "profile": [ 271 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 272 | ] 273 | }, 274 | "status": "final", 275 | "category": [ 276 | { 277 | "coding": [ 278 | { 279 | "system": "http://hl7.org/fhir/observation-category", 280 | "code": "laboratory", 281 | "display": "laboratory" 282 | } 283 | ] 284 | } 285 | ], 286 | "code": { 287 | "coding": [ 288 | { 289 | "system": "http://loinc.org", 290 | "code": "4548-4", 291 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 292 | } 293 | ], 294 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 295 | }, 296 | "subject": { 297 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 298 | }, 299 | "context": { 300 | "reference": "urn:uuid:7a7196d2-3e48-4587-9e62-9afdefba94ec" 301 | }, 302 | "effectiveDateTime": "2015-07-01T23:19:08-08:00", 303 | "issued": "2015-07-01T23:19:08-08:00", 304 | "valueQuantity": { 305 | "value": 7.720473318404359, 306 | "unit": "%", 307 | "system": "http://unitsofmeasure.org", 308 | "code": "%" 309 | } 310 | } 311 | }, 312 | { 313 | "fullUrl": "urn:uuid:26a95c2f-d50a-42cd-a102-991c3acafe14", 314 | "resource": { 315 | "resourceType": "Observation", 316 | "id": "26a95c2f-d50a-42cd-a102-991c3acafe14", 317 | "meta": { 318 | "profile": [ 319 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 320 | ] 321 | }, 322 | "status": "final", 323 | "category": [ 324 | { 325 | "coding": [ 326 | { 327 | "system": "http://hl7.org/fhir/observation-category", 328 | "code": "laboratory", 329 | "display": "laboratory" 330 | } 331 | ] 332 | } 333 | ], 334 | "code": { 335 | "coding": [ 336 | { 337 | "system": "http://loinc.org", 338 | "code": "4548-4", 339 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 340 | } 341 | ], 342 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 343 | }, 344 | "subject": { 345 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 346 | }, 347 | "context": { 348 | "reference": "urn:uuid:7b7e749b-32a7-4080-bf12-8aa183e8a5a8" 349 | }, 350 | "effectiveDateTime": "2015-10-07T23:19:08-08:00", 351 | "issued": "2015-10-07T23:19:08-08:00", 352 | "valueQuantity": { 353 | "value": 7.587282709575275, 354 | "unit": "%", 355 | "system": "http://unitsofmeasure.org", 356 | "code": "%" 357 | } 358 | } 359 | }, 360 | { 361 | "fullUrl": "urn:uuid:04afc662-7a07-4394-ad3f-749e2b17281b", 362 | "resource": { 363 | "resourceType": "Observation", 364 | "id": "04afc662-7a07-4394-ad3f-749e2b17281b", 365 | "meta": { 366 | "profile": [ 367 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 368 | ] 369 | }, 370 | "status": "final", 371 | "category": [ 372 | { 373 | "coding": [ 374 | { 375 | "system": "http://hl7.org/fhir/observation-category", 376 | "code": "laboratory", 377 | "display": "laboratory" 378 | } 379 | ] 380 | } 381 | ], 382 | "code": { 383 | "coding": [ 384 | { 385 | "system": "http://loinc.org", 386 | "code": "4548-4", 387 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 388 | } 389 | ], 390 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 391 | }, 392 | "subject": { 393 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 394 | }, 395 | "context": { 396 | "reference": "urn:uuid:f7e95d10-6f93-469e-b30a-cbd3412b9b5c" 397 | }, 398 | "effectiveDateTime": "2016-01-13T23:19:08-08:00", 399 | "issued": "2016-01-13T23:19:08-08:00", 400 | "valueQuantity": { 401 | "value": 7.424075660846981, 402 | "unit": "%", 403 | "system": "http://unitsofmeasure.org", 404 | "code": "%" 405 | } 406 | } 407 | }, 408 | { 409 | "fullUrl": "urn:uuid:929ba428-a118-4592-a69a-7531cf8e4ed2", 410 | "resource": { 411 | "resourceType": "Observation", 412 | "id": "929ba428-a118-4592-a69a-7531cf8e4ed2", 413 | "meta": { 414 | "profile": [ 415 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 416 | ] 417 | }, 418 | "status": "final", 419 | "category": [ 420 | { 421 | "coding": [ 422 | { 423 | "system": "http://hl7.org/fhir/observation-category", 424 | "code": "laboratory", 425 | "display": "laboratory" 426 | } 427 | ] 428 | } 429 | ], 430 | "code": { 431 | "coding": [ 432 | { 433 | "system": "http://loinc.org", 434 | "code": "4548-4", 435 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 436 | } 437 | ], 438 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 439 | }, 440 | "subject": { 441 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 442 | }, 443 | "context": { 444 | "reference": "urn:uuid:28c69a96-084e-46df-a751-2f68fdd3f60a" 445 | }, 446 | "effectiveDateTime": "2016-04-19T23:19:08-08:00", 447 | "issued": "2016-04-19T23:19:08-08:00", 448 | "valueQuantity": { 449 | "value": 7.28094933463786, 450 | "unit": "%", 451 | "system": "http://unitsofmeasure.org", 452 | "code": "%" 453 | } 454 | } 455 | }, 456 | { 457 | "fullUrl": "urn:uuid:92d68578-ca14-492a-bfc5-9454e29ac3cf", 458 | "resource": { 459 | "resourceType": "Observation", 460 | "id": "92d68578-ca14-492a-bfc5-9454e29ac3cf", 461 | "meta": { 462 | "profile": [ 463 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 464 | ] 465 | }, 466 | "status": "final", 467 | "category": [ 468 | { 469 | "coding": [ 470 | { 471 | "system": "http://hl7.org/fhir/observation-category", 472 | "code": "laboratory", 473 | "display": "laboratory" 474 | } 475 | ] 476 | } 477 | ], 478 | "code": { 479 | "coding": [ 480 | { 481 | "system": "http://loinc.org", 482 | "code": "4548-4", 483 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 484 | } 485 | ], 486 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 487 | }, 488 | "subject": { 489 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 490 | }, 491 | "context": { 492 | "reference": "urn:uuid:8d1da6db-3a34-4fc3-b53d-6a6235a42857" 493 | }, 494 | "effectiveDateTime": "2016-07-24T23:19:08-08:00", 495 | "issued": "2016-07-24T23:19:08-08:00", 496 | "valueQuantity": { 497 | "value": 7.153390965565693, 498 | "unit": "%", 499 | "system": "http://unitsofmeasure.org", 500 | "code": "%" 501 | } 502 | } 503 | }, 504 | { 505 | "fullUrl": "urn:uuid:175fdc9b-1727-4fb4-b942-e33024ad0ddc", 506 | "resource": { 507 | "resourceType": "Observation", 508 | "id": "175fdc9b-1727-4fb4-b942-e33024ad0ddc", 509 | "meta": { 510 | "profile": [ 511 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 512 | ] 513 | }, 514 | "status": "final", 515 | "category": [ 516 | { 517 | "coding": [ 518 | { 519 | "system": "http://hl7.org/fhir/observation-category", 520 | "code": "laboratory", 521 | "display": "laboratory" 522 | } 523 | ] 524 | } 525 | ], 526 | "code": { 527 | "coding": [ 528 | { 529 | "system": "http://loinc.org", 530 | "code": "4548-4", 531 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 532 | } 533 | ], 534 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 535 | }, 536 | "subject": { 537 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 538 | }, 539 | "context": { 540 | "reference": "urn:uuid:0eed2216-5e45-4415-aa0d-1aaf004dd530" 541 | }, 542 | "effectiveDateTime": "2016-10-30T23:19:08-08:00", 543 | "issued": "2016-10-30T23:19:08-08:00", 544 | "valueQuantity": { 545 | "value": 6.983752572620736, 546 | "unit": "%", 547 | "system": "http://unitsofmeasure.org", 548 | "code": "%" 549 | } 550 | } 551 | }, 552 | { 553 | "fullUrl": "urn:uuid:7e0ee6d6-45f4-4229-aa7c-5a57c0c95523", 554 | "resource": { 555 | "resourceType": "Observation", 556 | "id": "7e0ee6d6-45f4-4229-aa7c-5a57c0c95523", 557 | "meta": { 558 | "profile": [ 559 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 560 | ] 561 | }, 562 | "status": "final", 563 | "category": [ 564 | { 565 | "coding": [ 566 | { 567 | "system": "http://hl7.org/fhir/observation-category", 568 | "code": "laboratory", 569 | "display": "laboratory" 570 | } 571 | ] 572 | } 573 | ], 574 | "code": { 575 | "coding": [ 576 | { 577 | "system": "http://loinc.org", 578 | "code": "4548-4", 579 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 580 | } 581 | ], 582 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 583 | }, 584 | "subject": { 585 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 586 | }, 587 | "context": { 588 | "reference": "urn:uuid:5b77f0f7-b408-406a-acfd-d93d0b9d7d9e" 589 | }, 590 | "effectiveDateTime": "2017-01-05T23:19:08-08:00", 591 | "issued": "2017-01-05T23:19:08-08:00", 592 | "valueQuantity": { 593 | "value": 6.84537487327645, 594 | "unit": "%", 595 | "system": "http://unitsofmeasure.org", 596 | "code": "%" 597 | } 598 | } 599 | }, 600 | { 601 | "fullUrl": "urn:uuid:3ed783da-71d5-4914-8d46-757c04a9e7de", 602 | "resource": { 603 | "resourceType": "Observation", 604 | "id": "3ed783da-71d5-4914-8d46-757c04a9e7de", 605 | "meta": { 606 | "profile": [ 607 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 608 | ] 609 | }, 610 | "status": "final", 611 | "category": [ 612 | { 613 | "coding": [ 614 | { 615 | "system": "http://hl7.org/fhir/observation-category", 616 | "code": "laboratory", 617 | "display": "laboratory" 618 | } 619 | ] 620 | } 621 | ], 622 | "code": { 623 | "coding": [ 624 | { 625 | "system": "http://loinc.org", 626 | "code": "4548-4", 627 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 628 | } 629 | ], 630 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 631 | }, 632 | "subject": { 633 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 634 | }, 635 | "context": { 636 | "reference": "urn:uuid:49bd9e15-dba4-4f3c-8f49-4be18b1fff08" 637 | }, 638 | "effectiveDateTime": "2017-04-11T23:19:08-08:00", 639 | "issued": "2017-04-11T23:19:08-08:00", 640 | "valueQuantity": { 641 | "value": 6.747846566555083, 642 | "unit": "%", 643 | "system": "http://unitsofmeasure.org", 644 | "code": "%" 645 | } 646 | } 647 | }, 648 | { 649 | "fullUrl": "urn:uuid:3fe44445-6796-4503-bddf-f2e87757b901", 650 | "resource": { 651 | "resourceType": "Observation", 652 | "id": "3fe44445-6796-4503-bddf-f2e87757b901", 653 | "meta": { 654 | "profile": [ 655 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 656 | ] 657 | }, 658 | "status": "final", 659 | "category": [ 660 | { 661 | "coding": [ 662 | { 663 | "system": "http://hl7.org/fhir/observation-category", 664 | "code": "laboratory", 665 | "display": "laboratory" 666 | } 667 | ] 668 | } 669 | ], 670 | "code": { 671 | "coding": [ 672 | { 673 | "system": "http://loinc.org", 674 | "code": "4548-4", 675 | "display": "Hemoglobin A1c/Hemoglobin.total in Blood" 676 | } 677 | ], 678 | "text": "Hemoglobin A1c/Hemoglobin.total in Blood" 679 | }, 680 | "subject": { 681 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 682 | }, 683 | "context": { 684 | "reference": "urn:uuid:8d6d914f-5ab9-408c-9167-d355730b9c04" 685 | }, 686 | "effectiveDateTime": "2017-07-05T00:19:08-07:00", 687 | "issued": "2017-07-05T00:19:08-07:00", 688 | "valueQuantity": { 689 | "value": 6.582403361347973, 690 | "unit": "%", 691 | "system": "http://unitsofmeasure.org", 692 | "code": "%" 693 | } 694 | } 695 | }, 696 | { 697 | "fullUrl": "urn:uuid:8b6fffd1-a1b9-4953-bdd2-59ed91a2aa96", 698 | "resource": { 699 | "resourceType": "Observation", 700 | "id": "8b6fffd1-a1b9-4953-bdd2-59ed91a2aa96", 701 | "meta": { 702 | "profile": [ 703 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 704 | ] 705 | }, 706 | "status": "final", 707 | "category": [ 708 | { 709 | "coding": [ 710 | { 711 | "system": "http://hl7.org/fhir/observation-category", 712 | "code": "laboratory", 713 | "display": "laboratory" 714 | } 715 | ] 716 | } 717 | ], 718 | "code": { 719 | "coding": [ 720 | { 721 | "system": "http://loinc.org", 722 | "code": "2571-8", 723 | "display": "Triglycerides" 724 | } 725 | ], 726 | "text": "Triglycerides" 727 | }, 728 | "subject": { 729 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 730 | }, 731 | "context": { 732 | "reference": "urn:uuid:7a7196d2-3e48-4587-9e62-9afdefba94ec" 733 | }, 734 | "effectiveDateTime": "2015-07-01T23:19:08-08:00", 735 | "issued": "2015-07-01T23:19:08-08:00", 736 | "valueQuantity": { 737 | "value": 227.58592492772482, 738 | "unit": "mg/dL", 739 | "system": "http://unitsofmeasure.org", 740 | "code": "mg/dL" 741 | } 742 | } 743 | }, 744 | { 745 | "fullUrl": "urn:uuid:980674c2-4cf8-4f17-b27e-35e3a0b24508", 746 | "resource": { 747 | "resourceType": "Observation", 748 | "id": "980674c2-4cf8-4f17-b27e-35e3a0b24508", 749 | "meta": { 750 | "profile": [ 751 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 752 | ] 753 | }, 754 | "status": "final", 755 | "category": [ 756 | { 757 | "coding": [ 758 | { 759 | "system": "http://hl7.org/fhir/observation-category", 760 | "code": "laboratory", 761 | "display": "laboratory" 762 | } 763 | ] 764 | } 765 | ], 766 | "code": { 767 | "coding": [ 768 | { 769 | "system": "http://loinc.org", 770 | "code": "2571-8", 771 | "display": "Triglycerides" 772 | } 773 | ], 774 | "text": "Triglycerides" 775 | }, 776 | "subject": { 777 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 778 | }, 779 | "context": { 780 | "reference": "urn:uuid:7b7e749b-32a7-4080-bf12-8aa183e8a5a8" 781 | }, 782 | "effectiveDateTime": "2015-10-07T23:19:08-08:00", 783 | "issued": "2015-10-07T23:19:08-08:00", 784 | "valueQuantity": { 785 | "value": 301.08436698689684, 786 | "unit": "mg/dL", 787 | "system": "http://unitsofmeasure.org", 788 | "code": "mg/dL" 789 | } 790 | } 791 | }, 792 | { 793 | "fullUrl": "urn:uuid:d85b6670-0f94-4122-9c3a-a3ac7b47b7f6", 794 | "resource": { 795 | "resourceType": "Observation", 796 | "id": "d85b6670-0f94-4122-9c3a-a3ac7b47b7f6", 797 | "meta": { 798 | "profile": [ 799 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 800 | ] 801 | }, 802 | "status": "final", 803 | "category": [ 804 | { 805 | "coding": [ 806 | { 807 | "system": "http://hl7.org/fhir/observation-category", 808 | "code": "laboratory", 809 | "display": "laboratory" 810 | } 811 | ] 812 | } 813 | ], 814 | "code": { 815 | "coding": [ 816 | { 817 | "system": "http://loinc.org", 818 | "code": "2571-8", 819 | "display": "Triglycerides" 820 | } 821 | ], 822 | "text": "Triglycerides" 823 | }, 824 | "subject": { 825 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 826 | }, 827 | "context": { 828 | "reference": "urn:uuid:f7e95d10-6f93-469e-b30a-cbd3412b9b5c" 829 | }, 830 | "effectiveDateTime": "2016-01-13T23:19:08-08:00", 831 | "issued": "2016-01-13T23:19:08-08:00", 832 | "valueQuantity": { 833 | "value": 229.76114531165564, 834 | "unit": "mg/dL", 835 | "system": "http://unitsofmeasure.org", 836 | "code": "mg/dL" 837 | } 838 | } 839 | }, 840 | { 841 | "fullUrl": "urn:uuid:67fc766c-a1ac-4cda-a59e-87d50f05c81d", 842 | "resource": { 843 | "resourceType": "Observation", 844 | "id": "67fc766c-a1ac-4cda-a59e-87d50f05c81d", 845 | "meta": { 846 | "profile": [ 847 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 848 | ] 849 | }, 850 | "status": "final", 851 | "category": [ 852 | { 853 | "coding": [ 854 | { 855 | "system": "http://hl7.org/fhir/observation-category", 856 | "code": "laboratory", 857 | "display": "laboratory" 858 | } 859 | ] 860 | } 861 | ], 862 | "code": { 863 | "coding": [ 864 | { 865 | "system": "http://loinc.org", 866 | "code": "2571-8", 867 | "display": "Triglycerides" 868 | } 869 | ], 870 | "text": "Triglycerides" 871 | }, 872 | "subject": { 873 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 874 | }, 875 | "context": { 876 | "reference": "urn:uuid:28c69a96-084e-46df-a751-2f68fdd3f60a" 877 | }, 878 | "effectiveDateTime": "2016-04-19T23:19:08-08:00", 879 | "issued": "2016-04-19T23:19:08-08:00", 880 | "valueQuantity": { 881 | "value": 157.38112384992252, 882 | "unit": "mg/dL", 883 | "system": "http://unitsofmeasure.org", 884 | "code": "mg/dL" 885 | } 886 | } 887 | }, 888 | { 889 | "fullUrl": "urn:uuid:e53e0961-6744-436c-a895-2fa758437005", 890 | "resource": { 891 | "resourceType": "Observation", 892 | "id": "e53e0961-6744-436c-a895-2fa758437005", 893 | "meta": { 894 | "profile": [ 895 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 896 | ] 897 | }, 898 | "status": "final", 899 | "category": [ 900 | { 901 | "coding": [ 902 | { 903 | "system": "http://hl7.org/fhir/observation-category", 904 | "code": "laboratory", 905 | "display": "laboratory" 906 | } 907 | ] 908 | } 909 | ], 910 | "code": { 911 | "coding": [ 912 | { 913 | "system": "http://loinc.org", 914 | "code": "2571-8", 915 | "display": "Triglycerides" 916 | } 917 | ], 918 | "text": "Triglycerides" 919 | }, 920 | "subject": { 921 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 922 | }, 923 | "context": { 924 | "reference": "urn:uuid:8d1da6db-3a34-4fc3-b53d-6a6235a42857" 925 | }, 926 | "effectiveDateTime": "2016-07-24T23:19:08-08:00", 927 | "issued": "2016-07-24T23:19:08-08:00", 928 | "valueQuantity": { 929 | "value": 157.19927859704245, 930 | "unit": "mg/dL", 931 | "system": "http://unitsofmeasure.org", 932 | "code": "mg/dL" 933 | } 934 | } 935 | }, 936 | { 937 | "fullUrl": "urn:uuid:5bedea82-c9d5-46b9-a270-7465589adaa2", 938 | "resource": { 939 | "resourceType": "Observation", 940 | "id": "5bedea82-c9d5-46b9-a270-7465589adaa2", 941 | "meta": { 942 | "profile": [ 943 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 944 | ] 945 | }, 946 | "status": "final", 947 | "category": [ 948 | { 949 | "coding": [ 950 | { 951 | "system": "http://hl7.org/fhir/observation-category", 952 | "code": "laboratory", 953 | "display": "laboratory" 954 | } 955 | ] 956 | } 957 | ], 958 | "code": { 959 | "coding": [ 960 | { 961 | "system": "http://loinc.org", 962 | "code": "2571-8", 963 | "display": "Triglycerides" 964 | } 965 | ], 966 | "text": "Triglycerides" 967 | }, 968 | "subject": { 969 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 970 | }, 971 | "context": { 972 | "reference": "urn:uuid:0eed2216-5e45-4415-aa0d-1aaf004dd530" 973 | }, 974 | "effectiveDateTime": "2016-10-30T23:19:08-08:00", 975 | "issued": "2016-10-30T23:19:08-08:00", 976 | "valueQuantity": { 977 | "value": 171.10228635810634, 978 | "unit": "mg/dL", 979 | "system": "http://unitsofmeasure.org", 980 | "code": "mg/dL" 981 | } 982 | } 983 | }, 984 | { 985 | "fullUrl": "urn:uuid:6ad1c677-7b09-4a43-82e7-4f538cb97430", 986 | "resource": { 987 | "resourceType": "Observation", 988 | "id": "6ad1c677-7b09-4a43-82e7-4f538cb97430", 989 | "meta": { 990 | "profile": [ 991 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 992 | ] 993 | }, 994 | "status": "final", 995 | "category": [ 996 | { 997 | "coding": [ 998 | { 999 | "system": "http://hl7.org/fhir/observation-category", 1000 | "code": "laboratory", 1001 | "display": "laboratory" 1002 | } 1003 | ] 1004 | } 1005 | ], 1006 | "code": { 1007 | "coding": [ 1008 | { 1009 | "system": "http://loinc.org", 1010 | "code": "2571-8", 1011 | "display": "Triglycerides" 1012 | } 1013 | ], 1014 | "text": "Triglycerides" 1015 | }, 1016 | "subject": { 1017 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1018 | }, 1019 | "context": { 1020 | "reference": "urn:uuid:5b77f0f7-b408-406a-acfd-d93d0b9d7d9e" 1021 | }, 1022 | "effectiveDateTime": "2017-01-05T23:19:08-08:00", 1023 | "issued": "2017-01-05T23:19:08-08:00", 1024 | "valueQuantity": { 1025 | "value": 160.00746205258304, 1026 | "unit": "mg/dL", 1027 | "system": "http://unitsofmeasure.org", 1028 | "code": "mg/dL" 1029 | } 1030 | } 1031 | }, 1032 | { 1033 | "fullUrl": "urn:uuid:0b362f75-9ea8-4663-90d1-8098f4126ebc", 1034 | "resource": { 1035 | "resourceType": "Observation", 1036 | "id": "0b362f75-9ea8-4663-90d1-8098f4126ebc", 1037 | "meta": { 1038 | "profile": [ 1039 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 1040 | ] 1041 | }, 1042 | "status": "final", 1043 | "category": [ 1044 | { 1045 | "coding": [ 1046 | { 1047 | "system": "http://hl7.org/fhir/observation-category", 1048 | "code": "laboratory", 1049 | "display": "laboratory" 1050 | } 1051 | ] 1052 | } 1053 | ], 1054 | "code": { 1055 | "coding": [ 1056 | { 1057 | "system": "http://loinc.org", 1058 | "code": "2571-8", 1059 | "display": "Triglycerides" 1060 | } 1061 | ], 1062 | "text": "Triglycerides" 1063 | }, 1064 | "subject": { 1065 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1066 | }, 1067 | "context": { 1068 | "reference": "urn:uuid:49bd9e15-dba4-4f3c-8f49-4be18b1fff08" 1069 | }, 1070 | "effectiveDateTime": "2017-04-11T23:19:08-08:00", 1071 | "issued": "2017-04-11T23:19:08-08:00", 1072 | "valueQuantity": { 1073 | "value": 158.32251590794615, 1074 | "unit": "mg/dL", 1075 | "system": "http://unitsofmeasure.org", 1076 | "code": "mg/dL" 1077 | } 1078 | } 1079 | }, 1080 | { 1081 | "fullUrl": "urn:uuid:467193b2-2319-48cb-a1a3-45e6c75142b3", 1082 | "resource": { 1083 | "resourceType": "Observation", 1084 | "id": "467193b2-2319-48cb-a1a3-45e6c75142b3", 1085 | "meta": { 1086 | "profile": [ 1087 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation" 1088 | ] 1089 | }, 1090 | "status": "final", 1091 | "category": [ 1092 | { 1093 | "coding": [ 1094 | { 1095 | "system": "http://hl7.org/fhir/observation-category", 1096 | "code": "laboratory", 1097 | "display": "laboratory" 1098 | } 1099 | ] 1100 | } 1101 | ], 1102 | "code": { 1103 | "coding": [ 1104 | { 1105 | "system": "http://loinc.org", 1106 | "code": "2571-8", 1107 | "display": "Triglycerides" 1108 | } 1109 | ], 1110 | "text": "Triglycerides" 1111 | }, 1112 | "subject": { 1113 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1114 | }, 1115 | "context": { 1116 | "reference": "urn:uuid:8d6d914f-5ab9-408c-9167-d355730b9c04" 1117 | }, 1118 | "effectiveDateTime": "2017-07-05T00:19:08-07:00", 1119 | "issued": "2017-07-05T00:19:08-07:00", 1120 | "valueQuantity": { 1121 | "value": 150.37260205560224, 1122 | "unit": "mg/dL", 1123 | "system": "http://unitsofmeasure.org", 1124 | "code": "mg/dL" 1125 | } 1126 | } 1127 | }, 1128 | { 1129 | "fullUrl": "urn:uuid:da7c28f6-11e1-4c64-9a5d-fc14de863d96", 1130 | "resource": { 1131 | "resourceType": "Observation", 1132 | "id": "da7c28f6-11e1-4c64-9a5d-fc14de863d96", 1133 | "meta": { 1134 | "profile": [ 1135 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1136 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1137 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1138 | ] 1139 | }, 1140 | "status": "final", 1141 | "category": [ 1142 | { 1143 | "coding": [ 1144 | { 1145 | "system": "http://hl7.org/fhir/observation-category", 1146 | "code": "vital-signs", 1147 | "display": "vital-signs" 1148 | } 1149 | ] 1150 | } 1151 | ], 1152 | "code": { 1153 | "coding": [ 1154 | { 1155 | "system": "http://loinc.org", 1156 | "code": "55284-4", 1157 | "display": "Blood Pressure" 1158 | } 1159 | ], 1160 | "text": "Blood Pressure" 1161 | }, 1162 | "subject": { 1163 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1164 | }, 1165 | "context": { 1166 | "reference": "urn:uuid:7a7196d2-3e48-4587-9e62-9afdefba94ec" 1167 | }, 1168 | "effectiveDateTime": "2015-07-01T23:19:08-08:00", 1169 | "issued": "2015-07-01T23:19:08-08:00", 1170 | "component": [ 1171 | { 1172 | "code": { 1173 | "coding": [ 1174 | { 1175 | "system": "http://loinc.org", 1176 | "code": "8462-4", 1177 | "display": "Diastolic Blood Pressure" 1178 | } 1179 | ], 1180 | "text": "Diastolic Blood Pressure" 1181 | }, 1182 | "valueQuantity": { 1183 | "value": 83.53764791456369, 1184 | "unit": "mmHg", 1185 | "system": "http://unitsofmeasure.org", 1186 | "code": "mmHg" 1187 | } 1188 | }, 1189 | { 1190 | "code": { 1191 | "coding": [ 1192 | { 1193 | "system": "http://loinc.org", 1194 | "code": "8480-6", 1195 | "display": "Systolic Blood Pressure" 1196 | } 1197 | ], 1198 | "text": "Systolic Blood Pressure" 1199 | }, 1200 | "valueQuantity": { 1201 | "value": 135.5873570893114, 1202 | "unit": "mmHg", 1203 | "system": "http://unitsofmeasure.org", 1204 | "code": "mmHg" 1205 | } 1206 | } 1207 | ] 1208 | } 1209 | }, 1210 | { 1211 | "fullUrl": "urn:uuid:6e0e78da-c3d0-4e5c-8b2c-81ad60d0aff5", 1212 | "resource": { 1213 | "resourceType": "Observation", 1214 | "id": "6e0e78da-c3d0-4e5c-8b2c-81ad60d0aff5", 1215 | "meta": { 1216 | "profile": [ 1217 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1218 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1219 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1220 | ] 1221 | }, 1222 | "status": "final", 1223 | "category": [ 1224 | { 1225 | "coding": [ 1226 | { 1227 | "system": "http://hl7.org/fhir/observation-category", 1228 | "code": "vital-signs", 1229 | "display": "vital-signs" 1230 | } 1231 | ] 1232 | } 1233 | ], 1234 | "code": { 1235 | "coding": [ 1236 | { 1237 | "system": "http://loinc.org", 1238 | "code": "55284-4", 1239 | "display": "Blood Pressure" 1240 | } 1241 | ], 1242 | "text": "Blood Pressure" 1243 | }, 1244 | "subject": { 1245 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1246 | }, 1247 | "context": { 1248 | "reference": "urn:uuid:7b7e749b-32a7-4080-bf12-8aa183e8a5a8" 1249 | }, 1250 | "effectiveDateTime": "2015-10-07T23:19:08-08:00", 1251 | "issued": "2015-10-07T23:19:08-08:00", 1252 | "component": [ 1253 | { 1254 | "code": { 1255 | "coding": [ 1256 | { 1257 | "system": "http://loinc.org", 1258 | "code": "8462-4", 1259 | "display": "Diastolic Blood Pressure" 1260 | } 1261 | ], 1262 | "text": "Diastolic Blood Pressure" 1263 | }, 1264 | "valueQuantity": { 1265 | "value": 84.62679292129222, 1266 | "unit": "mmHg", 1267 | "system": "http://unitsofmeasure.org", 1268 | "code": "mmHg" 1269 | } 1270 | }, 1271 | { 1272 | "code": { 1273 | "coding": [ 1274 | { 1275 | "system": "http://loinc.org", 1276 | "code": "8480-6", 1277 | "display": "Systolic Blood Pressure" 1278 | } 1279 | ], 1280 | "text": "Systolic Blood Pressure" 1281 | }, 1282 | "valueQuantity": { 1283 | "value": 136.4189854066481, 1284 | "unit": "mmHg", 1285 | "system": "http://unitsofmeasure.org", 1286 | "code": "mmHg" 1287 | } 1288 | } 1289 | ] 1290 | } 1291 | }, 1292 | { 1293 | "fullUrl": "urn:uuid:207f5cce-9b2b-4673-8b1f-46b48e4dc29f", 1294 | "resource": { 1295 | "resourceType": "Observation", 1296 | "id": "207f5cce-9b2b-4673-8b1f-46b48e4dc29f", 1297 | "meta": { 1298 | "profile": [ 1299 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1300 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1301 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1302 | ] 1303 | }, 1304 | "status": "final", 1305 | "category": [ 1306 | { 1307 | "coding": [ 1308 | { 1309 | "system": "http://hl7.org/fhir/observation-category", 1310 | "code": "vital-signs", 1311 | "display": "vital-signs" 1312 | } 1313 | ] 1314 | } 1315 | ], 1316 | "code": { 1317 | "coding": [ 1318 | { 1319 | "system": "http://loinc.org", 1320 | "code": "55284-4", 1321 | "display": "Blood Pressure" 1322 | } 1323 | ], 1324 | "text": "Blood Pressure" 1325 | }, 1326 | "subject": { 1327 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1328 | }, 1329 | "context": { 1330 | "reference": "urn:uuid:f7e95d10-6f93-469e-b30a-cbd3412b9b5c" 1331 | }, 1332 | "effectiveDateTime": "2016-01-13T23:19:08-08:00", 1333 | "issued": "2016-01-13T23:19:08-08:00", 1334 | "component": [ 1335 | { 1336 | "code": { 1337 | "coding": [ 1338 | { 1339 | "system": "http://loinc.org", 1340 | "code": "8462-4", 1341 | "display": "Diastolic Blood Pressure" 1342 | } 1343 | ], 1344 | "text": "Diastolic Blood Pressure" 1345 | }, 1346 | "valueQuantity": { 1347 | "value": 75.655037228975, 1348 | "unit": "mmHg", 1349 | "system": "http://unitsofmeasure.org", 1350 | "code": "mmHg" 1351 | } 1352 | }, 1353 | { 1354 | "code": { 1355 | "coding": [ 1356 | { 1357 | "system": "http://loinc.org", 1358 | "code": "8480-6", 1359 | "display": "Systolic Blood Pressure" 1360 | } 1361 | ], 1362 | "text": "Systolic Blood Pressure" 1363 | }, 1364 | "valueQuantity": { 1365 | "value": 116.86773188319738, 1366 | "unit": "mmHg", 1367 | "system": "http://unitsofmeasure.org", 1368 | "code": "mmHg" 1369 | } 1370 | } 1371 | ] 1372 | } 1373 | }, 1374 | { 1375 | "fullUrl": "urn:uuid:4b458b02-b8f5-47ee-bf42-92a4bd4fa101", 1376 | "resource": { 1377 | "resourceType": "Observation", 1378 | "id": "4b458b02-b8f5-47ee-bf42-92a4bd4fa101", 1379 | "meta": { 1380 | "profile": [ 1381 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1382 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1383 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1384 | ] 1385 | }, 1386 | "status": "final", 1387 | "category": [ 1388 | { 1389 | "coding": [ 1390 | { 1391 | "system": "http://hl7.org/fhir/observation-category", 1392 | "code": "vital-signs", 1393 | "display": "vital-signs" 1394 | } 1395 | ] 1396 | } 1397 | ], 1398 | "code": { 1399 | "coding": [ 1400 | { 1401 | "system": "http://loinc.org", 1402 | "code": "55284-4", 1403 | "display": "Blood Pressure" 1404 | } 1405 | ], 1406 | "text": "Blood Pressure" 1407 | }, 1408 | "subject": { 1409 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1410 | }, 1411 | "context": { 1412 | "reference": "urn:uuid:28c69a96-084e-46df-a751-2f68fdd3f60a" 1413 | }, 1414 | "effectiveDateTime": "2016-04-19T23:19:08-08:00", 1415 | "issued": "2016-04-19T23:19:08-08:00", 1416 | "component": [ 1417 | { 1418 | "code": { 1419 | "coding": [ 1420 | { 1421 | "system": "http://loinc.org", 1422 | "code": "8462-4", 1423 | "display": "Diastolic Blood Pressure" 1424 | } 1425 | ], 1426 | "text": "Diastolic Blood Pressure" 1427 | }, 1428 | "valueQuantity": { 1429 | "value": 83.02262487535631, 1430 | "unit": "mmHg", 1431 | "system": "http://unitsofmeasure.org", 1432 | "code": "mmHg" 1433 | } 1434 | }, 1435 | { 1436 | "code": { 1437 | "coding": [ 1438 | { 1439 | "system": "http://loinc.org", 1440 | "code": "8480-6", 1441 | "display": "Systolic Blood Pressure" 1442 | } 1443 | ], 1444 | "text": "Systolic Blood Pressure" 1445 | }, 1446 | "valueQuantity": { 1447 | "value": 124.3214920451465, 1448 | "unit": "mmHg", 1449 | "system": "http://unitsofmeasure.org", 1450 | "code": "mmHg" 1451 | } 1452 | } 1453 | ] 1454 | } 1455 | }, 1456 | { 1457 | "fullUrl": "urn:uuid:0066cacd-e17b-4e20-8f87-ef4b9632bd0d", 1458 | "resource": { 1459 | "resourceType": "Observation", 1460 | "id": "0066cacd-e17b-4e20-8f87-ef4b9632bd0d", 1461 | "meta": { 1462 | "profile": [ 1463 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1464 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1465 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1466 | ] 1467 | }, 1468 | "status": "final", 1469 | "category": [ 1470 | { 1471 | "coding": [ 1472 | { 1473 | "system": "http://hl7.org/fhir/observation-category", 1474 | "code": "vital-signs", 1475 | "display": "vital-signs" 1476 | } 1477 | ] 1478 | } 1479 | ], 1480 | "code": { 1481 | "coding": [ 1482 | { 1483 | "system": "http://loinc.org", 1484 | "code": "55284-4", 1485 | "display": "Blood Pressure" 1486 | } 1487 | ], 1488 | "text": "Blood Pressure" 1489 | }, 1490 | "subject": { 1491 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1492 | }, 1493 | "context": { 1494 | "reference": "urn:uuid:8d1da6db-3a34-4fc3-b53d-6a6235a42857" 1495 | }, 1496 | "effectiveDateTime": "2016-07-24T23:19:08-08:00", 1497 | "issued": "2016-07-24T23:19:08-08:00", 1498 | "component": [ 1499 | { 1500 | "code": { 1501 | "coding": [ 1502 | { 1503 | "system": "http://loinc.org", 1504 | "code": "8462-4", 1505 | "display": "Diastolic Blood Pressure" 1506 | } 1507 | ], 1508 | "text": "Diastolic Blood Pressure" 1509 | }, 1510 | "valueQuantity": { 1511 | "value": 74.02323159057394, 1512 | "unit": "mmHg", 1513 | "system": "http://unitsofmeasure.org", 1514 | "code": "mmHg" 1515 | } 1516 | }, 1517 | { 1518 | "code": { 1519 | "coding": [ 1520 | { 1521 | "system": "http://loinc.org", 1522 | "code": "8480-6", 1523 | "display": "Systolic Blood Pressure" 1524 | } 1525 | ], 1526 | "text": "Systolic Blood Pressure" 1527 | }, 1528 | "valueQuantity": { 1529 | "value": 106.04834359564369, 1530 | "unit": "mmHg", 1531 | "system": "http://unitsofmeasure.org", 1532 | "code": "mmHg" 1533 | } 1534 | } 1535 | ] 1536 | } 1537 | }, 1538 | { 1539 | "fullUrl": "urn:uuid:6c55e5bc-ccce-43e2-9e33-5aad7d10a5ae", 1540 | "resource": { 1541 | "resourceType": "Observation", 1542 | "id": "6c55e5bc-ccce-43e2-9e33-5aad7d10a5ae", 1543 | "meta": { 1544 | "profile": [ 1545 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1546 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1547 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1548 | ] 1549 | }, 1550 | "status": "final", 1551 | "category": [ 1552 | { 1553 | "coding": [ 1554 | { 1555 | "system": "http://hl7.org/fhir/observation-category", 1556 | "code": "vital-signs", 1557 | "display": "vital-signs" 1558 | } 1559 | ] 1560 | } 1561 | ], 1562 | "code": { 1563 | "coding": [ 1564 | { 1565 | "system": "http://loinc.org", 1566 | "code": "55284-4", 1567 | "display": "Blood Pressure" 1568 | } 1569 | ], 1570 | "text": "Blood Pressure" 1571 | }, 1572 | "subject": { 1573 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1574 | }, 1575 | "context": { 1576 | "reference": "urn:uuid:0eed2216-5e45-4415-aa0d-1aaf004dd530" 1577 | }, 1578 | "effectiveDateTime": "2016-10-30T23:19:08-08:00", 1579 | "issued": "2016-10-30T23:19:08-08:00", 1580 | "component": [ 1581 | { 1582 | "code": { 1583 | "coding": [ 1584 | { 1585 | "system": "http://loinc.org", 1586 | "code": "8462-4", 1587 | "display": "Diastolic Blood Pressure" 1588 | } 1589 | ], 1590 | "text": "Diastolic Blood Pressure" 1591 | }, 1592 | "valueQuantity": { 1593 | "value": 88.92102305525182, 1594 | "unit": "mmHg", 1595 | "system": "http://unitsofmeasure.org", 1596 | "code": "mmHg" 1597 | } 1598 | }, 1599 | { 1600 | "code": { 1601 | "coding": [ 1602 | { 1603 | "system": "http://loinc.org", 1604 | "code": "8480-6", 1605 | "display": "Systolic Blood Pressure" 1606 | } 1607 | ], 1608 | "text": "Systolic Blood Pressure" 1609 | }, 1610 | "valueQuantity": { 1611 | "value": 121.98162892630496, 1612 | "unit": "mmHg", 1613 | "system": "http://unitsofmeasure.org", 1614 | "code": "mmHg" 1615 | } 1616 | } 1617 | ] 1618 | } 1619 | }, 1620 | { 1621 | "fullUrl": "urn:uuid:1a205402-a1f6-4dfd-b871-fda653bc0a50", 1622 | "resource": { 1623 | "resourceType": "Observation", 1624 | "id": "1a205402-a1f6-4dfd-b871-fda653bc0a50", 1625 | "meta": { 1626 | "profile": [ 1627 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1628 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1629 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1630 | ] 1631 | }, 1632 | "status": "final", 1633 | "category": [ 1634 | { 1635 | "coding": [ 1636 | { 1637 | "system": "http://hl7.org/fhir/observation-category", 1638 | "code": "vital-signs", 1639 | "display": "vital-signs" 1640 | } 1641 | ] 1642 | } 1643 | ], 1644 | "code": { 1645 | "coding": [ 1646 | { 1647 | "system": "http://loinc.org", 1648 | "code": "55284-4", 1649 | "display": "Blood Pressure" 1650 | } 1651 | ], 1652 | "text": "Blood Pressure" 1653 | }, 1654 | "subject": { 1655 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1656 | }, 1657 | "context": { 1658 | "reference": "urn:uuid:5b77f0f7-b408-406a-acfd-d93d0b9d7d9e" 1659 | }, 1660 | "effectiveDateTime": "2017-01-05T23:19:08-08:00", 1661 | "issued": "2017-01-05T23:19:08-08:00", 1662 | "component": [ 1663 | { 1664 | "code": { 1665 | "coding": [ 1666 | { 1667 | "system": "http://loinc.org", 1668 | "code": "8462-4", 1669 | "display": "Diastolic Blood Pressure" 1670 | } 1671 | ], 1672 | "text": "Diastolic Blood Pressure" 1673 | }, 1674 | "valueQuantity": { 1675 | "value": 76.28795388054104, 1676 | "unit": "mmHg", 1677 | "system": "http://unitsofmeasure.org", 1678 | "code": "mmHg" 1679 | } 1680 | }, 1681 | { 1682 | "code": { 1683 | "coding": [ 1684 | { 1685 | "system": "http://loinc.org", 1686 | "code": "8480-6", 1687 | "display": "Systolic Blood Pressure" 1688 | } 1689 | ], 1690 | "text": "Systolic Blood Pressure" 1691 | }, 1692 | "valueQuantity": { 1693 | "value": 115.96358938293686, 1694 | "unit": "mmHg", 1695 | "system": "http://unitsofmeasure.org", 1696 | "code": "mmHg" 1697 | } 1698 | } 1699 | ] 1700 | } 1701 | }, 1702 | { 1703 | "fullUrl": "urn:uuid:f1c1ad01-860d-4de5-9c1c-4611e709a4a9", 1704 | "resource": { 1705 | "resourceType": "Observation", 1706 | "id": "f1c1ad01-860d-4de5-9c1c-4611e709a4a9", 1707 | "meta": { 1708 | "profile": [ 1709 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1710 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1711 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1712 | ] 1713 | }, 1714 | "status": "final", 1715 | "category": [ 1716 | { 1717 | "coding": [ 1718 | { 1719 | "system": "http://hl7.org/fhir/observation-category", 1720 | "code": "vital-signs", 1721 | "display": "vital-signs" 1722 | } 1723 | ] 1724 | } 1725 | ], 1726 | "code": { 1727 | "coding": [ 1728 | { 1729 | "system": "http://loinc.org", 1730 | "code": "55284-4", 1731 | "display": "Blood Pressure" 1732 | } 1733 | ], 1734 | "text": "Blood Pressure" 1735 | }, 1736 | "subject": { 1737 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1738 | }, 1739 | "context": { 1740 | "reference": "urn:uuid:49bd9e15-dba4-4f3c-8f49-4be18b1fff08" 1741 | }, 1742 | "effectiveDateTime": "2017-04-11T23:19:08-08:00", 1743 | "issued": "2017-04-11T23:19:08-08:00", 1744 | "component": [ 1745 | { 1746 | "code": { 1747 | "coding": [ 1748 | { 1749 | "system": "http://loinc.org", 1750 | "code": "8462-4", 1751 | "display": "Diastolic Blood Pressure" 1752 | } 1753 | ], 1754 | "text": "Diastolic Blood Pressure" 1755 | }, 1756 | "valueQuantity": { 1757 | "value": 75.99644475414075, 1758 | "unit": "mmHg", 1759 | "system": "http://unitsofmeasure.org", 1760 | "code": "mmHg" 1761 | } 1762 | }, 1763 | { 1764 | "code": { 1765 | "coding": [ 1766 | { 1767 | "system": "http://loinc.org", 1768 | "code": "8480-6", 1769 | "display": "Systolic Blood Pressure" 1770 | } 1771 | ], 1772 | "text": "Systolic Blood Pressure" 1773 | }, 1774 | "valueQuantity": { 1775 | "value": 122.237200794796, 1776 | "unit": "mmHg", 1777 | "system": "http://unitsofmeasure.org", 1778 | "code": "mmHg" 1779 | } 1780 | } 1781 | ] 1782 | } 1783 | }, 1784 | { 1785 | "fullUrl": "urn:uuid:4f1fbab5-12e4-4ae4-b0c1-787c9b92bd72", 1786 | "resource": { 1787 | "resourceType": "Observation", 1788 | "id": "4f1fbab5-12e4-4ae4-b0c1-787c9b92bd72", 1789 | "meta": { 1790 | "profile": [ 1791 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1792 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1793 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BloodPressure" 1794 | ] 1795 | }, 1796 | "status": "final", 1797 | "category": [ 1798 | { 1799 | "coding": [ 1800 | { 1801 | "system": "http://hl7.org/fhir/observation-category", 1802 | "code": "vital-signs", 1803 | "display": "vital-signs" 1804 | } 1805 | ] 1806 | } 1807 | ], 1808 | "code": { 1809 | "coding": [ 1810 | { 1811 | "system": "http://loinc.org", 1812 | "code": "55284-4", 1813 | "display": "Blood Pressure" 1814 | } 1815 | ], 1816 | "text": "Blood Pressure" 1817 | }, 1818 | "subject": { 1819 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1820 | }, 1821 | "context": { 1822 | "reference": "urn:uuid:8d6d914f-5ab9-408c-9167-d355730b9c04" 1823 | }, 1824 | "effectiveDateTime": "2017-07-05T00:19:08-07:00", 1825 | "issued": "2017-07-05T00:19:08-07:00", 1826 | "component": [ 1827 | { 1828 | "code": { 1829 | "coding": [ 1830 | { 1831 | "system": "http://loinc.org", 1832 | "code": "8462-4", 1833 | "display": "Diastolic Blood Pressure" 1834 | } 1835 | ], 1836 | "text": "Diastolic Blood Pressure" 1837 | }, 1838 | "valueQuantity": { 1839 | "value": 84.45359848937281, 1840 | "unit": "mmHg", 1841 | "system": "http://unitsofmeasure.org", 1842 | "code": "mmHg" 1843 | } 1844 | }, 1845 | { 1846 | "code": { 1847 | "coding": [ 1848 | { 1849 | "system": "http://loinc.org", 1850 | "code": "8480-6", 1851 | "display": "Systolic Blood Pressure" 1852 | } 1853 | ], 1854 | "text": "Systolic Blood Pressure" 1855 | }, 1856 | "valueQuantity": { 1857 | "value": 132.66469065744013, 1858 | "unit": "mmHg", 1859 | "system": "http://unitsofmeasure.org", 1860 | "code": "mmHg" 1861 | } 1862 | } 1863 | ] 1864 | } 1865 | }, 1866 | { 1867 | "fullUrl": "urn:uuid:10550d61-955f-4ddd-a1f1-bc80c4b93675", 1868 | "resource": { 1869 | "resourceType": "Observation", 1870 | "id": "10550d61-955f-4ddd-a1f1-bc80c4b93675", 1871 | "meta": { 1872 | "profile": [ 1873 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1874 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1875 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 1876 | ] 1877 | }, 1878 | "status": "final", 1879 | "category": [ 1880 | { 1881 | "coding": [ 1882 | { 1883 | "system": "http://hl7.org/fhir/observation-category", 1884 | "code": "vital-signs", 1885 | "display": "vital-signs" 1886 | } 1887 | ] 1888 | } 1889 | ], 1890 | "code": { 1891 | "coding": [ 1892 | { 1893 | "system": "http://loinc.org", 1894 | "code": "29463-7", 1895 | "display": "Body Weight" 1896 | } 1897 | ], 1898 | "text": "Body Weight" 1899 | }, 1900 | "subject": { 1901 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1902 | }, 1903 | "context": { 1904 | "reference": "urn:uuid:7a7196d2-3e48-4587-9e62-9afdefba94ec" 1905 | }, 1906 | "effectiveDateTime": "2015-07-01T23:19:08-08:00", 1907 | "issued": "2015-07-01T23:19:08-08:00", 1908 | "valueQuantity": { 1909 | "value": 103.05777708428671, 1910 | "unit": "kg", 1911 | "system": "http://unitsofmeasure.org", 1912 | "code": "kg" 1913 | } 1914 | } 1915 | }, 1916 | { 1917 | "fullUrl": "urn:uuid:917d93e4-a7a7-4195-bc96-696e8f9c2235", 1918 | "resource": { 1919 | "resourceType": "Observation", 1920 | "id": "917d93e4-a7a7-4195-bc96-696e8f9c2235", 1921 | "meta": { 1922 | "profile": [ 1923 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1924 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1925 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 1926 | ] 1927 | }, 1928 | "status": "final", 1929 | "category": [ 1930 | { 1931 | "coding": [ 1932 | { 1933 | "system": "http://hl7.org/fhir/observation-category", 1934 | "code": "vital-signs", 1935 | "display": "vital-signs" 1936 | } 1937 | ] 1938 | } 1939 | ], 1940 | "code": { 1941 | "coding": [ 1942 | { 1943 | "system": "http://loinc.org", 1944 | "code": "29463-7", 1945 | "display": "Body Weight" 1946 | } 1947 | ], 1948 | "text": "Body Weight" 1949 | }, 1950 | "subject": { 1951 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 1952 | }, 1953 | "context": { 1954 | "reference": "urn:uuid:7b7e749b-32a7-4080-bf12-8aa183e8a5a8" 1955 | }, 1956 | "effectiveDateTime": "2015-10-07T23:19:08-08:00", 1957 | "issued": "2015-10-07T23:19:08-08:00", 1958 | "valueQuantity": { 1959 | "value": 101.56909775076161, 1960 | "unit": "kg", 1961 | "system": "http://unitsofmeasure.org", 1962 | "code": "kg" 1963 | } 1964 | } 1965 | }, 1966 | { 1967 | "fullUrl": "urn:uuid:c437a568-0872-4372-bdfa-f649b67c9325", 1968 | "resource": { 1969 | "resourceType": "Observation", 1970 | "id": "c437a568-0872-4372-bdfa-f649b67c9325", 1971 | "meta": { 1972 | "profile": [ 1973 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 1974 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 1975 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 1976 | ] 1977 | }, 1978 | "status": "final", 1979 | "category": [ 1980 | { 1981 | "coding": [ 1982 | { 1983 | "system": "http://hl7.org/fhir/observation-category", 1984 | "code": "vital-signs", 1985 | "display": "vital-signs" 1986 | } 1987 | ] 1988 | } 1989 | ], 1990 | "code": { 1991 | "coding": [ 1992 | { 1993 | "system": "http://loinc.org", 1994 | "code": "29463-7", 1995 | "display": "Body Weight" 1996 | } 1997 | ], 1998 | "text": "Body Weight" 1999 | }, 2000 | "subject": { 2001 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2002 | }, 2003 | "context": { 2004 | "reference": "urn:uuid:f7e95d10-6f93-469e-b30a-cbd3412b9b5c" 2005 | }, 2006 | "effectiveDateTime": "2016-01-13T23:19:08-08:00", 2007 | "issued": "2016-01-13T23:19:08-08:00", 2008 | "valueQuantity": { 2009 | "value": 99.74492288840915, 2010 | "unit": "kg", 2011 | "system": "http://unitsofmeasure.org", 2012 | "code": "kg" 2013 | } 2014 | } 2015 | }, 2016 | { 2017 | "fullUrl": "urn:uuid:f5d1326c-0171-44aa-96f4-c0212f75f401", 2018 | "resource": { 2019 | "resourceType": "Observation", 2020 | "id": "f5d1326c-0171-44aa-96f4-c0212f75f401", 2021 | "meta": { 2022 | "profile": [ 2023 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2024 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2025 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2026 | ] 2027 | }, 2028 | "status": "final", 2029 | "category": [ 2030 | { 2031 | "coding": [ 2032 | { 2033 | "system": "http://hl7.org/fhir/observation-category", 2034 | "code": "vital-signs", 2035 | "display": "vital-signs" 2036 | } 2037 | ] 2038 | } 2039 | ], 2040 | "code": { 2041 | "coding": [ 2042 | { 2043 | "system": "http://loinc.org", 2044 | "code": "29463-7", 2045 | "display": "Body Weight" 2046 | } 2047 | ], 2048 | "text": "Body Weight" 2049 | }, 2050 | "subject": { 2051 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2052 | }, 2053 | "context": { 2054 | "reference": "urn:uuid:28c69a96-084e-46df-a751-2f68fdd3f60a" 2055 | }, 2056 | "effectiveDateTime": "2016-04-19T23:19:08-08:00", 2057 | "issued": "2016-04-19T23:19:08-08:00", 2058 | "valueQuantity": { 2059 | "value": 98.14519145250657, 2060 | "unit": "kg", 2061 | "system": "http://unitsofmeasure.org", 2062 | "code": "kg" 2063 | } 2064 | } 2065 | }, 2066 | { 2067 | "fullUrl": "urn:uuid:0e9bc2ac-a36f-4783-88cb-b8997af5b4e5", 2068 | "resource": { 2069 | "resourceType": "Observation", 2070 | "id": "0e9bc2ac-a36f-4783-88cb-b8997af5b4e5", 2071 | "meta": { 2072 | "profile": [ 2073 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2074 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2075 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2076 | ] 2077 | }, 2078 | "status": "final", 2079 | "category": [ 2080 | { 2081 | "coding": [ 2082 | { 2083 | "system": "http://hl7.org/fhir/observation-category", 2084 | "code": "vital-signs", 2085 | "display": "vital-signs" 2086 | } 2087 | ] 2088 | } 2089 | ], 2090 | "code": { 2091 | "coding": [ 2092 | { 2093 | "system": "http://loinc.org", 2094 | "code": "29463-7", 2095 | "display": "Body Weight" 2096 | } 2097 | ], 2098 | "text": "Body Weight" 2099 | }, 2100 | "subject": { 2101 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2102 | }, 2103 | "context": { 2104 | "reference": "urn:uuid:8d1da6db-3a34-4fc3-b53d-6a6235a42857" 2105 | }, 2106 | "effectiveDateTime": "2016-07-24T23:19:08-08:00", 2107 | "issued": "2016-07-24T23:19:08-08:00", 2108 | "valueQuantity": { 2109 | "value": 96.71946399675491, 2110 | "unit": "kg", 2111 | "system": "http://unitsofmeasure.org", 2112 | "code": "kg" 2113 | } 2114 | } 2115 | }, 2116 | { 2117 | "fullUrl": "urn:uuid:c5a856b1-1e73-4908-ad37-7f86eb6ba4bc", 2118 | "resource": { 2119 | "resourceType": "Observation", 2120 | "id": "c5a856b1-1e73-4908-ad37-7f86eb6ba4bc", 2121 | "meta": { 2122 | "profile": [ 2123 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2124 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2125 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2126 | ] 2127 | }, 2128 | "status": "final", 2129 | "category": [ 2130 | { 2131 | "coding": [ 2132 | { 2133 | "system": "http://hl7.org/fhir/observation-category", 2134 | "code": "vital-signs", 2135 | "display": "vital-signs" 2136 | } 2137 | ] 2138 | } 2139 | ], 2140 | "code": { 2141 | "coding": [ 2142 | { 2143 | "system": "http://loinc.org", 2144 | "code": "29463-7", 2145 | "display": "Body Weight" 2146 | } 2147 | ], 2148 | "text": "Body Weight" 2149 | }, 2150 | "subject": { 2151 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2152 | }, 2153 | "context": { 2154 | "reference": "urn:uuid:0eed2216-5e45-4415-aa0d-1aaf004dd530" 2155 | }, 2156 | "effectiveDateTime": "2016-10-30T23:19:08-08:00", 2157 | "issued": "2016-10-30T23:19:08-08:00", 2158 | "valueQuantity": { 2159 | "value": 94.82340561869287, 2160 | "unit": "kg", 2161 | "system": "http://unitsofmeasure.org", 2162 | "code": "kg" 2163 | } 2164 | } 2165 | }, 2166 | { 2167 | "fullUrl": "urn:uuid:13343787-adb0-4887-bcb4-028b560e93da", 2168 | "resource": { 2169 | "resourceType": "Observation", 2170 | "id": "13343787-adb0-4887-bcb4-028b560e93da", 2171 | "meta": { 2172 | "profile": [ 2173 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2174 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2175 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2176 | ] 2177 | }, 2178 | "status": "final", 2179 | "category": [ 2180 | { 2181 | "coding": [ 2182 | { 2183 | "system": "http://hl7.org/fhir/observation-category", 2184 | "code": "vital-signs", 2185 | "display": "vital-signs" 2186 | } 2187 | ] 2188 | } 2189 | ], 2190 | "code": { 2191 | "coding": [ 2192 | { 2193 | "system": "http://loinc.org", 2194 | "code": "29463-7", 2195 | "display": "Body Weight" 2196 | } 2197 | ], 2198 | "text": "Body Weight" 2199 | }, 2200 | "subject": { 2201 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2202 | }, 2203 | "context": { 2204 | "reference": "urn:uuid:5b77f0f7-b408-406a-acfd-d93d0b9d7d9e" 2205 | }, 2206 | "effectiveDateTime": "2017-01-05T23:19:08-08:00", 2207 | "issued": "2017-01-05T23:19:08-08:00", 2208 | "valueQuantity": { 2209 | "value": 93.2767498668678, 2210 | "unit": "kg", 2211 | "system": "http://unitsofmeasure.org", 2212 | "code": "kg" 2213 | } 2214 | } 2215 | }, 2216 | { 2217 | "fullUrl": "urn:uuid:8c36b903-7237-442a-86cc-e476906a5111", 2218 | "resource": { 2219 | "resourceType": "Observation", 2220 | "id": "8c36b903-7237-442a-86cc-e476906a5111", 2221 | "meta": { 2222 | "profile": [ 2223 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2224 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2225 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2226 | ] 2227 | }, 2228 | "status": "final", 2229 | "category": [ 2230 | { 2231 | "coding": [ 2232 | { 2233 | "system": "http://hl7.org/fhir/observation-category", 2234 | "code": "vital-signs", 2235 | "display": "vital-signs" 2236 | } 2237 | ] 2238 | } 2239 | ], 2240 | "code": { 2241 | "coding": [ 2242 | { 2243 | "system": "http://loinc.org", 2244 | "code": "29463-7", 2245 | "display": "Body Weight" 2246 | } 2247 | ], 2248 | "text": "Body Weight" 2249 | }, 2250 | "subject": { 2251 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2252 | }, 2253 | "context": { 2254 | "reference": "urn:uuid:49bd9e15-dba4-4f3c-8f49-4be18b1fff08" 2255 | }, 2256 | "effectiveDateTime": "2017-04-11T23:19:08-08:00", 2257 | "issued": "2017-04-11T23:19:08-08:00", 2258 | "valueQuantity": { 2259 | "value": 92.1866701988928, 2260 | "unit": "kg", 2261 | "system": "http://unitsofmeasure.org", 2262 | "code": "kg" 2263 | } 2264 | } 2265 | }, 2266 | { 2267 | "fullUrl": "urn:uuid:50c6ef8e-d845-4ccc-a38a-dd3ddda736eb", 2268 | "resource": { 2269 | "resourceType": "Observation", 2270 | "id": "50c6ef8e-d845-4ccc-a38a-dd3ddda736eb", 2271 | "meta": { 2272 | "profile": [ 2273 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-finding-Observation", 2274 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-VitalSign", 2275 | "http://standardhealthrecord.org/fhir/StructureDefinition/shr-vital-BodyWeight" 2276 | ] 2277 | }, 2278 | "status": "final", 2279 | "category": [ 2280 | { 2281 | "coding": [ 2282 | { 2283 | "system": "http://hl7.org/fhir/observation-category", 2284 | "code": "vital-signs", 2285 | "display": "vital-signs" 2286 | } 2287 | ] 2288 | } 2289 | ], 2290 | "code": { 2291 | "coding": [ 2292 | { 2293 | "system": "http://loinc.org", 2294 | "code": "29463-7", 2295 | "display": "Body Weight" 2296 | } 2297 | ], 2298 | "text": "Body Weight" 2299 | }, 2300 | "subject": { 2301 | "reference": "urn:uuid:02b639cb-83f2-4574-9bcc-a1544bf2b57c" 2302 | }, 2303 | "context": { 2304 | "reference": "urn:uuid:8d6d914f-5ab9-408c-9167-d355730b9c04" 2305 | }, 2306 | "effectiveDateTime": "2017-07-05T00:19:08-07:00", 2307 | "issued": "2017-07-05T00:19:08-07:00", 2308 | "valueQuantity": { 2309 | "value": 90.33750168296515, 2310 | "unit": "kg", 2311 | "system": "http://unitsofmeasure.org", 2312 | "code": "kg" 2313 | } 2314 | } 2315 | } 2316 | ] 2317 | } -------------------------------------------------------------------------------- /testData/test-encryption-keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "explanation": "index 0-39 are users", 3 | "public_encryption_keys": [ 4 | "g8R3h9lGMiXkX7o8pxqkkOn1ZO/sE0GDT/daKBSsN1A=", 5 | "hTs0qGu2HI99eTOeThYm3WYtHyzgG4mmQSlqzM6Hmzk=", 6 | "RIpXrakuAx1yZZKi0DqN2Go1XNHHmu/u7+nL1KHi6W0=", 7 | "PthBEeg40wI5P8i+AEoVqmzghDYmKxQXMFcvPNp4RHQ=", 8 | "66qJ/NJihkHDb7NZlzca5rpgpeNV0+zXRA0ASAdAIm4=", 9 | "WKPj2e8zWd/+8Yk5Pc/tuYFVDPCpMHThynrqE/OJOQU=", 10 | "69FZrnZyFTScw92WOcyOIc3HSvYJL8FWUNn8KpFbh1k=", 11 | "GmBEohp+H0YI6Mo/orkxUXb+R2AqrhiSU8i3mRVfmCs=", 12 | "/Dg8cW/ARgaF/fDJ+i87mCMYoJ0GHFZj2D7c9T4SpTY=", 13 | "idto4mtgg+E2cj4JVcAUuQSvS4WVJwjTXh98C030DBE=", 14 | "ZH3bJ2lirv9omngE4WkOCr7jhxu+QiZM+HjVYD9bqiE=", 15 | "zLoa/rYrIGPUTNLu62ykjzikTXnDQ/5JEqk4QzuSHAY=", 16 | "ADmBz45n8B8+5O/m7sT9Hg4zshWSLdnPxta2ZRIdB08=", 17 | "BFCUZ7PZptVXMuE+TZR833S60PJ6WXP4mFHOYkIYcDY=", 18 | "GNzdp32VxRRMHEdwZFJIqG4leHrHClA2XqaATpbBPCk=", 19 | "eWUE4ANHnZP8EQB3pv2jZy+/Co2VeEqaJtgIcXqKSzk=", 20 | "6d90aJl3gZCcREXMdXednBUovO+twQKS8BS917X+q1E=", 21 | "22MlvXJViOtNhq/m//bETLyCdgSktHW3dCq12Mmb5RA=", 22 | "DQcaF2YNRqPqNajmv76z1Jx2IigNXgvxwSN6pWg2cgY=", 23 | "4YVGs2HCDerZx1ZlyOwvd7As8VxfXg6cfp3JBB9H4Go=", 24 | "4ofsopYSNcOy1su6/xPb6YVfTicE2qIQRm9XuSv+7QY=", 25 | "IJNdjQFb/wVvPeUs7Qnex/cfefHEnIsybFjrVslyE2c=", 26 | "zbxz7L7Io7NOehIt/h1G+NUsxhJggS1bhO2OImp7UBs=", 27 | "vILyyjlB6o4IK2rBvRHKT/5Z5ptPiUtDOFTnX55dRU8=", 28 | "r8Rv1o13N0BSZ9klTp39f+lv/LAFmfwbzAkaJV1khz4=", 29 | "naAwBwZ2bxZ7eUlrGSeWksmEYimdJba0fsl2yRqhX1E=", 30 | "1/1vp2G2TJlE/+Gwk5oznPo/cN5IrlovUvxkU6sQ7AU=", 31 | "W+4mIw9xzG7AdCFUqJOmQKjPf8SQSeaK1ybBvBUIqzg=", 32 | "WStscNH71cfnWR/bXWUeDwyoUNVw1bhnctpZQCRsUCo=", 33 | "NRhQuVCgEjiQrOMZqLR4gMpjOelcfVFvgr9sBO+LtH8=", 34 | "A3nemKiZRlqGKd1QcXuMDUpypvfJ/L58d0eu2r8mUyM=", 35 | "b4GoYuxy7+gt63wPhm4iys0l5Ssus23JngVIDnIWTAs=", 36 | "1rLRV6/QLT3ijN2ZYEZLPQgfMzuNS0+rSBFKAOEFsDM=", 37 | "DM0OsOMJ5DwYJXu7PSSZrdpmLG3iYdaqW4YHmWc3H38=", 38 | "pq/QTst5P1MYT8G3rdF2EQDt2uSgDgCS7dxJstO3xAQ=", 39 | "stDBQ5hIZrtYH0upsn98IsPxZJiXOPtmSAuJv/ep9DU=", 40 | "GGITGMwrQ/xsz3dlNbb+spNIABnkCEzxEiWP9/AJez4=", 41 | "kIAD7pxi+OEcB9KiBKw4g0K5gr/yTYz6BQiExuCjET0=", 42 | "4iGtBfY5+O9cGuJKV+o69v/6zlQguiQ8+MK1FG1sgB4=", 43 | "yRyUPGvgeiDXSEPZpsfT28qaHgI3VxboCuP7Gj2sZmA=" 44 | ] 45 | } -------------------------------------------------------------------------------- /testData/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const {promisify} = require('util'); 4 | // const Stow = require('@stowprotocol/stow-js'); 5 | const Stow = require('@linniaprotocol/linnia-js'); 6 | 7 | const readdir = promisify(fs.readdir); 8 | const {ipfs, web3} = require('./config'); 9 | 10 | const encrypt = async (publicKey, data) => { 11 | const encrypted = await Stow.util.encrypt( 12 | publicKey, 13 | data, 14 | ); 15 | return encrypted; 16 | }; 17 | 18 | const ipfsPush = async (encrypted) => { 19 | const ipfsHash = await new Promise((resolve, reject) => { 20 | ipfs.add(JSON.stringify(encrypted), (ipfsErr, ipfsRed) => { 21 | ipfsErr ? reject(ipfsErr) : resolve(ipfsRed); 22 | }); 23 | }); 24 | return ipfsHash; 25 | }; 26 | 27 | const getFiles = async () => { 28 | const dataFolder = path.resolve(path.join(__dirname, '..', 'testData/synthetic_patients_data')); 29 | let files = await readdir(dataFolder); 30 | files = files.map(fname => `./synthetic_patients_data/${ fname}`); 31 | return files; 32 | }; 33 | 34 | const getAccounts = async () => { 35 | const accounts = await web3.eth.getAccounts(); 36 | return accounts; 37 | }; 38 | 39 | module.exports = {encrypt, ipfsPush, getFiles, getAccounts}; 40 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | require('babel-register')({ 2 | ignore: /node_modules\/(?!openzeppelin-solidity)/ 3 | }); 4 | require('babel-polyfill'); 5 | 6 | module.exports = { 7 | networks: { 8 | development: { 9 | host: 'localhost', 10 | port: 7545, 11 | gas: 6000000, 12 | network_id: 5777 13 | }, 14 | coverage: { 15 | host: "localhost", 16 | network_id: "*", 17 | port: 8555, 18 | gas: 0xfffffffffff, 19 | gasPrice: 0x01 20 | }, 21 | } 22 | }; 23 | --------------------------------------------------------------------------------