├── .circleci └── config.yml ├── .eslintignore ├── .gitignore ├── .jsdoc.json ├── AUDIT.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER_GUIDE.md ├── LICENSE ├── README.md ├── _config.yml ├── audit-ci.json ├── contracts └── .gitkeep ├── doc ├── NonceManager.html ├── escrow.js.html ├── global.html ├── idv-registry.js.html ├── index.html ├── logger_index.js.html ├── module-escrow.html ├── module-idvRegistry.html ├── module-ontology.html ├── module-pricing.html ├── module-support_asserts.html ├── module-support_errors-InvalidNonceError.html ├── module-support_errors.html ├── module-support_nonceManager-Account.html ├── module-support_nonceManager.html ├── module-support_nonce_nonceManager.html ├── module-support_tx.html ├── ontology.js.html ├── pricing.js.html ├── scripts │ ├── collapse.js │ ├── jquery-3.1.1.min.js │ ├── linenumber.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ └── search.js ├── styles │ ├── jsdoc.css │ └── prettify.css ├── support_asserts.js.html ├── support_coin.js.html ├── support_errors.js.html ├── support_nonce.js.html ├── support_nonce_index.js.html ├── support_nonce_manager.js.html ├── support_nonce_setup.js.html ├── support_sender.js.html ├── support_transactionDetails.js.html ├── support_tx.js.html ├── support_util.js.html └── token.js.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── config │ └── index.js ├── escrow.js ├── idv-registry.js ├── logger │ ├── index.js │ └── setup.js ├── marketplace-tx.js ├── ontology.js ├── pricing.js ├── support │ ├── asserts.js │ ├── coin.js │ ├── constants.js │ ├── errors.js │ ├── nonce │ │ ├── accountInspector.js │ │ ├── index.js │ │ ├── inmemory.js │ │ ├── setup.js │ │ └── util.js │ ├── sender.js │ ├── transactionDetails.js │ ├── tx.js │ └── util.js └── token.js └── test ├── assets └── contracts │ └── mockContract.json ├── escrow.js ├── integration ├── 00-error-mapping-and-initialisation.js ├── 01-tx-send.js ├── 02-coin.js ├── 03-token.js ├── 04-escrow-funds.js ├── 05-ontology.js ├── 06-validator-registry.js ├── 07-pricing.js ├── 08-transaction-details.js ├── docker-ganache │ └── docker-compose.yml ├── docker-geth │ └── docker-compose.yml ├── signtx.js └── users.js ├── nonce.js ├── ontology.js ├── pricing.js ├── sender.js ├── token.js ├── tx.js └── util.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | - &update_npm_on_machine 3 | run: npm install npm@6.6.0 -g 4 | - &cache_restore 5 | restore_cache: 6 | keys: 7 | - v1-dependencies-{{ checksum "package.json" }}-{{checksum "package-lock.json" }} 8 | - &cache_save 9 | save_cache: 10 | paths: 11 | - node_modules 12 | key: v1-dependencies-{{ checksum "package.json" }}-{{checksum "package-lock.json" }} 13 | - &install_node 14 | # set node version to 8.9.0 to support the get-latest-contracts script. 15 | # This is more complicated than it needs to be 16 | # because each 'run' script runs in its own shell. If we switch to docker we can remove this 17 | # see https://discuss.circleci.com/t/how-to-change-node-version-in-circleci-2-0/17455/4 18 | run: 19 | name: Install node@8.9.0 20 | command: | 21 | set +e 22 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.5/install.sh | bash 23 | export NVM_DIR="/opt/circleci/.nvm" 24 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 25 | nvm install v8.9.0 26 | nvm alias default v8.9.0 27 | # Each step uses the same `$BASH_ENV`, so need to modify it 28 | echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV 29 | echo "[ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\"" >> $BASH_ENV 30 | 31 | version: 2 32 | workflows: 33 | version: 2 34 | build-and-test: 35 | jobs: 36 | - build 37 | - build-marketplace-tx-server-latest: 38 | context: Development 39 | requires: 40 | - build 41 | filters: 42 | branches: 43 | only: master 44 | - build-marketplace-tx-server-test: 45 | context: Development 46 | requires: 47 | - build 48 | filters: 49 | branches: 50 | only: release 51 | 52 | integration-test: 53 | jobs: 54 | - integration-test-on-geth 55 | 56 | jobs: 57 | build: 58 | docker: 59 | - image: circleci/node:8.15 60 | - image: trufflesuite/ganache-cli 61 | command: [ganache, -m, 'drum muscle maximum umbrella night initial prevent auction pink old fault media', -h, '0.0.0.0', -l, '7992181', -g, '1000000000', -e, '1000'] 62 | 63 | working_directory: ~/repo 64 | 65 | steps: 66 | - checkout 67 | - *cache_restore 68 | - run: npm ci 69 | - run: npm run audit-ci 70 | - *cache_save 71 | - run: npm run check-ci 72 | 73 | integration-test-on-geth: 74 | # use machine rather than docker as the tests use docker-compose (https://circleci.com/docs/2.0/docker-compose/) 75 | machine: true 76 | 77 | working_directory: ~/repo 78 | 79 | steps: 80 | - checkout 81 | - *cache_restore 82 | - *install_node 83 | - *update_npm_on_machine 84 | - run: npm ci 85 | - *cache_save 86 | - run: 87 | name: Pull latest docker images 88 | command: | 89 | docker-compose -f test/integration/docker-geth/docker-compose.yml pull 90 | - run: npm run integration-test-geth 91 | 92 | build-marketplace-tx-server-latest: 93 | docker: 94 | - image: circleci/node:8.15 95 | 96 | steps: 97 | - run: curl -d "build_parameters[CIRCLE_JOB]=build-docker-latest" "https://circleci.com/api/v1.1/project/github/civicteam/civic_js_node_server/tree/dev?circle-token=${CIRCLE_TOKEN}" 98 | 99 | build-marketplace-tx-server-test: 100 | docker: 101 | - image: circleci/node:8.15 102 | 103 | steps: 104 | - run: curl -d "build_parameters[CIRCLE_JOB]=build-docker-test" "https://circleci.com/api/v1.1/project/github/civicteam/civic_js_node_server/tree/master?circle-token=${CIRCLE_TOKEN}" 105 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | dist 4 | node_modules 5 | 6 | # Integration test output from geth node 7 | /geth.log 8 | /.nyc_output/ 9 | 10 | # Logs 11 | logs 12 | *.log 13 | 14 | # Directories used by tools like mocha & istanbul 15 | coverage 16 | shippable 17 | 18 | # Shared artifacts 19 | /contracts/**/*.json -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "template": "node_modules/docdash" 4 | }, 5 | "plugins": [ 6 | "plugins/markdown" 7 | ], 8 | "source": { 9 | "includePattern": ".+\\.js(doc)?$", 10 | "include": ["./src", "./README.md"] 11 | } 12 | } -------------------------------------------------------------------------------- /AUDIT.md: -------------------------------------------------------------------------------- 1 | This project uses [npm audit](https://docs.npmjs.com/cli/audit) to scan dependencies for vulnerabilities 2 | and automatically install any compatible updates to vulnerable dependencies. 3 | The security audit is also integrated into the project's CI pipeline via [audit-ci](https://github.com/IBM/audit-ci) command 4 | which fails the build if there is any vulnerability found. 5 | It is possible to ignore specific errors by whitelisting them in [audit-ci config.](./audit-ci.json). 6 | 7 | ## NPM audit whitelist 8 | Whenever you whitelist a specific advisory it is required to refer it here and justify the whitelisting. 9 | 10 | ### Advisories 11 | 12 | | # | Level | Module | Title | Explanation | 13 | |------|-------|---------|------|-------------| 14 | | 782 | Moderate | lodash | Prototype Pollution | Dev dependency, path: smart-contracts: > truffle-artifactor > lodash | 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Open Source Code of Conduct 2 | This code of conduct outlines our expectations for participants within the Identity.com Open Source community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 3 | 4 | 5 | # Our open source community strives to: 6 | 7 | 8 | * Be friendly and patient. 9 | * Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 10 | * Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. 11 | * Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. 12 | * Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: 13 | * Violent threats or language directed against another person. 14 | * Discriminatory jokes and language. 15 | * Posting sexually explicit or violent material. 16 | * Posting (or threatening to post) other people’s personally identifying information (“doxing”). 17 | * Personal insults, especially those using racist or sexist terms. 18 | * Unwelcome sexual attention. 19 | * Advocating for, or encouraging, any of the above behavior. 20 | * Repeated harassment of others. In general, if someone asks you to stop, then stop. 21 | * When we disagree, try to understand why: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. 22 | * Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 23 | 24 | 25 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. 26 | 27 | 28 | # Diversity Statement 29 | We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. 30 | 31 | 32 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. 33 | 34 | 35 | # Reporting Issues 36 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via https://www.identity.com/about/contact-us/. All reports will be handled with discretion. In your report please include: 37 | 38 | * Your contact information. 39 | * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 40 | * Any additional information that may be helpful. 41 | 42 | 43 | After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. 44 | 45 | 46 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | We’re still working out the kinks to make contributing to this project as easy and transparent as possible, but we’re not quite there yet. Hopefully this document makes the process for contributing clear and answers some questions that you may have. 3 | 4 | # Code of Conduct 5 | Identity.com has adopted a [Code of Conduct](CODE_OF_CONDUCT.md) that we expect project participants to adhere to. Please read the full text so that you can understand what actions will and will not be tolerated. 6 | 7 | # Contributors & Team Members Development 8 | Most internal changes made by Civic & Identity.com engineers will be synced to GitHub. Civic & Identity.com use this code in production. The team is likely to have an enterprise version of the code containing specific environment details and that part is not synced with the code here. 9 | Changes from the community are handled through GitHub pull requests which go through our review process. Once a change made on GitHub is approved, it will be imported to the Civic & Identity.com internal repositories. 10 | 11 | # Branch Organization 12 | We will do our best to keep the master branch in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We recommend that you use the latest stable and published version. 13 | If you send a pull request, please do it against the master branch. We maintain stable branches for major versions separately.We accept pull requests against latest major branch directly if it is a bugfix related to its version. This fix will be also applied to the master branch by the Core team. 14 | 15 | # Semantic Versioning 16 | This software follows semantic versioning. We release patch versions for bug fixes, minor versions for new features, and major versions for any breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance. 17 | Every significant change is documented in the changelog file. 18 | 19 | # Bugs 20 | ## Where to Find Known Issues 21 | We are using GitHub Issues for our public bugs. Core team will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist. 22 | ## Reporting New Issues 23 | The best way to get your bug fixed is to provide a reduced test case. 24 | How to Get in Touch 25 | 26 | GitHub Issues: Create a ticket with the specific tag: [question] ; [feature request]; [suggestion] ; [discussion] 27 | Identity.com website contact form 28 | 29 | # Proposing a Change 30 | If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend filing an issue. This lets us reach an agreement on your proposal before you put significant effort into it. 31 | If you’re only fixing a bug, it’s fine to submit a pull request right away but we still recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. 32 | 33 | # Your First Pull Request 34 | Working on your first Pull Request? You can learn how from this free video series: 35 | [How to Contribute](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) to an Open Source Project on GitHub 36 | If you decide to fix an issue, please be sure to check the comment thread in case somebody is already working on a fix. If nobody is working on it at the moment, please leave a comment stating that you intend to work on it so other people don’t accidentally duplicate your effort. 37 | If somebody claims an issue but doesn’t follow up for more than two weeks, it’s fine to take it over but you should still leave a comment. 38 | 39 | # Sending a Pull Request 40 | The Identity.com team is monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation. 41 | Before submitting a pull request, please make sure the following is done: 42 | Fork the repository and create your branch from master. 43 | Run `npm install` in the repository root. 44 | If you’ve fixed a bug or added code that should be tested, add tests! 45 | 46 | To ensure your tests pass, your code follows the formatting and style guides, and 47 | your PR does not reduce the code coverage, run `npm run check`. 48 | 49 | If you haven’t already, complete the CLA. 50 | 51 | # Contributor License Agreement (CLA) 52 | By contributing to Identity.com projects, you agree that your contributions will be licensed under its MIT license. 53 | Contribution Prerequisites 54 | 55 | # Prerequisites 56 | Please follow [README](README.md) and [DEVELOPER_GUIDE](DEVELOPER_GUIDE.md) instructions. 57 | 58 | # Development Workflow 59 | Please follow [README](README.md) and [DEVELOPER_GUIDE](DEVELOPER_GUIDE.md) instructions. 60 | 61 | # Style Guide 62 | We use an automatic code formatter called ESLint. Run `npm run lint` after making any changes to the code. 63 | Then, our linter will catch most issues that may exist in your code. 64 | However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at Airbnb’s Style Guide will guide you in the right direction. 65 | 66 | # License 67 | By contributing to Identity.com projects, you agree that your contributions will be licensed under its MIT license. 68 | -------------------------------------------------------------------------------- /DEVELOPER_GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Developer Guide 2 | 3 | ### Testing 4 | 5 | Run all tests locally with: 6 | 7 | ``` 8 | npm install 9 | npm run check 10 | ``` 11 | 12 | This will run unit tests, integration tests, coverage checks and the linter. 13 | 14 | Note - the integration tests use [ganache-cli](https://github.com/trufflesuite/ganache-cli) and require docker. 15 | 16 | If you get "Invalid nonce values" when running the tests, ensure your ganache docker container and volume 17 | were shut down and deleted correctly after the tests ran. 18 | 19 | To run the integration tests without docker, start up your own blockchain node at localhost:8545 and 20 | run `npm run test-blockchain`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Identity.com 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 | # marketplace-tx 2 | 3 | This is the JavaScript client library for the Identity.com Marketplace. It provides an interface for all capabilities on the blockchain, including accessing smart contract data, sending transactions (modifying blockchain data, smart contract state), exchanging CVC tokens and exchanging ETH (aka platform coin). 4 | 5 | [API Documentation](https://identity-com.github.io/marketplace-tx-js/doc/index.html) 6 | 7 | ## Getting started 8 | 9 | ### Terminology 10 | 11 | We use the standard Identity.com terms for roles on the network: 12 | 13 | - __IDR__: ID requester 14 | 15 | - __IDV__: ID validator 16 | 17 | - __Scope request ID__: The UUID for PII request made by IDR to IDV, on the blockchain represented by a bytes32 string (64 hex chars) 18 | 19 | - __Credential Item__: Single piece of PII, can be claim (atom) or credential (molecule) 20 | 21 | - __Credential Item Internal ID__: Credential items are identified by external or internal ID. The Internal ID is stored in the blockchain (bytes32 string of 64 hex chars) and is the keccak256 of the external ID. 22 | 23 | - __Credential Item External ID__: The External ID consists of `Type, Name and Version`, using dash as a separator, e.g. `credential-ProofOfIdentity-v1.0`. 24 | 25 | ### Installing 26 | 27 | For development, fetch the library from github: [https://github.com/identity-com/marketplace-tx-js](https://github.com/identity-com/marketplace-tx-js) 28 | 29 | The library is not yet listed on npmjs.org. 30 | 31 | Integrate with your application using: 32 | 33 | ```js 34 | const MarketplaceTx = require('marketplace-tx'); 35 | ``` 36 | 37 | #### Using in browser 38 | The library uses ES2017 features therefore it may need to be transpiled before it can be used in browser. 39 | If you are using Babel, you need to use the [env](https://babeljs.io/docs/en/env) preset (or similar). 40 | Add the correct preset to your `.babelrc` config. 41 | 42 | ```js 43 | { 44 | "presets": ["env"] 45 | } 46 | ``` 47 | 48 | #### Using in React app 49 | In order to integrate the MarketplaceTx into your React application you might use [react-app-rewired](https://github.com/timarney/react-app-rewired) library. 50 | It allows to tweak the create-react-app webpack config without using 'eject' and without creating a fork of the react-scripts. 51 | 52 | First add the correct presets to your `.baberc` config. 53 | 54 | ```js 55 | { 56 | "presets": ["env", "react"] 57 | } 58 | ``` 59 | 60 | Then add the `config-overrides.js` file and tweak the Babel loader as following: 61 | 62 | ```js 63 | const path = require('path'); 64 | const fs = require('fs'); 65 | const rewireBabelLoader = require('react-app-rewire-babel-loader'); 66 | const { getBabelLoader } = require('react-app-rewired'); 67 | const appDirectory = fs.realpathSync(process.cwd()); 68 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 69 | 70 | module.exports = function override(config, env) { 71 | // Make Babel loader read the configuration from .babelrc file. 72 | const babelLoader = getBabelLoader(config.module.rules); 73 | babelLoader.options.babelrc = true; 74 | 75 | config = rewireBabelLoader.include( 76 | config, 77 | resolveApp('node_modules/marketplace-tx') 78 | ); 79 | 80 | return config; 81 | }; 82 | ``` 83 | 84 | ### Use with Infura 85 | 86 | MarketplaceTx is currently *not* compatible with Infura, as it requires access to the `txpool` 87 | Ethereum RPC API for nonce management. 88 | 89 | ### Asynchronous calls 90 | 91 | The library returns a `Promise` on any async call. Use `async/await` or `.then().catch()` according to your environment. 92 | 93 | ### Initialising 94 | 95 | Before using the library, you should: 96 | 97 | ```js 98 | const MarketplaceTx = require('marketplace-tx'); 99 | const marketplaceTx = new MarketplaceTx({web3}); 100 | ``` 101 | 102 | Where ```web3``` is a valid web3 object connected to a node. 103 | 104 | ### Configuration 105 | 106 | MarketplaceTx is configured by the file config/.json, where STAGE is passed in as an environment variable. 107 | 108 | Alternatively, it is possible to pass in config to the constructor: 109 | 110 | The MarketplaceTx constructor accepts two arguments: 111 | 1. context - to inject dependencies, like web3, logger, etc. 112 | 2. config - to provide configuration parameters 113 | 114 | 115 | ```js 116 | const context = { web3 }; 117 | const config = { ... }; 118 | const marketplaceTx = new MarketplaceTx(context, config); 119 | ``` 120 | 121 | ### Logging 122 | 123 | MarketplaceTx will log automatically to the console. To use your own logger: 124 | 125 | ```js 126 | const logger = winston(); 127 | const marketplaceTx = new MarketplaceTx({web3, logger}, config); 128 | ``` 129 | 130 | ### Contracts 131 | 132 | MarketplaceTx requires contract artifacts - JSON files produced by [https://github.com/identity-com/smart-contracts](Marketplace Smart Contracts library) containing contract name, ABI, addresses on specified networks. 133 | You can specify the path to to the artifacts directory by passing it to the config upon the initialisation: 134 | 135 | ```js 136 | const config = { contracts: { dir: 'contracts/' } }; 137 | const marketplaceTx = new MarketplaceTx(context, config); 138 | ``` 139 | 140 | It is also possible to refer the artifacts by providing absolute URL: 141 | ```js 142 | const config = { contracts: { url: 'https://s3.amazonaws.com/' } }; 143 | const marketplaceTx = new MarketplaceTx(context, config); 144 | ``` 145 | 146 | ## Usage 147 | 148 | ### Structure 149 | 150 | The library is structured into these sub-modules: 151 | 152 | See the [API Documentation](https://identity-com.github.io/marketplace-tx-js/doc/index.html) for more details 153 | 154 | #### Core modules 155 | 156 | ```marketplaceTx.escrow``` for placing tokens into and releasing them from the escrow contract 157 | 158 | ```marketplaceTx.idv-registry``` for adding and managing registered IDVs 159 | 160 | ```marketplaceTx.ontology``` for managing the ontology of attestation types 161 | 162 | ```marketplaceTx.pricing``` for contributing and querying prices 163 | 164 | ```marketplaceTx.token``` for transferring tokens and querying balances 165 | 166 | #### Support modules 167 | 168 | ```marketplaceTx.asserts``` containing input validation functions 169 | 170 | ```marketplaceTx.nonce``` for managing nonces for externally signed transactions 171 | 172 | ```marketplaceTx.tx``` for creating transactions on smart contracts (used by other submodules, discouraged to use directly) 173 | 174 | ```marketplaceTx.sender``` for sending transactions to the blockchain (used by other submodules, discouraged to use directly) 175 | 176 | ```marketplaceTx.coin``` for transferring platform coin and querying balances 177 | 178 | ```marketplaceTx.transactionDetails``` for looking up specific transaction details 179 | 180 | ```marketplaceTx.util``` utilities for handling conversion between types, etc. 181 | 182 | ### Creating transactions 183 | 184 | Transaction object (rawTx) can be created by the ```marketplaceTx.tx``` module. 185 | Nonce management is done by the ```marketplaceTx.nonce``` module. It respects the ethereum node's txpool 186 | and uses a nonce acquire/release mechanism to allow sequential chains of transactions 187 | in case you require strict order of execution. 188 | 189 | Note - to use the nonce manager, the node needs access to the ethereum `txpool RPC interface. 190 | 191 | ### Signing transactions 192 | 193 | Typically with ```web3```, transactions are signed by a local ethereum client node (with private keys being stored in the node). 194 | For phone and distributed apps, this is not ideal. 195 | 196 | We have elected to sign transactions in the library before submitting the transactions to a cluster of nodes. 197 | 198 | The library makes no assumption about how this will be done. Functions that need to sign a transaction take a function that must return a promise: 199 | 200 | ```js 201 | const signingFunction = (address, rawTx) => {....}; 202 | ``` 203 | 204 | Parameter ```address``` is the address that should sign, ```rawTx``` is a JSON transactions (or array of JSON transactions) 205 | 206 | This function asynchronously signs the transaction and returns it through the promise (if ```rawTx``` is an array, then the return type is an array of signed transactions). The returned value(s) will be passed to ```web3.eth.sendRawTransaction```. A typical implementation is: 207 | 208 | ```js 209 | const EthTx = require('ethereumjs-tx') 210 | 211 | // const rawTx = {nonce: <>, gasLimit: <>, to: <>, value: <>, ....} 212 | 213 | const signingFunction = (address, rawTx) => { 214 | return new Promise ((resolve, reject) => { 215 | const ethtx = new EthTx(rawTx) 216 | ethtx.sign(Buffer.from(privateKey, 'hex')) 217 | const hex = ethtx.serialize().toString('hex'); 218 | resolve(hex); 219 | }); 220 | } 221 | ``` 222 | 223 | ## Other details 224 | 225 | ### Sending transactions 226 | 227 | ```marketplaceTx.sender``` module is responsible for sending transactions. It accepts an object with tx parameters and uses ```marketplaceTx.tx``` to create transaction. If an optional signTx parameter is passed, tx will be signed externally. ```send``` function returns a promise which resolves to tx hash and rejects in case something went wrong (i.e. signing failed, chainID is wrong, insufficient wei etc.). 228 | 229 | ### Mining transactions 230 | 231 | ```marketplaceTx.tx.waitForMine``` function can be used to check that a transaction was successfully mined. It expects a promise which resolves to a tx hash (a return value of ```marketplaceTx.sender.send``` could be used) and polls the blockchain each 0.5 sec for 2 minutes until the tx receipt is returned. Then it asserts that the tx status is a success. 232 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky -------------------------------------------------------------------------------- /audit-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "low": true, 3 | "package-manager": "auto", 4 | "report": true, 5 | "advisories": [782], 6 | "whitelist": [] 7 | } 8 | -------------------------------------------------------------------------------- /contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/identity-com/marketplace-tx-js/d8baf4cb78526eba1e869fbeb38328983ffa5689/contracts/.gitkeep -------------------------------------------------------------------------------- /doc/logger_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | logger/index.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

logger/index.js

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
/**
45 |  * Exports a logger created by marketplace-tx initialisation
46 |  * so that it is available cleanly to all other modules.
47 |  *
48 |  * This file should be imported only after setup has been
49 |  * called for the first time with a logger:
50 |  *
51 |  * require('./logger/setup')(logger);
52 |  */
53 | module.exports = require('./setup')();
54 | 
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | 65 |
66 | 67 |
68 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /doc/module-support_errors-InvalidNonceError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | InvalidNonceError - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

InvalidNonceError

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 |

47 | support/errors~ 48 | 49 | InvalidNonceError 50 |

51 | 52 |

There is a mined transaction with the same nonce from given account.

53 | 54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 | 64 |

Constructor

65 | 66 | 67 |

new InvalidNonceError()

68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | 76 | 77 |
Source:
78 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 |
164 | 165 |
166 | 167 | 168 | 169 | 170 | 171 | 172 |
173 | 174 |
175 | 176 |
177 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:53 GMT+0200 (EET) using the docdash theme. 178 |
179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /doc/module-support_errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/errors - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

support/errors

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |

Classes

67 | 68 |
69 |
InvalidNonceError
70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |

Methods

82 | 83 | 84 | 85 | 86 | 87 | 88 |

(inner) mapError(error) → {CvcError|Error}

89 | 90 | 91 | 92 | 93 | 94 | 95 |
96 | 97 | 98 |
Source:
99 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | 138 | 139 |
140 |

Maps a blockchain error and returns an instance of CvcError when possible. 141 | This allows to implement error normalisation. 142 | Unknown errors are propagated without modifications.

143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
Parameters:
156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
NameTypeDescription
error 184 | 185 | 186 | Error 187 | 188 | 189 | 190 |

A blockchain error object.

202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 |
Returns:
217 | 218 | 219 |
220 |

Mapped error object.

221 |
222 | 223 | 224 | 225 |
226 |
227 | Type 228 |
229 |
230 | 231 | CvcError 232 | | 233 | 234 | Error 235 | 236 | 237 |
238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 |
250 | 251 |
252 | 253 | 254 | 255 | 256 | 257 | 258 |
259 | 260 |
261 | 262 |
263 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:53 GMT+0200 (EET) using the docdash theme. 264 |
265 | 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /doc/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | $("nav > ul > li > ul li").hide(); 4 | 5 | //only current page (if it exists) should be opened 6 | var file = window.location.pathname.split("/").pop(); 7 | $("nav > ul > li > a[href^='"+file+"']").parent().find("> ul li").show(); 8 | } 9 | $( document ).ready(function() { 10 | hideAllButCurrent(); 11 | }); -------------------------------------------------------------------------------- /doc/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /doc/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /doc/scripts/search.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | jQuery.expr[':'].Contains = function(a,i,m){ 3 | return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0; 4 | }; 5 | //on search 6 | $("#nav-search").on("keyup", function(event) { 7 | var search = $(this).val(); 8 | if (!search) { 9 | //no search, show all results 10 | $("nav > ul > li").show(); 11 | 12 | if(typeof hideAllButCurrent === "function"){ 13 | //let's do what ever collapse wants to do 14 | hideAllButCurrent(); 15 | } 16 | else{ 17 | //menu by default should be opened 18 | $("nav > ul > li > ul li").show(); 19 | } 20 | } 21 | else{ 22 | //we are searching 23 | //show all parents 24 | $("nav > ul > li").show(); 25 | //hide all results 26 | $("nav > ul > li > ul li").hide(); 27 | //show results matching filter 28 | $("nav > ul > li > ul").find("a:Contains("+search+")").parent().show(); 29 | //hide parents without children 30 | $("nav > ul > li").each(function(){ 31 | if($(this).find("a:Contains("+search+")").length == 0 && $(this).children("ul").length === 0){ 32 | //has no child at all and does not contain text 33 | $(this).hide(); 34 | } 35 | else if($(this).find("a:Contains("+search+")").length == 0 && $(this).find("ul").children(':visible').length == 0){ 36 | //has no visible child and does not contain text 37 | $(this).hide(); 38 | } 39 | }); 40 | } 41 | }); 42 | }); -------------------------------------------------------------------------------- /doc/styles/prettify.css: -------------------------------------------------------------------------------- 1 | .pln { 2 | color: #ddd; 3 | } 4 | 5 | /* string content */ 6 | .str { 7 | color: #61ce3c; 8 | } 9 | 10 | /* a keyword */ 11 | .kwd { 12 | color: #fbde2d; 13 | } 14 | 15 | /* a comment */ 16 | .com { 17 | color: #aeaeae; 18 | } 19 | 20 | /* a type name */ 21 | .typ { 22 | color: #8da6ce; 23 | } 24 | 25 | /* a literal value */ 26 | .lit { 27 | color: #fbde2d; 28 | } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #ddd; 33 | } 34 | 35 | /* lisp open bracket */ 36 | .opn { 37 | color: #000000; 38 | } 39 | 40 | /* lisp close bracket */ 41 | .clo { 42 | color: #000000; 43 | } 44 | 45 | /* a markup tag name */ 46 | .tag { 47 | color: #8da6ce; 48 | } 49 | 50 | /* a markup attribute name */ 51 | .atn { 52 | color: #fbde2d; 53 | } 54 | 55 | /* a markup attribute value */ 56 | .atv { 57 | color: #ddd; 58 | } 59 | 60 | /* a declaration */ 61 | .dec { 62 | color: #EF5050; 63 | } 64 | 65 | /* a variable name */ 66 | .var { 67 | color: #c82829; 68 | } 69 | 70 | /* a function name */ 71 | .fun { 72 | color: #4271ae; 73 | } 74 | 75 | /* Specify class=linenums on a pre to get line numbering */ 76 | ol.linenums { 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | } 80 | -------------------------------------------------------------------------------- /doc/support_asserts.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/asserts.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

support/asserts.js

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
/** @module support/asserts */
 45 | const asserts = {};
 46 | module.exports = asserts;
 47 | 
 48 | const { TOTAL_SUPPLY, CREDENTIAL_ITEM_TYPES } = require('./constants');
 49 | 
 50 | /**
 51 |  * @alias module:support/asserts.assertAmount
 52 |  * @memberOf support/asserts
 53 |  * @description Asserts amount for sanity check.
 54 |  * @param {number} amount The escrow amount in creds.
 55 |  * @returns {number}
 56 |  */
 57 | asserts.assertAmount = amount => {
 58 |   if (amount < 1 || amount > TOTAL_SUPPLY) {
 59 |     throw new Error(`Amount ${amount} is out of range (1-${TOTAL_SUPPLY})`);
 60 |   }
 61 | 
 62 |   return amount;
 63 | };
 64 | 
 65 | /**
 66 |  * @alias module:support/asserts.assertCredentialItems
 67 |  * @memberOf support/asserts
 68 |  * @description Asserts credential items array is not empty.
 69 |  * @param {Array} credentialItems An array containing credential item IDs
 70 |  * @return {Array}
 71 |  */
 72 | asserts.assertCredentialItems = credentialItems => {
 73 |   if (!credentialItems || !credentialItems.length) {
 74 |     throw new Error('Credential items must be non-empty array');
 75 |   }
 76 | 
 77 |   return credentialItems;
 78 | };
 79 | 
 80 | /**
 81 |  * @alias module:support/asserts.assertCredentialItemPrice
 82 |  * @memberOf support/asserts
 83 |  * @description Asserts that provided number matches valid price criteria.
 84 |  * @returns {number}
 85 |  * @param price
 86 |  */
 87 | asserts.assertCredentialItemPrice = price => {
 88 |   if (price < 0 || price > TOTAL_SUPPLY) {
 89 |     throw new Error(`Price ${price} is out of range (0-${TOTAL_SUPPLY})`);
 90 |   }
 91 | 
 92 |   return price;
 93 | };
 94 | 
 95 | /**
 96 |  * @alias module:support/asserts.assertAddress
 97 |  * @memberOf support/asserts
 98 |  * @description Checks if provided string is a valid ETH address.
 99 |  * @param {string} addressToTest
100 |  * @returns {string}
101 |  */
102 | asserts.assertAddress = addressToTest => {
103 |   if (!asserts.web3.isAddress(addressToTest)) {
104 |     throw new Error(`Address (${addressToTest}) is not a valid ETH address`);
105 |   }
106 | 
107 |   return addressToTest;
108 | };
109 | 
110 | /**
111 |  * @alias module:support/asserts.assertCredentialItemType
112 |  * @memberOf support/asserts
113 |  * @description Allows only certain credential item types
114 |  * @param {string} type
115 |  * @return {string}
116 |  */
117 | asserts.assertCredentialItemType = type => {
118 |   if (!CREDENTIAL_ITEM_TYPES.includes(type)) {
119 |     throw new Error(`Credential item type '${type}' is not supported`);
120 |   }
121 | 
122 |   return type;
123 | };
124 | 
125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 137 |
138 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 139 |
140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /doc/support_coin.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/coin.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

support/coin.js

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
const coin = {};
 45 | module.exports = coin;
 46 | 
 47 | const util = require('util');
 48 | // This shim is necessary so that marketplaceTx can be imported in the browser
 49 | // See https://github.com/serverless-heaven/serverless-webpack/issues/291#issuecomment-348790713
 50 | // See https://github.com/alexjlockwood/avocado/commit/7455bea1052c4d271fe0e6db59f3fb3efdd0349d
 51 | require('util.promisify').shim();
 52 | 
 53 | const tx = require('./tx');
 54 | const sender = require('./sender');
 55 | const logger = require('../logger');
 56 | 
 57 | /**
 58 |  * Returns platform coin balance for given address.
 59 |  * @param address
 60 |  * @returns {Promise<any>}
 61 |  */
 62 | coin.getBalance = address => util.promisify(cb => tx.web3.eth.getBalance(address, cb))();
 63 | 
 64 | /**
 65 |  * Returns platform coin balances for multiple addresses.
 66 |  * @param users
 67 |  * @returns {Promise<any[]>}
 68 |  */
 69 | coin.getBalances = users =>
 70 |   Promise.all(users.map(user => coin.getBalance(user.address).then(balance => Object.assign({}, user, { balance }))));
 71 | 
 72 | /**
 73 |  *
 74 |  * @param {string} fromAddress - The address to send the coins from.
 75 |  * @param {function} signTx - The callback to use to sign the transaction.
 76 |  * @param {string} toAddress - The address to send the coins to.
 77 |  * @param {int} value - The amount of coins to send
 78 |  * @returns {Promise<{transactionHash}>}
 79 |  */
 80 | coin.transfer = async function(fromAddress, signTx, toAddress, value) {
 81 |   try {
 82 |     return await sender.sendPlatformCoin({ fromAddress, signTx, toAddress, value });
 83 |   } catch (error) {
 84 |     logger.error(`Error transferring platform coin: ${error.message}`);
 85 |     throw error;
 86 |   }
 87 | };
 88 | 
89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 99 |
100 | 101 |
102 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /doc/support_errors.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/errors.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

support/errors.js

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
/** @module support/errors */
 45 | const { BaseError } = require('make-error-cause');
 46 | 
 47 | class CvcError extends BaseError {}
 48 | 
 49 | /**
 50 |  * There is a mined transaction with the same nonce from given account.
 51 |  */
 52 | class InvalidNonceError extends CvcError {
 53 |   constructor(cause) {
 54 |     super('Invalid nonce value', cause);
 55 |   }
 56 | }
 57 | 
 58 | class FailedTxChainError extends CvcError {
 59 |   constructor(transactions, cause) {
 60 |     super(`Failed to send ${transactions.length} chained transactions`, cause);
 61 |     this.transactions = transactions;
 62 |   }
 63 | }
 64 | 
 65 | class NotDeployedError extends CvcError {
 66 |   constructor(address) {
 67 |     super(`No code deployed at address ${address}.`);
 68 |   }
 69 | }
 70 | 
 71 | class NoNetworkInContractError extends CvcError {
 72 |   constructor(contractName, cause) {
 73 |     super(`Could not detect '${contractName}' in network.`, cause);
 74 |   }
 75 | }
 76 | 
 77 | class SignerSenderAddressMismatchError extends CvcError {
 78 |   constructor(signer, sender) {
 79 |     super(`Expected from sender address ${sender} does not match actual signing ${signer} address.`);
 80 |   }
 81 | }
 82 | 
 83 | class NotFoundError extends CvcError {}
 84 | 
 85 | /**
 86 |  * Maps a blockchain error and returns an instance of CvcError when possible.
 87 |  * This allows to implement error normalisation.
 88 |  * Unknown errors are propagated without modifications.
 89 |  * @param {Error} error A blockchain error object.
 90 |  * @returns {CvcError|Error} Mapped error object.
 91 |  */
 92 | function mapError(error) {
 93 |   // Prevent wrapping mapped errors.
 94 |   if (error instanceof CvcError) return error;
 95 |   // Check for invalid nonce error.
 96 |   if (/nonce|replacement\stransaction\sunderpriced|known\stransaction/.test(error)) return new InvalidNonceError(error);
 97 | 
 98 |   return error;
 99 | }
100 | 
101 | module.exports = {
102 |   mapError,
103 |   CvcError,
104 |   InvalidNonceError,
105 |   FailedTxChainError,
106 |   NotDeployedError,
107 |   NoNetworkInContractError,
108 |   SignerSenderAddressMismatchError,
109 |   NotFoundError
110 | };
111 | 
112 |
113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 |
121 | 122 |
123 | 124 |
125 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /doc/support_nonce_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/nonce/index.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 30 | 31 |
32 | 33 |

support/nonce/index.js

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |
/**
44 |  * Exports a nonce manager created by marketplace-tx initialisation
45 |  * so that it is available cleanly to all other modules.
46 |  *
47 |  * This file should be imported only after setup has been
48 |  * called for the first time with required dependencies:
49 |  *
50 |  * require('./support/nonce/setup')(web3, nonceManager);
51 |  */
52 | module.exports = require('./setup')();
53 | 
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 |
65 | 66 |
67 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /doc/support_util.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | support/util.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 31 | 32 |
33 | 34 |

support/util.js

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
const util = {};
 45 | module.exports = util;
 46 | 
 47 | const Bn = require('bignumber.js');
 48 | const { ONE_CVC } = require('./constants');
 49 | 
 50 | util.bnToHexString = number => `0x${number.toString(16)}`;
 51 | 
 52 | util.hexToString = hex => {
 53 |   let s = '';
 54 |   for (let c = 2; c < hex.length; c += 2) {
 55 |     const ch = parseInt(hex.substr(c, 2), 16);
 56 |     if (ch === 0) break;
 57 |     s += String.fromCharCode(ch);
 58 |   }
 59 |   return s;
 60 | };
 61 | 
 62 | /**
 63 |  * @param n
 64 |  * @return {Bn}
 65 |  */
 66 | util.bnToCVC = n => new Bn(n).div(ONE_CVC);
 67 | 
 68 | /**
 69 |  * @param n
 70 |  * @return {Bn}
 71 |  */
 72 | util.CVCToBN = n => new Bn(n).mul(ONE_CVC);
 73 | 
 74 | class TimeoutError extends Error {}
 75 | 
 76 | /**
 77 |  * Rejects the promise after specified timeout.
 78 |  * @param promise Promise to apply timeout for.
 79 |  * @param ms Timeout in milliseconds
 80 |  * @param msg
 81 |  * @returns {Promise<any>}
 82 |  */
 83 | util.timeout = (promise, ms, msg) => {
 84 |   let timerId = null;
 85 |   // Create timer promise, with will be rejected after specified timeout.
 86 |   const timer = new Promise((resolve, reject) => {
 87 |     timerId = setTimeout(() => {
 88 |       reject(new TimeoutError(msg));
 89 |     }, ms);
 90 |   });
 91 |   // Ensure timeout handle released.
 92 |   const clear = () => clearTimeout(timerId);
 93 |   promise.then(clear, clear);
 94 | 
 95 |   return Promise.race([promise, timer]);
 96 | };
 97 | util.timeout.Error = TimeoutError;
 98 | 
99 |
100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 |
110 | 111 |
112 | Documentation generated by JSDoc 3.5.5 on Mon Jan 14 2019 16:11:52 GMT+0200 (EET) using the docdash theme. 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/marketplace-tx'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marketplace-tx", 3 | "version": "1.0.0", 4 | "description": "JS Client library for the Identity.com marketplace", 5 | "main": "src/marketplace-tx.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "check-ci": "npm-run-all test-with-coverage build-integration-test test-blockchain-with-coverage coverage lint", 11 | "check": "npm-run-all test-with-coverage integration-test-ganache coverage lint", 12 | "eslint": "eslint --max-warnings=0 src test", 13 | "lint-autofix": "eslint --fix src test", 14 | "coverage": "cross-env FORCE_COLOR=1 nyc report && nyc check-coverage", 15 | "lint": "npm run eslint", 16 | "test": "cross-env NODE_ENV=test mocha test/*.js", 17 | "test-with-coverage": "cross-env NODE_ENV=test nyc --reporter=none mocha test/*.js", 18 | "doc": "jsdoc -r -c .jsdoc.json -d doc", 19 | "build-integration-test": "cd node_modules/identity-com-smart-contracts && npm i && npm run build && cp artifacts/deployed/*.json ../../contracts", 20 | "test-blockchain": "cross-env LOCAL=true TX_SIGNING_TIMEOUT=100 NODE_ENV=test ETH_NODE_URL='http://localhost:8545' mocha test/integration --timeout 120000", 21 | "test-blockchain-with-coverage": "cross-env TX_SIGNING_TIMEOUT=100 LOCAL=true NODE_ENV=test ETH_NODE_URL='http://localhost:8545' nyc --reporter=none mocha test/integration --timeout 10000", 22 | "ganache-up": "docker-compose -f test/integration/docker-ganache/docker-compose.yml up -d > ganache.log", 23 | "ganache-down": "docker-compose -f test/integration/docker-ganache/docker-compose.yml down -v", 24 | "geth-up": "docker-compose -f test/integration/docker-geth/docker-compose.yml up -d > geth.log ", 25 | "geth-down": "docker-compose -f test/integration/docker-geth/docker-compose.yml down -v", 26 | "preintegration-test-ganache": "npm-run-all ganache-up build-integration-test", 27 | "integration-test-ganache": "npm-run-all test-blockchain-with-coverage ganache-down --continue-on-error", 28 | "preintegration-test-geth": "npm-run-all geth-up build-integration-test", 29 | "integration-test-geth": "npm-run-all test-blockchain geth-down --continue-on-error", 30 | "audit-ci": "audit-ci --config audit-ci.json" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/identity-com/marketplace-tx-js.git" 35 | }, 36 | "author": "", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/identity-com/marketplace-tx-js/issues" 40 | }, 41 | "homepage": "https://github.com/identity-com/marketplace-tx-js#readme", 42 | "dependencies": { 43 | "babel-register": "^6.26.0", 44 | "bignumber.js": "^4.0.4", 45 | "cross-fetch": "^3.0.1", 46 | "ethereumjs-tx": "^1.3.3", 47 | "ethereumjs-util": "^5.1.2", 48 | "js-cache": "^1.0.2", 49 | "lodash": "^4.17.11", 50 | "make-error-cause": "^2.0.0", 51 | "serialize-error": "^2.1.0", 52 | "truffle-contract": "^3.0.4", 53 | "util.promisify": "^1.0.0", 54 | "web3": "^0.20.1", 55 | "web3admin": "github:identity-com/web3admin#0.0.1" 56 | }, 57 | "devDependencies": { 58 | "audit-ci": "^1.3.0", 59 | "chai": "^4.1.2", 60 | "chai-as-promised": "^7.1.1", 61 | "chai-bignumber": "^2.0.2", 62 | "cross-env": "^5.1.3", 63 | "docdash": "^1.0.0", 64 | "eslint": "^4.18.2", 65 | "eslint-config-airbnb-base": "^12.1.0", 66 | "eslint-config-prettier": "^2.9.0", 67 | "eslint-plugin-import": "^2.8.0", 68 | "eslint-plugin-named-unassigned-functions": "0.0.2", 69 | "eslint-plugin-prettier": "^2.5.0", 70 | "fetch-mock": "^7.0.7", 71 | "identity-com-smart-contracts": "github:identity-com/smart-contracts", 72 | "jsdoc": "3.5.5", 73 | "longjohn": "^0.2.12", 74 | "mocha": "^4.1.0", 75 | "node-fetch": "^2.3.0", 76 | "npm-run-all": "^4.1.5", 77 | "nyc": "^13.3.0", 78 | "prettier": "1.10.2", 79 | "proxyquire": "^2.1.0", 80 | "sinon": "^6.0.1", 81 | "web3-fake-provider": "^0.1.0" 82 | }, 83 | "nyc": { 84 | "lines": 80, 85 | "statements": 80, 86 | "functions": 80, 87 | "branches": 60, 88 | "exclude": [ 89 | "test/*" 90 | ] 91 | }, 92 | "eslintConfig": { 93 | "extends": [ 94 | "airbnb-base", 95 | "plugin:prettier/recommended" 96 | ], 97 | "plugins": [ 98 | "prettier", 99 | "named-unassigned-functions" 100 | ], 101 | "env": { 102 | "node": true, 103 | "mocha": true 104 | }, 105 | "globals": { 106 | "artifacts": true 107 | }, 108 | "rules": { 109 | "max-len": [ 110 | "error", 111 | { 112 | "code": 120 113 | } 114 | ], 115 | "func-names": [ 116 | "warn", 117 | "never" 118 | ], 119 | "no-plusplus": [ 120 | "error", 121 | { 122 | "allowForLoopAfterthoughts": true 123 | } 124 | ], 125 | "no-use-before-define": [ 126 | "error", 127 | { 128 | "functions": false, 129 | "classes": true 130 | } 131 | ], 132 | "prettier/prettier": [ 133 | "error", 134 | { 135 | "printWidth": 120, 136 | "singleQuote": true 137 | } 138 | ] 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | // This is default marketplaceTx config. 4 | // You can overwrite these values by passing custom config to MarketplaceTx constructor. 5 | const configure = passedInConfig => 6 | _.merge( 7 | { 8 | contracts: { 9 | dir: '../../contracts' 10 | }, 11 | preloadContracts: true, // by default, check that the contracts exist on startup 12 | gasPrice: 1e9, // 1 gwei 13 | txMiningTimeout: 120, // 2 min (set in seconds) 14 | txSigningTimeout: 60000, // 1 min (set in milliseconds) 15 | 16 | // Block number in which first marketplace contract deployed (zero by default for test networks i.e. ganache). 17 | // Can be used as a starting point for event filtering to speed up the process. 18 | marketplaceDeploymentBlock: 0 19 | }, 20 | passedInConfig 21 | ); 22 | 23 | // Config is stored as a singleton so you can pass in custom values only once in your app 24 | let singletonConfigObject = null; 25 | 26 | module.exports = (passedInConfig = {}) => { 27 | if (!singletonConfigObject) { 28 | singletonConfigObject = configure(passedInConfig); 29 | } 30 | 31 | return singletonConfigObject; 32 | }; 33 | -------------------------------------------------------------------------------- /src/idv-registry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module idvRegistry 3 | * 4 | * @description Functions to register and retrieve ID Validators in Identity.com 5 | * */ 6 | const idvRegistry = {}; 7 | module.exports = idvRegistry; 8 | 9 | const _ = require('lodash'); 10 | const { assertAddress } = require('./support/asserts'); 11 | const tx = require('./support/tx'); 12 | const sender = require('./support/sender'); 13 | const { CONTRACT_VALIDATOR_REGISTRY } = require('./support/constants'); 14 | 15 | // Default IDV mining gas limit 16 | const IDV_REGISTRY_SET_GAS_LIMIT = 250000; 17 | 18 | /** 19 | * Converts an array returned from web3 to a IDV data object. 20 | * @param name 21 | * @param description 22 | * @returns {{name: *, description: *}} 23 | */ 24 | const mapIdvRegistryRecord = ([name, description]) => ({ 25 | name, 26 | description 27 | }); 28 | 29 | /** 30 | * Verifies 31 | * @param record 32 | * @return {*} 33 | */ 34 | const assertIdvRegistryRecord = record => { 35 | if (record.name === '') { 36 | throw new Error('IDV record does not exist'); 37 | } 38 | }; 39 | 40 | /** 41 | * @alias module:idvRegistry.set 42 | * @memberOf idvRegistry 43 | * @description Adds a new identity validator record or updates the existing one. 44 | * @param {string} fromAddress - The transaction sender address. 45 | * @param {function} signTx - Transaction signing function. 46 | * @param {string} idvAddress - The identity validator address. 47 | * @param {string} idvName - The identity validator name. 48 | * @param {string} idvDescription - The identity validator description. 49 | * @param {object} txOptions - transaction options. 50 | * @param {number} [txOptions.nonce] - The transaction sequence number. 51 | * @param {number} [txOptions.gas] - The gas value provided by the sender. 52 | * @param {number} [txOptions.gasPrice] - The gas price value provided by the sender in Wei. 53 | * @param {number} [txOptions.chainId] - The network chain id according to EIP-155. 54 | * @return {Promise<{ transactionHash: string }>} A promise of the transaction hash. 55 | */ 56 | idvRegistry.set = function(fromAddress, signTx, idvAddress, idvName, idvDescription, txOptions = {}) { 57 | // Merging txOptions with gas override 58 | const updatedTxOptions = _.merge({}, { gas: IDV_REGISTRY_SET_GAS_LIMIT }, txOptions); 59 | assertAddress(fromAddress); 60 | assertAddress(idvAddress); 61 | return sender.send({ 62 | fromAddress, 63 | signTx, 64 | contractName: CONTRACT_VALIDATOR_REGISTRY, 65 | method: 'set', 66 | params: [idvAddress, idvName, idvDescription], 67 | txOptions: updatedTxOptions 68 | }); 69 | }; 70 | 71 | /** 72 | * @alias module:idvRegistry.get 73 | * @memberOf idvRegistry 74 | * @description Returns the identity validator entry. 75 | * @param {string} idvAddress - The identity validator address. 76 | * @returns {Promise<{name: string, description: string}>} - A promise of the identity validator details. 77 | */ 78 | idvRegistry.get = async function(idvAddress) { 79 | assertAddress(idvAddress); 80 | const idvRegistryContract = await tx.contractInstance(CONTRACT_VALIDATOR_REGISTRY); 81 | const idvRegistryRecord = mapIdvRegistryRecord(await idvRegistryContract.get(idvAddress)); 82 | assertIdvRegistryRecord(idvRegistryRecord); 83 | 84 | return { address: idvAddress, ...idvRegistryRecord }; 85 | }; 86 | 87 | /** 88 | * @alias module:idvRegistry.exists 89 | * @memberOf idvRegistry 90 | * @description Verifies whether an identity validator is registered. 91 | * @param {string} idvAddress - The identity validator address. 92 | * @returns {Promise} - A promise of identity validator status. 93 | */ 94 | idvRegistry.exists = async function(idvAddress) { 95 | assertAddress(idvAddress); 96 | const idvRegistryContract = await tx.contractInstance(CONTRACT_VALIDATOR_REGISTRY); 97 | 98 | return idvRegistryContract.exists(idvAddress); 99 | }; 100 | -------------------------------------------------------------------------------- /src/logger/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports a logger created by marketplace-tx initialisation 3 | * so that it is available cleanly to all other modules. 4 | * 5 | * This file should be imported only after setup has been 6 | * called for the first time with a logger: 7 | * 8 | * require('./logger/setup')(logger); 9 | */ 10 | module.exports = require('./setup')(); 11 | -------------------------------------------------------------------------------- /src/logger/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /** 3 | * Sets up a logger by storing a singleton and exporting it on future calls. 4 | * If a logger is passed in, it replaces the current singleton. 5 | * The initial value for the singleton is the console. 6 | * 7 | */ 8 | 9 | // proxy the console, allowing for the fact that console.debug does not 10 | // exist on older node versions 11 | const defaultLogger = { 12 | error: (...args) => console.error(...args), 13 | warn: (...args) => console.warn(...args), 14 | info: (...args) => console.info(...args), 15 | debug: (...args) => (console.debug ? console.debug(...args) : console.info(...args)) 16 | }; 17 | 18 | let logger = defaultLogger; 19 | 20 | module.exports = newLogger => { 21 | logger = newLogger || logger; 22 | 23 | logger.debugLogResolvedValue = message => result => { 24 | logger.debug(message, result); 25 | return result; 26 | }; 27 | 28 | logger.debugLogTap = (...messages) => result => { 29 | logger.debug(...messages); 30 | return result; 31 | }; 32 | 33 | return logger; 34 | }; 35 | -------------------------------------------------------------------------------- /src/marketplace-tx.js: -------------------------------------------------------------------------------- 1 | const web3admin = require('web3admin'); 2 | 3 | function MarketplaceTx({ web3, logger, nonceManager }, config) { 4 | // Extend web3 to expose admin methods. 5 | web3admin.extend(web3); 6 | 7 | /* eslint-disable global-require */ 8 | // ensure this is called before requiring the submodules 9 | // as we store config in singleton variable and the next config requires will ignore custom values 10 | const resolvedConfig = require('./config')(config); 11 | require('./logger/setup')(logger); 12 | require('./support/nonce/setup')(web3, nonceManager); 13 | 14 | this.constants = require('./support/constants'); 15 | this.tx = require('./support/tx'); 16 | this.sender = require('./support/sender'); 17 | this.coin = require('./support/coin'); 18 | this.token = require('./token'); 19 | this.util = require('./support/util'); 20 | this.escrow = require('./escrow'); 21 | this.nonce = require('./support/nonce'); 22 | this.errors = require('./support/errors'); 23 | this.asserts = require('./support/asserts'); 24 | this.ontology = require('./ontology'); 25 | this.pricing = require('./pricing'); 26 | this.idvRegistry = require('./idv-registry'); 27 | this.transactionDetails = require('./support/transactionDetails'); 28 | /* eslint-enable global-require */ 29 | 30 | this.tx.web3 = web3; 31 | this.asserts.web3 = web3; 32 | 33 | if (resolvedConfig.preloadContracts) { 34 | this.tx.loadContracts().catch(error => { 35 | throw error; 36 | }); 37 | } 38 | } 39 | 40 | module.exports = MarketplaceTx; 41 | -------------------------------------------------------------------------------- /src/ontology.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @module ontology 4 | * 5 | * @description Exposes the CvcOntology contract interface. Functions to add and retrieve 6 | * {@link CredentialItem}s for sale in Identity.com 7 | * 8 | * */ 9 | 10 | /** 11 | * A credential item in the Ontology contract. This represents an item that can be priced 12 | * @global 13 | * @typedef {Object} module:ontology.CredentialItem 14 | * @property {string} id - The credential item record internalID. 15 | * @property {string} type - The credential item type (e.g. credential, claim) 16 | * @property {string} name - The name of the credential item (unique for type and version) 17 | * @property {string} version - The credential item version (e.g. v1.0) 18 | * @property {string} reference - The location of the reference source that describes 19 | * this credential item 20 | * @property {string} referenceType - The type of the reference, e.g. URL, DID 21 | * @property {string} referenceHash - A hash of the document at the reference source, 22 | * used to ensure it has not been altered 23 | * @property {boolean} deprecated - True if this is a deprecated credential item 24 | * that should no longer be used 25 | */ 26 | 27 | const ontology = {}; 28 | module.exports = ontology; 29 | 30 | const tx = require('./support/tx'); 31 | const sender = require('./support/sender'); 32 | const { assertCredentialItemType, assertAddress } = require('./support/asserts'); 33 | const { CONTRACT_ONTOLOGY } = require('./support/constants'); 34 | const { NotFoundError } = require('./support/errors'); 35 | 36 | /** 37 | * @description Converts an array returned from web3 to a CredentialItem. 38 | * @see CredentialItem 39 | * @param {string} id - see {@link CredentialItem}.id 40 | * @param {string} type - see {@link CredentialItem}.type 41 | * @param {string} name - see {@link CredentialItem}.name 42 | * @param {string} version - see {@link CredentialItem}.version 43 | * @param {string} reference - see {@link CredentialItem}.reference 44 | * @param {string} referenceType - see {@link CredentialItem}.referenceType 45 | * @param {string} referenceHash - see {@link CredentialItem}.referenceHash 46 | * @param {boolean} deprecated - see {@link CredentialItem}.deprecated 47 | * @return {CredentialItem} The credential item 48 | */ 49 | const mapCredentialItemRecord = ([id, type, name, version, reference, referenceType, referenceHash, deprecated]) => ({ 50 | id, 51 | type, 52 | name, 53 | version, 54 | reference, 55 | referenceType, 56 | referenceHash, 57 | deprecated 58 | }); 59 | 60 | /** 61 | * @param {CredentialItem} credentialItem - Credential item object 62 | * @return {Array} 63 | */ 64 | const assertCredentialItem = credentialItem => { 65 | if (credentialItem.id.match(/0x0{64}/)) { 66 | throw new NotFoundError('Credential item does not exist'); 67 | } 68 | }; 69 | 70 | /** 71 | * @alias module:ontology.getById 72 | * @memberOf ontology 73 | * @description Retrieve a credential item by internal ID 74 | * @see CredentialItem 75 | * @param {string} id - see {@link CredentialItem}.id 76 | * @return {Promise} 77 | */ 78 | ontology.getById = async function(id) { 79 | const ontologyContract = await tx.contractInstance(CONTRACT_ONTOLOGY); 80 | const credentialItem = mapCredentialItemRecord(await ontologyContract.getById(id)); 81 | assertCredentialItem(credentialItem); 82 | 83 | return credentialItem; 84 | }; 85 | 86 | /** 87 | * @alias module:ontology.getByTypeNameVersion 88 | * @memberOf ontology 89 | * @description Retrieve a credential item by type, name and version 90 | * @see CredentialItem 91 | * @param {string} type see {@link CredentialItem}.type 92 | * @param {string} name see {@link CredentialItem}.name 93 | * @param {string} version see {@link CredentialItem}.version 94 | * @return {Promise} 95 | */ 96 | ontology.getByTypeNameVersion = async function(type, name, version) { 97 | const ontologyContract = await tx.contractInstance(CONTRACT_ONTOLOGY); 98 | const credentialItem = mapCredentialItemRecord(await ontologyContract.getByTypeNameVersion(type, name, version)); 99 | assertCredentialItem(credentialItem); 100 | 101 | return credentialItem; 102 | }; 103 | 104 | /** 105 | * @alias module:ontology.getIdByTypeNameVersion 106 | * @memberOf ontology 107 | * @description Get the ID of a credential item by its type, name and version 108 | * @see CredentialItem 109 | * @param {string} type see {@link CredentialItem}.type 110 | * @param {string} name see {@link CredentialItem}.name 111 | * @param {string} version see {@link CredentialItem}.version 112 | * @return {Promise} 113 | */ 114 | ontology.getIdByTypeNameVersion = async function(type, name, version) { 115 | return (await ontology.getByTypeNameVersion(type, name, version)).id; 116 | }; 117 | 118 | /** 119 | * @alias module:ontology.getAll 120 | * @memberOf ontology 121 | * @description Returns all credential items, 2-dimensional array 122 | * @return {Promise} 123 | */ 124 | ontology.getAll = async function() { 125 | const ontologyContract = await tx.contractInstance(CONTRACT_ONTOLOGY); 126 | const ids = await ontologyContract.getAllIds(); 127 | const credentialItems = await Promise.all(ids.map(id => ontologyContract.getById(id))); 128 | 129 | return credentialItems.map(mapCredentialItemRecord); 130 | }; 131 | 132 | /** 133 | * @alias module:ontology.add 134 | * @memberOf ontology 135 | * @description Add a credential item to the contract 136 | * @see CredentialItem 137 | * @param {string} fromAddress - The address of the sender. 138 | * @param {function} signTx - The callback to use to sign the transaction 139 | * @param {string} type - see {@link CredentialItem}.type 140 | * @param {string} name - see {@link CredentialItem}.name 141 | * @param {string} version - see {@link CredentialItem}.version 142 | * @param {string} reference - see {@link CredentialItem}.reference 143 | * @param {string} referenceType - see {@link CredentialItem}.referenceType 144 | * @param {string} referenceHash - see {@link CredentialItem}.referenceHash 145 | * @returns {Promise<{transactionHash}>} 146 | */ 147 | ontology.add = async function(fromAddress, signTx, type, name, version, reference, referenceType, referenceHash) { 148 | assertAddress(fromAddress); 149 | const args = [assertCredentialItemType(type), name, version, reference, referenceType, referenceHash]; 150 | args.forEach(arg => { 151 | if (!arg || typeof arg !== 'string' || arg.length === 0) { 152 | throw new Error(`Empty argument passed to Ontology.add (${JSON.stringify(args)})`); 153 | } 154 | }); 155 | 156 | return sender.send({ 157 | fromAddress, 158 | signTx, 159 | contractName: CONTRACT_ONTOLOGY, 160 | method: 'add', 161 | params: args 162 | }); 163 | }; 164 | 165 | /** 166 | * @alias module:ontology.deprecate 167 | * @memberOf ontology 168 | * @description Deprecates a credential item by external ID (type, name and version) 169 | * @see CredentialItem 170 | * @param {string} fromAddress - The address of the sender. 171 | * @param {function} signTx - The callback to use to sign the transaction 172 | * @param {string} type - see {@link CredentialItem}.type 173 | * @param {string} name - see {@link CredentialItem}.name 174 | * @param {string} version - see {@link CredentialItem}.version 175 | * @returns {Promise<{transactionHash}>} 176 | */ 177 | ontology.deprecate = function(fromAddress, signTx, type, name, version) { 178 | assertAddress(fromAddress); 179 | const args = [assertCredentialItemType(type), name, version]; 180 | return sender.send({ 181 | fromAddress, 182 | signTx, 183 | contractName: CONTRACT_ONTOLOGY, 184 | method: 'deprecate', 185 | params: args 186 | }); 187 | }; 188 | 189 | /** 190 | * @alias module:ontology.deprecateById 191 | * @memberOf ontology 192 | * @description Deprecates a credential item by internal ID 193 | * @param {string} fromAddress - The address of the sender. 194 | * @param {function} signTx - The callback to use to sign the transaction 195 | * @param internalId 196 | * @returns {Promise<{transactionHash}>} 197 | */ 198 | ontology.deprecateById = function(fromAddress, signTx, internalId) { 199 | assertAddress(fromAddress); 200 | return sender.send({ 201 | fromAddress, 202 | signTx, 203 | contractName: CONTRACT_ONTOLOGY, 204 | method: 'deprecateById', 205 | params: [internalId] 206 | }); 207 | }; 208 | 209 | /** 210 | * @alias module:ontology.parseExternalId 211 | * @memberOf ontology 212 | * @description Converts an "external ID" of a credential item 213 | * of the form "type-name-version" 214 | * to an array of strings [type, name, version] 215 | * @param {string} typeNameVersion 216 | * @return {string[]} 217 | */ 218 | ontology.parseExternalId = function(typeNameVersion) { 219 | const results = typeNameVersion.split('-'); 220 | if (results.length !== 3) { 221 | throw new Error(`Invalid ontology external ID '${typeNameVersion}'. Expected: 'type-name-version'.`); 222 | } 223 | const [recordType, name, version] = results; 224 | return [assertCredentialItemType(recordType), name, version]; 225 | }; 226 | 227 | /** 228 | * @alias module:ontology.parseExternalId 229 | * @memberOf ontology 230 | * @description Convert parameters type, name, version into an "external ID" 231 | * of the form "type-name-version" 232 | * @param {string} type - see {@link CredentialItem}.type 233 | * @param {string} name - see {@link CredentialItem}.name 234 | * @param {string} version - see {@link CredentialItem}.version 235 | * @return {string} 236 | */ 237 | ontology.composeExternalId = function(type, name, version) { 238 | return [assertCredentialItemType(type), name, version].join('-'); 239 | }; 240 | -------------------------------------------------------------------------------- /src/pricing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module pricing 3 | * 4 | * @description Handles setting, retrieving and deleting prices for {@link CredentialItem}s in 5 | * the CvcPricing contract. 6 | */ 7 | 8 | /** 9 | * A credential item price entry in the Pricing contract. It refers to a 10 | * credential item in the ontology contract and its price for a given IDV 11 | * @global 12 | * @typedef {Object} module:price.CredentialItemPrice 13 | * @see CredentialItem 14 | * @property {string} id - The credential item price record internal ID. 15 | * @property {string} price - The price of the credential item in creds (CVC x 10e-8) 16 | * @property {string} idv - The address of the IDV to whom the price belongs 17 | * @property {string} type - The credential item type (e.g. credential, claim) 18 | * (see {@link CredentialItem}.type) 19 | * @property {string} name - The name of the credential item (unique for type and version) 20 | * (see {@link CredentialItem}.name) 21 | * @property {string} version - The credential item version (e.g. v1.0) 22 | * (see {@link CredentialItem}.version) 23 | * @property {boolean} deprecated - True if this is a deprecated credential item 24 | * that should no longer be used 25 | */ 26 | 27 | const pricing = {}; 28 | module.exports = pricing; 29 | 30 | const { assertAddress, assertCredentialItemPrice } = require('./support/asserts'); 31 | const { CONTRACT_PRICING } = require('./support/constants'); 32 | const tx = require('./support/tx'); 33 | const sender = require('./support/sender'); 34 | const { NotFoundError } = require('./support/errors'); 35 | 36 | /** 37 | * Maps an array with price data and returns the price object with corresponding properties. 38 | * @param {string} id - see {@link CredentialItemPrice}.id 39 | * @param {string} price - see {@link CredentialItemPrice}.price 40 | * @param {string} idv - see {@link CredentialItemPrice}.idv 41 | * @param {string} type - see {@link CredentialItemPrice}.type 42 | * @param {string} name - see {@link CredentialItemPrice}.name 43 | * @param {string} version - see {@link CredentialItemPrice}.version 44 | * @param {boolean} deprecated - see {@link CredentialItemPrice}.deprecated 45 | * @returns {CredentialItemPrice} 46 | */ 47 | const mapCredentialItemPrice = function([id, price, idv, type, name, version, deprecated]) { 48 | return { 49 | id, 50 | price, 51 | idv, 52 | credentialItem: { type, name, version }, 53 | deprecated 54 | }; 55 | }; 56 | 57 | /** 58 | * Checks whether the price exists. 59 | * @param {CredentialItemPrice} credentialItemPrice - Credential item price object 60 | */ 61 | const assertPrice = function(credentialItemPrice) { 62 | // By convention all existing records must have non empty ID. 63 | if (!credentialItemPrice.id || /0x0{64}/.test(credentialItemPrice.id)) { 64 | throw new NotFoundError('Undefined price'); 65 | } 66 | }; 67 | 68 | /** 69 | * @alias module:pricing.getPrice 70 | * @memberOf pricing 71 | * @description Retrieve credential Item price by type, name and version for specific IDV. 72 | * @param {string} idv - The IDV address 73 | * @param {string} type - see {@link CredentialItemPrice}.type 74 | * @param {string} name - see {@link CredentialItemPrice}.name 75 | * @param {string} version - see {@link CredentialItemPrice}.version 76 | * @returns {*} 77 | */ 78 | pricing.getPrice = async function(idv, type, name, version) { 79 | assertAddress(idv); 80 | const pricingContract = await tx.contractInstance(CONTRACT_PRICING); 81 | const price = mapCredentialItemPrice(await pricingContract.getPrice(idv, type, name, version)); 82 | assertPrice(price); 83 | 84 | return price; 85 | }; 86 | 87 | /** 88 | * @alias module:pricing.getAllPrices 89 | * @memberOf pricing 90 | * @description Returns all prices. 91 | * @return {Promise} 92 | */ 93 | pricing.getAllPrices = async function() { 94 | const pricingContract = await tx.contractInstance(CONTRACT_PRICING); 95 | const ids = await pricingContract.getAllIds(); 96 | const prices = await Promise.all(ids.map(id => pricingContract.getPriceById(id))); 97 | 98 | return prices.map(mapCredentialItemPrice); 99 | }; 100 | 101 | /** 102 | * @alias module:pricing.setPrice 103 | * @memberOf pricing 104 | * @description Set credential Item price by type, name and version for specific IDV. 105 | * @see CredentialItem 106 | * @param {string} fromAddress - The address of the sender. 107 | * @param {function} signTx - The callback to use to sign the transaction 108 | * @param {string} type - see {@link CredentialItemPrice}.type 109 | * @param {string} name - see {@link CredentialItemPrice}.name 110 | * @param {string} version - see {@link CredentialItemPrice}.version 111 | * @param {string} price - the credential item price in creds (CVC x 10e-8) 112 | * @returns {Promise<{transactionHash}>} 113 | */ 114 | pricing.setPrice = function(fromAddress, signTx, type, name, version, price) { 115 | assertAddress(fromAddress); 116 | assertCredentialItemPrice(price); 117 | return sender.send({ 118 | fromAddress, 119 | signTx, 120 | contractName: CONTRACT_PRICING, 121 | method: 'setPrice', 122 | params: [type, name, version, price] 123 | }); 124 | }; 125 | 126 | /** 127 | * @alias module:pricing.deletePrice 128 | * @memberOf pricing 129 | * @description Deletes a credential Item price by type, name and version for specific IDV. 130 | * @see CredentialItem 131 | * @param {string} fromAddress - The address of the sender. 132 | * @param {function} signTx - The callback to use to sign the transaction 133 | * @param {string} type - see {@link CredentialItemPrice}.type 134 | * @param {string} name - see {@link CredentialItemPrice}.name 135 | * @param {string} version - see {@link CredentialItemPrice}.version 136 | * @returns {Promise<{transactionHash}>} 137 | */ 138 | pricing.deletePrice = function(fromAddress, signTx, type, name, version) { 139 | assertAddress(fromAddress); 140 | return sender.send({ 141 | fromAddress, 142 | signTx, 143 | contractName: CONTRACT_PRICING, 144 | method: 'deletePrice', 145 | params: [type, name, version] 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /src/support/asserts.js: -------------------------------------------------------------------------------- 1 | /** @module support/asserts */ 2 | const asserts = {}; 3 | module.exports = asserts; 4 | 5 | const { TOTAL_SUPPLY, CREDENTIAL_ITEM_TYPES } = require('./constants'); 6 | 7 | /** 8 | * @alias module:support/asserts.assertAmount 9 | * @memberOf support/asserts 10 | * @description Asserts amount for sanity check. 11 | * @param {number} amount The escrow amount in creds. 12 | * @returns {number} 13 | */ 14 | asserts.assertAmount = amount => { 15 | if (amount < 1 || amount > TOTAL_SUPPLY) { 16 | throw new Error(`Amount ${amount} is out of range (1-${TOTAL_SUPPLY})`); 17 | } 18 | 19 | return amount; 20 | }; 21 | 22 | /** 23 | * @alias module:support/asserts.assertCredentialItems 24 | * @memberOf support/asserts 25 | * @description Asserts credential items array is not empty. 26 | * @param {Array} credentialItems An array containing credential item IDs 27 | * @return {Array} 28 | */ 29 | asserts.assertCredentialItems = credentialItems => { 30 | if (!credentialItems || !credentialItems.length) { 31 | throw new Error('Credential items must be non-empty array'); 32 | } 33 | 34 | return credentialItems; 35 | }; 36 | 37 | /** 38 | * @alias module:support/asserts.assertCredentialItemPrice 39 | * @memberOf support/asserts 40 | * @description Asserts that provided number matches valid price criteria. 41 | * @returns {number} 42 | * @param price 43 | */ 44 | asserts.assertCredentialItemPrice = price => { 45 | if (price < 0 || price > TOTAL_SUPPLY) { 46 | throw new Error(`Price ${price} is out of range (0-${TOTAL_SUPPLY})`); 47 | } 48 | 49 | return price; 50 | }; 51 | 52 | /** 53 | * @alias module:support/asserts.assertAddress 54 | * @memberOf support/asserts 55 | * @description Checks if provided string is a valid ETH address. 56 | * @param {string} addressToTest 57 | * @returns {string} 58 | */ 59 | asserts.assertAddress = addressToTest => { 60 | if (!asserts.web3.isAddress(addressToTest)) { 61 | throw new Error(`Address (${addressToTest}) is not a valid ETH address`); 62 | } 63 | 64 | return addressToTest; 65 | }; 66 | 67 | /** 68 | * @alias module:support/asserts.assertCredentialItemType 69 | * @memberOf support/asserts 70 | * @description Allows only certain credential item types 71 | * @param {string} type 72 | * @return {string} 73 | */ 74 | asserts.assertCredentialItemType = type => { 75 | if (!CREDENTIAL_ITEM_TYPES.includes(type)) { 76 | throw new Error(`Credential item type '${type}' is not supported`); 77 | } 78 | 79 | return type; 80 | }; 81 | -------------------------------------------------------------------------------- /src/support/coin.js: -------------------------------------------------------------------------------- 1 | const coin = {}; 2 | module.exports = coin; 3 | 4 | const util = require('util'); 5 | // This shim is necessary so that marketplaceTx can be imported in the browser 6 | // See https://github.com/serverless-heaven/serverless-webpack/issues/291#issuecomment-348790713 7 | // See https://github.com/alexjlockwood/avocado/commit/7455bea1052c4d271fe0e6db59f3fb3efdd0349d 8 | require('util.promisify').shim(); 9 | 10 | const tx = require('./tx'); 11 | const sender = require('./sender'); 12 | const logger = require('../logger'); 13 | 14 | /** 15 | * Returns platform coin balance for given address. 16 | * @param address 17 | * @returns {Promise} 18 | */ 19 | coin.getBalance = address => util.promisify(cb => tx.web3.eth.getBalance(address, cb))(); 20 | 21 | /** 22 | * Returns platform coin balances for multiple addresses. 23 | * @param users 24 | * @returns {Promise} 25 | */ 26 | coin.getBalances = users => 27 | Promise.all(users.map(user => coin.getBalance(user.address).then(balance => Object.assign({}, user, { balance })))); 28 | 29 | /** 30 | * 31 | * @param {string} fromAddress - The address to send the coins from. 32 | * @param {function} signTx - The callback to use to sign the transaction. 33 | * @param {string} toAddress - The address to send the coins to. 34 | * @param {int} value - The amount of coins to send 35 | * @returns {Promise<{transactionHash}>} 36 | */ 37 | coin.transfer = async function(fromAddress, signTx, toAddress, value) { 38 | try { 39 | return await sender.sendPlatformCoin({ fromAddress, signTx, toAddress, value }); 40 | } catch (error) { 41 | logger.error(`Error transferring platform coin: ${error.message}`); 42 | throw error; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/support/constants.js: -------------------------------------------------------------------------------- 1 | const CVC_DECIMALS = 8; 2 | const ONE_CVC = 10 ** CVC_DECIMALS; 3 | const TOTAL_SUPPLY = 1e9 * ONE_CVC; 4 | const CONTRACT_TOKEN = 'CvcToken'; 5 | const CONTRACT_ESCROW = 'CvcEscrow'; 6 | const CONTRACT_PRICING = 'CvcPricing'; 7 | const CONTRACT_ONTOLOGY = 'CvcOntology'; 8 | const CONTRACT_VALIDATOR_REGISTRY = 'CvcValidatorRegistry'; 9 | const EVENT_ESCROW_PLACED = 'EscrowPlaced'; 10 | const EVENT_ESCROW_MOVED = 'EscrowMoved'; 11 | const EVENT_ESCROW_RELEASED = 'EscrowReleased'; 12 | const EVENT_ESCROW_REFUNDED = 'EscrowCanceled'; 13 | const EVENT_CREDENTIAL_ITEM_PRICE_SET = 'CredentialItemPriceSet'; 14 | const EVENT_CREDENTIAL_ITEM_PRICE_DELETED = 'CredentialItemPriceDeleted'; 15 | 16 | const CONTRACTS = [CONTRACT_TOKEN, CONTRACT_ESCROW, CONTRACT_PRICING, CONTRACT_ONTOLOGY, CONTRACT_VALIDATOR_REGISTRY]; 17 | 18 | const TX_STATUS = { 19 | PENDING: 'pending', 20 | QUEUED: 'queued', 21 | MINED: 'mined', 22 | UNKNOWN: 'unknown', 23 | UNSUPPORTED: 'unsupported' 24 | }; 25 | 26 | const CREDENTIAL_ITEM_TYPES = ['claim', 'credential']; 27 | 28 | module.exports = { 29 | CVC_DECIMALS, 30 | ONE_CVC, 31 | TOTAL_SUPPLY, 32 | CONTRACTS, 33 | CONTRACT_TOKEN, 34 | CONTRACT_ESCROW, 35 | CONTRACT_PRICING, 36 | CONTRACT_ONTOLOGY, 37 | CONTRACT_VALIDATOR_REGISTRY, 38 | EVENT_ESCROW_PLACED, 39 | EVENT_ESCROW_MOVED, 40 | EVENT_ESCROW_RELEASED, 41 | EVENT_ESCROW_REFUNDED, 42 | EVENT_CREDENTIAL_ITEM_PRICE_SET, 43 | EVENT_CREDENTIAL_ITEM_PRICE_DELETED, 44 | TX_STATUS, 45 | CREDENTIAL_ITEM_TYPES 46 | }; 47 | -------------------------------------------------------------------------------- /src/support/errors.js: -------------------------------------------------------------------------------- 1 | /** @module support/errors */ 2 | const { BaseError } = require('make-error-cause'); 3 | 4 | class CvcError extends BaseError {} 5 | 6 | /** 7 | * There is a mined transaction with the same nonce from given account. 8 | */ 9 | class InvalidNonceError extends CvcError { 10 | constructor(cause) { 11 | super('Invalid nonce value', cause); 12 | } 13 | } 14 | 15 | class FailedTxChainError extends CvcError { 16 | constructor(transactions, cause) { 17 | super(`Failed to send ${transactions.length} chained transactions`, cause); 18 | this.transactions = transactions; 19 | } 20 | } 21 | 22 | class NotDeployedError extends CvcError { 23 | constructor(address) { 24 | super(`No code deployed at address ${address}.`); 25 | } 26 | } 27 | 28 | class NoNetworkInContractError extends CvcError { 29 | constructor(contractName, cause) { 30 | super(`Could not detect '${contractName}' in network.`, cause); 31 | } 32 | } 33 | 34 | class SignerSenderAddressMismatchError extends CvcError { 35 | constructor(signer, sender) { 36 | super(`Expected from sender address ${sender} does not match actual signing ${signer} address.`); 37 | } 38 | } 39 | 40 | class NotFoundError extends CvcError {} 41 | 42 | /** 43 | * Maps a blockchain error and returns an instance of CvcError when possible. 44 | * This allows to implement error normalisation. 45 | * Unknown errors are propagated without modifications. 46 | * @param {Error} error A blockchain error object. 47 | * @returns {CvcError|Error} Mapped error object. 48 | */ 49 | function mapError(error) { 50 | // Prevent wrapping mapped errors. 51 | if (error instanceof CvcError) return error; 52 | // Check for invalid nonce error. 53 | if (/nonce|replacement\stransaction\sunderpriced|known\stransaction/.test(error)) return new InvalidNonceError(error); 54 | 55 | return error; 56 | } 57 | 58 | module.exports = { 59 | mapError, 60 | CvcError, 61 | InvalidNonceError, 62 | FailedTxChainError, 63 | NotDeployedError, 64 | NoNetworkInContractError, 65 | SignerSenderAddressMismatchError, 66 | NotFoundError 67 | }; 68 | -------------------------------------------------------------------------------- /src/support/nonce/accountInspector.js: -------------------------------------------------------------------------------- 1 | const ethUtil = require('ethereumjs-util'); 2 | const util = require('util'); 3 | 4 | module.exports = class AccountInspector { 5 | /** 6 | * @param web3 7 | */ 8 | constructor(web3) { 9 | this.web3 = web3; 10 | } 11 | 12 | /** 13 | * Returns current transaction count for specific address. 14 | * 15 | * NOTE: There are reports of incorrect behaviour of web3.eth.getTransactionCount 16 | * which affects the count of pending transactions. 17 | * https://github.com/ethereum/go-ethereum/issues/2880 18 | * At this time we could only rely on the count of mined transactions. 19 | * 20 | * @param address The address to get the numbers of transactions from. 21 | * @param defaultBlock The default block number to use when querying a state. 22 | * "earliest", the genesis block 23 | * "latest", the latest block (current head of the blockchain) 24 | * "pending", the currently mined block (including pending transactions) 25 | * @returns {Promise} 26 | */ 27 | async getTransactionCount(address, defaultBlock = 'latest') { 28 | const getTransactionCountPromise = util.promisify(cb => 29 | this.web3.eth.getTransactionCount(address, defaultBlock, cb) 30 | ); 31 | return getTransactionCountPromise(); 32 | } 33 | 34 | /** 35 | * Retrieves txpool content (pending and queued transactions) for specific address. 36 | * @param address 37 | * @returns {Promise} 38 | */ 39 | async inspectTxPool(address) { 40 | return new Promise((resolve, reject) => { 41 | this.web3.txpool.inspect((error, result) => { 42 | if (error) { 43 | if (error.message.includes('Method txpool_inspect not supported.')) { 44 | // handle cases where txpool.inspect is not available 45 | // we just have to assume there is nothing pending in this case 46 | return resolve({ pending: {}, queued: {} }); 47 | } 48 | return reject(error); 49 | } 50 | const checksummedAddress = ethUtil.toChecksumAddress(address); 51 | return resolve({ 52 | pending: result.pending[checksummedAddress] || {}, 53 | queued: result.queued[checksummedAddress] || {} 54 | }); 55 | }); 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/support/nonce/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports a nonce manager created by marketplace-tx initialisation 3 | * so that it is available cleanly to all other modules. 4 | * 5 | * This file should be imported only after setup has been 6 | * called for the first time with required dependencies: 7 | * 8 | * require('./support/nonce/setup')(web3, nonceManager); 9 | */ 10 | module.exports = require('./setup')(); 11 | -------------------------------------------------------------------------------- /src/support/nonce/inmemory.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const logger = require('../../logger'); 3 | const { calculateNonce } = require('./util'); 4 | 5 | /** 6 | * @classdesc A service that dispenses nonces, resolving any nonce gaps and 7 | * ensuring concurrent transactions do not result in nonce clashes. 8 | * */ 9 | class InMemoryNonceManager { 10 | /** 11 | * @param accountInspector 12 | */ 13 | constructor(accountInspector) { 14 | this.accountInspector = accountInspector; 15 | this.store = {}; 16 | } 17 | 18 | /** 19 | * Returns the next correct transaction nonce for address. 20 | * @param address The address to get the nonce for. 21 | * @returns {Promise} 22 | */ 23 | async getNonceForAccount(address) { 24 | logger.debug(`Requesting nonce for address ${address}`); 25 | 26 | // Retrieve current transaction count and transaction pool state for the provided account. 27 | const [txCount, txPool] = await Promise.all([ 28 | this.accountInspector.getTransactionCount(address), 29 | this.accountInspector.inspectTxPool(address) 30 | ]); 31 | 32 | // Everything between store read and write must be sync to avoid any race conditions. 33 | const storedNonces = this.store[address] || {}; 34 | 35 | // Create debug log callback to with address for easier tracing. 36 | const calculateDebugLog = message => logger.debug(`Nonce manager for account '${address}': ${message}`); 37 | 38 | const { nextNonce, acquiredNonces } = calculateNonce(calculateDebugLog, storedNonces, txCount, txPool); 39 | 40 | // Since nonce manager is a singleton, this prevents other threads to use the same nonce twice. 41 | this.store[address] = acquiredNonces; 42 | 43 | return nextNonce; 44 | } 45 | 46 | /** 47 | * Releases a specific nonce and returns it back to the pool, so that it can be used for other transaction. 48 | * @param address 49 | * @param nonce 50 | * @returns void 51 | */ 52 | releaseAccountNonce(address, nonce) { 53 | _.unset(this.store, [address, Number(nonce)]); 54 | logger.debug(`Nonce manager for account '${address}': nonce released: ${Number(nonce)}`); 55 | } 56 | 57 | /** 58 | * Releases multiple nonces at once and returns them back to the pool, 59 | * so that they can be used for other transactions. 60 | * @param address 61 | * @param nonces 62 | * @returns void 63 | */ 64 | releaseAccountNonces(address, nonces) { 65 | _.each(nonces, nonce => this.releaseAccountNonce(address, nonce)); 66 | } 67 | 68 | /** 69 | * Clears stored nonce data of all accounts. 70 | * @returns void 71 | */ 72 | clearAccounts() { 73 | logger.debug(`Clearing nonce store...`); 74 | const addresses = Object.keys(this.store); 75 | this.store = {}; 76 | addresses.forEach(address => logger.debug(`Nonce manager for account '${address}': nonce cache cleared`)); 77 | } 78 | } 79 | 80 | module.exports = InMemoryNonceManager; 81 | -------------------------------------------------------------------------------- /src/support/nonce/setup.js: -------------------------------------------------------------------------------- 1 | const InMemoryNonceManager = require('./inmemory'); 2 | const AccountInspector = require('./accountInspector'); 3 | 4 | let nonceManager; 5 | 6 | module.exports = (web3, providedNonceManager = null) => { 7 | if (!nonceManager) { 8 | nonceManager = providedNonceManager || new InMemoryNonceManager(new AccountInspector(web3)); 9 | } 10 | 11 | return nonceManager; 12 | }; 13 | -------------------------------------------------------------------------------- /src/support/nonce/util.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = { 4 | calculateNonce(debugLog, storedNonces, txCount, { pending, queued }) { 5 | // Keep nonces which are not mined yet 6 | // and release nonces which values are below the account tx count (i.e. lowest possible value). 7 | const acquiredNonces = _.pickBy(storedNonces, (value, nonce) => nonce >= txCount); 8 | if (_.size(acquiredNonces) !== _.size(storedNonces)) { 9 | debugLog(`released nonces: ${_.difference(_.keys(storedNonces), _.keys(acquiredNonces)).join(', ')}`); 10 | } 11 | 12 | // Get all known transactions by combining acquired nonces with data from tx pool. 13 | const knownTransactions = _.assign({}, acquiredNonces, pending, queued); 14 | 15 | // Get all known nonces. 16 | const knownNonces = _.keys(knownTransactions); 17 | if (knownNonces.length) { 18 | debugLog(`known nonces: ${knownNonces.join(', ')}`); 19 | } 20 | 21 | // Calculate max known nonce. 22 | const maxKnownNonce = knownNonces.reduce((a, b) => Math.max(a, b), txCount); 23 | 24 | // Go from current tx count value (i.e. lowest possible value) to max known nonce looking for the gaps. 25 | let nextNonce = txCount; 26 | while (nextNonce <= maxKnownNonce) { 27 | // Stop at the first non-used nonce (i.e. first gap). 28 | if (!(nextNonce in knownTransactions)) break; 29 | // Increment nonce. If no gaps found, return the value next after max used nonce. 30 | nextNonce += 1; 31 | } 32 | 33 | // Mark this nonce as acquired to make it unavailable for others 34 | acquiredNonces[nextNonce] = true; 35 | 36 | debugLog(`nonce acquired: ${nextNonce}`); 37 | 38 | return { nextNonce, acquiredNonces }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/support/transactionDetails.js: -------------------------------------------------------------------------------- 1 | const transactionDetails = {}; 2 | module.exports = transactionDetails; 3 | 4 | const util = require('util'); 5 | // This shim is necessary so that marketplaceTx can be imported in the browser 6 | // See https://github.com/serverless-heaven/serverless-webpack/issues/291#issuecomment-348790713 7 | // See https://github.com/alexjlockwood/avocado/commit/7455bea1052c4d271fe0e6db59f3fb3efdd0349d 8 | require('util.promisify').shim(); 9 | const ethUtil = require('ethereumjs-util'); 10 | const _ = require('lodash'); 11 | const { TX_STATUS } = require('./constants'); 12 | const tx = require('./tx'); 13 | 14 | const txPoolStatuses = [TX_STATUS.PENDING, TX_STATUS.QUEUED]; 15 | const formatResult = (status, details) => ({ status, details }); 16 | 17 | /** 18 | * Get transaction details from either txPool.content or getTransactionReceipt 19 | * @param {string} fromAddress - The address of the sender. 20 | * @param txHash 21 | * @returns {Promise} 22 | */ 23 | transactionDetails.getTransaction = async function(fromAddress, txHash) { 24 | const receipt = await tx.getTransactionReceipt(txHash); 25 | if (receipt) { 26 | // The transaction hash was found via getTransactionReceipt and therefore has been mined 27 | return formatResult(TX_STATUS.MINED, receipt); 28 | } 29 | 30 | // Otherwise check the txPool 31 | const txPoolContentPromise = util.promisify(cb => tx.web3.txpool.content(cb)); 32 | 33 | try { 34 | const result = await txPoolContentPromise(); 35 | // Convert the fromAddress to the correct casing in order to find it by key notation 36 | const checksumFromAddress = ethUtil.toChecksumAddress(fromAddress); 37 | const foundTransaction = searchTxPoolContentResult(result, checksumFromAddress, txHash); 38 | 39 | return foundTransaction || formatResult(TX_STATUS.UNKNOWN, null); 40 | } catch (error) { 41 | if (error.message.includes('Method txpool_content not supported.')) { 42 | return formatResult(TX_STATUS.UNSUPPORTED, null); 43 | } 44 | throw error; 45 | } 46 | }; 47 | 48 | /** 49 | * Look into the txpool and find a transaction's status. 50 | * @param {string} fromAddress - The address of the sender. 51 | * @param nonce 52 | * @returns {Promise} 53 | */ 54 | // eslint-disable-next-line consistent-return 55 | transactionDetails.getTransactionStatus = async function(fromAddress, nonce) { 56 | const txPoolInspectPromise = util.promisify(cb => tx.web3.txpool.inspect(cb)); 57 | 58 | try { 59 | const result = await txPoolInspectPromise(); 60 | // Convert the fromAddress to the correct casing in order to find it by key notation 61 | const checksumFromAddress = ethUtil.toChecksumAddress(fromAddress); 62 | 63 | // Find the status which contains the transaction, otherwise default to Unknown 64 | const transactionPoolStatus = getTransactionPoolStatus(result, checksumFromAddress, nonce); 65 | 66 | // If the transaction was found in the txPool, resolve with corresponding status 67 | if (transactionPoolStatus !== TX_STATUS.UNKNOWN) { 68 | return transactionPoolStatus; 69 | } 70 | 71 | // getTransactionCount could include a pending transaction, so we assert this after checking the txPool 72 | const txCount = await tx.getTransactionCount(fromAddress); 73 | return nonce < txCount ? TX_STATUS.MINED : TX_STATUS.UNKNOWN; 74 | } catch (error) { 75 | if (error.message.includes('Method txpool_inspect not supported.')) { 76 | return TX_STATUS.UNSUPPORTED; 77 | } 78 | throw error; 79 | } 80 | }; 81 | 82 | /** 83 | * Search a txPool.Content result for matching address and txHash, return it and corresponding status. 84 | * @param txPoolContentResult 85 | * @param {string} fromAddress - The address of the sender. 86 | * @param txHash 87 | * @returns {object} 88 | */ 89 | function searchTxPoolContentResult(txPoolContentResult, fromAddress, txHash) { 90 | // This is a predicate for use with a find function and a collection of possible txPoolStatuses to search 91 | // values() creates an array of transactions instead of an object with nonce keyed transactions 92 | // find() then effectively searches across all nonces for the txHash 93 | const getTransactionByStatus = statusToFind => 94 | _.chain(txPoolContentResult) 95 | .get([statusToFind, fromAddress]) 96 | .values() 97 | .find(['hash', txHash]) 98 | .value(); 99 | 100 | // For each txPool status, search using getTransactionByStatus and set foundTransaction if found. 101 | let foundTransaction; 102 | _.each(txPoolStatuses, status => { 103 | const transaction = getTransactionByStatus(status); 104 | if (transaction) { 105 | foundTransaction = formatResult(status, transaction); 106 | return false; 107 | } 108 | return true; 109 | }); 110 | return foundTransaction; 111 | } 112 | 113 | /** 114 | * Search a txPool.Inspect result for matching address and nonce, return the corresponding status. 115 | * @param txPoolInspectResult 116 | * @param {string} fromAddress - The address of the sender. 117 | * @param txHash 118 | * @returns {string} 119 | */ 120 | function getTransactionPoolStatus(txPoolInspectResult, fromAddress, nonce) { 121 | // This is a predicate for use with a find function and a collection of possible txPoolStatuses to search 122 | const transactionIsInStatus = statusToFind => _.get(txPoolInspectResult, [statusToFind, fromAddress, nonce]); 123 | 124 | return _.chain(txPoolStatuses) 125 | .find(transactionIsInStatus) 126 | .defaultTo(TX_STATUS.UNKNOWN) 127 | .value(); 128 | } 129 | -------------------------------------------------------------------------------- /src/support/util.js: -------------------------------------------------------------------------------- 1 | const util = {}; 2 | module.exports = util; 3 | 4 | const Bn = require('bignumber.js'); 5 | const { ONE_CVC } = require('./constants'); 6 | 7 | util.bnToHexString = number => `0x${Number(number).toString(16)}`; 8 | 9 | util.hexToString = hex => { 10 | let s = ''; 11 | for (let c = 2; c < hex.length; c += 2) { 12 | const ch = parseInt(hex.substr(c, 2), 16); 13 | if (ch === 0) break; 14 | s += String.fromCharCode(ch); 15 | } 16 | return s; 17 | }; 18 | 19 | /** 20 | * @param n 21 | * @return {Bn} 22 | */ 23 | util.bnToCVC = n => new Bn(n).div(ONE_CVC); 24 | 25 | /** 26 | * @param n 27 | * @return {Bn} 28 | */ 29 | util.CVCToBN = n => new Bn(n).mul(ONE_CVC); 30 | 31 | class TimeoutError extends Error {} 32 | 33 | /** 34 | * Rejects the promise after specified timeout. 35 | * @param promise Promise to apply timeout for. 36 | * @param ms Timeout in milliseconds 37 | * @param msg 38 | * @returns {Promise} 39 | */ 40 | util.timeout = (promise, ms, msg) => { 41 | let timerId = null; 42 | // Create timer promise, with will be rejected after specified timeout. 43 | const timer = new Promise((resolve, reject) => { 44 | timerId = setTimeout(() => { 45 | reject(new TimeoutError(msg)); 46 | }, ms); 47 | }); 48 | // Ensure timeout handle released. 49 | const clear = () => clearTimeout(timerId); 50 | promise.then(clear, clear); 51 | 52 | return Promise.race([promise, timer]); 53 | }; 54 | util.timeout.Error = TimeoutError; 55 | -------------------------------------------------------------------------------- /src/token.js: -------------------------------------------------------------------------------- 1 | const token = {}; 2 | module.exports = token; 3 | 4 | const Bn = require('bignumber.js'); 5 | const tx = require('./support/tx'); 6 | const sender = require('./support/sender'); 7 | const logger = require('./logger'); 8 | const { CONTRACT_TOKEN, ONE_CVC } = require('./support/constants'); 9 | const { assertAddress } = require('./support/asserts'); 10 | 11 | const approve = async (fromAddress, signTx, spender, value) => { 12 | assertAddress(fromAddress); 13 | assertAddress(spender); 14 | 15 | return sender.send({ 16 | fromAddress, 17 | signTx, 18 | contractName: CONTRACT_TOKEN, 19 | method: 'approve', 20 | params: [spender, value] 21 | }); 22 | }; 23 | 24 | token.getBalances = async function(users) { 25 | const tokenContract = await tx.contractInstance(CONTRACT_TOKEN); 26 | 27 | const balancePromises = users.map(async user => { 28 | const balance = await tokenContract.balanceOf(user.address); 29 | return Object.assign({}, user, { balance }); 30 | }); 31 | return Promise.all(balancePromises); 32 | }; 33 | 34 | token.getBalance = async function(address) { 35 | const tokenContract = await tx.contractInstance(CONTRACT_TOKEN); 36 | return tokenContract.balanceOf(address); 37 | }; 38 | 39 | token.transfer = async function(fromAddress, signTx, to, value) { 40 | assertAddress(fromAddress); 41 | assertAddress(to); 42 | 43 | try { 44 | return await sender.send({ 45 | fromAddress, 46 | signTx, 47 | contractName: CONTRACT_TOKEN, 48 | method: 'transfer', 49 | params: [to, value] 50 | }); 51 | } catch (error) { 52 | logger.error(`Error transferring token: ${error.message}`); 53 | throw error; 54 | } 55 | }; 56 | 57 | token.approveWithReset = async function(fromAddress, signTx, spender, value) { 58 | try { 59 | const tokenContract = await tx.contractInstance(CONTRACT_TOKEN); 60 | const currentAllowance = await tokenContract.allowance(fromAddress, spender); 61 | if (currentAllowance > 0) { 62 | // Non-zero allowance cannot be updated, so reset it to zero first. 63 | await approve(fromAddress, signTx, spender, 0); 64 | } 65 | return await approve(fromAddress, signTx, spender, value); 66 | } catch (error) { 67 | logger.error(`Error approving token transfer: ${error.message}`); 68 | throw error; 69 | } 70 | }; 71 | 72 | token.allowance = async function(owner, spender) { 73 | const tokenContract = await tx.contractInstance(CONTRACT_TOKEN); 74 | return tokenContract.allowance(owner, spender); 75 | }; 76 | 77 | token.approve = async function(fromAddress, signTx, spender, value) { 78 | try { 79 | return await approve(fromAddress, signTx, spender, value); 80 | } catch (error) { 81 | logger.error(`Error approving token transfer: ${error.message}`); 82 | throw error; 83 | } 84 | }; 85 | 86 | /** 87 | * Converts the amount from creds to CVC. 88 | * @param {number|string|BigNumber} amount - Amount in creds. 89 | * @return {number} Amount in CVC. 90 | */ 91 | token.toCVC = amount => new Bn(amount).div(ONE_CVC).toNumber(); 92 | 93 | /** 94 | * Converts amount from CVC to creds. 95 | * @param {number|string|BigNumber} amount - Amount in CVC. 96 | * @return {number} Amount in creds. 97 | */ 98 | token.toCred = amount => new Bn(amount).mul(ONE_CVC).toNumber(); 99 | -------------------------------------------------------------------------------- /test/assets/contracts/mockContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "CvcValidatorRegistry" 3 | } -------------------------------------------------------------------------------- /test/escrow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | require('longjohn'); 3 | const chai = require('chai'); 4 | const Web3 = require('web3'); 5 | const FakeProvider = require('web3-fake-provider'); 6 | const MarketplaceTx = require('../src/marketplace-tx'); 7 | 8 | const { expect } = chai; 9 | chai.use(require('chai-as-promised')); 10 | 11 | const web3 = new Web3(new FakeProvider()); 12 | const marketplaceTx = new MarketplaceTx({ web3 }, { preloadContracts: false }); 13 | 14 | describe('escrow.js', () => { 15 | const { escrow } = marketplaceTx; 16 | const validAddress = '0x48089757dbc23bd8e49436247c9966ff15802978'; 17 | const invalidAddress = 'qwerty'; 18 | const validCredentialItems = ['claim-age-v1.0']; 19 | 20 | it('fails on invalid addresses provided to place/placeBatch', () => { 21 | expect(escrow.place(invalidAddress, 1, validAddress, '1', 1, validCredentialItems)).to.be.rejectedWith(/qwerty/); 22 | expect(escrow.placeBatch(invalidAddress, 1, validAddress, ['1'], 1, validCredentialItems)).to.be.rejectedWith( 23 | /qwerty/ 24 | ); 25 | expect(escrow.place(validAddress, 1, invalidAddress, '1', 1, validCredentialItems)).to.be.rejectedWith(/qwerty/); 26 | expect(escrow.placeBatch(validAddress, 1, invalidAddress, ['1'], 1, validCredentialItems)).to.be.rejectedWith( 27 | /qwerty/ 28 | ); 29 | }); 30 | 31 | it('fails on invalid args provided to place/placeBatch', () => { 32 | expect(escrow.place(validAddress, 1, validAddress, '1', 100, [])).to.be.rejectedWith(/empty/); 33 | expect(escrow.placeBatch(validAddress, 1, validAddress, ['1'], 100, [])).to.be.rejectedWith(/empty/); 34 | expect(escrow.place(validAddress, 1, validAddress, '1', 100, ['abcdef'])).to.be.rejectedWith(/abcdef/); 35 | expect(escrow.placeBatch(validAddress, 1, validAddress, ['1'], 100, ['abcdef'])).to.be.rejectedWith(/abcdef/); 36 | expect(escrow.place(validAddress, 1, validAddress, '1', 1e30, validCredentialItems)).to.be.rejectedWith(/1e\+30/); 37 | expect(escrow.placeBatch(validAddress, 1, validAddress, ['1'], 1e30, validCredentialItems)).to.be.rejectedWith( 38 | /1e\+30/ 39 | ); 40 | expect(escrow.place(validAddress, 1, validAddress, '1', -1, validCredentialItems)).to.be.rejectedWith(/-1/); 41 | expect(escrow.placeBatch(validAddress, 1, validAddress, ['1'], -1, validCredentialItems)).to.be.rejectedWith(/-1/); 42 | }); 43 | 44 | it('fails on invalid IDV address provided to release', () => { 45 | expect(escrow.release(invalidAddress, 1, validAddress, validAddress, '1')).to.be.rejectedWith(/qwerty/); 46 | expect(escrow.release(validAddress, 1, invalidAddress, validAddress, '1')).to.be.rejectedWith(/qwerty/); 47 | expect(escrow.release(validAddress, 1, validAddress, invalidAddress, '1')).to.be.rejectedWith(/qwerty/); 48 | }); 49 | 50 | it('fails on invalid IDV address provided to verify', () => { 51 | expect(escrow.verify(validAddress, invalidAddress, '1')).to.be.rejectedWith(/qwerty/); 52 | expect(escrow.verify(invalidAddress, validAddress, '1')).to.be.rejectedWith(/qwerty/); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/integration/00-error-mapping-and-initialisation.js: -------------------------------------------------------------------------------- 1 | require('longjohn'); 2 | const { expect } = require('chai'); 3 | const Web3 = require('web3'); 4 | const { mapError, CvcError, InvalidNonceError } = require('../../src/support/errors'); 5 | const MarketplaceTx = require('../../src/marketplace-tx'); 6 | 7 | // connect to a RSK or Ethereum node 8 | const url = process.env.ETH_NODE_URL; 9 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 10 | 11 | // initialise the marketplace-tx library and set the web3 connection 12 | const marketplaceTx = new MarketplaceTx({ web3 }); 13 | 14 | describe('Error mapper', () => { 15 | const nonceTooLowError = new Error('nonce too low'); 16 | const underpricedReplacementError = new Error('replacement transaction underpriced'); 17 | const knownTransactionError = new Error('known transaction: 0x00000'); 18 | 19 | it('should retain initial error', () => { 20 | const cvcError = new CvcError('message', nonceTooLowError); 21 | expect(cvcError.cause).equal(nonceTooLowError); 22 | expect(cvcError.message).equal('message'); 23 | }); 24 | 25 | it('should propagate unknown errors', () => { 26 | const error = new Error('some unknown error'); 27 | expect(mapError(error)).to.equal(error); 28 | }); 29 | 30 | it('should not wrap mapped error', () => { 31 | const cvcError = mapError(nonceTooLowError); 32 | expect(mapError(cvcError)).to.equal(cvcError); 33 | }); 34 | 35 | it('should map low nonce error', () => { 36 | expect(mapError(nonceTooLowError)).instanceOf(InvalidNonceError); 37 | }); 38 | 39 | it('should map known tx error', () => { 40 | expect(mapError(knownTransactionError)).instanceOf(InvalidNonceError); 41 | }); 42 | 43 | it('should map underpriced replacement error', () => { 44 | expect(mapError(underpricedReplacementError)).instanceOf(InvalidNonceError); 45 | }); 46 | }); 47 | 48 | describe('The service, on initialisation', () => { 49 | const { tx } = marketplaceTx; 50 | const { CONTRACTS } = marketplaceTx.constants; 51 | 52 | it('should fail to load if a contract cannot be found', async () => { 53 | // blatantly taking advantage of the fact that javascript 54 | // does not have real immutability or real constants 55 | CONTRACTS.push('unknownContract'); 56 | 57 | const shouldFail = tx.loadContracts(); 58 | 59 | // eslint-disable-next-line no-unused-expressions 60 | expect(shouldFail).to.eventually.be.rejected; 61 | 62 | CONTRACTS.pop(); 63 | }); 64 | 65 | it('should load all contracts', () => tx.loadContracts()); 66 | }); 67 | -------------------------------------------------------------------------------- /test/integration/02-coin.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-bignumber')()); 3 | 4 | const { expect } = chai; 5 | const Web3 = require('web3'); 6 | const MarketplaceTx = require('../../src/marketplace-tx'); 7 | const users = require('./users'); 8 | const signTx = require('./signtx'); 9 | 10 | // connect to a RSK or Ethereum node 11 | const url = process.env.ETH_NODE_URL; 12 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 13 | 14 | // initialise the marketplace-tx library and set the web3 connection 15 | const marketplaceTx = new MarketplaceTx({ web3 }); 16 | 17 | const { waitForMine } = marketplaceTx.tx; 18 | 19 | describe('Platform Coin', () => { 20 | // users[0] is a current coinbase account, therefore it cannot be used as a sender address 21 | // because of ever-changing balance due to the mining fees inflow. 22 | // This could be changed after CVC-673 is merged. 23 | const address1 = users[1].address; 24 | const address2 = users[2].address; 25 | 26 | it('should transfer coins and return correct balances', async () => { 27 | const transferAmount = 1e18; 28 | const [address1Before, address2Before] = await marketplaceTx.coin.getBalances([ 29 | { address: address1 }, 30 | { address: address2 } 31 | ]); 32 | 33 | const receipt = await waitForMine(marketplaceTx.coin.transfer(address1, signTx, address2, transferAmount)); 34 | const [address1After, address2After] = await marketplaceTx.coin.getBalances([ 35 | { address: address1 }, 36 | { address: address2 } 37 | ]); 38 | 39 | const txFee = parseInt(web3.toWei(receipt.gasUsed, 'gwei'), 10); 40 | expect(address1Before.balance.minus(address1After.balance).toNumber()).to.equal(transferAmount + txFee); 41 | expect(address2After.balance.minus(address2Before.balance).toNumber()).to.equal(transferAmount); 42 | }); 43 | 44 | it('should return correct balance for single address', async () => { 45 | const [account1, account2] = await marketplaceTx.coin.getBalances([{ address: address1 }, { address: address2 }]); 46 | 47 | expect(await marketplaceTx.coin.getBalance(address1)).bignumber.equal(account1.balance); 48 | expect(await marketplaceTx.coin.getBalance(address2)).bignumber.equal(account2.balance); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/integration/03-token.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-bignumber')()); 3 | 4 | const { expect } = chai; 5 | const Web3 = require('web3'); 6 | const MarketplaceTx = require('../../src/marketplace-tx'); 7 | const users = require('./users'); 8 | const signTx = require('./signtx'); 9 | 10 | // connect to a RSK or Ethereum node 11 | const url = process.env.ETH_NODE_URL; 12 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 13 | 14 | // initialise the marketplace-tx library and set the web3 connection 15 | const marketplaceTx = new MarketplaceTx({ web3 }); 16 | 17 | const { waitForMine } = marketplaceTx.tx; 18 | 19 | describe('Cvc Token', () => { 20 | const address1 = users[0].address; 21 | const address2 = users[1].address; 22 | 23 | it('should transfer tokens and return correct token balances', async () => { 24 | const transferAmount = marketplaceTx.token.toCred(10); 25 | const [account1Before, account2Before] = await marketplaceTx.token.getBalances([ 26 | { address: address1 }, 27 | { address: address2 } 28 | ]); 29 | 30 | await waitForMine(marketplaceTx.token.transfer(address1, signTx, address2, transferAmount)); 31 | 32 | const [account1After, account2After] = await marketplaceTx.token.getBalances([ 33 | { address: address1 }, 34 | { address: address2 } 35 | ]); 36 | 37 | expect(account1Before.balance.minus(account1After.balance).toNumber()).to.equal(transferAmount); 38 | expect(account2After.balance.minus(account2Before.balance).toNumber()).to.equal(transferAmount); 39 | }); 40 | 41 | it('should return correct token balance for single address', async () => { 42 | const [account1, account2] = await marketplaceTx.coin.getBalances([{ address: address1 }, { address: address2 }]); 43 | 44 | expect(await marketplaceTx.coin.getBalance(address1)).bignumber.equal(account1.balance); 45 | expect(await marketplaceTx.coin.getBalance(address2)).bignumber.equal(account2.balance); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/integration/05-ontology.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const Web3 = require('web3'); 3 | const MarketplaceTx = require('../../src/marketplace-tx'); 4 | const users = require('./users'); 5 | const signTx = require('./signtx'); 6 | 7 | const { expect } = chai; 8 | 9 | // connect to a RSK or Ethereum node 10 | const url = process.env.ETH_NODE_URL; 11 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 12 | 13 | // initialise the marketplace-tx library and set the web3 connection 14 | const marketplaceTx = new MarketplaceTx({ web3 }); 15 | 16 | describe('Ontology', () => { 17 | const type = 'credential'; 18 | const name = 'proofOfIdentity'; 19 | const version = 'v1.0'; 20 | const reference = 'https://www.identity.com/'; 21 | const referenceType = 'JSON-LD-Context'; 22 | const referenceHash = '0x2cd9bf92c5e20b1b410f5ace94d963a96e89156fbe65b70365e8596b37f1f165'; 23 | const { ontology } = marketplaceTx; 24 | const { waitForMine } = marketplaceTx.tx; 25 | const admin = users[0].address; 26 | 27 | describe('get', () => { 28 | it('can get by type, name and version', async () => { 29 | const credentialItem = await ontology.getByTypeNameVersion(type, name, version); 30 | expect(credentialItem).to.be.an('object'); 31 | expect(credentialItem) 32 | .to.have.property('id') 33 | .to.match(/^0x[0-f]{64}$/) 34 | .to.not.match(/0x0{64}/); 35 | expect(credentialItem).to.have.property('type', type); 36 | expect(credentialItem).to.have.property('name', name); 37 | expect(credentialItem).to.have.property('version', version); 38 | expect(credentialItem).to.have.property('reference', reference); 39 | expect(credentialItem).to.have.property('referenceType', referenceType); 40 | expect(credentialItem).to.have.property('referenceHash', referenceHash); 41 | expect(credentialItem).to.have.property('deprecated', false); 42 | }); 43 | 44 | it('can get by id', async () => { 45 | const byTypeNameVersion = await ontology.getByTypeNameVersion(type, name, version); 46 | const credentialItem = await ontology.getByTypeNameVersion(type, name, version); 47 | expect(credentialItem).to.deep.equal(byTypeNameVersion); 48 | }); 49 | 50 | it('can get all credential items', async () => { 51 | const proofOfIdentity = await ontology.getByTypeNameVersion(type, name, version); 52 | const proofOfAge = await ontology.getByTypeNameVersion('credential', 'proofOfAge', 'v1.0'); 53 | const proofOfResidence = await ontology.getByTypeNameVersion('credential', 'proofOfResidence', 'v1.0'); 54 | const credentialItems = await ontology.getAll(); 55 | expect(credentialItems) 56 | .to.be.an('array') 57 | .that.deep.includes(proofOfAge) 58 | .and.includes(proofOfIdentity) 59 | .and.includes(proofOfResidence); 60 | }); 61 | 62 | describe('throws when not found', () => { 63 | it('by id', () => 64 | expect( 65 | ontology.getById('0x0000000000000000000000000100000000000000000000000000000000000000') 66 | ).to.be.rejectedWith(/does not exist/)); 67 | it('by type-name-version', () => 68 | expect(ontology.getByTypeNameVersion('some', 'invalid', 'id')).to.be.rejectedWith(/does not exist/)); 69 | it('id by type-name-version', () => 70 | expect(ontology.getById('some', 'invalid', 'id')).to.be.rejectedWith(/does not exist/)); 71 | }); 72 | }); 73 | 74 | describe('add', () => { 75 | it('can add credential item', async () => { 76 | await waitForMine(ontology.add(admin, signTx, 'claim', 'age', 'v1.0', reference, referenceType, referenceHash)); 77 | 78 | const newClaim = await ontology.getByTypeNameVersion('claim', 'age', 'v1.0'); 79 | expect(newClaim).to.be.an('object'); 80 | expect(newClaim) 81 | .to.have.property('id') 82 | .to.match(/^0x[0-f]{64}$/) 83 | .to.not.match(/0x0{64}/); 84 | }); 85 | 86 | it('cannot modify existing credential item', () => 87 | expect(waitForMine(ontology.add(admin, signTx, type, name, version, reference, referenceType, referenceHash))) 88 | .rejected); 89 | 90 | describe('argument validation', () => { 91 | it('denies invalid type', () => 92 | expect(ontology.add(admin, signTx, 'Some random type', name, version, reference, referenceType, referenceHash)) 93 | .rejected); 94 | 95 | describe('denies empty arguments', () => { 96 | it('empty type', () => 97 | expect(ontology.add(admin, signTx, '', 'age', 'v2.0', reference, referenceType, referenceHash)).rejected); 98 | it('empty name', () => 99 | expect(ontology.add(admin, signTx, 'claim', '', 'v2.0', reference, referenceType, referenceHash)).rejected); 100 | it('empty version', () => 101 | expect(ontology.add(admin, signTx, 'claim', 'dob', '', reference, referenceType, referenceHash)).rejected); 102 | it('empty reference', () => 103 | expect(ontology.add(admin, signTx, 'claim', 'age', 'v2.0', '', referenceType, referenceHash)).rejected); 104 | it('empty referenceType', () => 105 | expect(ontology.add(admin, signTx, 'claim', 'age', 'v2.0', reference, '', referenceHash)).rejected); 106 | it('empty referenceHash', () => 107 | expect(ontology.add(admin, signTx, 'claim', 'age', 'v2.0', reference, referenceType, '')).rejected); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('deprecate credential item', () => { 113 | before('add outdated credential items for deprecation', async () => { 114 | await waitForMine(ontology.add(admin, signTx, 'claim', 'dob', 'v2.0', reference, referenceType, referenceHash)); 115 | await waitForMine(ontology.add(admin, signTx, 'claim', 'dob', 'v3.0', reference, referenceType, referenceHash)); 116 | }); 117 | 118 | it('by external ID', async () => { 119 | const freshClaim = await ontology.getByTypeNameVersion('claim', 'dob', 'v2.0'); 120 | await waitForMine(ontology.deprecate(admin, signTx, 'claim', 'dob', 'v2.0')); 121 | const deprecatedClaim = await ontology.getByTypeNameVersion('claim', 'dob', 'v2.0'); 122 | expect(deprecatedClaim.deprecated).to.be.true.and.not.equals(freshClaim.deprecated); 123 | }); 124 | 125 | it('by internal ID', async () => { 126 | const freshClaim = await ontology.getByTypeNameVersion('claim', 'dob', 'v3.0'); 127 | // eslint-disable-next-line no-unused-expressions 128 | expect(freshClaim.deprecated).to.be.false; 129 | await waitForMine(ontology.deprecateById(admin, signTx, freshClaim.id)); 130 | const deprecatedClaim = await ontology.getByTypeNameVersion('claim', 'dob', 'v3.0'); 131 | expect(deprecatedClaim.deprecated).not.equals(freshClaim.deprecated); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/integration/06-validator-registry.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const Web3 = require('web3'); 3 | const ethUtil = require('ethereumjs-util'); 4 | const MarketplaceTx = require('../../src/marketplace-tx'); 5 | const signTx = require('./signtx'); 6 | const users = require('./users'); 7 | 8 | const { expect } = chai; 9 | 10 | // connect to a RSK or Ethereum node 11 | const url = process.env.ETH_NODE_URL; 12 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 13 | 14 | // initialise the marketplace-tx library and set the web3 connection 15 | const marketplaceTx = new MarketplaceTx({ web3 }); 16 | 17 | describe('Validator Registry', () => { 18 | const { idvRegistry } = marketplaceTx; 19 | const registryOwner = users[0].address; 20 | const anotherAccount = users[1].address; 21 | 22 | const idvAddress = '0x1a88a35421a4a0d3e13fe4e8ebcf18e9a249dc5a'; 23 | const idvName = 'IDV'; 24 | const idvDescription = 'IDV company'; 25 | 26 | describe('Setting IDV entry data:', () => { 27 | describe('when the sender is not the registry owner', () => { 28 | const from = anotherAccount; 29 | it('fails', async () => { 30 | await expect(marketplaceTx.tx.waitForMine(idvRegistry.set(from, signTx, idvAddress, idvName, idvDescription))) 31 | .to.be.eventually.rejected; 32 | }); 33 | }); 34 | 35 | describe('when the sender is the registry owner', () => { 36 | const from = registryOwner; 37 | const newIdvAddress = '0x21bcd4080fa6c19da0669ab6a9bd2f259cbc8f02'; 38 | 39 | describe('when entry is new', () => { 40 | beforeEach(async () => expect(await idvRegistry.exists(newIdvAddress)).to.be.false); 41 | 42 | it('adds new entry', async () => { 43 | await marketplaceTx.tx.waitForMine(idvRegistry.set(from, signTx, newIdvAddress, idvName, idvDescription)); 44 | const idvRecord = await idvRegistry.get(newIdvAddress); 45 | assertValidatorRecord(idvRecord, { idvAddress: newIdvAddress, idvName, idvDescription }); 46 | }); 47 | }); 48 | 49 | describe('when existing entry', () => { 50 | beforeEach(async () => expect(await idvRegistry.exists(newIdvAddress)).to.be.true); 51 | 52 | it('updates the existing entry', async () => { 53 | const newName = `${idvName} Updated`; 54 | const newDescription = `${idvDescription} Updated`; 55 | await marketplaceTx.tx.waitForMine(idvRegistry.set(from, signTx, newIdvAddress, newName, newDescription)); 56 | const idvRecord = await idvRegistry.get(newIdvAddress); 57 | 58 | assertValidatorRecord(idvRecord, { 59 | idvAddress: newIdvAddress, 60 | idvName: newName, 61 | idvDescription: newDescription 62 | }); 63 | }); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('Getting IDV entry data:', () => { 69 | it('returns default entry data', async () => { 70 | const idvRecord = await idvRegistry.get(idvAddress); 71 | assertValidatorRecord(idvRecord, { idvAddress, idvName, idvDescription }); 72 | }); 73 | 74 | describe('when entry does not exist', () => { 75 | it('rejects the promise', async () => { 76 | const unknownIdvAddress = ethUtil.zeroAddress(); 77 | await expect(idvRegistry.get(unknownIdvAddress)).to.be.rejected; 78 | }); 79 | }); 80 | }); 81 | 82 | describe('Verifying IDV entry existence:', () => { 83 | describe('when existing entry', () => { 84 | it('returns true', async () => expect(await idvRegistry.exists(idvAddress)).to.be.true); 85 | }); 86 | 87 | describe('when entry does not exist', () => { 88 | it('returns false', async () => expect(await idvRegistry.exists(ethUtil.zeroAddress())).to.be.false); 89 | }); 90 | }); 91 | }); 92 | 93 | function assertValidatorRecord(idvRecord, { idvAddress, idvName, idvDescription }) { 94 | expect(idvRecord).to.have.property('address', idvAddress); 95 | expect(idvRecord).to.have.property('name', idvName); 96 | expect(idvRecord).to.have.property('description', idvDescription); 97 | } 98 | -------------------------------------------------------------------------------- /test/integration/07-pricing.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const Web3 = require('web3'); 3 | const _ = require('lodash'); 4 | chai.use(require('chai-bignumber')()); 5 | chai.use(require('chai-as-promised')); 6 | const Bn = require('bignumber.js'); 7 | const MarketplaceTx = require('../../src/marketplace-tx'); 8 | const signTx = require('./signtx'); 9 | const users = require('./users'); 10 | 11 | const { expect } = chai; 12 | 13 | // connect to a RSK or Ethereum node 14 | const url = process.env.ETH_NODE_URL; 15 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 16 | 17 | // initialise the marketplace-tx library and set the web3 connection 18 | const marketplaceTx = new MarketplaceTx({ web3 }); 19 | 20 | describe('Pricing', () => { 21 | const { pricing, ontology } = marketplaceTx; 22 | const { waitForMine } = marketplaceTx.tx; 23 | 24 | // Accounts 25 | const admin = users[0].address; 26 | const idv = users[6].address; 27 | const testIdv = users[7].address; 28 | 29 | // Credential Item 30 | const type = 'credential'; 31 | const name = 'proofOfIdentity'; 32 | const version = 'v1.0'; 33 | const reference = 'https://www.identity.com/'; 34 | const referenceType = 'JSON-LD-Context'; 35 | const referenceHash = '0x2cd9bf92c5e20b1b410f5ace94d963a96e89156fbe65b70365e8596b37f1f165'; 36 | const price = new Bn(2000); 37 | const deprecated = false; 38 | 39 | // Return price to default value. 40 | after(() => marketplaceTx.tx.waitForMine(pricing.setPrice(idv, signTx, type, name, version, price))); 41 | 42 | describe('Getting prices:', () => { 43 | describe('when looking up single price', () => { 44 | describe('when price is not available', () => { 45 | it('fails', async () => expect(pricing.getPrice(idv, type, 'unknown', version)).to.be.rejected); 46 | }); 47 | 48 | describe('when price is available', () => { 49 | it('returns the correct price', async () => { 50 | const credentialItemPrice = await pricing.getPrice(idv, type, name, version); 51 | assertCredentialItemPrice(credentialItemPrice, [price, idv, type, name, version, deprecated]); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('when fetching all prices', () => { 57 | const newCredentialItemNames = ['A', 'B', 'C'].map(i => `${name}${i}`); 58 | before(async () => { 59 | // eslint-disable-next-line no-restricted-syntax 60 | for (const credentialItemName of newCredentialItemNames) { 61 | // eslint-disable-next-line no-await-in-loop 62 | await waitForMine( 63 | ontology.add(admin, signTx, type, credentialItemName, version, reference, referenceType, referenceHash) 64 | ); 65 | // eslint-disable-next-line no-await-in-loop 66 | await waitForMine(pricing.setPrice(idv, signTx, type, credentialItemName, version, price)); 67 | } 68 | }); 69 | 70 | it('returns all prices', async () => { 71 | const pricedCredentialItems = [name, 'proofOfAge', ...newCredentialItemNames]; 72 | const prices = await pricing.getAllPrices(); 73 | expect(prices).to.be.an('array'); 74 | expect(_.map(prices, 'credentialItem.name')).to.include.members(pricedCredentialItems); 75 | expect(_.map(prices, 'idv')).to.include.members([idv, testIdv]); 76 | expect(prices).to.have.nested.property('[0].deprecated', deprecated, 'Includes deprecated flag'); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('Setting prices:', () => { 82 | it('sets the correct price', async () => { 83 | const newPrice = new Bn(5000); 84 | await waitForMine(pricing.setPrice(idv, signTx, type, name, version, newPrice)); 85 | const credentialItemPrice = await pricing.getPrice(idv, type, name, version); 86 | assertCredentialItemPrice(credentialItemPrice, [newPrice, idv, type, name, version, deprecated]); 87 | }); 88 | 89 | describe('on deprecated item', () => { 90 | before('deprecate item first', async () => { 91 | await waitForMine( 92 | ontology.add(admin, signTx, 'claim', 'deprecated', 'outdated', reference, referenceType, referenceHash) 93 | ); 94 | await waitForMine(pricing.setPrice(testIdv, signTx, 'claim', 'deprecated', 'outdated', 1234)); 95 | await waitForMine(ontology.deprecate(admin, signTx, 'claim', 'deprecated', 'outdated')); 96 | }); 97 | 98 | it('returns the deprecated price', async () => { 99 | const credentialItemPrice = await pricing.getPrice(testIdv, 'claim', 'deprecated', 'outdated'); 100 | assertCredentialItemPrice(credentialItemPrice, [1234, testIdv, 'claim', 'deprecated', 'outdated', true]); 101 | }); 102 | 103 | it('cannot set price on deprecated item', () => 104 | expect(waitForMine(pricing.setPrice(idv, signTx, 'claim', 'deprecated', 'outdated', 1234))).to.be.rejected); 105 | 106 | it('cannot change price on deprecated item', () => 107 | expect(waitForMine(pricing.setPrice(testIdv, signTx, 'claim', 'deprecated', 'outdated', 4321))).to.be.rejected); 108 | }); 109 | }); 110 | 111 | describe('Deleting prices:', () => { 112 | it('deletes price', async () => { 113 | await expect(pricing.getPrice(idv, type, name, version)).to.be.fulfilled; 114 | await waitForMine(pricing.deletePrice(idv, signTx, type, name, version)); 115 | await expect(pricing.getPrice(idv, type, name, version)).to.be.rejected; 116 | }); 117 | }); 118 | }); 119 | 120 | function assertCredentialItemPrice(credentialItemPrice, [price, idv, type, name, version, deprecated]) { 121 | expect(credentialItemPrice).to.have.property('id'); 122 | expect(credentialItemPrice.price).to.bignumber.equal(price); 123 | expect(credentialItemPrice).to.have.property('idv', idv); 124 | expect(credentialItemPrice).to.have.deep.property('credentialItem', { type, name, version }); 125 | expect(credentialItemPrice).to.have.property('deprecated', deprecated); 126 | } 127 | -------------------------------------------------------------------------------- /test/integration/08-transaction-details.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow,no-param-reassign */ 2 | const chai = require('chai'); 3 | 4 | const { expect } = chai; 5 | chai.use(require('chai-as-promised')); 6 | const Web3 = require('web3'); 7 | const MarketplaceTx = require('../../src/marketplace-tx'); 8 | const users = require('./users'); 9 | const signTx = require('./signtx'); 10 | 11 | // connect to a RSK or Ethereum node 12 | const url = process.env.ETH_NODE_URL; 13 | const web3 = new Web3(new Web3.providers.HttpProvider(url)); 14 | 15 | // initialise the marketplace-tx library and set the web3 connection 16 | const marketplaceTx = new MarketplaceTx({ web3 }); 17 | 18 | describe('TX Details', () => { 19 | const address1 = users[0].address; 20 | 21 | /** 22 | * This allows adjustment of the nonce before signing to simulate various status scenarios. 23 | * 24 | * @param nonceAdjustment Adjustment to the nonce as an integer 25 | * @returns {function(*=, *=)} 26 | */ 27 | function getSignTx(nonceAdjustment = 0) { 28 | return (addressFrom, rawTx) => { 29 | if (nonceAdjustment) { 30 | rawTx.nonce = `0x${(parseInt(rawTx.nonce, 16) + nonceAdjustment).toString(16)}`; 31 | } 32 | return signTx(addressFrom, rawTx); 33 | }; 34 | } 35 | 36 | // detect Ganache (TestRPC) which doesn't support txPool, 37 | // nor does it allow nonces higher than current txCount to test queuing 38 | const isGanache = web3.version.node.startsWith('EthereumJS TestRPC'); 39 | 40 | after(() => { 41 | marketplaceTx.nonce.clearAccounts(); 42 | }); 43 | 44 | describe('Checking transaction status unsupported', () => { 45 | before('before unsupported test', function() { 46 | if (!isGanache) { 47 | this.skip(); 48 | } 49 | }); 50 | 51 | it('should find transaction status to be unsupported', async () => { 52 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, 99999); 53 | expect(getStatusResult).to.equal('unsupported'); 54 | }); 55 | 56 | it('should find get transaction to be unsupported by hash', async () => { 57 | const getStatusResult = await marketplaceTx.transactionDetails.getTransaction( 58 | address1, 59 | '0x0005ec1a5423dc0044f000b880b0331d0114ce020746b294b277bf489f7694d9' 60 | ); 61 | 62 | expect(getStatusResult).to.be.an('object'); 63 | expect(getStatusResult.status).to.equal('unsupported'); 64 | expect(getStatusResult.details).to.equal(null); 65 | expect(Object.keys(getStatusResult).length).to.be.equal(2); 66 | }); 67 | }); 68 | 69 | describe('Checking transaction status mined', () => { 70 | let sendPromiseResultTxHash; 71 | 72 | before('send and mine normal transaction', async function() { 73 | if (isGanache) { 74 | this.skip(); 75 | } 76 | const sendPromise = marketplaceTx.sender.send({ 77 | fromAddress: address1, 78 | signTx: getSignTx(), 79 | contractName: 'CvcToken', 80 | method: 'approve', 81 | params: [address1, 0] 82 | }); 83 | const sendPromiseResult = await sendPromise; 84 | sendPromiseResultTxHash = sendPromiseResult.transactionHash; 85 | await marketplaceTx.tx.waitForMine(sendPromise); 86 | }); 87 | 88 | it('should find transaction status to be mined', async () => { 89 | const nonce = await marketplaceTx.tx.getTransactionCount(address1); 90 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, nonce - 1); 91 | expect(getStatusResult).to.equal('mined'); 92 | }); 93 | 94 | it('should get mined transaction details by hash', async () => { 95 | const getStatusResult = await marketplaceTx.transactionDetails.getTransaction(address1, sendPromiseResultTxHash); 96 | expect(getStatusResult).to.be.an('object'); 97 | expect(getStatusResult.status).to.equal('mined'); 98 | expect(getStatusResult.details).to.be.an('object'); 99 | expect(getStatusResult.details.status).to.equal('0x1'); 100 | expect(Object.keys(getStatusResult).length).to.be.equal(2); 101 | expect(Object.keys(getStatusResult.details).length).to.be.equal(12); 102 | }); 103 | }); 104 | 105 | describe('Checking transaction status pending', () => { 106 | let sendPromise; 107 | let sendPromiseResultTxHash; 108 | 109 | before('send normal transaction', async function() { 110 | if (isGanache) { 111 | this.skip(); 112 | } 113 | marketplaceTx.tx.web3.miner.stop(); 114 | sendPromise = marketplaceTx.sender.send({ 115 | fromAddress: address1, 116 | signTx: getSignTx(), 117 | contractName: 'CvcToken', 118 | method: 'approve', 119 | params: [address1, 0] 120 | }); 121 | const sendPromiseResult = await sendPromise; 122 | sendPromiseResultTxHash = sendPromiseResult.transactionHash; 123 | }); 124 | 125 | after('wait for mine', async function() { 126 | if (isGanache) { 127 | this.skip(); 128 | } 129 | marketplaceTx.tx.web3.miner.start(1); 130 | await marketplaceTx.tx.waitForMine(sendPromise); 131 | }); 132 | 133 | it('should find transaction status to be pending', async () => { 134 | const nonce = await marketplaceTx.tx.getTransactionCount(address1); 135 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, nonce); 136 | expect(getStatusResult).to.equal('pending'); 137 | }); 138 | 139 | it('should get pending transaction details by hash', async () => { 140 | const getStatusResult = await marketplaceTx.transactionDetails.getTransaction(address1, sendPromiseResultTxHash); 141 | expect(getStatusResult).to.be.an('object'); 142 | expect(getStatusResult.status).to.equal('pending'); 143 | expect(getStatusResult.details).to.be.an('object'); 144 | expect(Object.keys(getStatusResult).length).to.be.equal(2); 145 | expect(Object.keys(getStatusResult.details).length).to.be.equal(14); 146 | }); 147 | }); 148 | 149 | describe('Checking transaction status queued', () => { 150 | let sendPromise; 151 | let sendPromiseResultTxHash; 152 | 153 | before('send nonce + 1 transaction', async function() { 154 | if (isGanache) { 155 | this.skip(); 156 | } 157 | sendPromise = marketplaceTx.sender.send({ 158 | fromAddress: address1, 159 | signTx: getSignTx(1), 160 | contractName: 'CvcToken', 161 | method: 'approve', 162 | params: [address1, 0] 163 | }); 164 | const sendPromiseResult = await sendPromise; 165 | sendPromiseResultTxHash = sendPromiseResult.transactionHash; 166 | }); 167 | 168 | it('should find transaction status to be queued', async () => { 169 | const nonce = await marketplaceTx.tx.getTransactionCount(address1); 170 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, nonce + 1); 171 | expect(getStatusResult).to.equal('queued'); 172 | }); 173 | 174 | it('should get queued transaction details by hash', async () => { 175 | const getStatusResult = await marketplaceTx.transactionDetails.getTransaction(address1, sendPromiseResultTxHash); 176 | expect(getStatusResult).to.be.an('object'); 177 | expect(getStatusResult.status).to.equal('queued'); 178 | expect(getStatusResult.details).to.be.an('object'); 179 | expect(Object.keys(getStatusResult).length).to.be.equal(2); 180 | expect(Object.keys(getStatusResult.details).length).to.be.equal(14); 181 | }); 182 | }); 183 | 184 | describe('Checking transaction status unknown', () => { 185 | before('check if is supported', async function() { 186 | if (isGanache) { 187 | this.skip(); 188 | } 189 | }); 190 | 191 | it('should find transaction status to be unknown', async () => { 192 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, 99999); 193 | expect(getStatusResult).to.equal('unknown'); 194 | }); 195 | 196 | it('should find transaction status to be unknown by hash', async () => { 197 | const getStatusResult = await marketplaceTx.transactionDetails.getTransaction( 198 | address1, 199 | '0x0005ec1a5423dc0044f000b880b0331d0114ce020746b294b277bf489f7694d9' 200 | ); 201 | expect(getStatusResult.status).to.equal('unknown'); 202 | expect(getStatusResult.details).to.equal(null); 203 | }); 204 | 205 | it('should find transaction status to be unknown if set to transaction count (while not pending)', async () => { 206 | const nonce = await marketplaceTx.tx.getTransactionCount(address1); 207 | const getStatusResult = await marketplaceTx.transactionDetails.getTransactionStatus(address1, nonce); 208 | expect(getStatusResult).to.equal('unknown'); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/integration/docker-ganache/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | 3 | services: 4 | geth: 5 | image: trufflesuite/ganache-cli:v6.2.4 6 | ports: 7 | - 8545:8545 8 | entrypoint: 9 | - node 10 | - ./ganache-core.docker.cli.js 11 | - --mnemonic=drum muscle maximum umbrella night initial prevent auction pink old fault media 12 | - --host=0.0.0.0 13 | - --port=8545 14 | - --gasLimit=7992181 15 | - --gasPrice=1000000000 16 | - --defaultBalanceEther=1000 17 | -------------------------------------------------------------------------------- /test/integration/docker-geth/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | 3 | services: 4 | # starts a pre-initialised geth node with several prefunded addresses 5 | geth: 6 | image: civicteam/geth-privatenet-poa:stable 7 | ports: 8 | - 8545:8545 9 | healthcheck: 10 | # the node is considered healthy once it is possible to attach to it via HTTP 11 | test: geth attach http://localhost:8545 --exec "true" 12 | interval: 5s 13 | timeout: 2s 14 | retries: 10 15 | 16 | # bring up ready to start all services and wait until they are responding 17 | ready: 18 | image: tianon/true 19 | depends_on: 20 | geth: 21 | condition: service_healthy 22 | 23 | -------------------------------------------------------------------------------- /test/integration/signtx.js: -------------------------------------------------------------------------------- 1 | const EthTx = require('ethereumjs-tx'); 2 | const users = require('./users'); 3 | 4 | const mapAddressToKey = address => users.filter(item => item.address === address)[0].privateKey; 5 | 6 | const signTx = (address, rawTx) => 7 | new Promise((resolve, reject) => { 8 | const ethtx = new EthTx(rawTx); 9 | const key = mapAddressToKey(address); 10 | if (!key) { 11 | reject(new Error(`Private key for ${address} not found in users.js`)); 12 | } 13 | ethtx.sign(Buffer.from(key, 'hex')); 14 | const hex = `0x${ethtx.serialize().toString('hex')}`; 15 | resolve(hex); 16 | }); 17 | 18 | module.exports = (address, tx) => 19 | Array.isArray(tx) ? Promise.all(tx.map(rawTx => signTx(address, rawTx))) : signTx(address, tx); 20 | -------------------------------------------------------------------------------- /test/integration/users.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | name: 'user1', 4 | privateKey: '4d2276a189703729252cda6b79470ae9e6a59afa5c55ec3a96ce35da937f78b8', 5 | address: '0x48089757dbc23bd8e49436247c9966ff15802978' 6 | }, 7 | { 8 | name: 'user2', 9 | privateKey: '51a86ef96696673de58e4ef2481f78ed8a65100876d30ce1432cadfdc2efc6de', 10 | address: '0x21bcd4080fa6c19da0669ab6a9bd2f259cbc8f02' 11 | }, 12 | { 13 | name: 'user3', 14 | privateKey: '16f6da922171e3e9a2baed6484af906811ef755baf2eca32f5c93eca1d15e11c', 15 | address: '0xfa4104ad946e0becde5b79a77047b590ae752daa' 16 | }, 17 | { 18 | name: 'user4', 19 | privateKey: '3219bda2c7c8a8c9a3c2c0e3f95476da8eb346c107a51581456a86ede04e4dc5', 20 | address: '0xec9c370130ff9446e2297ccf07c177474eba26d9' 21 | }, 22 | { 23 | name: 'user5', 24 | privateKey: '1ec8d8077deed7551ab2aab14a9a40441de2517187377d7d1aba5e22c9afed14', 25 | address: '0x2b9b3b3588021c3b12662693573e4184d425a6ae' 26 | }, 27 | { 28 | name: 'user6', 29 | privateKey: '3cfaa7dd0393064343fbf906606d502626845a28deb569f2e452976f80158732', 30 | address: '0xb2cc0433a07509007b2fd72e47bec107d73729d6' 31 | }, 32 | { 33 | name: 'user7', 34 | privateKey: 'ca63016c32b38a3ada0de17770bae33258c36f9979d017c662fb504cb80ed5b4', 35 | address: '0x1a88a35421a4a0d3e13fe4e8ebcf18e9a249dc5a' 36 | }, 37 | { 38 | name: 'user8', 39 | privateKey: '943efe954f165109e7a048d92f2ffecdad943691b47eb9355cfc56cd543ce59b', 40 | address: '0xb69271f06da20cf1b2545e1fb969cd827e281434' 41 | } 42 | ]; 43 | module.exports = users; 44 | -------------------------------------------------------------------------------- /test/nonce.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const util = require('../src/support/nonce/util'); 5 | const ethUtil = require('ethereumjs-util'); 6 | const AccountInspector = require('../src/support/nonce/accountInspector'); 7 | const InMemoryNonceManager = require('../src/support/nonce/inmemory'); 8 | const Web3 = require('web3'); 9 | const FakeProvider = require('web3-fake-provider'); 10 | const _ = require('lodash'); 11 | chai.use(require('chai-as-promised')); 12 | 13 | const sandbox = sinon.createSandbox(); 14 | const { expect } = chai; 15 | const account = '0x123'; 16 | const emptyTxPool = { pending: {}, queued: {} }; 17 | 18 | describe('nonce management', () => { 19 | afterEach(() => { 20 | sandbox.restore(); 21 | }); 22 | 23 | describe('nonce calculation', () => { 24 | let debugLogSpy; 25 | 26 | beforeEach(() => { 27 | debugLogSpy = sandbox.spy(); 28 | }); 29 | 30 | it('picks the first nonce for fresh account', () => { 31 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, {}, 0, emptyTxPool); 32 | expect(nextNonce).to.equal(0); 33 | expect(acquiredNonces).to.deep.equal({ [nextNonce]: true }); 34 | }); 35 | 36 | it('picks the next nonce with empty txpool', () => { 37 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, {}, 7, emptyTxPool); 38 | expect(nextNonce).to.equal(7); 39 | expect(acquiredNonces).to.deep.equal({ [nextNonce]: true }); 40 | }); 41 | 42 | it('picks the next nonce respecting pending txs', () => { 43 | const txPool = { ...emptyTxPool, pending: { 4: 'tx', 5: 'tx' } }; 44 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, {}, 4, txPool); 45 | expect(nextNonce).to.equal(6); 46 | expect(acquiredNonces).to.deep.equal({ [nextNonce]: true }); 47 | }); 48 | 49 | it('ignores queued transactions', () => { 50 | const txPool = { ...emptyTxPool, queued: { 4: 'tx', 5: 'tx' } }; 51 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, {}, 3, txPool); 52 | expect(nextNonce).to.equal(3); 53 | expect(acquiredNonces).to.deep.equal({ [nextNonce]: true }); 54 | }); 55 | 56 | it('releases mined transactions', () => { 57 | const storedNonces = { 4: true, 5: true }; 58 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, storedNonces, 7, emptyTxPool); 59 | expect(nextNonce).to.equal(7); 60 | expect(acquiredNonces).to.deep.equal({ [nextNonce]: true }); 61 | expect(debugLogSpy.calledWith('released nonces: 4, 5')).to.be.true; 62 | }); 63 | 64 | it('respects acquired nonces from storage', () => { 65 | const storedNonces = { 4: true, 5: true }; 66 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, storedNonces, 4, emptyTxPool); 67 | expect(nextNonce).to.equal(6); 68 | expect(acquiredNonces).to.include(storedNonces); 69 | expect(acquiredNonces).to.deep.include({ [nextNonce]: true }); 70 | }); 71 | 72 | it('fills in gap in stored nonces', () => { 73 | const storedNonces = { 2: true, 3: true, 5: true, 6: true }; 74 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, storedNonces, 2, emptyTxPool); 75 | expect(nextNonce).to.equal(4); 76 | expect(acquiredNonces).to.include(storedNonces); 77 | expect(acquiredNonces).to.deep.include({ [nextNonce]: true }); 78 | }); 79 | 80 | it('merges pending and stored nonces to fill gaps', () => { 81 | const storedNonces = { 2: true, 5: true }; 82 | const txPool = { ...emptyTxPool, pending: { 3: 'tx', 6: 'tx' } }; 83 | const { nextNonce, acquiredNonces } = util.calculateNonce(debugLogSpy, storedNonces, 2, txPool); 84 | expect(nextNonce).to.equal(4); 85 | expect(acquiredNonces).to.include(storedNonces); 86 | expect(acquiredNonces).to.deep.include({ [nextNonce]: true }); 87 | }); 88 | }); 89 | 90 | describe('in-memory nonce manager', () => { 91 | let calculateNonceStub; 92 | let accountInspectorStub; 93 | const txCount = 3; 94 | 95 | beforeEach(() => { 96 | calculateNonceStub = sandbox.stub(util, 'calculateNonce'); 97 | accountInspectorStub = sandbox.createStubInstance(AccountInspector); 98 | 99 | accountInspectorStub.getTransactionCount.resolves(txCount); 100 | accountInspectorStub.inspectTxPool.resolves(emptyTxPool); 101 | calculateNonceStub.withArgs(sinon.match.func, sinon.match.object, txCount, emptyTxPool).returns(txCount); 102 | }); 103 | 104 | it('should get nonce for account', async () => { 105 | const manager = new InMemoryNonceManager(accountInspectorStub); 106 | const nonce = await manager.getNonceForAccount(account); 107 | expect(nonce).to.equal(txCount); 108 | const nextNonce = await manager.getNonceForAccount(account); 109 | expect(nextNonce).to.be.not.equal(nonce); 110 | expect(nextNonce).to.equal(txCount + 1); 111 | }); 112 | 113 | it('should release nonce', async () => { 114 | const manager = new InMemoryNonceManager(accountInspectorStub); 115 | const nonce = await manager.getNonceForAccount(account); 116 | expect(nonce).to.equal(txCount); 117 | manager.releaseAccountNonce(account, nonce); 118 | const nextNonce = await manager.getNonceForAccount(account); 119 | expect(nextNonce).to.be.equal(nonce, 'must be the same nonce, because it was released for re-use'); 120 | }); 121 | 122 | it('should release multiple nonces', async () => { 123 | const manager = new InMemoryNonceManager(accountInspectorStub); 124 | const nonces = await Promise.all(_.times(3, () => manager.getNonceForAccount(account))); 125 | expect(nonces) 126 | .to.have.members([txCount, txCount + 1, txCount + 2]) 127 | .and.have.lengthOf(3); 128 | manager.releaseAccountNonces(account, nonces); 129 | const nextNonce = await manager.getNonceForAccount(account); 130 | expect(nextNonce).to.be.equal(txCount, 'must be the first nonce, because it was released for re-use'); 131 | }); 132 | 133 | it('should clear account store', async () => { 134 | const manager = new InMemoryNonceManager(accountInspectorStub); 135 | const nonce = await manager.getNonceForAccount(account); 136 | expect(nonce).to.equal(txCount); 137 | manager.clearAccounts(); 138 | const nextNonce = await manager.getNonceForAccount(account); 139 | expect(nextNonce).to.be.equal(nonce, 'must be the same nonce, because account store was cleared'); 140 | }); 141 | }); 142 | 143 | describe('account inspector', () => { 144 | let accountInspector; 145 | const address1 = '0x123ABC'; 146 | const address2 = '0x321CBA'; 147 | const address1Checksummed = ethUtil.toChecksumAddress(address1); 148 | const address2Checksummed = ethUtil.toChecksumAddress(address2); 149 | 150 | const txPool = { 151 | pending: { 152 | [address1Checksummed]: { 153 | 10: 'tx' 154 | }, 155 | [address2Checksummed]: { 156 | 10: 'tx' 157 | } 158 | }, 159 | queued: { 160 | [address1Checksummed]: { 161 | 12: 'tx' 162 | } 163 | } 164 | }; 165 | 166 | beforeEach(() => { 167 | const web3 = new Web3(new FakeProvider()); 168 | web3.txpool = { inspect: cb => cb(null, txPool) }; 169 | accountInspector = new AccountInspector(web3); 170 | }); 171 | 172 | it('should inspect tx pool for non-checksummed addresses', async () => { 173 | const accountTxPool = await accountInspector.inspectTxPool(address1); 174 | expect(accountTxPool).to.deep.equal({ 175 | pending: txPool.pending[address1Checksummed] || {}, 176 | queued: txPool.queued[address1Checksummed] || {} 177 | }); 178 | }); 179 | 180 | it('should inspect tx pool for checksummed addresses', async () => { 181 | const accountTxPool = await accountInspector.inspectTxPool(address2Checksummed); 182 | expect(accountTxPool).to.deep.equal({ 183 | pending: txPool.pending[address2Checksummed] || {}, 184 | queued: txPool.queued[address2Checksummed] || {} 185 | }); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/ontology.js: -------------------------------------------------------------------------------- 1 | require('longjohn'); 2 | const chai = require('chai'); 3 | const Web3 = require('web3'); 4 | const FakeProvider = require('web3-fake-provider'); 5 | const MarketplaceTx = require('../src/marketplace-tx'); 6 | 7 | const { expect } = chai; 8 | 9 | const web3 = new Web3(new FakeProvider()); 10 | const marketplaceTx = new MarketplaceTx({ web3 }, { preloadContracts: false }); 11 | 12 | describe('ontology.js', () => { 13 | const { ontology } = marketplaceTx; 14 | const invalidCredentialItem = 'credential-proofOfIdentity'; 15 | const validCredentialItem = 'credential-proofOfIdentity-v1.0'; 16 | const validType = 'credential'; 17 | const validName = 'proofOfIdentity'; 18 | const validVersion = 'v1.0'; 19 | 20 | it('parses a credential item external id into type, name and version', () => { 21 | const [type, name, version] = ontology.parseExternalId(validCredentialItem); 22 | expect(type).to.equal(validType); 23 | expect(name).to.equal(validName); 24 | expect(version).to.equal(validVersion); 25 | }); 26 | 27 | it('throws an error when external id format is incorrect', () => { 28 | expect(() => ontology.parseExternalId(invalidCredentialItem)).to.throw(); 29 | }); 30 | 31 | it('composes a type, name and version into a credential item external id', () => { 32 | const credentialItem = ontology.composeExternalId(validType, validName, validVersion); 33 | expect(credentialItem).to.equal(validCredentialItem); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/pricing.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3'); 2 | const FakeProvider = require('web3-fake-provider'); 3 | const chai = require('chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | const sandbox = require('sinon').createSandbox(); 6 | 7 | const pricing = require('../src/pricing'); 8 | const asserts = require('../src/support/asserts'); 9 | const tx = require('../src/support/tx'); 10 | const { NotFoundError } = require('../src/support/errors'); 11 | 12 | chai.use(chaiAsPromised); 13 | 14 | const { expect } = chai; 15 | 16 | const pricingContract = { 17 | getPrice: sandbox.stub() 18 | }; 19 | 20 | asserts.web3 = new Web3(new FakeProvider()); 21 | 22 | const idv = '0xb69271f06da20cf1b2545e1fb969cd827e281434'; 23 | const credentialItem = { 24 | type: 'credential', 25 | name: 'proofOfIdentity', 26 | version: '1.0' 27 | }; 28 | const priceIdMissing = '0x0000000000000000000000000000000000000000000000000000000000000000'; 29 | const priceIdPresent = '0x1111111111111111111111111111111111111111111111111111111111111111'; 30 | const priceValue = 1000; 31 | 32 | describe('pricing', () => { 33 | beforeEach('stub tx module', () => { 34 | sandbox.stub(tx, 'contractInstance').returns(Promise.resolve(pricingContract)); 35 | }); 36 | 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | describe('getPrice', () => { 42 | it('should reject with a NotFoundError if getPrice returns nothing', () => { 43 | pricingContract.getPrice.returns([]); 44 | 45 | const pricePromise = pricing.getPrice(idv, credentialItem.type, credentialItem.name, credentialItem.version); 46 | 47 | return expect(pricePromise).to.be.rejectedWith(NotFoundError); 48 | }); 49 | 50 | it('should reject with a NotFoundError if the price is not found (0x0...0 id)', async () => { 51 | pricingContract.getPrice.returns([ 52 | priceIdMissing, 53 | priceValue, 54 | idv, 55 | credentialItem.type, 56 | credentialItem.name, 57 | credentialItem.version 58 | ]); 59 | 60 | const pricePromise = pricing.getPrice(idv, credentialItem.type, credentialItem.name, credentialItem.version); 61 | 62 | return expect(pricePromise).to.be.rejectedWith(NotFoundError); 63 | }); 64 | 65 | it('should return a price if the price is found', async () => { 66 | pricingContract.getPrice.returns([ 67 | priceIdPresent, 68 | priceValue, 69 | idv, 70 | credentialItem.type, 71 | credentialItem.name, 72 | credentialItem.version 73 | ]); 74 | 75 | const price = await pricing.getPrice(idv, credentialItem.type, credentialItem.name, credentialItem.version); 76 | 77 | expect(price.price).to.equal(priceValue); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/sender.js: -------------------------------------------------------------------------------- 1 | require('longjohn'); 2 | const chai = require('chai'); 3 | const Web3 = require('web3'); 4 | const FakeProvider = require('web3-fake-provider'); 5 | const EthTx = require('ethereumjs-tx'); 6 | const sandbox = require('sinon').createSandbox(); 7 | const MarketplaceTx = require('../src/marketplace-tx'); 8 | const nonce = require('../src/support/nonce'); 9 | const tx = require('../src/support/tx'); 10 | 11 | const { expect } = chai; 12 | chai.use(require('chai-as-promised')); 13 | 14 | const web3 = new Web3(new FakeProvider()); 15 | const marketplaceTx = new MarketplaceTx({ web3 }, { preloadContracts: false }); 16 | 17 | describe('sender.js', () => { 18 | describe('assert sender and signer address match', () => { 19 | const signerPrivateKey = 'b0895a2390fdcf8dd1b2ca3788c3e936eb75452203453c06ea1ed968a1c2b8b5'; 20 | // For reference, signerAddress is '0x3425efd013805b95fe1acb51482ed816a377c364' 21 | const senderPrivateKey = 'b0895a2390fdcf8dd1b2ca3788c3e936eb75452203453c06ea1ed968a1c2b8b2'; 22 | const senderAddress = '0x57a6d277516869afea3306335207c82f97e0d5d2'; 23 | 24 | const signTxWithKey = privateKey => (from, toSign) => { 25 | const sign = rawTx => { 26 | const ethTx = new EthTx(rawTx); 27 | ethTx.sign(Buffer.from(privateKey, 'hex')); 28 | return Promise.resolve(`0x${ethTx.serialize().toString('hex')}`); 29 | }; 30 | 31 | return Array.isArray(toSign) ? Promise.all(toSign.map(sign)) : sign(toSign); 32 | }; 33 | 34 | const { sender } = marketplaceTx; 35 | 36 | before(() => { 37 | // Whenever nonce is required return default one 38 | sandbox.stub(nonce, 'getNonceForAccount').resolves(0); 39 | sandbox.stub(nonce, 'releaseAccountNonces').resolves(); 40 | 41 | // Stub create tx to avoid loading contract instance for generating data 42 | sandbox.stub(tx, 'createTx').callsFake(txParams => 43 | Promise.resolve({ 44 | to: senderAddress, 45 | value: '0x0', 46 | from: txParams.fromAddress, 47 | data: '0x12345678', 48 | chainId: '0x123', 49 | txOptions: { 50 | gasPrice: `0x111111`, 51 | gas: '0xfffff' 52 | } 53 | }) 54 | ); 55 | }); 56 | 57 | after(() => { 58 | sandbox.restore(); 59 | }); 60 | 61 | describe('when sender and signer address mismatch', () => { 62 | it('throws on batch', () => 63 | expect( 64 | sender.sendChain({ 65 | fromAddress: senderAddress, 66 | signTx: signTxWithKey(signerPrivateKey), 67 | transactions: [{}] 68 | }) 69 | ) 70 | .to.be.eventually.rejectedWith(marketplaceTx.errors.FailedBatchError) 71 | .property('cause') 72 | .instanceOf(marketplaceTx.errors.SignerSenderAddressMismatchError)); 73 | 74 | it('throws on send', () => 75 | expect( 76 | sender.send({ 77 | fromAddress: senderAddress, 78 | signTx: signTxWithKey(signerPrivateKey) 79 | }) 80 | ).to.be.rejectedWith(marketplaceTx.errors.SignerSenderAddressMismatchError)); 81 | 82 | it('throws on platform coin send', () => 83 | expect( 84 | sender.sendPlatformCoin({ 85 | fromAddress: senderAddress, 86 | toAddress: senderAddress, 87 | signTx: signTxWithKey(signerPrivateKey), 88 | value: 0 89 | }) 90 | ).to.be.rejectedWith(marketplaceTx.errors.SignerSenderAddressMismatchError)); 91 | }); 92 | 93 | describe('when using matching private key', () => { 94 | it('fullfils', () => 95 | expect( 96 | sender.sendPlatformCoin({ 97 | fromAddress: senderAddress, 98 | toAddress: senderAddress, 99 | value: 0, 100 | signTx: signTxWithKey(senderPrivateKey) 101 | }) 102 | ).to.be.fulfilled); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/token.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const chai = require('chai'); 3 | const Web3 = require('web3'); 4 | const FakeProvider = require('web3-fake-provider'); 5 | const MarketplaceTx = require('../src/marketplace-tx'); 6 | 7 | const { expect } = chai; 8 | chai.use(require('chai-as-promised')); 9 | 10 | const web3 = new Web3(new FakeProvider()); 11 | const marketplaceTx = new MarketplaceTx({ web3 }, { preloadContracts: false }); 12 | 13 | describe('token.js', () => { 14 | const { token } = marketplaceTx; 15 | const invalidAddress = '0x123'; 16 | const validAddress = '0x48089757dbc23bd8e49436247c9966ff15802978'; 17 | 18 | it('validates addresses', () => { 19 | expect(token.transfer(invalidAddress, 1, validAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 20 | expect(token.transfer(validAddress, 1, invalidAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 21 | 22 | expect(token.approve(validAddress, 1, invalidAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 23 | expect(token.approve(invalidAddress, 1, validAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 24 | 25 | expect(token.approveWithReset(validAddress, 1, invalidAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 26 | expect(token.approveWithReset(validAddress, 1, validAddress, 100)).to.be.rejectedWith(`/${invalidAddress}/`); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/tx.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | require('longjohn'); 3 | const chai = require('chai'); 4 | const sandbox = require('sinon').createSandbox(); 5 | const nonce = require('../src/support/nonce'); 6 | const fetchMock = require('fetch-mock'); 7 | const proxyquire = require('proxyquire'); 8 | 9 | const { expect } = chai; 10 | chai.use(require('chai-as-promised')); 11 | 12 | describe('tx.js', () => { 13 | describe('when createTx throws', () => { 14 | // eslint-disable-next-line global-require 15 | const tx = require('../src/support/tx'); 16 | let releaseNonceStub; 17 | beforeEach('stub', () => { 18 | const getDataApplyStub = sandbox.stub().throws('Error', 'Invalid number of arguments to Solidity function'); 19 | const contractInstanceStub = { 20 | contract: { someMethodToCall: { getData: sandbox.stub({ apply: () => {} }, 'apply').throws(getDataApplyStub) } } 21 | }; 22 | sandbox.stub(tx, 'contractInstance').resolves(contractInstanceStub); 23 | sandbox.stub(nonce, 'getNonceForAccount').resolves(12345); 24 | releaseNonceStub = sandbox.stub(nonce, 'releaseAccountNonce'); 25 | }); 26 | 27 | afterEach(() => { 28 | sandbox.reset(); 29 | }); 30 | 31 | it('releases nonce', async () => { 32 | await expect( 33 | tx.createTx({ 34 | fromAddress: '0x48089757dbc23bd8e49436247c9966ff15802978', 35 | contractName: 'Contract', 36 | method: 'someMethodToCall', 37 | args: ['arg1', 'arg2'], 38 | assignedNonce: true 39 | }) 40 | ).to.be.rejectedWith('Invalid number of arguments to Solidity function'); 41 | 42 | expect(releaseNonceStub.calledOnceWithExactly('0x48089757dbc23bd8e49436247c9966ff15802978', 12345)).to.be.true; 43 | }); 44 | }); 45 | 46 | describe('waitForMine', () => { 47 | // eslint-disable-next-line global-require 48 | const tx = require('../src/support/tx'); 49 | before('Mining never resolves to a tx receipt', () => { 50 | sandbox.stub(tx, 'getTransactionReceipt').resolves(null); 51 | }); 52 | 53 | after(() => { 54 | sandbox.restore(); 55 | }); 56 | 57 | it('waitForMine timeout', () => 58 | expect(tx.waitForMine(Promise.resolve({ transactionHash: '0x' }), 1)).to.be.rejectedWith( 59 | /getTransactionReceiptMined timeout/ 60 | )).timeout(4000); 61 | }); 62 | 63 | describe('When we pass contract.url to contractInstance', () => { 64 | const tx = proxyquire('../src/support/tx', { 65 | '../config': () => ({ 66 | contracts: { url: 'http://localhost' } 67 | }), 68 | 'truffle-contract': () => ({ 69 | setProvider: () => {}, 70 | deployed: () => ({ 71 | catch: () => ({ 72 | then: () => Promise.resolve({ foo: 'bar' }) 73 | }) 74 | }) 75 | }) 76 | }); 77 | before('stub', () => { 78 | tx.web3 = sandbox.stub().returns({}); 79 | fetchMock.mock('http://localhost/CvcEscrow.json', { contractName: 'CvcEscrow' }); 80 | }); 81 | 82 | after(() => { 83 | sandbox.restore(); 84 | }); 85 | 86 | it('should fetch the contract', async () => { 87 | const result = await tx.contractInstance('CvcEscrow'); 88 | expect(result).to.deep.equal({ foo: 'bar' }); 89 | }); 90 | }); 91 | 92 | describe('When we pass contract.dir to contractInstance', () => { 93 | const tx = proxyquire('../src/support/tx', { 94 | '../config': () => ({ 95 | contracts: { dir: '../../test/assets/contracts' } 96 | }), 97 | 'truffle-contract': () => ({ 98 | setProvider: () => {}, 99 | deployed: () => ({ 100 | catch: () => ({ 101 | then: () => Promise.resolve({ foo: 'bar' }) 102 | }) 103 | }) 104 | }) 105 | }); 106 | before('stub', () => { 107 | tx.web3 = sandbox.stub().returns({}); 108 | }); 109 | 110 | after(() => { 111 | sandbox.restore(); 112 | }); 113 | 114 | it('should fetch the contract', async () => { 115 | const result = await tx.contractInstance('mockContract'); 116 | expect(result).to.deep.equal({ foo: 'bar' }); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | require('longjohn'); 2 | const chai = require('chai'); 3 | const Web3 = require('web3'); 4 | const FakeProvider = require('web3-fake-provider'); 5 | const MarketplaceTx = require('../src/marketplace-tx'); 6 | 7 | const { expect } = chai; 8 | chai.use(require('chai-as-promised')); 9 | 10 | const web3 = new Web3(new FakeProvider()); 11 | const marketplaceTx = new MarketplaceTx({ web3 }, { preloadContracts: false }); 12 | 13 | describe('util.js', () => { 14 | const { util } = marketplaceTx; 15 | it('should convert to and from CVC', () => { 16 | expect(util.bnToCVC(1.25e8).toNumber()).to.equal(1.25); 17 | expect(util.CVCToBN(1.25).toNumber()).to.equal(1.25e8); 18 | expect(util.bnToCVC(util.CVCToBN(1)).toNumber()).to.equal(1); 19 | }); 20 | 21 | describe('util timeout', () => { 22 | const { timeout } = util; 23 | const sleep = ms => new Promise(r => setTimeout(r, ms)); 24 | 25 | it('promise resolves after timeout', () => { 26 | const promise = sleep(100).then(() => 'Ok'); 27 | const wrapped = timeout(promise, 50, 'Timeout'); 28 | return wrapped.then( 29 | () => expect.fail('should throw'), 30 | error => 31 | expect(error) 32 | .instanceOf(timeout.Error) 33 | .to.have.property('message', 'Timeout') 34 | ); 35 | }); 36 | 37 | it('promise rejects after timeout', () => { 38 | const promise = sleep(100).then(() => { 39 | throw new Error('Failed'); 40 | }); 41 | const wrapped = timeout(promise, 50, 'Timeout'); 42 | return wrapped.then( 43 | () => expect.fail('should throw'), 44 | error => 45 | expect(error) 46 | .instanceOf(timeout.Error) 47 | .to.have.property('message', 'Timeout') 48 | ); 49 | }); 50 | 51 | it('promise resolves before timeout', () => { 52 | const promise = sleep(50).then(() => 'Ok'); 53 | const wrapped = timeout(promise, 100, 'Timeout'); 54 | return wrapped.then(res => expect(res).to.equal('Ok')); 55 | }); 56 | 57 | it('promise rejects before timeout', () => { 58 | const promise = sleep(50).then(() => { 59 | throw new Error('Failed'); 60 | }); 61 | const wrapped = timeout(promise, 100, 'Timeout'); 62 | return wrapped.then( 63 | () => expect.fail('should throw'), 64 | error => { 65 | expect(error).not.instanceOf(timeout.Error); 66 | expect(error).to.have.property('message', 'Failed'); 67 | } 68 | ); 69 | }); 70 | }); 71 | }); 72 | --------------------------------------------------------------------------------