[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"es6\": true,\n    \"node\": true\n  },\n  \"extends\": [\"eslint:recommended\", \"plugin:prettier/recommended\"],\n  \"parserOptions\": {\n    \"ecmaVersion\": 2018\n  },\n  \"plugins\": [\"prettier\"],\n  \"rules\": {\n    \"prettier/prettier\": \"error\",\n    \"linebreak-style\": [\"error\", \"unix\"],\n    \"no-console\": \"off\",\n    \"camelcase\": [\n      \"error\",\n      {\n        \"properties\": \"never\",\n        \"ignoreDestructuring\": true\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": ".env\n.DS_Store\n.data/\n.vscode\n.vscode/\n.nyc_output\nnode_modules/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\ntest.js\ninit/init.json\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"semi\": false,\n  \"printWidth\": 120\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Node.js 8.x LTS on Debian Stretch Linux\n# see: https://github.com/nodejs/LTS\n# see: https://hub.docker.com/_/node/\nFROM node:12.14.1-stretch\n\nLABEL MAINTAINER=\"Jacob Henderson <jacob@tierion.com>\"\n\n# The `node` user and its home dir is provided by\n# the base image. Create a subdir where app code lives.\nRUN mkdir /home/node/app\n\nWORKDIR /home/node/app\n\nCOPY package.json yarn.lock server.js /home/node/app/\nRUN yarn policies set-version 1.22.10\nRUN yarn\n\nRUN mkdir -p /home/node/app/scripts\nCOPY ./scripts/*.sh /home/node/app/scripts/\n\nRUN mkdir -p /home/node/app/lib\nCOPY ./lib/*.js /home/node/app/lib/\n\nRUN mkdir -p /home/node/app/lib/endpoints\nCOPY ./lib/endpoints/*.js /home/node/app/lib/endpoints/\n\nRUN mkdir -p /home/node/app/lib/models\nCOPY ./lib/models/*.js /home/node/app/lib/models/\n\nRUN mkdir -p /root/.lnd\nRUN mkdir -p /root/.chainpoint/gateway/data/rocksdb\nRUN chmod -R 777 /root\n\nEXPOSE 80\n\nCMD [\"yarn\", \"start\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "# First target in the Makefile is the default.\nall: help\n\n# without this 'source' won't work.\nSHELL := /bin/bash\n\n# Get the location of this makefile.\nROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\n# Get home directory of current users\nGATEWAY_DATADIR := $(shell eval printf \"~$$USER\")/.chainpoint/gateway\n\n# Get home directory of current users\nHOMEDIR := $(shell eval printf \"~$$USER\")\nCORE_DATADIR := ${HOMEDIR}/.chainpoint/core\n\nUID := $(shell id -u $$USER)\nGID := $(shell id -g $$USER)\n\n.PHONY : help\nhelp : Makefile\n\t@sed -n 's/^##//p' $<\n\n## logs            : Tail Gateway logs\n.PHONY : logs\nlogs:\n\tdocker service logs -f chainpoint-gateway_chainpoint-gateway --raw\n\n## up              : Start Gateway in dev mode\n.PHONY : up\nup: build-config build build-rocksdb\n\tdocker-compose up -d\n\n## down            : Shutdown Gateway\n.PHONY : down\ndown:\n\tdocker-compose down\n\n## clean           : Shutdown and **destroy** all local Gateway data\n.PHONY : clean\nclean: stop\n\t@rm -rf ${GATEWAY_DATADIR}/data/rocksdb/*\n\t@chmod 777 ${GATEWAY_DATADIR}/data/rocksdb\n\n## burn            : Shutdown and **destroy** all local Gateway data\n.PHONY : burn\nburn: clean\n\t@rm -rf ${HOMEDIR}/.chainpoint/gateway/.lnd\n\t@rm -rf init/init.json\n\t@docker swarm leave --force || echo \"already left swarm\"\n\n## restart         : Restart Gateway in dev mode\n.PHONY : restart\nrestart: down up\n\n## build           : Build Gateway image\n.PHONY : build\nbuild:\n\tdocker build -t chainpoint-gateway .\n\tdocker tag chainpoint-gateway gcr.io/chainpoint-registry/github-chainpoint-chainpoint-gateway:latest\n\tdocker container prune -f\n\n## build-config    : Copy the .env config from .env.sample\n.PHONY : build-config\nbuild-config:\n\t@[ ! -f ./.env ] && \\\n\t\tcp .env.sample .env && \\\n\t\techo 'Copied config .env.sample to .env' || true\n\n## build-rocksdb   : Ensure the RocksDB data dir exists\n.PHONY : build-rocksdb\nbuild-rocksdb:\n\t@echo Setting up directories...\n\t@mkdir -p ${GATEWAY_DATADIR}/data/rocksdb && chmod 777 ${GATEWAY_DATADIR}/data/rocksdb\n\n## pull            : Pull Docker images\n.PHONY : pull\npull:\n\tdocker-compose pull\n\n## git-pull        : Git pull latest\n.PHONY : git-pull\ngit-pull:\n\t@git pull --all\n\t@git submodule update --init --remote --recursive\n\n## upgrade         : Same as `make down && git pull && make up`\n.PHONY : upgrade\nupgrade: down git-pull up\n\n## install-deps\t         : Install system dependencies\ninstall-deps:\n\tscripts/install_deps.sh\n\t@echo Please login and logout to enable docker\n\n## init\t         : Bring up yarn, swarm, and generate secrets\ninit: build-rocksdb init-yarn init-swarm\n\n## init-yarn       : Initialize dependencies\ninit-yarn:\n\t@echo Installing packages...\n\t@yarn >/dev/null\n\n## init-swarm      : Initialize a docker swarm\n.PHONY : init-swarm\ninit-swarm:\n\t@node ./init/index.js\n\n## init-swarm-restart     : Initialize a docker swarm, abandon current configuration\n.PHONY : init-swarm-restart\ninit-swarm-restart: stop\n\t@rm -rf ~/.chainpoint/gateway/.lnd\n\t@rm -rf ./init/init.json\n\t@node ./init/index.js\n\t@docker swarm leave --force || echo \"already left swarm\"\n\n## init-restart         : Bring up yarn, swarm, and generate secrets, abondon current configuration\ninit-restart: build-rocksdb init-yarn init-swarm-restart\n\n## deploy          : deploys a swarm stack\ndeploy:\n\tset -a && source .env && set +a && export USERID=${UID} && export GROUPID=${GID} && docker stack deploy -c swarm-compose.yaml chainpoint-gateway\n\n## optimize-network: increases number of sockets host can use\noptimize-network:\n\t@sudo sysctl net.core.somaxconn=1024\n\t@sudo sysctl net.ipv4.tcp_fin_timeout=30\n\t@sudo sysctl net.ipv4.tcp_tw_reuse=1\n\t@sudo sysctl net.core.netdev_max_backlog=2000\n\t@sudo sysctl net.ipv4.tcp_max_syn_backlog=2048\n\n## stop\t         : removes a swarm stack\nstop:\n\tdocker stack rm chainpoint-gateway\n\trm -rf ${HOMEDIR}/.chainpoint/gateway/.lnd/tls.*\n"
  },
  {
    "path": "README.md",
    "content": "# Chainpoint Gateway\n\n[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\nSee [Chainpoint Start](https://github.com/chainpoint/chainpoint-start) for an overview of the Chainpoint Network.\n\nA Chainpoint Gateway is a dedicated server for generating many Chainpoint proofs with a single request to the Chainpoint Network.\n\nEach Gateway has an integrated Lightning Node running [LND](https://github.com/lightningnetwork/lnd). Gateways use [Lightning Service Authentication Tokens](https://www.npmjs.com/package/lsat-js) (LSATs) to pay Cores an `anchor fee` when submitting a Merkle root. The default anchor fee is 2 [satoshis](<https://en.bitcoin.it/wiki/Satoshi_(unit)>). Core operators can set their `anchor fee` to adapt to changing market conditions, and compete to receive transactions from Gateways\n\nGateway setup takes 45 - 90 mins, due to activities that require the automated setup tools to interact with the Bitcoin Blockchain.\n\n- Lightning Node sync (10 - 15 minutes)\n- Funding the Lightning wallet and waiting for 3 confirmations (avg 30 mins)\n\n## Installation\n\n### Requirements\n\nThe following software is required:\n\n- `*Nix-based OS (Ubuntu Linux and MacOS have been tested)`\n- `BASH`\n- `Git`\n- `Docker`\n\nA BASH script to install all other dependencies (make, openssl, nodejs, yarn) on Ubuntu and Mac can be run from `make install-deps`.\n\nChainpoint Gateway has been tested with different hardware configurations.\n\nMinimum:\n\n- `4GB RAM`\n- `1 CPU Cores`\n- `128+ GB SSD`\n- `Public IPv4 address`\n\nMid-Range:\n\n- `8GB RAM`\n- `2 CPU Cores`\n- `256+ GB SSD`\n- `Public IPv4 address`\n\n### Deployment\n\nRun the following commands to initiate your Gateway:\n\n#### Install Dependencies\n\n```bash\n$ sudo apt-get install make git\n$ git clone https://github.com/chainpoint/chainpoint-gateway.git\n$ cd chainpoint-gateway\n$ make install-deps\n\nLogout and login to allow your user to use Docker\n\n$ exit\n```\n\n#### Configure Gateway\n\n```\n$ ssh user@<your_ip>\n$ cd chainpoint-gateway\n$ make init\n\n\n ██████╗██╗  ██╗ █████╗ ██╗███╗   ██╗██████╗  ██████╗ ██╗███╗   ██╗████████╗     ██████╗  █████╗ ████████╗███████╗██╗    ██╗ █████╗ ██╗   ██╗\n██╔════╝██║  ██║██╔══██╗██║████╗  ██║██╔══██╗██╔═══██╗██║████╗  ██║╚══██╔══╝    ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║    ██║██╔══██╗╚██╗ ██╔╝\n██║     ███████║███████║██║██╔██╗ ██║██████╔╝██║   ██║██║██╔██╗ ██║   ██║       ██║  ███╗███████║   ██║   █████╗  ██║ █╗ ██║███████║ ╚████╔╝\n██║     ██╔══██║██╔══██║██║██║╚██╗██║██╔═══╝ ██║   ██║██║██║╚██╗██║   ██║       ██║   ██║██╔══██║   ██║   ██╔══╝  ██║███╗██║██╔══██║  ╚██╔╝\n╚██████╗██║  ██║██║  ██║██║██║ ╚████║██║     ╚██████╔╝██║██║ ╚████║   ██║       ╚██████╔╝██║  ██║   ██║   ███████╗╚███╔███╔╝██║  ██║   ██║\n ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝╚═╝      ╚═════╝ ╚═╝╚═╝  ╚═══╝   ╚═╝        ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝   ╚═╝\n\n\n? Will this Gateway use Bitcoin mainnet or testnet? testnet\n? Enter your Gateways's Public IP Address: 104.154.83.163\n```\n\n#### Initialize Lightning\n\n```\nInitializing Lightning wallet...\nCreate new address for wallet...\nCreating Docker secrets...\n****************************************************\nLightning initialization has completed successfully.\n****************************************************\nLightning Wallet Password: kPlIshurrduurSQoXa\nLND Wallet Seed: absorb behind drop safe like herp derp celery galaxy wait orient sign suit castle awake gadget pass pipe sudden ethics hill choose six orphan\nLightning Wallet Address:tb1qglvlrlg0velrserjuuy7s4uhrsrhuzwgl8hvgm\n******************************************************\nYou should back up this information in a secure place.\n******************************************************\n\nTODO: REMOVE Please fund the Lightning Wallet Address above with Bitcoin and wait for 6 confirmations before running 'make deploy'\n\nHow many Cores would you like to connect to? (max 4) 2\nWould you like to specify any Core IPs manually? No\n\nYou have chosen to connect to 2 Core(s).\nYou will now need to fund you wallet with a minimum amount of BTC to cover costs of the initial channel creation and future Core submissions.\n\n? How many Satoshi to commit to each channel/Core? (min 120000) 500000\n? 500000 per channel will require 1000000 Satoshi total funding. Is this OK? (Y/n) y\n\n**************************************************************************************************************\nPlease send 1000000 Satoshi (0.01 BTC) to your wallet with address tb1qglvlrlg0velrserjuuy7s4uhrsrhuzwgl8hvgm\n**************************************************************************************************************\n\nThis initialization process will now wait until your Lightning node is fully synced and your wallet is funded with at least 400000 Satoshi. The init process should resume automatically.\n\n2020-02-24T17:12:12.244Z> Syncing in progress... currently at block height 1576000\n2020-02-24T17:12:42.259Z> Syncing in progress... currently at block height 1596000\n2020-02-24T17:13:12.269Z> Syncing in progress... currently at block height 1608000\n2020-02-24T17:13:42.279Z> Syncing in progress... currently at block height 1626000\n2020-02-24T17:14:12.286Z> Syncing in progress... currently at block height 1650000\n2020-02-24T17:14:42.297Z> Syncing in progress... currently at block height 1662000\n\n*****************************************\nYour Lightning node is fully synced.\n*****************************************\n\n***********************************************\nYour Lightning wallet is adequately funded.\n***********************************************\n\n*********************************************************************************\nChainpoint Gateway and integrated Lightning node have been successfully initialized.\n*********************************************************************************\n\n$ make deploy\n```\n\nAfter running `make deploy`, the Gateway will automatically peer and open Lightning channels with Cores. This may take up to 6 confirmations (~60 minutes). This will allow the Gateway to authenticate with Cores and pay for Anchor Fees. This process may take several minutes upon first run.\n\n## Troubleshooting\n\nIf your issue isn't addressed here, please [submit an issue](https://github.com/chainpoint/chainpoint-core/issues) to the Chainpoint Core repo.\n\n### Init Problems\n\nIf `make init` fails and the Lightning wallet hasn't yet been generated and funded, run `make init-restart`, then run `make init` again. If the Lightning wallet has already been generated and funded, you can usually just run `make init` again to continue the initialization process.\n\n### Docker Secrets\n\nGateway uses docker secrets to store sensitive credentials. If you receive a `secret not found` error for either `HOT_WALLET_PASS` or `HOT_WALLET_ADDRESS` while deploying, this can be remedied by using your saved lnd credentials to recreate the secrets:\n`printf <hot wallet password without quotes> | docker secret create HOT_WALLET_PASS -` or `printf <hot wallet address without quotes> | docker secret create HOT_WALLET_ADDRESS -`.\n\n## Gateway Public API\n\nEvery Gateway provides a public HTTP API. This is documented in greater detail on the [Gateway HTTP API wiki](https://github.com/chainpoint/chainpoint-gateway/wiki/Gateway-HTTP-API)\n\nAdditionally, lightning node information for your Gateway can be found at `http://<gateway_ip>/config`.\n\n## License\n\n[Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0)\n\n```text\nCopyright (C) 2017-2020 Tierion\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n"
  },
  {
    "path": "chainpoint-gateway-openapi-3.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: 'Chainpoint Node'\n  description: 'Documentation for the Chainpoint Node API'\n  version: '2.0.0'\n  license:\n    name: 'Apache 2.0'\n    url: 'https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)'\nservers:\n  - url: 'http://35.231.41.69'\n    description: 'Development server (produces testnet proofs)'\npaths:\n  '/hashes':\n    post:\n      summary: 'Submit one or more hashes for anchoring'\n      operationId: 'submitHashes'\n      requestBody:\n        description: 'An array of hex string hashes to be anchored'\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PostHashesRequest'\n      responses:\n        '200':\n          description: 'An array of hash object and supporting meta information for that array'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/PostHashesResponse'\n        '409':\n          description: 'There was an invalid argument in the request'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n      tags:\n        - 'Hashes'\n  '/proofs/{proof_id}':\n    get:\n      summary: 'Retrieves a proof by proof_id'\n      operationId: 'getProof'\n      parameters:\n        - name: 'proof_id'\n          in: 'path'\n          required: true\n          description: 'The proof_id of the proof to retrieve'\n          schema:\n            type: 'string'\n            format: 'uuid'\n      responses:\n        '200':\n          description: 'The requested proof object'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/GetProofsBase64Response'\n            'application/vnd.chainpoint.ld+json':\n              schema:\n                $ref: '#/components/schemas/GetProofsJSONResponse'\n            'application/vnd.chainpoint.json+base64':\n              schema:\n                $ref: '#/components/schemas/GetProofsBase64Response'\n        '409':\n          description: 'There was an invalid argument in the request'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n            'application/vnd.chainpoint.ld+json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n            'application/vnd.chainpoint.json+base64':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n      tags:\n      - 'Proofs'\n  '/proofs':\n    get:\n      summary: 'Retrieves one or more proofs by proofids supplied in header'\n      operationId: 'getProofs'\n      parameters:\n        - name: 'proofids'\n          in: 'header'\n          required: true\n          description: 'Comma separated proof_id list of the proofs to retrieve'\n          schema:\n            type: 'string'\n      responses:\n        '200':\n          description: 'An array of the requested proof objects'\n          content:\n            'application/json':\n              schema:\n                type: 'array'\n                items:\n                  $ref: '#/components/schemas/GetProofsBase64Response'\n            'application/vnd.chainpoint.ld+json':\n              schema:\n                type: 'array'\n                items:\n                  $ref: '#/components/schemas/GetProofsJSONResponse'\n            'application/vnd.chainpoint.json+base64':\n              schema:\n                type: 'array'\n                items:\n                  $ref: '#/components/schemas/GetProofsBase64Response'\n        '409':\n          description: 'There was an invalid argument in the request'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n            'application/vnd.chainpoint.ld+json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n            'application/vnd.chainpoint.json+base64':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n      tags:\n      - 'Proofs'\n  '/verify':\n    post:\n      summary: 'Submit one or more proofs for verification'\n      operationId: 'verifyProofs'\n      requestBody:\n        description: 'Array of one or more proofs to be verified'\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PostVerifyRequest'\n      responses:\n        '200':\n          description: 'An array of the verification results'\n          content:\n            'application/json':\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/PostVerifyResponse'\n        '409':\n          description: 'There was an invalid argument in the request'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n      tags:\n      - 'Verify'\n  '/calendar/{tx_id}/data':\n    get:\n      summary: 'Retrieves the the data embedded in a calendar transaction'\n      operationId: 'getCalendarTxData'\n      parameters:\n        - name: 'tx_id'\n          in: 'path'\n          required: true\n          description: 'The calendar transaction id from which to retrieve the embedded data'\n          schema:\n            type: 'string'\n      responses:\n        '200':\n          description: 'The data value embedded within the calendar transaction'\n          content:\n            'application/json':\n              schema:\n                type: 'string'\n                example: 'f18bf0968b224f73528d99cc83ca9e79d467f34875e85f36e2c1f074ff2dc657'\n        '404':\n          description: 'The requested transaction was not found'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n        '409':\n          description: 'There was an invalid argument in the request'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/ErrorResponse'\n      tags:\n      - 'Calendar'\n  '/config':\n    get:\n      summary: 'Retrieves some basic information for the Node'\n      operationId: 'getNodeConfig'\n      responses:\n        '200':\n          description: 'Basic information about the Node and it''s environment'\n          content:\n            'application/json':\n              schema:\n                $ref: '#/components/schemas/GetConfigResponse'\n      tags:\n      - 'Config'\ncomponents:\n  schemas:\n    PostHashesRequest:\n      type: 'object'\n      properties:\n        hashes:\n          type: 'array'\n          items:\n            type: 'string'\n            example: '1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547'\n            pattern: '^([a-fA-F0-9]{2}){20,64}$'\n            minLength: 40\n            maxLength: 128\n    PostHashesResponse:\n      type: 'object'\n      properties:\n        meta:\n         type: 'object'\n         properties:\n          submitted_at:\n            type: 'string'\n            format: 'date-time'\n            example: '2017-05-02T15:16:44Z'\n          processing_hints:\n            type: 'object'\n            properties:\n              cal:\n                type: 'string'\n                format: 'date-time'\n                example: '2017-05-02T15:17:44Z'\n              btc:\n                type: 'string'\n                format: 'date-time'\n                example: '2017-05-02T16:17:44Z'\n        hashes:\n         type: 'array'\n         items:\n          type: 'object'\n          properties:\n            proof_id:\n              type: 'string'\n              example: '5a001650-2f4a-11e7-ad22-37b426116bc4'\n            hash:\n              type: 'string'\n              example: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89'\n    GetProofsJSONResponse:\n      type: 'object'\n      properties:\n        proof_id:\n          type: 'string'\n          example: '577c6c90-78d5-11e9-9c57-010a193d9f8c'\n        proof:\n          type: 'object'\n          example:\n            '@context': 'https://w3id.org/chainpoint/v3'\n            type: 'Chainpoint'\n            hash: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89'\n            proof_id: '577c6c90-78d5-11e9-9c57-010a193d9f8c'\n            hash_received: '2019-05-17T18:55:30Z'\n            branches:\n              - label: 'cal_anchor_branch'\n                ops:\n                  - l: 'node_id:577c6c90-78d5-11e9-9c57-010a193d9f8c'\n                  - op: 'sha-256'\n                  - l: 'core_id:5a22fb80-78d5-11e9-8186-01d1f712eccc'\n                  - op: 'sha-256'\n                  - l: 'nistv2:1558119240000:eb591780782f746fda5e7ac8011064fda657ae451bd1ae6b71e2f5d7e24e9d49bdc25db6d901ccf8736bbf135c451d1edc9c6065b577d69f3fd9be6a1a8d0763'\n                  - op: 'sha-256'\n                  - l: '1766c5a6c10cf8ae5cce76c6d89cb9bc8696a2acf8e7ed4dbe05a71802cae38a'\n                  - op: 'sha-256'\n                  - anchors:\n                      - type: 'cal'\n                        anchor_id: 'b220c0443b5f8b1394a38a102892590b4c21d8ad1382cd9e4d59b9834f6a769f'\n                        uris:\n                          - 'http://35.245.9.90/calendar/b220c0443b5f8b1394a38a102892590b4c21d8ad1382cd9e4d59b9834f6a769f/data'\n        anchors_complete:\n          type: 'array'\n          items:\n            type: 'string'\n            example: 'cal'\n    GetProofsBase64Response:\n      type: 'object'\n      properties:\n        proof_id:\n          type: 'string'\n          example: '577c6c90-78d5-11e9-9c57-010a193d9f8c'\n        proof:\n          type: 'string'\n          example: 'eJyNk79u1EAQh1+Gksvt7P91dRKvQJXmNDszy1k67JPtBCgDDW0KOpqQoAREg4QoeY97G+y7S4AAUrbzrr5vfyP/9u3NgtpmkJfDj9UwbPpqPn9haj5qu2dzWmHdbNq6Gean5mp4tZHPT+62rlbYr7YLAOKIJiqJ7AobJBsISozKJGUssFUZwEYnESVZz7GgYet90aZITF8mzbLmZdOybB+5EMhTUrMw6mYAkmaJXJgpUAjJcCqRvu+Q/iQ/r4dB9uQSh29aQZqpkQpPIVbOVUYd3+mp7SY9al1y/F0fIfpRz1ACaCH6Sz+R/9bb45vcYUMr6c9ff1xjlvVXwvVy2mq75f7sst30788u1tvHu5w1Vw+Z8exDu7nuVzjTzu/gXYoJfsAE9+F3Td0Pp7oC5yJA0laNq5LsEoSoQtQlWF8YnQSkqACUt+OndwHFOsgMKD4HEF0cB9FWEtuUmbTj7DkpICoxGJ9zAeNoZBiEKZFX3uVxXvapmMIpi0fAyCp4cz/lAoL35NATqFGH4ogkeBorkyinTNEnjxrHMwnClrMohwGi0oRiIt4XJqO10irezm1MqKC6bXk++lXvqe3V+OeqA3F20W0XQRBJMhoxbABRUzBG5zFkZBNztihZNEDBWGL23hvtQhFO1hSXAvwR53rfif78ze4dXY6XfTrUpObrw7VXJ13dn2+P/hdxPlLSMHbzAzCfqvoTAAQ82A=='\n        anchors_complete:\n          type: 'array'\n          items:\n            type: 'string'\n            example: 'cal'\n    PostVerifyRequest:\n      type: 'object'\n      properties:\n        proofs:\n          type: 'array'\n          items:\n            type: 'string'\n            example: 'eJyNk79u1EAQh1+Gksvt7P91dRKvQJXmNDszy1k67JPtBCgDDW0KOpqQoAREg4QoeY97G+y7S4AAUrbzrr5vfyP/9u3NgtpmkJfDj9UwbPpqPn9haj5qu2dzWmHdbNq6Gean5mp4tZHPT+62rlbYr7YLAOKIJiqJ7AobJBsISozKJGUssFUZwEYnESVZz7GgYet90aZITF8mzbLmZdOybB+5EMhTUrMw6mYAkmaJXJgpUAjJcCqRvu+Q/iQ/r4dB9uQSh29aQZqpkQpPIVbOVUYd3+mp7SY9al1y/F0fIfpRz1ACaCH6Sz+R/9bb45vcYUMr6c9ff1xjlvVXwvVy2mq75f7sst30788u1tvHu5w1Vw+Z8exDu7nuVzjTzu/gXYoJfsAE9+F3Td0Pp7oC5yJA0laNq5LsEoSoQtQlWF8YnQSkqACUt+OndwHFOsgMKD4HEF0cB9FWEtuUmbTj7DkpICoxGJ9zAeNoZBiEKZFX3uVxXvapmMIpi0fAyCp4cz/lAoL35NATqFGH4ogkeBorkyinTNEnjxrHMwnClrMohwGi0oRiIt4XJqO10irezm1MqKC6bXk++lXvqe3V+OeqA3F20W0XQRBJMhoxbABRUzBG5zFkZBNztihZNEDBWGL23hvtQhFO1hSXAvwR53rfif78ze4dXY6XfTrUpObrw7VXJ13dn2+P/hdxPlLSMHbzAzCfqvoTAAQ82A=='\n          minItems: 1\n          maxItems: 1000\n    PostVerifyResponse:\n      type: 'object'\n      properties:\n        proof_index:\n          type: 'integer'\n          example: 0\n        hash:\n          type: 'string'\n          example: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89'\n        proof_id:\n          type: 'string'\n          example: '577c6c90-78d5-11e9-9c57-010a193d9f8c'\n        hash_received:\n          type: 'string'\n          format: 'date-time'\n          example: '2019-05-17T18:55:30Z'\n        anchors:\n          type: 'array'\n          items:\n            type: 'object'\n            properties:\n              branch:\n                type: 'string'\n                example: 'cal_anchor_branch'\n              type:\n                type: 'string'\n                example: 'cal'\n              valid:\n                type: 'boolean'\n                example: true\n        status:\n          type: 'string'\n          example: 'verified'\n    GetConfigResponse:\n      type: 'object'\n      properties:\n        version:\n          type: 'string'\n          example: '2.0.0'\n        time:\n          type: 'string'\n          format: 'date-time'\n          example: '2019-05-17T19:53:49.140Z'\n    ErrorResponse:\n      type: 'object'\n      properties:\n        code:\n          type: 'string'\n        message:\n          type: 'string'\ntags:\n  - name: 'Hashes'\n    description: 'Your hashes to be anchored'\n  - name: 'Proofs'\n    description: 'Your Chainpoint proofs created for each of your hashes'\n  - name: 'Verify'\n    description: 'Verification process for your proofs'\n  - name: 'Calendar'\n    description: 'Chainpoint calendar transaction data'\n  - name: 'Config'\n    description: 'Configuration information about the Node'\nexternalDocs:\n  description: 'Find out more about Chainpoint'\n  url: 'https://chainpoint.org'\n"
  },
  {
    "path": "cloudbuild.yaml",
    "content": "steps:\n- name: 'gcr.io/cloud-builders/git'\n  args: ['submodule', 'update', '--init', '--recursive']\n- name: 'gcr.io/cloud-builders/docker'\n  args: [ 'build', '-f', 'Dockerfile', '-t', 'gcr.io/chainpoint-registry/$REPO_NAME:$COMMIT_SHA', '-t', 'gcr.io/chainpoint-registry/$REPO_NAME:latest', '.' ]\n  id: 'chainpoint-gateway'\ntimeout: 1000s\nimages:\n- 'gcr.io/chainpoint-registry/$REPO_NAME:latest'\n- 'gcr.io/chainpoint-registry/$REPO_NAME:$COMMIT_SHA'\noptions:\n machineType: 'N1_HIGHCPU_8'\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: '3.4'\n\nnetworks:\n  chainpoint-gateway:\n    driver: bridge\n\nservices:\n  chainpoint-gateway:\n    restart: on-failure\n    volumes:\n      - ./ip-blacklist.txt:/home/node/app/ip-blacklist.txt:ro\n      - ~/.chainpoint/gateway/data/rocksdb:/root/.chainpoint/gateway/data/rocksdb\n      - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro\n    build: .\n    container_name: chainpoint-gateway\n    ports:\n      # - '${PORT}:${CHAINPOINT_NODE_PORT}'\n      - '80:8080'\n    networks:\n      - chainpoint-gateway\n    environment:\n      HOME: /root\n      HOT_WALLET_PASS: ${HOT_WALLET_PASS}\n      HOT_WALLET_ADDRESS: ${HOT_WALLET_ADDRESS}\n      LND_SOCKET: ${LND_SOCKET}\n      LND_MACAROON: ${LND_MACAROON}\n      LND_TLS_CERT: ${LND_TLS_CERT}\n      CHAINPOINT_CORE_CONNECT_IP_LIST: '${CHAINPOINT_CORE_CONNECT_IP_LIST}'\n      PORT: '${PORT:-80}'\n      AGGREGATION_INTERVAL_SECONDS: '${AGGREGATION_INTERVAL_SECONDS}'\n      PROOF_EXPIRE_MINUTES: '${PROOF_EXPIRE_MINUTES}'\n      CHAINPOINT_NODE_PORT: '${CHAINPOINT_NODE_PORT:-9090}'\n      POST_HASHES_MAX: '${POST_HASHES_MAX}'\n      POST_VERIFY_PROOFS_MAX: '${POST_VERIFY_PROOFS_MAX}'\n      GET_PROOFS_MAX: '${GET_PROOFS_MAX}'\n      MAX_SATOSHI_PER_HASH: '${MAX_SATOSHI_PER_HASH}'\n      NETWORK: ${NETWORK}\n      NODE_ENV: ${NODE_ENV}\n      CHANNEL_AMOUNT: ${CHANNEL_AMOUNT}\n      FUND_AMOUNT: ${FUND_AMOUNT}\n      NO_LSAT_CORE_WHITELIST: ${NO_LSAT_CORE_WHITELIST}\n      GOOGLE_UA_ID: ''\n      PUBLIC_IP: ${LND_PUBLIC_IP}\n    tty: true\n\n  # Lightning node\n  lnd:\n    image: tierion/lnd:${NETWORK:-testnet}-0.9.2\n    user: ${USERID}:${GROUPID}\n    entrypoint: './start-lnd.sh'\n    container_name: lnd-node\n    ports:\n      - target: 8080\n        published: 8080\n        protocol: tcp\n        mode: host\n      - target: 9735\n        published: 9735\n        protocol: tcp\n        mode: host\n      - target: 10009\n        published: 10009\n        protocol: tcp\n        mode: host\n    restart: always\n    environment:\n      - PUBLICIP=${LND_PUBLIC_IP}\n      - RPCUSER\n      - RPCPASS\n      - NETWORK=${NETWORK:-testnet}\n      - CHAIN\n      - DEBUG=info\n      - BACKEND=neutrino\n      - NEUTRINO=faucet.lightning.community:18333\n      - LND_REST_PORT\n      - LND_RPC_PORT\n      - TLSPATH\n      - TLSEXTRADOMAIN=lnd\n    volumes:\n      - ~/.chainpoint/gateway/.lnd:/root/.lnd:z\n    networks:\n      - chainpoint-gateway\n\n  # ln-accounting\n  # Returns accounting reports in harmony format for lnd node\n  ln-accounting:\n    image: tierion/ln-accounting\n    ports:\n      - '9000'\n    environment:\n      NETWORK: ${NETWORK}\n      LND_DIR: /root/.lnd\n      LND_SOCKET: ${LND_SOCKET}\n      ACCOUNTING_PORT: ${ACCOUNTING_PORT:-9000}\n    volumes:\n      - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro\n    networks:\n      - chainpoint-gateway\n"
  },
  {
    "path": "init/index.js",
    "content": "/* Copyright (C) 2019 Tierion\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n */\n\nconst inquirer = require('inquirer')\nconst validator = require('validator')\nconst chalk = require('chalk')\nconst exec = require('executive')\nconst generator = require('generate-password')\nconst lightning = require('../lib/lightning')\nconst home = require('os').homedir()\nconst fs = require('fs')\nconst path = require('path')\nconst utils = require('../lib/utils.js')\nconst _ = require('lodash')\nconst rp = require('request-promise-native')\nconst retry = require('async-retry')\n\nconst QUIET_OUTPUT = true\n\nconst LND_SOCKET = '127.0.0.1:10009'\nconst MIN_CHANNEL_SATOSHI = 100000\nconst CHANNEL_OPEN_OVERHEAD_SAFE = 20000\n\nconst CORE_SEED_IPS_MAINNET = ['18.220.31.138']\nconst CORE_SEED_IPS_TESTNET = ['3.133.119.65', '52.14.49.31', '3.135.54.225']\n\nconst initQuestionConfig = [\n  {\n    type: 'list',\n    name: 'NETWORK',\n    message: 'Will this Gateway use Bitcoin mainnet or testnet?',\n    choices: [\n      {\n        name: 'Mainnet',\n        value: 'mainnet'\n      },\n      {\n        name: 'Testnet',\n        value: 'testnet'\n      }\n    ],\n    default: 'mainnet'\n  },\n  {\n    type: 'input',\n    name: 'LND_PUBLIC_IP',\n    message: \"Enter your Node's Public IP Address:\",\n    validate: input => {\n      if (input) {\n        return validator.isIP(input, 4)\n      } else {\n        return true\n      }\n    }\n  }\n]\n\nfunction displayTitleScreen() {\n  const txt = `\n ██████╗██╗  ██╗ █████╗ ██╗███╗   ██╗██████╗  ██████╗ ██╗███╗   ██╗████████╗ \n██╔════╝██║  ██║██╔══██╗██║████╗  ██║██╔══██╗██╔═══██╗██║████╗  ██║╚══██╔══╝  \n██║     ███████║███████║██║██╔██╗ ██║██████╔╝██║   ██║██║██╔██╗ ██║   ██║    \n██║     ██╔══██║██╔══██║██║██║╚██╗██║██╔═══╝ ██║   ██║██║██║╚██╗██║   ██║       \n╚██████╗██║  ██║██║  ██║██║██║ ╚████║██║     ╚██████╔╝██║██║ ╚████║   ██║      \n ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝╚═╝      ╚═════╝ ╚═╝╚═╝  ╚═══╝   ╚═╝     \n`\n  const gateway = `\n ██████╗  █████╗ ████████╗███████╗██╗    ██╗ █████╗ ██╗   ██╗\n██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║    ██║██╔══██╗╚██╗ ██╔╝\n██║  ███╗███████║   ██║   █████╗  ██║ █╗ ██║███████║ ╚████╔╝ \n██║   ██║██╔══██║   ██║   ██╔══╝  ██║███╗██║██╔══██║  ╚██╔╝  \n╚██████╔╝██║  ██║   ██║   ███████╗╚███╔███╔╝██║  ██║   ██║   \n ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝   ╚═╝   \n  `\n  console.log('\\n')\n  console.log(chalk.dim.magenta(txt))\n  console.log(chalk.dim.magenta(gateway))\n  console.log('\\n')\n}\n\nasync function startLndNodeAsync(initAnswers) {\n  try {\n    let uid = (await exec.quiet('id -u $USER')).stdout.trim()\n    let gid = (await exec.quiet('id -g $USER')).stdout.trim()\n    console.log(chalk.yellow(`Starting Lightning node...`))\n    await exec.quiet([\n      `docker-compose pull lnd &&\n      mkdir -p ${home}/.chainpoint/gateway/.lnd && \n      export USERID=${uid} && \n      export GROUPID=${gid} && \n      export NETWORK=${initAnswers.NETWORK} &&\n      export PUBLICIP=${initAnswers.LND_PUBLIC_IP} &&\n      docker-compose run -d --service-ports lnd`\n    ])\n  } catch (error) {\n    throw new Error(`Could not start Lightning node : ${error.message}`)\n  }\n\n  await utils.sleepAsync(20000)\n}\n\nasync function initializeLndNodeAsync(initAnswers) {\n  await startLndNodeAsync(initAnswers)\n\n  let walletSecret = generator.generate({ length: 20, numbers: false })\n  try {\n    console.log(chalk.yellow(`Initializing Lightning wallet...`))\n    let lnd = new lightning(LND_SOCKET, initAnswers.NETWORK, true, true)\n    let seed = await lnd.callMethodRawAsync('unlocker', 'genSeedAsync', {}, true)\n    await lnd.callMethodRawAsync('unlocker', 'initWalletAsync', {\n      wallet_password: walletSecret,\n      cipher_seed_mnemonic: seed.cipher_seed_mnemonic\n    })\n\n    await utils.sleepAsync(10000)\n\n    console.log(chalk.yellow(`Create new address for wallet...`))\n    lnd = new lightning(LND_SOCKET, initAnswers.NETWORK, false, true)\n    let newAddress = await lnd.callMethodAsync('lightning', 'newAddressAsync', { type: 0 }, walletSecret)\n    return { cipherSeedMnemonic: seed.cipher_seed_mnemonic, newAddress: newAddress.address, walletSecret: walletSecret }\n  } catch (error) {\n    throw new Error(`Could not initialize Lightning wallet : ${error.message}`)\n  }\n}\n\nasync function createDockerSecretsAsync(initAnswers, walletInfo) {\n  try {\n    console.log(chalk.yellow('Creating Docker secrets...'))\n    await exec.quiet([`docker swarm init --advertise-addr=${initAnswers.LND_PUBLIC_IP}`])\n    await utils.sleepAsync(2000) // wait for swarm to initialize\n    await exec.quiet([\n      `printf ${walletInfo.walletSecret} | docker secret create HOT_WALLET_PASS -`,\n      `printf '${walletInfo.cipherSeedMnemonic.join(' ')}' | docker secret create HOT_WALLET_SEED -`,\n      `printf ${walletInfo.newAddress} | docker secret create HOT_WALLET_ADDRESS -`\n    ])\n  } catch (error) {\n    throw new Error(`Could not create Docker secrets : ${error.message}`)\n  }\n}\n\nfunction displayInitResults(walletInfo) {\n  console.log(chalk.green(`\\n****************************************************`))\n  console.log(chalk.green(`Lightning initialization has completed successfully.`))\n  console.log(chalk.green(`****************************************************\\n`))\n  console.log(chalk.yellow(`Lightning Wallet Password: `) + walletInfo.walletSecret)\n  console.log(chalk.yellow(`Lightning Wallet Seed: `) + walletInfo.cipherSeedMnemonic.join(' '))\n  console.log(chalk.yellow(`Lightning Wallet Address:`) + walletInfo.newAddress)\n  console.log(chalk.magenta(`\\n******************************************************`))\n  console.log(chalk.magenta(`You should back up this information in a secure place.`))\n  console.log(chalk.magenta(`******************************************************\\n\\n`))\n  console.log(\n    chalk.green(\n      `\\nPlease fund the Lightning Wallet Address above with Bitcoin and wait for 6 confirmation before running 'make deploy'\\n`\n    )\n  )\n}\n\nasync function setENVValuesAsync(newENVData) {\n  // check for existence of .env file\n  let envFileExists = fs.existsSync(path.resolve(__dirname, '../', '.env'))\n  // load .env file if it exists, otherwise load the .env.sample file\n  let envContents = fs.readFileSync(path.resolve(__dirname, '../', `.env${!envFileExists ? '.sample' : ''}`)).toString()\n\n  let updatedEnvContents = Object.keys(newENVData).reduce((contents, key) => {\n    let regexMatch = new RegExp(`^${key}=.*`, 'gim')\n    if (!contents.match(regexMatch)) return contents + `\\n${key}=${newENVData[key]}`\n    return contents.replace(regexMatch, `${key}=${newENVData[key]}`)\n  }, envContents)\n\n  fs.writeFileSync(path.resolve(__dirname, '../', '.env'), updatedEnvContents)\n}\n\nasync function getCorePeerListAsync(seedIPs) {\n  seedIPs = _.shuffle(seedIPs)\n\n  let peersReceived = false\n  while (!peersReceived && seedIPs.length > 0) {\n    let targetIP = seedIPs.pop()\n    let options = {\n      uri: `http://${targetIP}/peers`,\n      method: 'GET',\n      json: true,\n      gzip: true,\n      resolveWithFullResponse: true\n    }\n    try {\n      let response = await retry(async () => await rp(options), { retries: 3 })\n      return response.body.concat([targetIP])\n    } catch (error) {\n      console.log(`Core IP ${targetIP} not repsonding to peers requests`)\n    }\n  }\n  throw new Error('Unable to retrieve Core peer list')\n}\n\nasync function askCoreConnectQuestionsAsync(progress) {\n  let peerList = _.shuffle(\n    await getCorePeerListAsync(progress.network === 'maininet' ? CORE_SEED_IPS_MAINNET : CORE_SEED_IPS_TESTNET)\n  )\n  let peerCount = peerList.length\n\n  const coreConnectQuestion = [\n    {\n      type: 'number',\n      name: 'CORE_COUNT',\n      message: `Connecting with more Cores improves the reliability but is more expensive (2 is recommended).\\nHow many Cores would you like to connect to? (max ${peerCount})`,\n      validate: input => input > 0 && input <= peerCount\n    },\n    {\n      type: 'confirm',\n      name: 'MANUAL_IP',\n      message: 'Would you like to specify any Core IPs manually?',\n      default: false\n    }\n  ]\n\n  let coreConnectAnswers = await inquirer.prompt(coreConnectQuestion)\n\n  let coreConnectIPs = []\n\n  if (coreConnectAnswers.MANUAL_IP) {\n    let manualCount = await inquirer.prompt({\n      type: 'number',\n      name: 'TOTAL',\n      message: `How many Core IPs would you like to specify manually? (max ${coreConnectAnswers.CORE_COUNT})`,\n      validate: input => input > 0 && input <= coreConnectAnswers.CORE_COUNT\n    })\n    for (let x = 0; x < manualCount.TOTAL; x++) {\n      let manualInput = await inquirer.prompt({\n        type: 'input',\n        name: 'IP',\n        message: `Enter Core IP manual entry #${x + 1}:`,\n        validate: input => peerList.includes(input) && !coreConnectIPs.includes(input)\n      })\n      coreConnectIPs.push(manualInput.IP)\n    }\n  }\n\n  let randomCoreIPCount = coreConnectAnswers.CORE_COUNT - coreConnectIPs.length\n  let unusedPeers = peerList.filter(ip => !coreConnectIPs.includes(ip))\n  for (let x = 0; x < randomCoreIPCount; x++) coreConnectIPs.push(unusedPeers.pop())\n\n  // Update ENV file with core IP list\n  await setENVValuesAsync({ CHAINPOINT_CORE_CONNECT_IP_LIST: coreConnectIPs.join(',') })\n\n  let coreLNDUris = []\n  for (let coreIP of coreConnectIPs) {\n    let options = {\n      uri: `http://${coreIP}/status`,\n      method: 'GET',\n      json: true,\n      gzip: true,\n      resolveWithFullResponse: true\n    }\n    try {\n      let response = await retry(async () => await rp(options), { retries: 3 })\n      coreLNDUris.push(response.body.uris[0])\n    } catch (error) {\n      throw new Error(`Unable to retrive status of Core at ${coreIP}`)\n    }\n  }\n\n  progress.coreLNDUris = coreLNDUris\n  writeInitProgress(progress)\n\n  return coreLNDUris\n}\n\nasync function askFundAmountAsync(progress) {\n  const coreConnectCount = progress.coreLNDUris.length\n  console.log(chalk.yellow(`\\nYou have chosen to connect to ${coreConnectCount} Core(s).`))\n  console.log(\n    chalk.yellow(\n      'You will now need to fund your wallet with a minimum amount of BTC to cover costs of the initial Lightning channel(s) creation and future Core submissions.\\nThe init process will wait for your funding to confirm with the Bitcoin Network.'\n    )\n  )\n\n  const minAmount = MIN_CHANNEL_SATOSHI + CHANNEL_OPEN_OVERHEAD_SAFE\n\n  let finalFundAmount = null\n  let finalChannelAmount = null\n  while (finalFundAmount === null) {\n    const fundQuestion1 = [\n      {\n        type: 'number',\n        name: 'AMOUNT',\n        message: `How many Satoshi to commit to each channel/Core? (min ${minAmount})`,\n        validate: input => input >= minAmount\n      }\n    ]\n    let fundAnswer1 = await inquirer.prompt(fundQuestion1)\n\n    const totalFundsNeeded = fundAnswer1.AMOUNT * coreConnectCount\n    const fundQuestion2 = [\n      {\n        type: 'confirm',\n        name: 'AGREE',\n        message: `${fundAnswer1.AMOUNT} per channel will require ${totalFundsNeeded} Satoshi total funding. Is this OK?`,\n        default: true\n      }\n    ]\n    let fundAnswer2 = await inquirer.prompt(fundQuestion2)\n    if (fundAnswer2.AGREE) {\n      finalChannelAmount = fundAnswer1.AMOUNT\n      finalFundAmount = totalFundsNeeded\n    }\n  }\n\n  console.log(\n    chalk.magenta(\n      `\\n**************************************************************************************************************`\n    )\n  )\n  console.log(\n    chalk.magenta(\n      `Please send ${finalFundAmount} Satoshi (${finalFundAmount / 10 ** 8} BTC) to your wallet with address ${\n        progress.walletAddress\n      }`\n    )\n  )\n  console.log(\n    chalk.magenta(\n      `**************************************************************************************************************\\n`\n    )\n  )\n\n  progress.finalFundAmount = finalFundAmount\n  progress.finalChannelAmount = finalChannelAmount\n  writeInitProgress(progress)\n\n  return progress\n}\n\nasync function waitForSyncAndFundingAsync(progress) {\n  console.log(\n    chalk.yellow(\n      `This initialization process will now wait until your Lightning node is fully synced and your wallet is funded with at least ${progress.finalFundAmount} Satoshi. The init process should resume automatically. \\n`\n    )\n  )\n\n  let isSynced = false\n  let isFunded = false\n  let lnd = new lightning(LND_SOCKET, progress.network, false, true)\n  while (!isSynced) {\n    try {\n      let info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, progress.walletSecret)\n      if (info.synced_to_chain) {\n        console.log(chalk.green('\\n*****************************************'))\n        console.log(chalk.green('Your lightning node is fully synced.'))\n        console.log(chalk.green('*****************************************'))\n        isSynced = true\n      } else {\n        console.log(\n          chalk.magenta(\n            `${new Date().toISOString()}> Syncing in progress... currently at block height ${info.block_height}`\n          )\n        )\n      }\n    } catch (error) {\n      console.log(chalk.red(`An error occurred while checking node state : ${error.message}`))\n    } finally {\n      if (!isSynced) await utils.sleepAsync(30000)\n    }\n  }\n  while (!isFunded) {\n    try {\n      let balance = await lnd.callMethodAsync('lightning', 'walletBalanceAsync', null, progress.walletSecret)\n      if (balance.confirmed_balance >= progress.finalFundAmount) {\n        console.log(chalk.green('\\n***********************************************'))\n        console.log(chalk.green('Your lightning wallet is adequately funded.'))\n        console.log(chalk.green('***********************************************\\n'))\n        console.log(\n          chalk.yellow(\n            'Your wallet may require up to 5 more confirmations (~60 Minutes) before your gateway can open payment channels to submit hashes\\n'\n          )\n        )\n        isFunded = true\n      } else {\n        console.log(\n          chalk.magenta(\n            `${new Date().toISOString()}> Awaiting funds for wallet... wallet has a current balance of ${\n              balance.confirmed_balance\n            }`\n          )\n        )\n      }\n    } catch (error) {\n      console.log(chalk.red(`An error occurred while checking wallet balance : ${error.message}`))\n    } finally {\n      if (!isFunded) await utils.sleepAsync(30000)\n    }\n  }\n}\n\nfunction displayFinalConnectionSummary() {\n  console.log(chalk.green('\\n*********************************************************************************'))\n  console.log(chalk.green('Chainpoint Gateway and supporting Lighning node have been successfully initialized.'))\n  console.log(chalk.green('*********************************************************************************\\n'))\n}\n\nfunction readInitProgress() {\n  // check for existence of .init file\n  let initFileExists = fs.existsSync(path.resolve(__dirname, './init.json'))\n  // load .init file if it exists\n  if (initFileExists) return JSON.parse(fs.readFileSync(path.resolve(__dirname, './init.json')).toString())\n  return {}\n}\n\nfunction writeInitProgress(progress) {\n  fs.writeFileSync(path.resolve(__dirname, './init.json'), JSON.stringify(progress, null, 2))\n}\n\nfunction setInitProgressCompleteAsync() {\n  fs.writeFileSync(path.resolve(__dirname, './init.json'), JSON.stringify({ complete: true }, null, 2))\n}\n\nasync function start() {\n  try {\n    // Check if init has already recorded some progress\n    // This allows recovery from last known step in process\n    // Exit of initialization has already completed successfully\n    let progress = await readInitProgress()\n    if (progress.complete) {\n      console.log(chalk.green('Initialization has already been completed successfully.'))\n      return\n    }\n    // Display the title screen\n    displayTitleScreen()\n    if (!progress.walletAddress) {\n      // Ask initialization questions\n      let initAnswers = await inquirer.prompt(initQuestionConfig)\n      // Initialize the LND wallet and create a new address\n      let walletInfo = await initializeLndNodeAsync(initAnswers)\n      // Store relevant values as Docker secrets\n      await createDockerSecretsAsync(initAnswers, walletInfo)\n      // Update the .env file with generated data\n      await setENVValuesAsync(initAnswers)\n      // Display the generated wallet information to the user\n      displayInitResults(walletInfo)\n      progress.network = initAnswers.NETWORK\n      progress.walletAddress = walletInfo.newAddress\n      progress.walletSecret = walletInfo.walletSecret\n      writeInitProgress(progress)\n    } else {\n      await startLndNodeAsync({ NETWORK: progress.network })\n    }\n    if (!progress.coreLNDUris) {\n      // Determine which Core(s) to connect to\n      let coreLNDUris = await askCoreConnectQuestionsAsync(progress)\n      progress.coreLNDUris = coreLNDUris\n    }\n    // Ask funding questions\n    if (!progress.finalChannelAmount > 0) {\n      progress = await askFundAmountAsync(progress)\n    }\n    // Wait for sync and wallet funding\n    await waitForSyncAndFundingAsync(progress)\n    await setENVValuesAsync({\n      CHANNEL_AMOUNT: progress.finalChannelAmount,\n      FUND_AMOUNT: progress.finalFundAmount\n    })\n    await displayFinalConnectionSummary()\n    // Initialization complete, mark progress file as such\n    setInitProgressCompleteAsync()\n  } catch (error) {\n    console.error(chalk.red(`An unexpected error has occurred : ${error.message}. Please run 'make init' again.`))\n  } finally {\n    try {\n      console.log(chalk.yellow(`Shutting down Lightning node...`))\n      await exec([`docker-compose down`], { quiet: QUIET_OUTPUT })\n      console.error(chalk.yellow(`Shutdown complete`))\n    } catch (error) {\n      console.error(chalk.red(`Unable to shut down Lightning node : ${error.message}`))\n    }\n  }\n}\n\nstart()\n"
  },
  {
    "path": "ip-blacklist.txt",
    "content": "# ip-blacklist.txt\n#\n# Add a single IPv4 address per line that you'd\n# like to block from connecting to this Node.\n#\n# Lines beginning with '#' are ignored.\n"
  },
  {
    "path": "lib/aggregator.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst MerkleTools = require('merkle-tools')\nconst uuidTime = require('uuid-time')\nlet cores = require('./cores.js')\nconst BLAKE2s = require('blake2s-js')\nlet rocksDB = require('./models/RocksDB.js')\nconst logger = require('./logger.js')\nconst env = require('./parse-env.js').env\nconst utils = require('./utils.js')\nconst { UlidMonotonic } = require('id128')\n\n// a boolean value indicating whether or not aggregateSubmitAndPersistAsync is currently running\nlet AGG_IN_PROCESS = false\n\n// The merkle tools object for building trees and generating proof paths\nconst merkleTools = new MerkleTools()\n\nasync function getSubmittedHashData() {\n  let submittedHashData = [[], []]\n  try {\n    submittedHashData = await rocksDB.getIncomingHashesUpToAsync(Date.now())\n  } catch (error) {\n    logger.error('Could not read submitted hash data')\n    return [[], []]\n  }\n  return submittedHashData\n}\n\n// Build a merkle tree from HashData queued in RocksDB, submit the root to Core, persist resulting state data\nasync function aggregateSubmitAndPersistAsync() {\n  // Return if the previous call to this function has not yet completed\n  if (AGG_IN_PROCESS) {\n    return\n  } else {\n    AGG_IN_PROCESS = true\n  }\n\n  let [hashesForTree, hashDataForTreeDeleteOps] = await getSubmittedHashData()\n  let aggregationRoot = null\n\n  if (hashesForTree.length > 0) {\n    // clear the merkleTools instance to prepare for a new tree\n    merkleTools.resetTree()\n\n    // concatenate and hash the hash ids and hash values into new array\n    let leaves = hashesForTree.map(hashObj => {\n      return Buffer.from(hashObj.hash, 'hex')\n    })\n\n    // Add every hash in hashesForTree to new Merkle tree\n    merkleTools.addLeaves(leaves)\n    merkleTools.makeTree()\n\n    let nodeProofDataItems = []\n    aggregationRoot = merkleTools.getMerkleRoot().toString('hex')\n    let treeSize = merkleTools.getLeafCount()\n\n    for (let x = 0; x < treeSize; x++) {\n      // push the hash_id and corresponding proof onto the array\n      let nodeProofDataItem = {}\n      nodeProofDataItem.proofId = hashesForTree[x].proof_id\n      nodeProofDataItem.hash = hashesForTree[x].hash\n      nodeProofDataItem.proofState = merkleTools.getProof(x, true)\n      nodeProofDataItems.push(nodeProofDataItem)\n    }\n\n    // submit merkle root to Core\n    try {\n      let submitResults = await submitHashToCoresAsync(aggregationRoot)\n      if (submitResults.length === 0) throw new Error(`Unable to submit hash to Core : No Cores responded`)\n\n      submitResults = submitResults.filter(submitResult => {\n        // Log all proofId values returned by Cores\n        logger.info(\n          `Aggregator : Core IP : ${submitResult.coreIP} : proofId : ${submitResult.proofId} : ${JSON.stringify(\n            submitResult\n          )} : ${hashesForTree.length}`\n        )\n        if (utils.isULID(submitResult.proofId)) {\n          try {\n            UlidMonotonic.fromCanonical(submitResult.proofId)\n            return true\n          } catch (error) {\n            logger.error(`unable to validate ProofID ulid ${error.message}`)\n          }\n        } else if (utils.isUUID(submitResult.proofId)) {\n          // validate BLAKE2s\n          let hashTimestampMS = parseInt(uuidTime.v1(submitResult.proofId))\n          let h = new BLAKE2s(32, {\n            personalization: Buffer.from('CHAINPNT')\n          })\n          let hashStr = [\n            hashTimestampMS.toString(),\n            hashTimestampMS.toString().length,\n            submitResult.hash,\n            submitResult.hash.length\n          ].join(':')\n          h.update(Buffer.from(hashStr))\n          let expectedData = Buffer.concat([Buffer.from([0x01]), h.digest().slice(27)]).toString('hex')\n          let embeddedData = submitResult.proofId.slice(24)\n          if (embeddedData == expectedData) {\n            return true\n          } else {\n            logger.error(\n              `Aggregator : Submit : ProofID UUID from Core refused : Cannot validate embedded BLAKE2s data : ${embeddedData} != ${expectedData}`\n            )\n            logger.error(`hashStr was ${hashStr}`)\n          }\n        }\n        return false\n      })\n      if (submitResults.length === 0)\n        throw new Error(`Unable to submit hash to Core : No Cores responded with valid HashID`)\n\n      // add the submission info including core IP and proofId values from Cores for each item in proofDataItems\n      let submitId = UlidMonotonic.generate().toCanonical() // the identifier for all hashes submitted in this batch\n      let coreInfo = submitResults.map(submitResult => {\n        return {\n          ip: submitResult.coreIP,\n          proofId: submitResult.proofId\n        }\n      })\n      nodeProofDataItems = nodeProofDataItems.map(nodeProofDataItem => {\n        nodeProofDataItem.submission = {\n          submitId: submitId,\n          cores: coreInfo\n        }\n        return nodeProofDataItem\n      })\n\n      // persist these proofDataItems to storage\n      try {\n        await rocksDB.saveProofStatesBatchAsync(nodeProofDataItems)\n      } catch (error) {\n        throw new Error(`Unable to persist proof state data to disk : ${error.message}`)\n      }\n\n      try {\n        // Submission to Core was successful, purge hashes that were delivered from RocksDB\n        await rocksDB.deleteBatchAsync(hashDataForTreeDeleteOps)\n      } catch (error) {\n        logger.warn(`Aggregator : Submit to Core : Could not purge submitted hashes : ${error.message}`)\n      }\n\n      let submittedIPs = submitResults.map(item => item.coreIP).toString()\n      logger.info(`Aggregator : ${hashesForTree.length} hash(es) : Core IPs : ${submittedIPs} `)\n    } catch (error) {\n      logger.error(`Aggregator : Submit : ${error.message}`)\n      if (error.stack) {\n        logger.error(`Stacktrace: ${error.stack}`)\n      }\n    }\n  }\n\n  AGG_IN_PROCESS = false\n  return aggregationRoot\n}\n\nasync function submitHashToCoresAsync(hash) {\n  let response = await cores.submitHashAsync(hash)\n  if (response.length == 0) {\n    throw new Error('No Response from any Core')\n  }\n\n  return response.map(item => {\n    return {\n      coreIP: item.ip,\n      proofId: item.response.proof_id,\n      hash: item.response.hash\n    }\n  })\n}\n\nfunction startAggInterval() {\n  return setInterval(aggregateSubmitAndPersistAsync, env.AGGREGATION_INTERVAL_SECONDS * 1000)\n}\n\nmodule.exports = {\n  startAggInterval: startAggInterval,\n  // additional functions for testing purposes\n  aggregateSubmitAndPersistAsync: aggregateSubmitAndPersistAsync,\n  setRocksDB: db => {\n    rocksDB = db\n  },\n  setCores: c => {\n    cores = c\n  }\n}\n"
  },
  {
    "path": "lib/analytics.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// load environment variables\nlet env = require('./parse-env.js').env\nlet ua = require('universal-analytics')\nconst logger = require('./logger.js')\n\nlet visitor\nif (env.GOOGLE_UA_ID) {\n  visitor = ua(env.GOOGLE_UA_ID, env.PUBLIC_IP, { strictCidFormat: false })\n  logger.info(`Setup analytics for analytics id ${env.GOOGLE_UA_ID}`)\n}\n\nfunction setClientID(clientID) {\n  if (env.GOOGLE_UA_ID) {\n    visitor = ua(env.GOOGLE_UA_ID, clientID, { strictCidFormat: false })\n  }\n}\n\nfunction sendEvent(params) {\n  if (params && visitor) {\n    logger.info(`Sending event ${params.ea}`)\n    visitor.event(params).send()\n  }\n}\n\nmodule.exports = {\n  setClientID: setClientID,\n  sendEvent: sendEvent\n}\n"
  },
  {
    "path": "lib/api-server.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst restify = require('restify')\nconst errors = require('restify-errors')\nconst corsMiddleware = require('restify-cors-middleware')\nlet rp = require('request-promise-native')\nlet fs = require('fs')\nconst _ = require('lodash')\nconst validator = require('validator')\nlet rocksDB = require('./models/RocksDB.js')\nconst logger = require('./logger.js')\n\nconst { version } = require('../package.json')\nconst apiHashes = require('./endpoints/hashes.js')\nconst apiCalendar = require('./endpoints/calendar.js')\nconst apiProofs = require('./endpoints/proofs.js')\nconst apiVerify = require('./endpoints/verify.js')\nconst apiConfig = require('./endpoints/config.js')\n\n// RESTIFY SETUP\n// 'version' : all routes will default to this version\nconst httpOptions = {\n  name: 'chainpoint-node',\n  version: '2.0.0',\n  formatters: {\n    'application/json': restify.formatters['application/json; q=0.4'],\n    'application/javascript': restify.formatters['application/json; q=0.4'],\n    'text/html': restify.formatters['application/json; q=0.4'],\n    'application/octet-stream': restify.formatters['application/json; q=0.4']\n  }\n}\n\nconst TOR_IPS_KEY = 'blacklist:tor:ips'\nconst CHAINPOINT_NODE_HTTP_PORT = process.env.CHAINPOINT_NODE_PORT || 8080\n\n// state indicating if the Node is ready to accept new hashes for processing\nlet acceptingHashes = true\n\nlet registrationPassed = true\n\n// the list of IP to refuse connections from\nlet IPBlacklist = []\n\n// middleware to ensure the Node is accepting hashes\nfunction ensureAcceptingHashes(req, res, next) {\n  if (!acceptingHashes || !registrationPassed) {\n    return next(new errors.ServiceUnavailableError('Service is not currently accepting hashes'))\n  }\n  return next()\n}\n\nasync function refreshIPBlacklistAsync() {\n  let torExitIPs = await getTorExitIPAsync()\n  let localIPBlacklist = await getLocalIPBlacklistAsync()\n  let mergedIPBlacklist = torExitIPs.concat(localIPBlacklist)\n  return _.uniq(mergedIPBlacklist)\n}\n\nasync function getTorExitIPAsync() {\n  let options = {\n    headers: {\n      'User-Agent': `chainpoint-node/${version}`\n    },\n    method: 'GET',\n    uri: 'https://check.torproject.org/exit-addresses',\n    gzip: true,\n    timeout: 10000\n  }\n\n  // Retrieve latest exit IP list\n  let extractedTorExitIPs = null\n  try {\n    let response = await rp(options)\n    extractedTorExitIPs = parseTorExitIPs(response)\n  } catch (error) {\n    logger.error('Firewall : Unable to refresh Tor exit IP list from check.torproject.org')\n  }\n\n  // Save IPs to cache and return if retrieval succeeded\n  if (extractedTorExitIPs !== null) {\n    let compactTorExitIPs = _.compact(extractedTorExitIPs)\n    let uniqueTorExitIPs = _.uniq(compactTorExitIPs)\n    try {\n      await rocksDB.setAsync(TOR_IPS_KEY, uniqueTorExitIPs)\n    } catch (error) {\n      logger.error('Firewall : Unable to save Tor exit IP list to cache')\n    }\n    return uniqueTorExitIPs\n  } else {\n    // otherwise, read existing from cache and return\n    try {\n      let cachedTorExitIPs = await rocksDB.getAsync(TOR_IPS_KEY)\n      return cachedTorExitIPs.split(',')\n    } catch (error) {\n      logger.error('Firewall : Unable to load Tor exit IP list from cache')\n      return []\n    }\n  }\n}\n\nfunction parseTorExitIPs(response) {\n  let exitIPs = []\n\n  if (!_.isString(response)) {\n    return exitIPs\n  }\n\n  let respArr = response.split('\\n')\n  if (!_.isArray(respArr)) {\n    return exitIPs\n  }\n\n  _.forEach(respArr, value => {\n    if (/^ExitAddress/.test(value)) {\n      // The second segment of the ExitAddress line is the IP\n      let ip = value.split(' ')[1]\n\n      // Confirm its an IPv4 address\n      if (validator.isIP(ip.toString(), 4)) {\n        exitIPs.push(ip)\n      }\n    }\n  })\n\n  return exitIPs\n}\n\nasync function getLocalIPBlacklistAsync() {\n  try {\n    if (fs.existsSync('./ip-blacklist.txt')) {\n      let blacklist = fs.readFileSync('./ip-blacklist.txt', 'utf-8')\n      let splitBlacklist = blacklist.split('\\n')\n      let compactBlacklist = _.compact(splitBlacklist)\n      let uniqBlacklist = _.uniq(compactBlacklist)\n\n      let ipList = []\n      _.forEach(uniqBlacklist, ip => {\n        // any line that doesn't start with '#' comment\n        if (/^[^#]/.test(ip)) {\n          // Confirm its an IPv4 or IPv6 address\n          // IPv6 allowed is to handle the macOS/Docker\n          // situation where the IP is like: ::ffff:172.18.0.1\n          // See : https://stackoverflow.com/a/33790357/3902629\n          if (validator.isIP(ip.toString())) {\n            ipList.push(ip)\n          }\n        }\n      })\n\n      return ipList\n    } else {\n      return []\n    }\n  } catch (error) {\n    logger.warn('Firewall : Unable to parse local IP blacklist (ip-blacklist.txt) ')\n    return []\n  }\n}\n\nfunction ipFilter(req, res, next) {\n  var reqIPs = []\n  if (req.headers['x-forwarded-for']) {\n    let fwdIPs = req.headers['x-forwarded-for'].split(',')\n    reqIPs.push(fwdIPs[0])\n  }\n  reqIPs.push(req.connection.remoteAddress || '')\n\n  reqIPs = reqIPs\n    .filter(ip => validator.isIP(ip))\n    .reduce((ips, ip) => {\n      ips.push(ip, ip.replace(/^.*:/, ''))\n      return ips\n    }, [])\n\n  for (let ip of reqIPs) {\n    if (IPBlacklist.includes(ip)) return next(new errors.ForbiddenError())\n  }\n\n  return next()\n}\n\n// Put any routing, response, etc. logic here.\nfunction setupCommonRestifyConfigAndRoutes(server) {\n  // limit responses to only requests for acceptable types\n  server.pre(\n    restify.plugins.acceptParser([\n      'application/json',\n      'application/javascript',\n      'text/html',\n      'application/octet-stream',\n      'application/vnd.chainpoint.ld+json',\n      'application/vnd.chainpoint.json+base64'\n    ])\n  )\n\n  // Clean up sloppy paths like //todo//////1//\n  server.pre(restify.pre.sanitizePath())\n\n  // Checks whether the user agent is curl. If it is, it sets the\n  // Connection header to \"close\" and removes the \"Content-Length\" header\n  // See : http://restify.com/#server-api\n  server.pre(restify.pre.userAgentConnection())\n\n  // CORS\n  // See : https://github.com/TabDigital/restify-cors-middleware\n  // See : https://github.com/restify/node-restify/issues/1151#issuecomment-271402858\n  //\n  // Test w/\n  //\n  // curl \\\n  // --verbose \\\n  // --request OPTIONS \\\n  // http://127.0.0.1:9090/hashes \\\n  // --header 'Origin: http://localhost:9292' \\\n  // --header 'Access-Control-Request-Headers: Origin, Accept, Content-Type' \\\n  // --header 'Access-Control-Request-Method: POST' \\\n  // --header 'proofids: da5b6c70-d628-11e7-a676-0102636501e0'\n  //\n  let cors = corsMiddleware({\n    preflightMaxAge: 600,\n    origins: ['*'],\n    allowHeaders: ['proofids,auth'],\n    exposeHeaders: ['proofids']\n  })\n  server.pre(cors.preflight)\n  server.use(cors.actual)\n\n  server.use(restify.plugins.gzipResponse())\n  server.use(\n    restify.plugins.queryParser({\n      mapParams: true\n    })\n  )\n  server.use(\n    restify.plugins.bodyParser({\n      mapParams: true\n    })\n  )\n\n  // DROP all requests from blacklisted IP addresses\n  server.use(ipFilter)\n\n  const applyMiddleware = (middlewares = []) => {\n    if (process.env.NODE_ENV === 'development' || process.env.NETWORK === 'testnet') {\n      return []\n    } else {\n      return middlewares\n    }\n  }\n\n  let throttle = (burst, rate, opts = { ip: true }) => {\n    return restify.plugins.throttle(Object.assign({}, { burst, rate }, opts))\n  }\n\n  // API RESOURCES\n  // IMPORTANT : These routes MUST come after the firewall initialization!\n\n  // submit hash(es)\n  server.post(\n    { path: '/hashes', version: '2.0.0' },\n    ...applyMiddleware([throttle(50, 25)]),\n    ensureAcceptingHashes,\n    apiHashes.postHashesAsync\n  )\n  // get a data value from a calendar transaction\n  server.get(\n    { path: '/calendar/:tx_id/data', version: '2.0.0' },\n    ...applyMiddleware([throttle(15, 5)]),\n    apiCalendar.getDataValueByIDAsync\n  )\n  // get a single proof with a single hash_id\n  server.get(\n    { path: '/proofs/:proof_id', version: '2.0.0' },\n    ...applyMiddleware([throttle(15, 5)]),\n    apiProofs.getProofsByIDAsync\n  )\n  // get multiple proofs with 'proofids' header param\n  server.get({ path: '/proofs', version: '2.0.0' }, ...applyMiddleware([throttle(15, 5)]), apiProofs.getProofsByIDAsync)\n  // verify one or more proofs\n  server.post(\n    { path: '/verify', version: '2.0.0' },\n    ...applyMiddleware([throttle(15, 5)]),\n    apiVerify.postProofsForVerificationAsync\n  )\n  // get configuration information for this Node\n  server.get({ path: '/config', version: '2.0.0' }, ...applyMiddleware([throttle(1, 1)]), apiConfig.getConfigInfoAsync)\n\n  server.get({ path: '/login', version: '2.0.0' }, function(req, res, next) {\n    res.redirect('/', next)\n  })\n\n  server.get({ path: '/about', version: '2.0.0' }, function(req, res, next) {\n    res.redirect('/', next)\n  })\n}\n\n// HTTP Server\nasync function startInsecureRestifyServerAsync() {\n  let restifyServer = restify.createServer(httpOptions)\n  setupCommonRestifyConfigAndRoutes(restifyServer)\n\n  // Begin listening for requests\n  return new Promise((resolve, reject) => {\n    restifyServer.listen(CHAINPOINT_NODE_HTTP_PORT, err => {\n      if (err) return reject(err)\n      logger.info(`App : Chainpoint Node listening on port ${CHAINPOINT_NODE_HTTP_PORT}`)\n      return resolve(restifyServer)\n    })\n  })\n}\n\nfunction startIPBlacklistRefreshInterval() {\n  return setInterval(async () => {\n    IPBlacklist = await refreshIPBlacklistAsync()\n  }, 24 * 60 * 60 * 1000) // refresh IPBlacklist every 24 hours\n}\n\nasync function startAsync(lnd) {\n  try {\n    apiConfig.setLnd(lnd)\n    IPBlacklist = await refreshIPBlacklistAsync()\n    await startInsecureRestifyServerAsync()\n  } catch (error) {\n    logger.error(`Startup : ${error.message}`)\n  }\n}\n\nmodule.exports = {\n  startAsync: startAsync,\n  startInsecureRestifyServerAsync: startInsecureRestifyServerAsync,\n  startIPBlacklistRefreshInterval: startIPBlacklistRefreshInterval,\n  // additional functions for testing purposes\n  refreshIPBlacklistAsync: refreshIPBlacklistAsync,\n  setAcceptingHashes: isAccepting => {\n    acceptingHashes = isAccepting\n  },\n  setRocksDB: db => {\n    rocksDB = db\n  },\n  setRP: RP => {\n    rp = RP\n  },\n  setFS: FS => {\n    fs = FS\n  }\n}\n"
  },
  {
    "path": "lib/cached-proofs.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet cores = require('./cores.js')\nconst utils = require('./utils.js')\nconst _ = require('lodash')\nconst logger = require('./logger.js')\nlet env = require('./parse-env.js').env\n\n// The object containing the cached Core proof objects\nlet CORE_PROOF_CACHE = {}\n\nconst PRUNE_EXPIRED_INTERVAL_SECONDS = 10\n\nasync function getCachedCoreProofsAsync(coreSubmissions) {\n  // determine max core submission count for these submissions... the largest cores array of all submissions\n  // this will be constant under normal usage, only varying if the master submission count is updated\n  let maxCoreSubmissionCount = coreSubmissions.reduce((result, item) => {\n    if (item.cores.length > result) result = item.cores.length\n    return result\n  }, 0)\n\n  // create `proofId` to `submitId` lookup object, keyed by `proofId`\n  // and simultaneously create `coreSubmissionsLookup` object keyed by the coreSubmissions `submitId`\n  let [submitIdForHashIdCoreLookup, coreSubmissionsLookup] = coreSubmissions.reduce(\n    (result, item) => {\n      for (let core of item.cores) {\n        result[0][core.proofId] = item.submitId\n      }\n      result[1][item.submitId] = { cores: item.cores, proof: undefined }\n      return [result[0], result[1]]\n    },\n    [{}, {}]\n  )\n\n  // Attempt to read proofs from the cache.\n  // For all proofs that are not found (not cached), keep `proof` value as undefined\n  for (let submitId in coreSubmissionsLookup) {\n    coreSubmissionsLookup[submitId].proof = (function() {\n      if (CORE_PROOF_CACHE[submitId] && _.isNil(CORE_PROOF_CACHE[submitId].coreProof)) return null\n\n      return CORE_PROOF_CACHE[submitId] ? _.get(CORE_PROOF_CACHE, `${submitId}.coreProof`, undefined) : undefined\n    })()\n  }\n\n  // loop through the 1st, 2nd, ... cores array object for each submission until all proofs have been returned\n  // under normal operation, with all Cores operating as expected, only one iteration will be performed\n  // if a Core is offline, the second iterations will attempt to request proofs from the second IP/HashIdCore pair\n  // iterations will continue until all values have been retrieved, or all IP/HashIdCore pairs have been attempted\n  //\n  // use `newProofSubmitIds` to keep track of the submitIds that have new proof data returned from Core\n  // this information is used later to determine what new data needs to be cached\n  let newProofSubmitIds = []\n  for (let index = 0; index < maxCoreSubmissionCount; index++) {\n    // find core submissions that have `undefined` proofs, they will be requested from Core at cores index `index`\n    let undefinedProofSubmissions = Object.keys(coreSubmissionsLookup).reduce((result, submitId) => {\n      let coreInfo = coreSubmissionsLookup[submitId].cores[index]\n      // if the proof is undefined, and Core info exists in this submission for this `index`, add to results\n      if (coreSubmissionsLookup[submitId].proof === undefined && coreInfo) {\n        result.push({ submitId: submitId, ip: coreInfo.ip, proofId: coreInfo.proofId })\n      } else if (\n        coreSubmissionsLookup[submitId].proof !== undefined &&\n        coreSubmissionsLookup[submitId].proof != null &&\n        coreSubmissionsLookup[submitId].proof.hash_received !== undefined &&\n        coreInfo\n      ) {\n        let timer = Date.parse(coreSubmissionsLookup[submitId].proof.hash_received)\n        let btcDue = Date.now() - timer > 7200000 // 120 min have passed\n        // if 90 minutes have passed and we have a cal anchor and not a btc anchor\n        // then we add the proof to undefinedProofSubmissions to ensure re-retrieval\n        let containsRelevantAnchors =\n          coreSubmissionsLookup[submitId].anchorsComplete !== undefined &&\n          (!coreSubmissionsLookup[submitId].anchorsComplete.includes('btc') ||\n            !coreSubmissionsLookup[submitId].anchorsComplete.includes('tbtc'))\n        if (btcDue && containsRelevantAnchors) {\n          logger.info(`time to check for btc proof ${coreInfo.proofId} from ${coreInfo.ip}`)\n          result.push({ submitId: submitId, ip: coreInfo.ip, proofId: coreInfo.proofId })\n        }\n      }\n      return result\n    }, [])\n\n    // if none were found, then we have received proof data for all requested submissions, exit loop\n    if (undefinedProofSubmissions.length === 0) break\n\n    // split `undefinedProofSubmissions` into distinct array by unique Core IP\n    // this is needed so that we may request proofs from Core in batches grouped by Core IP\n    // start by determining all the unique IPs in play in undefinedProofSubmissions\n    let uniqueIPs = undefinedProofSubmissions.reduce((result, item) => {\n      // using unshift to build list in reverse for efficiency because we must iterate in reverse later\n      if (!result.includes(item.ip)) result.unshift(item.ip)\n      return result\n    }, [])\n\n    // build an array of submissions for each unique Core IP found\n    let submissionsGroupByIPs = []\n    for (let ip of uniqueIPs) {\n      let result = []\n      for (let x = undefinedProofSubmissions.length - 1; x >= 0; x--) {\n        if (undefinedProofSubmissions[x].ip === ip) result.push(...undefinedProofSubmissions.splice(x, 1))\n      }\n      submissionsGroupByIPs.push(result)\n    }\n\n    // for each resulting submission group array, request the proofs from Core\n    for (let submissionsGroup of submissionsGroupByIPs) {\n      // flatten the submission data for use in the getProofsAsync call\n      let flattenedSubmission = submissionsGroup.reduce(\n        (result, item) => {\n          result.ip = item.ip // need to make the setting only once, but will be the same for every item\n          result.proofIds.push(item.proofId)\n          return result\n        },\n        { ip: '', proofIds: [] }\n      )\n\n      // attempt to retrieve proofs from Core\n      let getProofsFromCoreResults = []\n      try {\n        getProofsFromCoreResults = await cores.getProofsAsync(flattenedSubmission.ip, flattenedSubmission.proofIds)\n      } catch (err) {\n        logger.error(\n          `getCachedCoreProofsAsync : Core ${flattenedSubmission.ip} : ProofID Count ${\n            flattenedSubmission.proofIds.length\n          } (${JSON.stringify(flattenedSubmission.proofIds)}) : ${err.message}`\n        )\n        // Cache as `null` Proof to prevent subsequent retries for 1min\n        CORE_PROOF_CACHE[submissionsGroup.submitId] = {\n          coreProof: null,\n          expiresAt: Date.now() + 1 * 60 * 1000 // 1min\n        }\n      }\n\n      // assign the returned proof values back to the `coreSubmissions` object,\n      // for each item in the results, using the `submitIdForHashIdCoreLookup` object\n      for (let result of getProofsFromCoreResults) {\n        let submitIdForResult = submitIdForHashIdCoreLookup[result.proof_id]\n        // be sure that the `submitIdForResult` is known in `coreSubmissions`\n        // assuming it is, as it always should be unless error, assign the proof to that key\n        if (coreSubmissionsLookup.hasOwnProperty(submitIdForResult)) {\n          coreSubmissionsLookup[submitIdForResult].proof = result.proof\n          coreSubmissionsLookup[submitIdForResult].anchorsComplete = _.isNil(\n            coreSubmissionsLookup[submitIdForResult].proof\n          )\n            ? []\n            : utils.parseAnchorsComplete(coreSubmissionsLookup[submitIdForResult].proof, env.NETWORK)\n          // track this submitId for caching later\n          newProofSubmitIds.push(submitIdForResult)\n        }\n      }\n    }\n  }\n\n  // cache any new results returned from Core\n  if (newProofSubmitIds.length > 0) {\n    // for all new proofs received from Core, create an proofType lookup object for use in determining\n    // proper cache TTL for the proof\n    let proofTypeLookup = newProofSubmitIds.reduce((result, submitId) => {\n      if (!_.isNil(coreSubmissionsLookup[submitId].proof)) {\n        if (env.NETWORK === 'mainnet') {\n          result[submitId] = coreSubmissionsLookup[submitId].anchorsComplete.includes('btc') ? 'btc' : 'cal'\n        } else {\n          result[submitId] = coreSubmissionsLookup[submitId].anchorsComplete.includes('tbtc') ? 'tbtc' : 'tcal'\n        }\n      }\n      return result\n    }, {})\n\n    // Store the non-null AND null proofs from Core in the local cache for subsequent requests\n    // First, create the array of objects to be written to the cache\n    let uncachedCoreProofObjects = newProofSubmitIds.reduce((result, submitId) => {\n      // `null` cached proofs expire after 1 minute\n      // `(t)cal` cached proofs expire after 15 minutes\n      // `(t)btc` cached proofs expire after 25 hours\n      let expMinutes = 1\n      if (coreSubmissionsLookup[submitId].proof !== null) {\n        expMinutes = ['btc', 'tbtc'].includes(proofTypeLookup[submitId]) ? 25 * 60 : 15\n      }\n      result.push({\n        submitId: submitId,\n        coreProof: coreSubmissionsLookup[submitId].proof,\n        expiresAt: Date.now() + expMinutes * 60 * 1000\n      })\n      return result\n    }, [])\n\n    // Next,write proofs to cache\n    for (let coreProofObject of uncachedCoreProofObjects) {\n      CORE_PROOF_CACHE[coreProofObject.submitId] = {\n        coreProof: coreProofObject.coreProof,\n        expiresAt: coreProofObject.expiresAt\n      }\n    }\n  }\n\n  // format `coreSubmissions` into the proper result object to return from this function\n  let finalProofResults = Object.keys(coreSubmissionsLookup).map(submitId => {\n    if (_.isNil(coreSubmissionsLookup[submitId].proof)) {\n      return { submitId: submitId, proof: null }\n    }\n    // A proof exists and has been found for this submitId\n    // Identify the anchors completed in this proof and append that information\n    return {\n      submitId: submitId,\n      proof: coreSubmissionsLookup[submitId].proof,\n      anchorsComplete: coreSubmissionsLookup[submitId].anchorsComplete\n    }\n  })\n\n  // Finally, return the proof items array\n  return finalProofResults\n}\n\nfunction pruneExpiredItems() {\n  let now = Date.now()\n  for (let key in CORE_PROOF_CACHE) {\n    if (CORE_PROOF_CACHE[key].expiresAt <= now) {\n      delete CORE_PROOF_CACHE[key]\n    }\n  }\n}\n\nfunction startPruneExpiredItemsInterval() {\n  return setInterval(pruneExpiredItems, PRUNE_EXPIRED_INTERVAL_SECONDS * 1000)\n}\n\nmodule.exports = {\n  getCachedCoreProofsAsync: getCachedCoreProofsAsync,\n  startPruneExpiredItemsInterval: startPruneExpiredItemsInterval,\n  // additional functions for testing purposes\n  pruneExpiredItems: pruneExpiredItems,\n  getPruneExpiredIntervalSeconds: () => PRUNE_EXPIRED_INTERVAL_SECONDS,\n  getCoreProofCache: () => CORE_PROOF_CACHE,\n  setCoreProofCache: obj => {\n    CORE_PROOF_CACHE = obj\n  },\n  setCores: c => {\n    cores = c\n  },\n  setENV: obj => {\n    env = obj\n  }\n}\n"
  },
  {
    "path": "lib/cores.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// load environment variables\nlet env = require('./parse-env.js').env\n\nlet rp = require('request-promise-native')\nconst { version } = require('../package.json')\nconst retry = require('async-retry')\nconst { Lsat } = require('lsat-js')\nconst _ = require('lodash')\nconst logger = require('./logger.js')\nconst utils = require('./utils.js')\nconst chalk = require('chalk')\nconst lightning = require('./lightning')\n\nconst PRUNE_EXPIRED_INTERVAL_SECONDS = 10\n\nlet CONNECTED_CORE_IPS = []\nlet CONNECTED_CORE_LN_URIS = []\n\nlet coreConnectionCount = 1\n\n// In some cases we may want a list of all Core Nodes (whether or not the Chainpoint Node is connected to it or not)\nlet ALL_CORE_IPS = []\n\n// This is the local in-memory cache of calendar transactions\n// CORE_TX_CACHE is an object keyed by txId, storing the transaction object\nlet CORE_TX_CACHE = {}\n\n// Initialize the Lightning grpc object\nlet lnd = new lightning(env.LND_SOCKET, env.NETWORK)\n\nasync function connectAsync() {\n  // Retrieve the list of Core IPs we can work with\n  let coreIPList = []\n  if (!_.isEmpty(env.CHAINPOINT_CORE_CONNECT_IP_LIST)) {\n    // Core IPs have been supplied in CHAINPOINT_CORE_CONNECT_IP_LIST, use those IPs only\n    coreIPList = env.CHAINPOINT_CORE_CONNECT_IP_LIST\n  }\n  //coreConnectionCount = coreIPList.length\n  logger.info(`Connecting to Cores...`)\n  // Select and establish connection to Core(s)\n  let connectedCoreIPResult = await getConnectedCoreIPsAsync(coreIPList, coreConnectionCount)\n  // warn users about env mismatch\n  if (connectedCoreIPResult.networkMismatch)\n    logger.warn(`Unable to connect to Cores with a different network setting. This Node is set to '${env.NETWORK}'`)\n  // ensure we have successfully communicated with `coreConnectionCount` Cores\n  // if we have not, the Node cannot continue, log error and exit\n  if (!connectedCoreIPResult.connected) {\n    throw new Error(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n  }\n  CONNECTED_CORE_IPS = connectedCoreIPResult.ips\n  CONNECTED_CORE_LN_URIS = connectedCoreIPResult.lnUris\n\n  // if we're not paying cores, then skip lightning\n  // TODO: ability to pay some cores but not others\n  if (env.NO_LSAT_CORE_WHITELIST.length > 0) {\n    return\n  }\n  let done = false\n  while (!done) {\n    try {\n      try {\n        logger.info(`unlocking lightning...`)\n        await lnd.handleUnlock(null, env.HOT_WALLET_PASS)\n      } catch (error) {\n        utils.sleepAsync(2000)\n      }\n      logger.info(`syncing lightning...`)\n      await waitForSync()\n      logger.info(`peering with lightning nodes...`)\n      await createCoreLNDPeerConnectionsAsync(CONNECTED_CORE_LN_URIS)\n      utils.sleepAsync(10000) // takes a few seconds for peers to connect\n      logger.info(`creating lightning channels...`)\n      await createCoreLNDChannelsAsync(CONNECTED_CORE_LN_URIS)\n      done = true\n      logger.info(`opening lightning channels to Core complete`)\n    } catch (error) {\n      console.log(`open peer or channel failed: ${error.message}`)\n      done = false\n      utils.sleepAsync(2000)\n    }\n  }\n\n  logger.info(`App : Core IPs : ${CONNECTED_CORE_IPS}`)\n}\n\nasync function waitForSync() {\n  let isSynced = false\n  while (!isSynced) {\n    try {\n      let info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, env.HOT_WALLET_PASS)\n      if (info.synced_to_chain) {\n        console.log(chalk.green('\\n*****************************************'))\n        console.log(chalk.green('Your lightning node is fully synced.'))\n        console.log(chalk.green('*****************************************'))\n        isSynced = true\n      } else {\n        console.log(\n          chalk.magenta(\n            `${new Date().toISOString()}> Syncing in progress... currently at block height ${info.block_height}`\n          )\n        )\n      }\n    } catch (error) {\n      if (\n        !(\n          error.message.includes('failed to connect to all addresses') ||\n          error.message.includes('unknown service lnrpc.Lightning')\n        )\n      ) {\n        console.log(chalk.red(`An error occurred while checking lnd state : ${error.message}`))\n      }\n    } finally {\n      if (!isSynced) await utils.sleepAsync(5000)\n    }\n  }\n}\n\nasync function createCoreLNDPeerConnectionsAsync(lnUris) {\n  let peerPubKeys = []\n  try {\n    let peerList = await lnd.callMethodAsync('lightning', 'listPeersAsync', null, env.HOT_WALLET_PASS)\n    for (let peer of peerList.peers) {\n      peerPubKeys.push(peer.pub_key)\n    }\n  } catch (error) {\n    throw new Error('Could not retrieve LND peer list')\n  }\n\n  for (let lndUri of lnUris) {\n    let [pubkey, host] = lndUri.split('@')\n    if (peerPubKeys.includes(pubkey)) continue // already peered to this node, skip\n    try {\n      await lnd.callMethodAsync(\n        'lightning',\n        'connectPeerAsync',\n        { addr: { pubkey, host }, perm: true },\n        env.HOT_WALLET_PASS\n      )\n      console.log(chalk.yellow(`Peer connection established with ${lndUri}`))\n    } catch (error) {\n      throw new Error(`Unable to establish a peer connection with ${lndUri} : ${error.message}`)\n    }\n  }\n}\n\nasync function getConnectedCoreIPsAsync(coreIPList, coreConnectionCount) {\n  let getStatusOptions = buildRequestOptions(null, 'GET', '/status')\n\n  let connectedCoreIPs = []\n  let lnUris = []\n  let networkMismatch = false\n  coreIPList = _.shuffle(coreIPList)\n  for (let coreIP of coreIPList) {\n    logger.info(`Connecting to core ${coreIP}`)\n    try {\n      let coreResponse = await coreRequestAsync(getStatusOptions, coreIP, 0)\n      let networkMatch = coreResponse.network === env.NETWORK\n      let isCoreSynced = coreResponse.sync_info.catching_up === false\n      if (!networkMatch) networkMismatch = true\n      if (networkMatch && isCoreSynced) connectedCoreIPs.push(coreIP)\n      if (coreResponse.uris.length > 0) lnUris.push(coreResponse.uris[0])\n    } catch (error) {\n      console.log(`unable to contact core ${coreIP}: ${error.message}`)\n    }\n    // if we've made enough connections, break out of loop and return IPs\n    if (connectedCoreIPs.length >= coreConnectionCount) break\n  }\n  return {\n    connected: connectedCoreIPs.length >= coreConnectionCount,\n    ips: connectedCoreIPs,\n    lnUris: lnUris,\n    networkMismatch\n  }\n}\n\nasync function createCoreLNDChannelsAsync(lnUris) {\n  let channelPubKeys = {}\n  try {\n    let channelList = await lnd.callMethodAsync('lightning', 'listChannelsAsync', {}, env.HOT_WALLET_PASS)\n    for (let channel of channelList.channels) {\n      channelPubKeys[channel.remote_pubkey] = channel\n    }\n  } catch (error) {\n    let msg = `Could not retrieve LND channel list: ${error.message}`\n    console.log(chalk.red(msg))\n    throw new Error(msg)\n  }\n  try {\n    let channelList = await lnd.callMethodAsync('lightning', 'pendingChannelsAsync', {}, env.HOT_WALLET_PASS)\n    for (let pendingChannel of channelList.pending_open_channels) {\n      channelPubKeys[pendingChannel.channel.remote_node_pub] = pendingChannel\n      console.log(\n        chalk.green(\n          `Channel to ${pendingChannel.channel.remote_node_pub} is pending and may require up to 6 confirmations (~60 minutes) to fully open`\n        )\n      )\n    }\n  } catch (error) {\n    let msg = `Could not retrieve pending LND channel list: ${error.message}`\n    console.log(chalk.red(msg))\n    throw new Error(msg)\n  }\n  for (let lndUri of lnUris) {\n    let pubkey = lndUri.split('@')[0]\n    if (channelPubKeys.hasOwnProperty(pubkey)) {\n      let chan = channelPubKeys[pubkey]\n      // close channel if all local funds are used up\n      if (\n        chan.hasOwnProperty('total_satoshis_sent') &&\n        chan.total_satoshis_sent > 0 &&\n        chan.local_balance <= env.MAX_SATOSHI_PER_HASH\n      ) {\n        try {\n          let fee = await lnd.callMethodAsync(\n            'lightning',\n            'estimateFeeAsync',\n            { target_confirmations: 6 },\n            env.HOT_WALLET_PASS\n          )\n          let closeChanReq = {\n            channel_point: {\n              funding_txid_bytes: Buffer.from(chan.transaction_id, 'hex').reverse(),\n              output_index: chan.transaction_vout\n            },\n            delivery_address: env.HOT_WALLET_ADDRESS,\n            force: false,\n            sat_per_byte: fee.tokens_per_vbyte,\n            target_conf: 6\n          }\n          let close = await lnd.callMethodAsync('lightning', 'closeChannelAsync', closeChanReq, env.HOT_WALLET_PASS)\n          console.log(`channel close txid: ${close.close_pending.txid.reverse().toString('hex')}`)\n        } catch (error) {\n          console.log(`unable to close channel: ${error.message}`)\n        }\n      } else {\n        continue\n      }\n    }\n    try {\n      let channelTxInfo = await lnd.callMethodAsync(\n        'lightning',\n        'openChannelSyncAsync',\n        {\n          node_pubkey_string: pubkey,\n          local_funding_amount: env.CHANNEL_AMOUNT,\n          push_sat: 0\n        },\n        env.HOT_WALLET_PASS\n      )\n      console.log(\n        `Channel created with ${lndUri} with the following transaction Id: ${Buffer.from(\n          channelTxInfo.funding_txid_bytes.data\n        ).toString('hex')}`\n      )\n    } catch (error) {\n      let msg = `Unable to create a channel with ${lndUri} : ${error.message}`\n      console.log(chalk.red(msg))\n      throw new Error(msg)\n    }\n  }\n}\n\nasync function coreRequestAsync(options, coreIP, retryCount = 3, timeout = 500) {\n  options.headers['X-Node-Version'] = version\n  options.uri = `http://${coreIP}${options.uriPath}`\n\n  let response\n  if (retryCount <= 0) {\n    try {\n      response = await rp(options)\n    } catch (error) {\n      if (error.statusCode === 402 || error.status === 402) return error.response\n      throw error\n    }\n  } else {\n    await retry(\n      async bail => {\n        try {\n          response = await rp(options)\n        } catch (error) {\n          // If no response was received or there is a status code >= 500 or payment is still pending/held,\n          // then we should retry the call, throw an error\n          if (!error.statusCode || error.statusCode >= 500 || error.statusCode === 402) throw error\n          // errors like 409 Conflict or 400 Bad Request are not retried because the request is bad and will never succeed\n          bail(error)\n        }\n      },\n      {\n        retries: retryCount, // The maximum amount of times to retry the operation. Default is 3\n        factor: 1, // The exponential factor to use. Default is 2\n        minTimeout: timeout, // The number of milliseconds before starting the first retry. Default is 200\n        randomize: true,\n        onRetry: error => {\n          if (error.statusCode === 402)\n            logger.warn(`Core request : 402: Payment Required. Core ${coreIP} : Request ${options.uri}. Retrying`)\n          else\n            logger.warn(\n              `Core request : ${error.statusCode || 'no response'} : Core ${coreIP} : Request ${\n                options.uri\n              } - ${JSON.stringify(options, null, 2)} : ${error.message} : retrying`\n            )\n        }\n      }\n    )\n  }\n\n  return response.body\n}\n\nasync function submitHashAsync(hash) {\n  let responses = []\n  for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) {\n    try {\n      // send initial request without LSAT. Expecting an LSAT w/ invoice in response\n      let postHashOptions = buildRequestOptions(null, 'POST', '/hash', { hash })\n      let submitResponse = await coreRequestAsync(postHashOptions, coreIP, 0)\n\n      if (!env.NO_LSAT_CORE_WHITELIST.includes(coreIP)) {\n        logger.info(`Aggregator : Response received from Core ${coreIP} : response : ${JSON.stringify(submitResponse)}`)\n\n        // get invoice for hash submission from LSAT challenge\n        let lsat = parse402Response(submitResponse)\n        let submitHashInvoiceId = lsat.paymentHash\n        let invoiceAmount = lsat.invoiceAmount\n        let decodedPaymentRequest = lsat.invoice\n\n        logger.info(\n          `Aggregator : Invoice received from Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : Invoice Amount : ${invoiceAmount} Satoshis`\n        )\n\n        // ensure that the invoice amount does not exceed max payment amount\n        if (invoiceAmount > env.MAX_SATOSHI_PER_HASH)\n          throw new Error(\n            `Aggregator : Invoice amount exceeds max setting of ${env.MAX_SATOSHI_PER_HASH} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}`\n          )\n\n        // pay the invoice\n        // since this is a hodl invoice, the request will stall until it is settled\n        // so we don't want to await the response but rather continue trying submission until complete\n        payInvoiceAsync(decodedPaymentRequest, submitHashInvoiceId)\n          .then(() => {\n            logger.info(\n              `Aggregator : Invoice paid to Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}`\n            )\n          })\n          .catch(e => {\n            logger.error(e.message)\n          })\n        // submit hash with paid invoice id\n        let headers = { Authorization: lsat.toToken() }\n        postHashOptions = buildRequestOptions(headers, 'POST', '/hash', { hash })\n        // setting retries to 5 since we can't await invoice payment\n        // and don't know exactly when the invoice is paid and held which is when the hash can be submitted\n        submitResponse = await coreRequestAsync(postHashOptions, coreIP, 5, 1000)\n\n        logger.info(\n          `Aggregator : Hash submitted to Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}`\n        )\n      } else {\n        logger.info(`Aggregator : Hash submitted to Core ${coreIP}`)\n      }\n\n      responses.push({ ip: coreIP, response: submitResponse })\n    } catch (error) {\n      // Ignore and try next coreIP\n      logger.warn(`submitHashAsync : Unable to submit to Core ${coreIP} : Hash = ${hash} : ${error.message}`)\n    }\n  }\n\n  return responses\n}\n\nasync function payInvoiceAsync(invoice, submitHashInvoiceId) {\n  return new Promise(async (resolve, reject) => {\n    var call = await lnd.callMethodAsync('lightning', 'sendPayment', {})\n    call.on('data', function(response) {\n      // A response was received from the server.\n      call.end()\n      if (response.payment_error)\n        return reject(\n          new Error(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${response.payment_error}`)\n        )\n      return resolve(response)\n    })\n\n    call.on('error', err => {\n      console.log(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${err.message}`)\n      ;(async () => await lnd.handleUnlock(err))()\n      return reject(new Error(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${err.message}`))\n    })\n\n    call.write({ payment_request: invoice })\n  })\n}\n\nasync function getProofsAsync(coreIP, proofIds) {\n  let getProofsOptions = buildRequestOptions(\n    {\n      proofids: proofIds.join(',')\n    },\n    'GET',\n    '/proofs',\n    null,\n    20000\n  )\n\n  try {\n    let coreResponse = await coreRequestAsync(getProofsOptions, coreIP, 0)\n    return coreResponse\n  } catch (error) {\n    if (error.statusCode) throw new Error(`Invalid response on GET proof : ${error.statusCode} : ${error.message}`)\n    throw new Error(`Invalid response received on GET proof : ${error.message || error}`)\n  }\n}\n\nasync function getLatestCalBlockInfoAsync() {\n  let getStatusOptions = buildRequestOptions(null, 'GET', '/status')\n\n  let lastError = null\n  for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) {\n    try {\n      let coreResponse = await coreRequestAsync(getStatusOptions, coreIP, 0)\n      // if the Core is catching up, we cannot use its status to retrieve the latest cal block hash\n      if (coreResponse.sync_info.catching_up) throw 'Core not fully synced'\n      return coreResponse.sync_info\n    } catch (error) {\n      // Record most recent error, ignore, and try next coreIP\n      lastError = error\n    }\n  }\n  if (lastError.statusCode) throw new Error(`Invalid response on GET status : ${lastError.statusCode}`)\n  throw new Error('Invalid response received on GET status')\n}\n\nasync function getCachedTransactionAsync(txID) {\n  // if the transaction already exists in the cache, return it\n  if (CORE_TX_CACHE[txID]) return CORE_TX_CACHE[txID].transaction\n\n  // otherwise, get the tranasction from Core\n  let getTxOptions = buildRequestOptions(null, 'GET', `/calendar/${txID}/data`)\n\n  for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) {\n    try {\n      let transaction = await coreRequestAsync(getTxOptions, coreIP)\n      if (transaction) {\n        // cache the result and return the transaction\n        CORE_TX_CACHE[txID] = {\n          transaction: transaction,\n          expiresAt: Date.now() + 120 * 60 * 1000 // in 2 hours\n        }\n        return transaction\n      }\n    } catch (error) {\n      console.log(chalk.red(error.message))\n    }\n  }\n\n  return null\n}\n\nfunction buildRequestOptions(headerValues, method, uriPath, body, timeout = 3000) {\n  return {\n    headers: headerValues || {},\n    method: method,\n    uriPath: uriPath,\n    body: body || undefined,\n    json: true,\n    gzip: true,\n    timeout: timeout,\n    resolveWithFullResponse: true,\n    agent: false,\n    forever: true\n  }\n}\n\nfunction pruneExpiredItems() {\n  let now = Date.now()\n  for (let key in CORE_TX_CACHE) {\n    if (CORE_TX_CACHE[key].expiresAt <= now) {\n      delete CORE_TX_CACHE[key]\n    }\n  }\n}\n\nfunction startPruneExpiredItemsInterval() {\n  return setInterval(pruneExpiredItems, PRUNE_EXPIRED_INTERVAL_SECONDS * 1000)\n}\n\nfunction startConnectionMonitoringInterval() {\n  return setInterval(async () => {\n    await connectAsync()\n  }, 3600000)\n}\n\nfunction parse402Response(response) {\n  if (response.statusCode !== 402) throw new Error('Expected a 402 response')\n  if (!response.headers['www-authenticate'])\n    throw new Error('Missing www-authenticate header. Cannot parse LSAT challenge')\n\n  try {\n    const lsat = Lsat.fromChallenge(response.headers['www-authenticate'])\n    return lsat\n  } catch (e) {\n    logger.error(`Could not generate LSAT from challenge: ${e.message}`)\n    throw new Error('Problem processing www-authenticate header challenge for LSAT')\n  }\n}\n\nmodule.exports = {\n  connectAsync: connectAsync,\n  coreRequestAsync: coreRequestAsync,\n  submitHashAsync: submitHashAsync,\n  getProofsAsync: getProofsAsync,\n  getLatestCalBlockInfoAsync: getLatestCalBlockInfoAsync,\n  getCachedTransactionAsync: getCachedTransactionAsync,\n  startPruneExpiredItemsInterval: startPruneExpiredItemsInterval,\n  parse402Response: parse402Response,\n  getAllCoreIPs: () => ALL_CORE_IPS,\n  // additional functions for testing purposes\n  setCoreConnectionCount: c => (coreConnectionCount = c),\n  getCoreConnectedIPs: () => CONNECTED_CORE_IPS,\n  startConnectionMonitoringInterval: startConnectionMonitoringInterval,\n  pruneExpiredItems: pruneExpiredItems,\n  getPruneExpiredIntervalSeconds: () => PRUNE_EXPIRED_INTERVAL_SECONDS,\n  getCoreTxCache: () => CORE_TX_CACHE,\n  setCoreTxCache: obj => {\n    CORE_TX_CACHE = obj\n  },\n  setENV: obj => {\n    env = obj\n  },\n  setRP: RP => {\n    rp = RP\n  },\n  setLN: LN => {\n    lnd = LN\n  },\n  getLn: () => {\n    return lnd\n  }\n}\n"
  },
  {
    "path": "lib/endpoints/calendar.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst errors = require('restify-errors')\nlet cores = require('../cores.js')\n\nasync function getDataValueByIDAsync(req, res, next) {\n  res.contentType = 'application/json'\n\n  let txId = req.params.tx_id\n\n  // validate TM txId is well formed\n  let containsValidTxId = /^([a-fA-F0-9]{2}){32}$/.test(txId)\n  if (!containsValidTxId) {\n    return next(new errors.InvalidArgumentError('invalid JSON body, invalid txId present'))\n  }\n\n  // check Core for the transaction\n  let txInfo = await cores.getCachedTransactionAsync(txId)\n  if (txInfo === null) {\n    return next(new errors.NotFoundError())\n  }\n\n  res.contentType = 'text/plain'\n  res.send(txInfo.toLowerCase())\n  return next()\n}\n\nmodule.exports = {\n  getDataValueByIDAsync: getDataValueByIDAsync,\n  // additional functions for testing purposes\n  setCores: c => {\n    cores = c\n  }\n}\n"
  },
  {
    "path": "lib/endpoints/config.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst { version } = require('../../package.json')\nlet env = require('../parse-env.js').env\n\n/**\n * GET /config handler\n *\n * Returns a configuration information object\n */\n\nlet lnd\n\nasync function getConfigInfoAsync(req, res, next) {\n  res.contentType = 'application/json'\n  let info\n  try {\n    info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, env.HOT_WALLET_PASS)\n  } catch (error) {\n    info = { error: error.message }\n  }\n  let balance\n  try {\n    balance = await lnd.callMethodAsync('lightning', 'walletBalanceAsync', null, env.HOT_WALLET_PASS)\n  } catch (error) {\n    balance = { error: error.message }\n  }\n  res.send({\n    lndInfo: info,\n    lightning_balance: balance,\n    version: version,\n    time: new Date().toISOString()\n  })\n  return next()\n}\n\nmodule.exports = {\n  getConfigInfoAsync: getConfigInfoAsync,\n  setLnd: lndObj => {\n    lnd = lndObj\n  }\n}\n"
  },
  {
    "path": "lib/endpoints/hashes.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// load environment variables\nlet env = require('../parse-env.js').env\n\nconst errors = require('restify-errors')\nconst _ = require('lodash')\nconst utils = require('../utils.js')\nlet rocksDB = require('../models/RocksDB.js')\nconst logger = require('../logger.js')\nconst analytics = require('../analytics.js')\nconst { UlidMonotonic } = require('id128')\n\n/**\n * Generate the values for the 'meta' property in a POST /hashes response.\n *\n * Returns an Object with metadata about a POST /hashes request\n * including a 'timestamp', and hints for estimated time to completion\n * for various operations.\n *\n * @returns {Object}\n */\nfunction generatePostHashesResponseMetadata() {\n  let metaDataObj = {}\n  let timestamp = new Date()\n  metaDataObj.hash_received = utils.formatDateISO8601NoMs(timestamp)\n  metaDataObj.processing_hints = generateProcessingHints(timestamp)\n\n  return metaDataObj\n}\n\n/**\n * Generate the expected proof ready times for each proof stage\n *\n * @param {Date} timestampDate - The hash submission timestamp\n * @returns {Object} An Object with 'cal' and 'btc' properties\n *\n */\n\nfunction generateProcessingHints(timestampDate) {\n  // cal proof aggregation occurs at :30 seconds past each minute\n  // allow and extra 30 seconds for processing\n  let maxLocalAggregationFromTimestamp = utils.addSeconds(timestampDate, env.AGGREGATION_INTERVAL_SECONDS)\n  let maxSeconds = maxLocalAggregationFromTimestamp.getSeconds()\n  let secondsUntil30Past = maxSeconds < 30 ? 30 - maxSeconds : 90 - maxSeconds\n  let calHint = utils.formatDateISO8601NoMs(utils.addSeconds(maxLocalAggregationFromTimestamp, secondsUntil30Past + 30))\n  let twoHoursFromTimestamp = utils.addMinutes(timestampDate, 120)\n  let btcHint = utils.formatDateISO8601NoMs(twoHoursFromTimestamp)\n\n  return {\n    cal: calHint,\n    btc: btcHint\n  }\n}\n\n/**\n * Converts an array of hash strings to a object suitable to\n * return to HTTP clients.\n *\n * @param {string[]} hashes - An array of string hashes to process\n * @returns {Object} An Object with 'meta' and 'hashes' properties\n */\nfunction generatePostHashesResponse(ip, hashes) {\n  let lcHashes = utils.lowerCaseHashes(hashes)\n\n  let hashObjects = lcHashes.map(hash => {\n    let proofId\n    try {\n      proofId = UlidMonotonic.generate().toCanonical()\n    } catch (error) {\n      UlidMonotonic.reset()\n      proofId = UlidMonotonic.generate().toCanonical()\n    }\n\n    let hashObj = {}\n    hashObj.proof_id = proofId\n    hashObj.hash = hash\n    logger.info(`Created proof_id ${proofId}`)\n\n    //send event to google UA\n    var hashEvent = {\n      ec: env.GATEWAY_NAME,\n      ea: 'CreateProof',\n      el: proofId,\n      cd1: hash,\n      cd2: utils.formatDateISO8601NoMs(new Date()),\n      cd3: env.PUBLIC_IP,\n      cd4: ip,\n      dp: '/hash'\n    }\n    analytics.setClientID(proofId)\n    analytics.sendEvent(hashEvent)\n    return hashObj\n  })\n\n  return {\n    meta: generatePostHashesResponseMetadata(hashObjects),\n    hashes: hashObjects\n  }\n}\n\n/**\n * POST /hashes handler\n *\n * Expects a JSON body with the form:\n *   {\"hashes\": [\"hash1\", \"hash2\", \"hashN\"]}\n *\n * The `hashes` key must reference a JSON Array\n * of strings representing each hash to anchor.\n *\n * Each hash must be:\n * - in Hexadecimal form [a-fA-F0-9]\n * - minimum 40 chars long (e.g. 20 byte SHA1)\n * - maximum 128 chars long (e.g. 64 byte SHA512)\n * - an even length string\n */\nasync function postHashesAsync(req, res, next) {\n  res.contentType = 'application/json'\n\n  // validate content-type sent was 'application/json'\n  if (req.contentType() !== 'application/json') {\n    return next(new errors.InvalidArgumentError('invalid content type'))\n  }\n\n  // validate params has parse a 'hashes' key\n  if (!req.params.hasOwnProperty('hashes')) {\n    return next(new errors.InvalidArgumentError('invalid JSON body, missing hashes'))\n  }\n\n  // validate hashes param is an Array\n  if (!_.isArray(req.params.hashes)) {\n    return next(new errors.InvalidArgumentError('invalid JSON body, hashes is not an Array'))\n  }\n\n  // validate hashes param Array has at least one hash\n  if (_.size(req.params.hashes) < 1) {\n    return next(new errors.InvalidArgumentError('invalid JSON body, hashes Array is empty'))\n  }\n\n  // validate hashes param Array is not larger than allowed max length\n  if (_.size(req.params.hashes) > env.POST_HASHES_MAX) {\n    return next(\n      new errors.InvalidArgumentError(`invalid JSON body, hashes Array max size of ${env.POST_HASHES_MAX} exceeded`)\n    )\n  }\n\n  // validate hashes are individually well formed\n  let containsValidHashes = _.every(req.params.hashes, hash => {\n    return /^([a-fA-F0-9]{2}){20,64}$/.test(hash)\n  })\n\n  if (!containsValidHashes) {\n    return next(new errors.InvalidArgumentError('invalid JSON body, invalid hashes present'))\n  }\n\n  let ip = utils.getClientIP(req)\n  logger.info(`Incoming hash from ${ip}`)\n  let responseObj = generatePostHashesResponse(ip, req.params.hashes)\n\n  // store hash data for later aggregation\n  try {\n    await rocksDB.queueIncomingHashObjectsAsync(responseObj.hashes)\n  } catch (error) {\n    return next(new errors.InternalServerError('Could not save hash data'))\n  }\n\n  res.send(responseObj)\n  return next()\n}\n\nmodule.exports = {\n  postHashesAsync: postHashesAsync,\n  generatePostHashesResponse: generatePostHashesResponse,\n  // additional functions for testing purposes\n  setRocksDB: db => {\n    rocksDB = db\n  },\n  setENV: obj => {\n    env = obj\n  }\n}\n"
  },
  {
    "path": "lib/endpoints/proofs.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet env = require('../parse-env.js').env\nconst errors = require('restify-errors')\nconst utils = require('../utils.js')\nconst uuidValidate = require('uuid-validate')\nconst uuidTime = require('uuid-time')\nconst chpBinary = require('chainpoint-binary')\nconst _ = require('lodash')\nlet cachedProofs = require('../cached-proofs.js')\nlet rocksDB = require('../models/RocksDB.js')\nconst logger = require('../logger.js')\nconst analytics = require('../analytics.js')\nconst { UlidMonotonic } = require('id128')\n\n// The custom MIME type for JSON proof array results containing Base64 encoded proof data\nconst BASE64_MIME_TYPE = 'application/vnd.chainpoint.json+base64'\n\n// The custom MIME type for JSON proof array results containing Base64 encoded proof data\nconst JSONLD_MIME_TYPE = 'application/vnd.chainpoint.ld+json'\n\n/**\n * Converts proof path array output from the merkle-tools package\n * to a Chainpoint v3 ops array\n *\n * @param {proof object array} proof - The proof array generated by merkle-tools\n * @param {string} op - The hash type performed throughout merkle tree construction (sha-256, sha-512, sha-256-x2, etc.)\n * @returns {ops object array}\n */\nfunction formatAsChainpointV3Ops(proof, op) {\n  let ChainpointV3Ops = proof.reduce((result, item) => {\n    if (item.left) {\n      item = { l: item.left }\n    } else {\n      item = { r: item.right }\n    }\n    result.push(item, { op: op })\n    return result\n  }, [])\n\n  return ChainpointV3Ops\n}\n\n/**\n * GET /proofs/:proof_id handler\n *\n * Expects a path parameter 'proof_id' in the form of a Version 1 UUID\n *\n * Returns a chainpoint proof for the requested Proof ID\n */\nasync function getProofsByIDAsync(req, res, next) {\n  res.contentType = 'application/json'\n  let ip = utils.getClientIP(req)\n  let proofIds = []\n\n  // check if proof_id parameter was included\n  if (req.params && req.params.proof_id) {\n    // a proof_id was specified in the url, so use that proof_id only\n    proofIds.push(req.params.proof_id)\n  } else if (req.headers && req.headers.proofids) {\n    // no proof_id was specified in url, read from headers.proofids\n    proofIds = req.headers.proofids.split(',').map(_.trim)\n  }\n\n  // ensure at least one proof_id was submitted\n  if (proofIds.length === 0) {\n    return next(new errors.InvalidArgumentError('invalid request, at least one proof id required'))\n  }\n\n  // ensure that the request count does not exceed the maximum setting\n  if (proofIds.length > env.GET_PROOFS_MAX) {\n    return next(new errors.InvalidArgumentError('invalid request, too many proof ids (' + env.GET_PROOFS_MAX + ' max)'))\n  }\n\n  // ensure all proof_ids are valid, send event\n  for (let proofId of proofIds) {\n    if (!(uuidValidate(proofId, 1) || utils.isULID(proofId))) {\n      return next(new errors.InvalidArgumentError('invalid request, bad proof_id'))\n    }\n    // Proof endpoint (always logged regardless of proof retrieval success)\n    var startProofEvent = {\n      ec: env.GATEWAY_NAME,\n      ea: 'GetProof',\n      el: proofId,\n      cd1: 'Start',\n      cd2: utils.formatDateISO8601NoMs(new Date()),\n      cd3: env.PUBLIC_IP,\n      cd4: ip,\n      dp: '/proof'\n    }\n    analytics.setClientID(proofId)\n    analytics.sendEvent(startProofEvent)\n  }\n\n  let requestedType =\n    req.accepts(JSONLD_MIME_TYPE) && !req.accepts(BASE64_MIME_TYPE) ? JSONLD_MIME_TYPE : BASE64_MIME_TYPE\n\n  // retrieve all the nodeProofDataItems for the requested proofIds\n  let nodeProofDataItems = []\n  try {\n    nodeProofDataItems = await rocksDB.getProofStatesBatchByProofIdsAsync(proofIds)\n  } catch (error) {\n    return next(new errors.InternalError('error retrieving node proof data items'))\n  }\n\n  // convert all proof state value to chpv3\n  nodeProofDataItems = nodeProofDataItems.map(nodeProofDataItem => {\n    if (nodeProofDataItem.proofState === null) return nodeProofDataItem\n    nodeProofDataItem.proofState = formatAsChainpointV3Ops(nodeProofDataItem.proofState, 'sha-256')\n    return nodeProofDataItem\n  })\n\n  // get an array of all unique core submission objects from the proof data items by submitId\n  let knownSubmitIds = []\n  let uniqueCoreSubmissions = nodeProofDataItems.reduce((result, val) => {\n    // if the node data item was not found for that proofId, skip this item\n    if (_.isNil(val.submission)) return result\n    // add this submission object to the results if it is unique\n    let submitId = val.submission.submitId\n    if (!knownSubmitIds.includes(submitId)) {\n      knownSubmitIds.push(submitId)\n      result.push(val.submission)\n    }\n    return result\n  }, [])\n\n  // get proofs for each unique submitId\n  let coreProofDataItems\n  try {\n    coreProofDataItems = await cachedProofs.getCachedCoreProofsAsync(uniqueCoreSubmissions)\n  } catch (error) {\n    logger.error(`Could not get proofs from Core : ${error.message}`)\n    return next(new errors.InternalError('error retrieving proofs from Core'))\n  }\n\n  // assemble all Core proofs received into an object keyed by submitId\n  coreProofDataItems = coreProofDataItems.reduce((results, coreProofDataItem) => {\n    results[coreProofDataItem.submitId] = coreProofDataItem\n    return results\n  }, {})\n\n  // build the resulting proofs from the collected data for each proof_id\n  let results = []\n  for (let nodeProofDataItem of nodeProofDataItems) {\n    let submitId = nodeProofDataItem.submission ? nodeProofDataItem.submission.submitId : null\n    let coreProof = coreProofDataItems[submitId] ? coreProofDataItems[submitId].proof : null\n\n    let fullProof = null\n    if (coreProof) {\n      fullProof = buildFullProof(coreProof, nodeProofDataItem)\n    }\n\n    let proofResult = fullProof\n    if (requestedType === BASE64_MIME_TYPE && fullProof) {\n      try {\n        proofResult = chpBinary.objectToBase64Sync(fullProof)\n      } catch (error) {\n        if (nodeProofDataItem) {\n          logger.error(`Could not convert binary proof for proof id : ${nodeProofDataItem.proofId}`)\n          logger.error(`Binary Conversion Error : ${error.message}`)\n          if (fullProof) {\n            logger.error(JSON.stringify(fullProof))\n          }\n        }\n        return next(error)\n      }\n    }\n\n    let result = {\n      proof_id: nodeProofDataItem.proofId,\n      proof: proofResult,\n      anchors_complete: coreProofDataItems[submitId] ? coreProofDataItems[submitId].anchorsComplete : []\n    }\n    results.push(result)\n\n    //send events\n    var event = {\n      ec: env.GATEWAY_NAME,\n      el: result.proof_id,\n      cd1: result.proof ? result.proof.hash : 'null',\n      cd2: utils.formatDateISO8601NoMs(new Date()),\n      cd3: env.PUBLIC_IP,\n      cd4: ip,\n      dp: '/proof'\n    }\n    analytics.setClientID(result.proof_id)\n    if (fullProof) {\n      try {\n        let proofString = JSON.stringify(fullProof)\n        if (proofString.includes('btc') || proofString.includes('tbtc')) { // eslint-disable-line\n          event.ea = 'GetProofSuccessBtc'\n          analytics.sendEvent(event)\n        } else if (proofString.includes('cal') || proofString.includes('tcal')) { // eslint-disable-line\n          event.ea = 'GetProofSuccessCal'\n          analytics.sendEvent(event)\n        }\n      } catch (error) {\n        logger.error(`could not get proof size of proof ${result.proof_id}`)\n      }\n    } else {\n      event.ea = 'GetProofFail'\n      analytics.sendEvent(event)\n    }\n  }\n\n  res.send(results)\n  return next()\n}\n\nfunction buildFullProof(coreProof, nodeProofDataItem) {\n  if (!coreProof || !nodeProofDataItem) return null\n\n  let fullProof = _.cloneDeep(coreProof)\n  let coreBranches = _.cloneDeep(fullProof.branches)\n  fullProof.proof_id = nodeProofDataItem.proofId\n  fullProof.hash = nodeProofDataItem.hash\n  if (utils.isULID(nodeProofDataItem.proofId)) {\n    fullProof.hash_received = utils.formatDateISO8601NoMs(UlidMonotonic.fromCanonical(nodeProofDataItem.proofId).time)\n  } else {\n    fullProof.hash_received = utils.formatDateISO8601NoMs(new Date(parseInt(uuidTime.v1(nodeProofDataItem.proofId))))\n  }\n  fullProof.branches[0].label = 'aggregator'\n  fullProof.branches[0].ops = nodeProofDataItem.proofState\n  fullProof.branches[0].branches = coreBranches\n  fullProof = utils.jsonTransform(\n    fullProof,\n    // eslint-disable-next-line no-unused-vars\n    (key, value) => key.includes('uris'),\n    (key, value) => {\n      value = value.replace('https://', 'http://')\n      if (!value.includes('http://')) {\n        value = 'http://' + value\n      }\n      return value\n    }\n  )\n  return fullProof\n}\n\nmodule.exports = {\n  getProofsByIDAsync: getProofsByIDAsync,\n  // additional functions for testing purposes\n  setRocksDB: db => {\n    rocksDB = db\n  },\n  setCachedProofs: cp => {\n    cachedProofs = cp\n  },\n  setENV: obj => {\n    env = obj\n  }\n}\n"
  },
  {
    "path": "lib/endpoints/verify.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet env = require('../parse-env.js').env\nconst _ = require('lodash')\nconst chpParse = require('chainpoint-parse')\nconst errors = require('restify-errors')\nlet cores = require('../cores.js')\nconst parallel = require('async-await-parallel')\nconst logger = require('../logger.js')\n\nasync function ProcessVerifyTasksAsync(verifyTasks) {\n  let processedTasks = []\n\n  for (let x = 0; x < verifyTasks.length; x++) {\n    let verifyTask = verifyTasks[x]\n\n    // check for malformatted proofs\n    let status = verifyTask.status\n    if (status === 'malformed') {\n      processedTasks.push({\n        proof_index: verifyTask.proof_index,\n        status: status\n      })\n      continue\n    }\n\n    // check for anchors on unsupported network\n    let supportedAnchorTypes = []\n    // if this Node is running in mainnet mode, only accept mainnet anchors\n    if (env.NETWORK === 'mainnet') supportedAnchorTypes = ['cal', 'btc']\n    // if this Node is running in testnet mode, only accept testnet anchors\n    if (env.NETWORK === 'testnet') supportedAnchorTypes = ['tcal', 'tbtc']\n    // if there is a network mismatch, do not attempt to verify proof\n    let mismatchFound = false\n    for (let x = 0; x < verifyTask.anchors.length; x++) {\n      if (!supportedAnchorTypes.includes(verifyTask.anchors[x].anchor.type)) {\n        processedTasks.push({\n          proof_index: verifyTask.proof_index,\n          status: `This is a '${env.NETWORK}' Node supporting '${supportedAnchorTypes.join(\n            \"' and '\"\n          )}' anchor types. Cannot verify '${verifyTask.anchors[x].anchor.type}' anchors.`\n        })\n        mismatchFound = true\n        break\n      }\n    }\n    if (mismatchFound) continue\n\n    let totalCount = 0\n    let validCount = 0\n\n    let anchorResults = []\n    let confirmTasks = []\n    for (let x = 0; x < verifyTask.anchors.length; x++) {\n      confirmTasks.push(async () => {\n        return confirmExpectedValueAsync(verifyTask.anchors[x].anchor)\n      })\n    }\n    let confirmResults = []\n    if (confirmTasks.length > 0) {\n      try {\n        confirmResults = await parallel(confirmTasks, 20)\n      } catch (error) {\n        logger.error(`Could not confirm proof data ${error.message}`)\n        throw new Error('error confirming proof data')\n      }\n    }\n\n    for (let x = 0; x < verifyTask.anchors.length; x++) {\n      try {\n        let anchor = verifyTask.anchors[x]\n        let confirmResult = confirmResults[x]\n        let anchorResult = {\n          branch: anchor.branch || null,\n          type: anchor.anchor.type,\n          valid: confirmResult\n        }\n        totalCount++\n        validCount = validCount + (anchorResult.valid === true ? 1 : 0)\n        anchorResults.push(anchorResult)\n      } catch (error) {\n        logger.error('Verification error')\n      }\n    }\n\n    if (validCount === 0) {\n      status = 'invalid'\n    } else if (validCount === totalCount) {\n      status = 'verified'\n    } else {\n      status = 'mixed'\n    }\n\n    let result = {\n      proof_index: verifyTask.proof_index,\n      hash: verifyTask.hash,\n      proof_id: verifyTask.proof_id,\n      hash_received: verifyTask.hash_received,\n      anchors: anchorResults,\n      status: status\n    }\n    processedTasks.push(result)\n  }\n\n  return processedTasks\n}\n\nfunction BuildVerifyTaskList(proofs) {\n  let results = []\n  let proofIndex = 0\n\n  // extract id, time, anchors, and calculate expected values\n  _.forEach(proofs, proof => {\n    try {\n      let parseObj = chpParse.parse(proof)\n      results.push(buildResultObject(parseObj, proofIndex++))\n    } catch (error) {\n      // continue regardless of error\n      results.push(buildResultObject(null, proofIndex++))\n    }\n  })\n\n  return results\n}\n\nfunction buildResultObject(parseObj, proofIndex) {\n  let hash = parseObj !== null ? parseObj.hash : undefined\n  let proofId = parseObj !== null ? parseObj.proof_id : undefined\n  let hashReceived = parseObj !== null ? parseObj.hash_received : undefined\n  let expectedValues = parseObj !== null ? flattenExpectedValues(parseObj.branches) : undefined\n\n  return {\n    proof_index: proofIndex,\n    hash: hash,\n    proof_id: proofId,\n    hash_received: hashReceived,\n    anchors: expectedValues,\n    status: parseObj === null ? 'malformed' : ''\n  }\n}\n\nasync function confirmExpectedValueAsync(anchorInfo) {\n  let anchorUri = anchorInfo.uris[0]\n  let anchorTxId = _.takeRight(anchorUri.split('/'), 2)[0]\n  let expectedValue = anchorInfo.expected_value\n\n  // check Core for the transaction\n  let txInfo = await cores.getCachedTransactionAsync(anchorTxId)\n  if (txInfo === null) {\n    throw new Error('Unable to retrieve transaction from Core to the confirm the anchor value')\n  }\n\n  return txInfo === expectedValue\n}\n\nfunction flattenExpectedValues(branchArray) {\n  let results = []\n  for (let b = 0; b < branchArray.length; b++) {\n    let anchors = branchArray[b].anchors\n    if (anchors.length > 0) {\n      for (let a = 0; a < anchors.length; a++) {\n        results.push({\n          branch: branchArray[b].label || undefined,\n          anchor: anchors[a]\n        })\n      }\n    }\n    if (branchArray[b].branches) {\n      results = results.concat(flattenExpectedValues(branchArray[b].branches))\n    }\n\n    return results\n  }\n}\n\n/**\n * POST /verify handler\n *\n * Expects a JSON body with the form:\n *   {\"proofs\": [ {proofJSON1}, {proofJSON2}, {proofJSON3} ]}\n *   or\n *   {\"proofs\": [ \"proof binary 1\", \"proof binary 2\", \"proof binary 3\" ]}\n *\n * The `proofs` key must reference a JSON Array of chainpoint proofs.\n * Proofs may be in either JSON form or base64 encoded binary form.\n *\n */\nasync function postProofsForVerificationAsync(req, res, next) {\n  res.contentType = 'application/json'\n\n  // validate content-type sent was 'application/json'\n  if (req.contentType() !== 'application/json') {\n    return next(new errors.InvalidArgumentError('Invalid content type'))\n  }\n\n  // validate params has parse a 'proofs' key\n  if (!req.params.hasOwnProperty('proofs')) {\n    return next(new errors.InvalidArgumentError('Invalid JSON body, missing proofs'))\n  }\n\n  // validate proofs param is an Array\n  if (!_.isArray(req.params.proofs)) {\n    return next(new errors.InvalidArgumentError('Invalid JSON body, proofs is not an Array'))\n  }\n\n  // validate proofs param Array has at least one hash\n  if (_.size(req.params.proofs) < 1) {\n    return next(new errors.InvalidArgumentError('Invalid JSON body, proofs Array is empty'))\n  }\n\n  // validate proofs param Array is not larger than allowed max length\n  if (_.size(req.params.proofs) > env.POST_VERIFY_PROOFS_MAX) {\n    return next(\n      new errors.InvalidArgumentError(\n        `Invalid JSON body, proofs Array max size of ${env.POST_VERIFY_PROOFS_MAX} exceeded`\n      )\n    )\n  }\n\n  let verifyTasks\n  try {\n    verifyTasks = BuildVerifyTaskList(req.params.proofs)\n  } catch (error) {\n    logger.error(`Could not build verify list: ${error.message}`)\n    return next(new errors.InternalError('Node internal error verifying proof(s)'))\n  }\n  let verifyResults\n  try {\n    verifyResults = await ProcessVerifyTasksAsync(verifyTasks)\n  } catch (error) {\n    return next(new errors.InternalError('Node internal error verifying proof(s)'))\n  }\n\n  res.send(verifyResults)\n  return next()\n}\n\nmodule.exports = {\n  postProofsForVerificationAsync: postProofsForVerificationAsync,\n  // additional functions for testing purposes\n  setCores: c => {\n    cores = c\n  },\n  setENV: obj => {\n    env = obj\n  }\n}\n"
  },
  {
    "path": "lib/lightning.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// load environment variables\nlet env = require('./parse-env.js').env\n\nconst lnRPCNodeClient = require('lnrpc-node-client')\nconst retry = require('async-retry')\nconst dotenv = require('dotenv')\n\n// track if unlock is in process to prevent multiple simultaneous unlock errors\nlet IS_UNLOCKING = false\nlet LND_DIR\nlet LND_SOCKET\nlet LND_TLS_CERT\nlet LND_MACAROON\ndotenv.config()\nlet lnd = function(socket, network, unlockOnly = false, inHostContext = false) {\n  if (!['mainnet', 'testnet'].includes(network)) throw new Error('Invalid network value')\n\n  LND_DIR = process.env.LND_DIR || `${process.env.HOME}/${inHostContext ? '.chainpoint/gateway/' : ''}.lnd`\n  LND_SOCKET = socket\n  LND_TLS_CERT = process.env.LND_TLS_CERT || `${LND_DIR}/tls.cert`\n  LND_MACAROON = process.env.LND_MACAROON || `${LND_DIR}/data/chain/bitcoin/${network}/admin.macaroon`\n\n  if (unlockOnly) {\n    lnRPCNodeClient.setTls(LND_SOCKET, LND_TLS_CERT)\n  } else {\n    lnRPCNodeClient.setCredentials(LND_SOCKET, LND_MACAROON, LND_TLS_CERT)\n  }\n\n  // Call the service method with the given parameter\n  // If a locked wallet is detected, wallet unlock is attempted and call is retried\n  this.callMethodAsync = async (service, method, params, hotWalletPass = null) => {\n    let lndService = lnRPCNodeClient[service]()\n    return await retry(async () => await lndService[method](params), {\n      retries: 30,\n      factor: 1,\n      minTimeout: 1000,\n      onRetry: async error => {\n        if (!error.message.includes('failed to connect to all addresses')) {\n          if (!error.message.includes('unknown service lnrpc.Lightning')) {\n            console.log(error)\n          }\n          await this.handleUnlock(error, hotWalletPass)\n        }\n      }\n    })\n  }\n\n  this.handleUnlock = async (err, hotWalletPass) => {\n    if (err == null || err.code === 2 || err.code === 12) {\n      // error code 12 indicates wallet may be locked\n      if (!IS_UNLOCKING) {\n        IS_UNLOCKING = true\n        try {\n          await lnRPCNodeClient\n            .unlocker()\n            .unlockWalletAsync({ wallet_password: hotWalletPass || env.HOT_WALLET_PASS, recovery_window: 10000 })\n        } catch (error) {\n          throw new Error(`Unable to unlock LND wallet : ${error.message}`)\n        } finally {\n          IS_UNLOCKING = false\n        }\n      }\n    }\n  }\n\n  // Call the service method with the given parameter\n  // No unlocking or retrying is performed with this method\n  this.callMethodRawAsync = async (service, method, params) => await lnRPCNodeClient[service]()[method](params)\n}\n\nmodule.exports = lnd\n"
  },
  {
    "path": "lib/logger.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// load environment variables\nlet env = require('./parse-env.js').env\n\nconst winston = require('winston')\n\nconst myFormat = winston.format.printf(({ level, message, timestamp }) => `${timestamp} [${level}] ${message}`)\n\nlet consoleOpts = {\n  level: 'info',\n  stderrLevels: ['error'],\n  format: winston.format.combine(winston.format.colorize({ all: true }), winston.format.timestamp(), myFormat)\n}\nif (env.NODE_ENV === 'test') consoleOpts.silent = true\n\nconst logger = winston.createLogger({\n  transports: [new winston.transports.Console(consoleOpts)]\n})\n\nmodule.exports = logger\n"
  },
  {
    "path": "lib/models/RocksDB.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet env = require('../parse-env.js').env\nconst level = require('level-rocksdb')\nconst crypto = require('crypto')\nconst path = require('path')\nconst JSBinaryType = require('js-binary').Type\nconst logger = require('../logger.js')\nconst utils = require('../utils.js')\nconst { Ulid } = require('id128')\n\n// See Options: https://github.com/level/leveldown#options\n// Setup with options, all default except:\n//   cacheSize : which was increased from 8MB to 32MB\nlet options = {\n  createIfMissing: true,\n  errorIfExists: false,\n  compression: true,\n  cacheSize: 32 * 1024 * 1024,\n  writeBufferSize: 4 * 1024 * 1024,\n  blockSize: 4096,\n  maxOpenFiles: 1000,\n  blockRestartInterval: 16,\n  maxFileSize: 2 * 1024 * 1024,\n  keyEncoding: 'binary',\n  valueEncoding: 'binary'\n}\n\nconst prefixBuffers = {\n  PROOF_STATE_INDEX: Buffer.from('b1a1', 'hex'),\n  PROOF_STATE_VALUE: Buffer.from('b1a2', 'hex'),\n  INCOMING_HASH_OBJECTS: Buffer.from('b1b1', 'hex'),\n  REP_ITEM_VALUE: Buffer.from('b1c1', 'hex'),\n  REP_ITEM_ID_INDEX: Buffer.from('b1c2', 'hex'),\n  REP_ITEM_PROOF_VALUE: Buffer.from('b1c3', 'hex')\n}\nconst PRUNE_BATCH_SIZE = 1000\nconst PRUNE_INTERVAL_SECONDS = 10\nlet PRUNE_IN_PROGRESS = false\n\nlet db = null\n\nasync function openConnectionAsync(dir = `${process.env.HOME}/.chainpoint/gateway/data/rocksdb`) {\n  return new Promise(resolve => {\n    level(path.resolve(dir), options, (err, conn) => {\n      if (err) {\n        logger.error(`Unable to open database : ${err.message}`)\n        process.exit(0)\n      } else {\n        db = conn\n        resolve(db)\n      }\n    })\n  })\n}\n\n/****************************************************************************************************\n * DEFINE SCHEMAS\n ****************************************************************************************************/\n// #region SCHEMAS\n\nconst nodeProofDataItemSchema = new JSBinaryType({\n  proofId: 'Buffer',\n  hash: 'Buffer',\n  proofState: ['Buffer'],\n  submission: {\n    submitId: 'Buffer',\n    cores: [{ ip: 'string', proofId: 'Buffer' }]\n  }\n})\n\nconst incomingHashObjectsSchema = new JSBinaryType([\n  {\n    proof_id: 'Buffer',\n    hash: 'Buffer'\n  }\n])\n\n// #endregion SCHEMAS\n\n/****************************************************************************************************\n * PROOF STATE FUNCTIONS\n ****************************************************************************************************/\n// #region PROOF STATE FUNCTIONS\n\nfunction encodeBinaryProofStateId(proofIdNode) {\n  let idBuffer\n  let idType\n  if (utils.isUUID(proofIdNode)) {\n    idBuffer = Buffer.from(proofIdNode.replace(/-/g, ''), 'hex')\n    idType = 'uuid'\n  } else if (utils.isULID(proofIdNode)) {\n    idBuffer = Buffer.from(Ulid.fromCanonicalTrusted(proofIdNode).bytes)\n    idType = 'ulid'\n  }\n  return { idType: idType, idBuffer: idBuffer }\n}\n\nfunction decodeBinaryProofStateId(idType, proofId) {\n  return idType == 'uuid'\n    ? utils.hexToUUIDv1(proofId.toString('hex'))\n    : Ulid.construct(Uint8Array.from(proofId)).toCanonical()\n}\n\nfunction encodeBinaryProofStateValueKey(proofIdNode) {\n  let idBuffer = encodeBinaryProofStateId(proofIdNode).idBuffer\n  return Buffer.concat([prefixBuffers.PROOF_STATE_VALUE, idBuffer])\n}\n\nfunction createBinaryProofStateTimeIndexKey() {\n  // generate a new key for the current time\n  let timestampBuffer = Buffer.alloc(8)\n  timestampBuffer.writeDoubleBE(Date.now())\n  let rndBuffer = crypto.randomBytes(16)\n  return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, timestampBuffer, rndBuffer])\n}\n\nfunction createBinaryProofStateTimeIndexMin() {\n  // generate the minimum key value for range query\n  let minBoundsBuffer = Buffer.alloc(24, 0)\n  return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, minBoundsBuffer])\n}\n\nfunction createBinaryProofStateTimeIndexMax(timestamp) {\n  // generate the maximum key value for range query up to given timestamp\n  let timestampBuffer = Buffer.alloc(8, 0)\n  timestampBuffer.writeDoubleBE(timestamp)\n  let rndBuffer = Buffer.alloc(16, 'ff', 'hex')\n  return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, timestampBuffer, rndBuffer])\n}\n\nfunction encodeProofStateValue(nodeProofDataItem) {\n  let stateObj = {\n    proofId: encodeBinaryProofStateId(nodeProofDataItem.proofId).idBuffer,\n    hash: Buffer.from(nodeProofDataItem.hash, 'hex'),\n    proofState: nodeProofDataItem.proofState,\n    submission: {\n      submitId: encodeBinaryProofStateId(nodeProofDataItem.submission.submitId).idBuffer,\n      cores: nodeProofDataItem.submission.cores.map(core => {\n        return { ip: core.ip, proofId: encodeBinaryProofStateId(core.proofId).idBuffer }\n      })\n    }\n  }\n  return nodeProofDataItemSchema.encode(stateObj)\n}\n\nfunction decodeProofStateValue(proofStateValue, idType) {\n  let leftCode = Buffer.from('\\x00')\n  let rightCode = Buffer.from('\\x01')\n  let stateObj = nodeProofDataItemSchema.decode(proofStateValue)\n  let nodeProofDataItem = {\n    proofId: decodeBinaryProofStateId(idType, stateObj.proofId),\n    hash: stateObj.hash.toString('hex'),\n    proofState: stateObj.proofState.reduce((result, op, index, proofState) => {\n      if (op.equals(leftCode)) result.push({ left: proofState[index + 1].toString('hex') })\n      if (op.equals(rightCode)) result.push({ right: proofState[index + 1].toString('hex') })\n      return result\n    }, []),\n    submission: {\n      submitId: decodeBinaryProofStateId(idType, stateObj.submission.submitId),\n      cores: stateObj.submission.cores.map(core => {\n        return {\n          ip: core.ip,\n          proofId: decodeBinaryProofStateId(idType, core.proofId)\n        }\n      })\n    }\n  }\n  return nodeProofDataItem\n}\n\nasync function getProofStatesBatchByProofIdsAsync(proofIds) {\n  let results = []\n  for (let proofId of proofIds) {\n    try {\n      let proofIdType = encodeBinaryProofStateId(proofId).idType\n      let proofStateValueKey = encodeBinaryProofStateValueKey(proofId)\n      let proofStateValue = await db.get(proofStateValueKey)\n      let nodeProofDataItem = decodeProofStateValue(proofStateValue, proofIdType)\n      results.push(nodeProofDataItem)\n    } catch (error) {\n      if (error.notFound) {\n        results.push({\n          proofId: proofId,\n          hash: null,\n          proofState: null,\n          submission: null\n        })\n      } else {\n        let err = `Unable to read proof state for hash with proofId = ${proofId} : ${error.message}`\n        throw err\n      }\n    }\n  }\n  return results\n}\n\nasync function saveProofStatesBatchAsync(nodeProofDataItems) {\n  let ops = []\n\n  for (let nodeProofDataItem of nodeProofDataItems) {\n    let proofStateValueKey = encodeBinaryProofStateValueKey(nodeProofDataItem.proofId)\n    let proofStateTimeIndexKey = createBinaryProofStateTimeIndexKey()\n    let proofStateValue = encodeProofStateValue(nodeProofDataItem)\n    ops.push({ type: 'put', key: proofStateValueKey, value: proofStateValue })\n    ops.push({ type: 'put', key: proofStateTimeIndexKey, value: proofStateValueKey })\n  }\n\n  try {\n    await db.batch(ops)\n  } catch (error) {\n    let err = `Unable to write proof state : ${error.message}`\n    throw err\n  }\n}\n\nasync function pruneProofStateDataSince(timestampMS) {\n  return new Promise((resolve, reject) => {\n    let delOps = []\n    let minKey = createBinaryProofStateTimeIndexMin()\n    let maxKey = createBinaryProofStateTimeIndexMax(timestampMS)\n    db.createReadStream({ gte: minKey, lte: maxKey })\n      .on('data', async data => {\n        delOps.push({ type: 'del', key: data.key })\n        delOps.push({ type: 'del', key: data.value })\n        // Execute in batches of PRUNE_BATCH_SIZE\n        if (delOps.length >= PRUNE_BATCH_SIZE) {\n          try {\n            let delOpsBatch = delOps.splice(0)\n            await db.batch(delOpsBatch)\n          } catch (error) {\n            let err = `Error during proof state batch delete : ${error.message}`\n            return reject(err)\n          }\n        }\n      })\n      .on('error', error => {\n        let err = `Error reading proof state keys for pruning : ${error.message}`\n        return reject(err)\n      })\n      .on('end', async () => {\n        try {\n          await db.batch(delOps)\n        } catch (error) {\n          return reject(error.message)\n        }\n        return resolve()\n      })\n  })\n}\n\nasync function pruneOldProofStateDataAsync() {\n  let pruneTime = Date.now() - env.PROOF_EXPIRE_MINUTES * 60 * 1000\n  try {\n    await pruneProofStateDataSince(pruneTime)\n  } catch (error) {\n    logger.warn(`An error occurred during proof state pruning : ${error.message}`)\n  }\n}\n\n// #endregion PROOF STATE FUNCTIONS\n\n/****************************************************************************************************\n * INCOMING HASH QUEUE FUNCTIONS\n ****************************************************************************************************/\n// #region INCOMING HASH QUEUE FUNCTIONS\n\nfunction createBinaryIncomingHashObjectsTimeIndexKey() {\n  // generate a new key for the current time\n  let timestampBuffer = Buffer.alloc(8)\n  timestampBuffer.writeDoubleBE(Date.now())\n  let rndBuffer = crypto.randomBytes(16)\n  return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, timestampBuffer, rndBuffer])\n}\n\nfunction createBinaryIncomingHashObjectsTimeIndexMin() {\n  // generate the minimum key value for range query\n  let minBoundsBuffer = Buffer.alloc(24, 0)\n  return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, minBoundsBuffer])\n}\n\nfunction createBinaryIncomingHashObjectsTimeIndexMax(timestamp) {\n  // generate the maximum key value for range query up to given timestamp\n  let timestampBuffer = Buffer.alloc(8, 0)\n  timestampBuffer.writeDoubleBE(timestamp)\n  let rndBuffer = Buffer.alloc(16, 'ff', 'hex')\n  return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, timestampBuffer, rndBuffer])\n}\n\nfunction encodeIncomingHashObjectsValue(hashObjects) {\n  hashObjects = hashObjects.map(hashObject => {\n    return {\n      proof_id: encodeBinaryProofStateId(hashObject.proof_id).idBuffer,\n      hash: Buffer.from(hashObject.hash, 'hex')\n    }\n  })\n  return incomingHashObjectsSchema.encode(hashObjects)\n}\n\nfunction decodeIncomingHashObjectsValue(hashObjectsBinary) {\n  if (hashObjectsBinary.length === 0) return []\n  let hashObjects = incomingHashObjectsSchema.decode(hashObjectsBinary)\n  hashObjects = hashObjects.map(hashObject => {\n    return {\n      proof_id: Ulid.construct(Uint8Array.from(hashObject.proof_id)).toCanonical(),\n      hash: hashObject.hash.toString('hex')\n    }\n  })\n  return hashObjects\n}\n\nasync function queueIncomingHashObjectsAsync(hashObjects) {\n  try {\n    let incomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexKey()\n    let incomingHashObjectsValue = encodeIncomingHashObjectsValue(hashObjects)\n    await db.put(incomingHashObjectsBinaryKey, incomingHashObjectsValue)\n  } catch (error) {\n    let err = `Unable to write incoming hash data : ${error.message}`\n    throw err\n  }\n}\n\nasync function getIncomingHashesUpToAsync(maxTimestamp) {\n  return new Promise((resolve, reject) => {\n    let hashesObjects = []\n    let delOps = []\n    let minIncomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexMin()\n    let maxIncomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexMax(maxTimestamp)\n    db.createReadStream({ gte: minIncomingHashObjectsBinaryKey, lte: maxIncomingHashObjectsBinaryKey })\n      .on('data', async data => {\n        hashesObjects.push(...decodeIncomingHashObjectsValue(data.value))\n        delOps.push({\n          type: 'del',\n          key: data.key\n        })\n      })\n      .on('error', error => {\n        let err = `Error reading incoming hashes : ${error.message}`\n        return reject(err)\n      })\n      .on('end', async () => {\n        return resolve([hashesObjects, delOps])\n      })\n  })\n}\n\n// #endregion INCOMING HASH QUEUE FUNCTIONS\n\n/****************************************************************************************************\n * GENERAL KEY - VALUE FUNCTIONS\n ****************************************************************************************************/\n// #region GENERAL KEY - VALUE FUNCTIONS\n\nasync function setAsync(key, value) {\n  try {\n    await db.put(Buffer.from(`custom_key:${key}`), Buffer.from(value.toString()))\n  } catch (error) {\n    let err = `Unable to write key : ${error.message}`\n    throw new Error(err)\n  }\n}\n\nasync function getAsync(key) {\n  try {\n    let result = await db.get(Buffer.from(`custom_key:${key}`))\n    return result.toString()\n  } catch (error) {\n    if (error.notFound) {\n      return null\n    } else {\n      let err = `Unable to read key : ${error.message}`\n      throw new Error(err)\n    }\n  }\n}\n\nasync function deleteBatchAsync(delOps) {\n  return db.batch(delOps)\n}\n\n// #endregion GENERAL KEY - VALUE FUNCTIONS\n\n/****************************************************************************************************\n * SUPPORT FUNCTIONS\n ****************************************************************************************************/\n// #region SUPPORT FUNCTIONS\n\n// #endregion SUPPORT FUNCTIONS\n\n/****************************************************************************************************\n * SET AUTOMATIC PRUNING INTERVALS\n ****************************************************************************************************/\n// #region SET AUTOMATIC PRUNING INTERVALS\n\nfunction startPruningInterval() {\n  return setInterval(async () => {\n    if (!PRUNE_IN_PROGRESS) {\n      PRUNE_IN_PROGRESS = true\n      await pruneOldProofStateDataAsync()\n      PRUNE_IN_PROGRESS = false\n    }\n  }, PRUNE_INTERVAL_SECONDS * 1000)\n}\n\n// #endregion SET AUTOMATIC PRUNING INTERVALS\n\nmodule.exports = {\n  openConnectionAsync: openConnectionAsync,\n  getProofStatesBatchByProofIdsAsync: getProofStatesBatchByProofIdsAsync,\n  saveProofStatesBatchAsync: saveProofStatesBatchAsync,\n  queueIncomingHashObjectsAsync: queueIncomingHashObjectsAsync,\n  getIncomingHashesUpToAsync: getIncomingHashesUpToAsync,\n  setAsync: setAsync,\n  getAsync: getAsync,\n  deleteBatchAsync: deleteBatchAsync,\n  startPruningInterval: startPruningInterval,\n  pruneOldProofStateDataAsync: pruneOldProofStateDataAsync,\n  // additional functions for testing purposes\n  setENV: obj => {\n    env = obj\n  }\n}\n"
  },
  {
    "path": "lib/parse-env.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst envalid = require('envalid')\nconst ip = require('ip')\n\nfunction valCoreIPList(list) {\n  // If IP list supplied, ensure it is valid, or continue with empty string\n  if (list === '') return ''\n  let IPs = list.split(',')\n  for (let val of IPs) {\n    if ((!ip.isV4Format(val) && !ip.isV6Format(val)) || val === '')\n      throw new Error('The Core IP list contains an invalid entry')\n  }\n  // ensure each IP is unique\n  let ipSet = new Set(IPs)\n  if (ipSet.size !== IPs.length) throw new Error('The Core IP list cannot contain duplicates')\n  return IPs\n}\n\nfunction valNetwork(name) {\n  if (name === '' || name === 'mainnet') return 'mainnet'\n  if (name === 'testnet') return 'testnet'\n  throw new Error('The NETWORK value is invalid')\n}\n\nconst validateCoreIPList = envalid.makeValidator(valCoreIPList)\nconst validateNetwork = envalid.makeValidator(valNetwork)\n\nlet envDefinitions = {\n  // Chainpoint Node environment related variables\n  NODE_ENV: envalid.str({ default: 'production', desc: 'The type of environment in which the service is running' }),\n  NETWORK: validateNetwork({ default: 'mainnet', desc: `The network to use, 'mainnet' or 'testnet'` }),\n\n  LND_SOCKET: envalid.str({ default: 'lnd:10009', desc: 'Lightning GRPC host and port' }),\n\n  PUBLIC_IP: envalid.str({ default: '127.0.0.1', desc: 'IP host and port' }),\n\n  GATEWAY_NAME: envalid.str({ default: 'UNNAMED', desc: 'A, B, or C' }),\n\n  GOOGLE_UA_ID: envalid.str({ default: '', desc: 'Google Universal Analytics ID' }),\n\n  // Chainpoint Core\n  CHAINPOINT_CORE_CONNECT_IP_LIST: validateCoreIPList({\n    default: '',\n    desc: 'A comma separated list of specific Core IPs to connect to (instead of using Core discovery)'\n  }),\n\n  AGGREGATION_INTERVAL_SECONDS: envalid.num({\n    default: 60,\n    desc: `The aggregation and Core submission frequency, in seconds`\n  }),\n\n  MAX_SATOSHI_PER_HASH: envalid.num({\n    default: 10,\n    desc: `The maximum amount you are willing to spend for each hash submission to Core, in Satoshi`\n  }),\n\n  PROOF_EXPIRE_MINUTES: envalid.num({\n    default: 1440,\n    desc: `The length of time proofs as stored on the node for retrieval, in minutes`\n  }),\n\n  POST_HASHES_MAX: envalid.num({\n    default: 1000,\n    desc: `The maximum number of hashes accepted in a single submit request`\n  }),\n\n  POST_VERIFY_PROOFS_MAX: envalid.num({\n    default: 1000,\n    desc: `The maximum number of proofs accepted in a single verification request`\n  }),\n\n  GET_PROOFS_MAX: envalid.num({\n    default: 250,\n    desc: `The maximum number of proofs to be returned in a single request`\n  }),\n\n  CHANNEL_AMOUNT: envalid.num({\n    default: 120000,\n    desc: `The amount to fund a channel with`\n  }),\n\n  FUND_AMOUNT: envalid.num({\n    default: 360000,\n    desc: `The total wallet funding required`\n  }),\n\n  NO_LSAT_CORE_WHITELIST: validateCoreIPList({\n    default: '',\n    desc: 'A comma separated list of specific Core IPs to skip LSAT auth'\n  })\n}\n\nmodule.exports = {\n  env: envalid.cleanEnv(process.env, envDefinitions, { strict: false }),\n  // additional functions for testing purposes\n  valCoreIPList: valCoreIPList,\n  valNetwork: valNetwork\n}\n"
  },
  {
    "path": "lib/utils.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst jmespath = require('jmespath')\nconst _ = require('lodash')\n\nconst flattenKeys = (obj, path = []) =>\n  !_.isObject(obj)\n    ? { [path.join('.')]: obj }\n    : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {})\n\n// wait for a specified number of milliseconds to elapse\nfunction sleepAsync(ms) {\n  return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * Add specified seconds to a Date object\n *\n * @param {Date} date - The starting date\n * @param {number} seconds - The seconds of seconds to add to the date\n * @returns {Date}\n */\nfunction addSeconds(date, seconds) {\n  return new Date(date.getTime() + seconds * 1000)\n}\n\n/**\n * Add specified minutes to a Date object\n *\n * @param {Date} date - The starting date\n * @param {number} minutes - The number of minutes to add to the date\n * @returns {Date}\n */\nfunction addMinutes(date, minutes) {\n  return new Date(date.getTime() + minutes * 60000)\n}\n\n/**\n * Convert Date to ISO8601 string, stripping milliseconds\n * '2017-03-19T23:24:32Z'\n *\n * @param {Date} date - The date to convert\n * @returns {string} An ISO8601 formatted time string\n */\nfunction formatDateISO8601NoMs(date) {\n  return date.toISOString().slice(0, 19) + 'Z'\n}\n\n/**\n * Convert strings in an Array of hashes to lower case\n *\n * @param {string[]} hashes - An array of string hashes to convert to lower case\n * @returns {string[]} An array of lowercase hash strings\n */\nfunction lowerCaseHashes(hashes) {\n  return hashes.map(hash => {\n    return hash.toLowerCase()\n  })\n}\n\nfunction parseAnchorsComplete(proofObject, network) {\n  // Because the minimum proof will contain a cal anchor, always start with cal\n  let anchorsComplete = [network === 'mainnet' ? 'cal' : 'tcal'].concat(\n    jmespath.search(proofObject, '[branches[].branches[].ops[].anchors[].type] | [0]')\n  )\n  return anchorsComplete\n}\n\n/**\n * Checks if value is a hexadecimal string\n *\n * @param {string} value - The value to check\n * @returns {bool} true if value is a hexadecimal string, otherwise false\n */\nfunction isHex(value) {\n  var hexRegex = /^[0-9a-f]{2,}$/i\n  var isHex = hexRegex.test(value) && !(value.length % 2)\n  return isHex\n}\n\n/**\n * Checks if value is a uuid string\n *\n * @param {string} value - The value to check\n * @returns {bool} true if value is a hexadecimal string, otherwise false\n */\nfunction isUUID(value) {\n  var uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i\n  return uuidRegex.test(value)\n}\n\n/**\n * Checks if value is a ulid string\n *\n * @param {string} value - The value to check\n * @returns {bool} true if value is a hexadecimal string, otherwise false\n */\nfunction isULID(value) {\n  var ulidRegex = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/i\n  return ulidRegex.test(value)\n}\n\n/**\n * converts a hex value to a uuid\n *\n * @param {string} value - The value to convert\n * @returns {string} the segmented uuid string\n */\nfunction hexToUUIDv1(hexString) {\n  if (hexString.length < 32) return null\n  let segment1 = hexString.substring(0, 8)\n  let segment2 = hexString.substring(8, 12)\n  let segment3 = hexString.substring(12, 16)\n  let segment4 = hexString.substring(16, 20)\n  let segment5 = hexString.substring(20, 32)\n  return `${segment1}-${segment2}-${segment3}-${segment4}-${segment5}`\n}\n\n/**\n * Returns a random Integer between min and max\n *\n * @param {Integer} min - The min value to be returned\n * @param {Integer} max - The max value to be returned\n * @returns {Integer} The selected random Integer between min and max\n */\nfunction randomIntFromInterval(min, max) {\n  return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nfunction nodeUIPasswordBooleanCheck(pw = '') {\n  if (_.isBoolean(pw) && pw === false) {\n    return false\n  } else {\n    let password = pw.toLowerCase()\n\n    if (password === 'false') return false\n  }\n\n  return pw\n}\n\n/**\n * Extracts the IP address from a Restify request object\n *\n * @param {req} value - The Restify request object\n * @returns {string} - The IP address, or null if it cannot be determined\n */\nfunction getClientIP(req) {\n  let xff, rcr, rsa\n  try {\n    xff = req.headers['x-forwarded-for']\n  } catch (error) {\n    xff = null\n  }\n  try {\n    rcr = req.connection.remoteAddress\n  } catch (error) {\n    rcr = null\n  }\n  try {\n    rsa = req.socket.remoteAddress\n  } catch (error) {\n    rsa = null\n  }\n\n  let result = xff || rcr || rsa\n  if (result) result = result.replace('::ffff:', '')\n\n  return result || null\n}\n\nfunction jsonTransform(json, conditionFn, modifyFn) {\n  // transform { responses: { category: 'first' } } to { 'responses.category': 'first' }\n  const flattenedKeys = Object.keys(flattenKeys(json))\n\n  // Easily iterate over the flat json\n  for (let i = 0; i < flattenedKeys.length; i++) {\n    const key = flattenedKeys[i]\n    const value = _.get(json, key)\n    // Did the condition match the one we passed?\n    if (conditionFn(key, value)) {\n      // Replace the value to the new one\n      _.set(json, key, modifyFn(key, value))\n    }\n  }\n\n  return json\n}\n\nmodule.exports = {\n  sleepAsync: sleepAsync,\n  addMinutes: addMinutes,\n  addSeconds: addSeconds,\n  formatDateISO8601NoMs: formatDateISO8601NoMs,\n  lowerCaseHashes: lowerCaseHashes,\n  parseAnchorsComplete: parseAnchorsComplete,\n  isHex: isHex,\n  isUUID: isUUID,\n  isULID: isULID,\n  hexToUUIDv1: hexToUUIDv1,\n  randomIntFromInterval: randomIntFromInterval,\n  nodeUIPasswordBooleanCheck: nodeUIPasswordBooleanCheck,\n  getClientIP: getClientIP,\n  jsonTransform: jsonTransform\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chainpoint-gateway\",\n  \"description\": \"A Chainpoint Network Gateway is a key part of a scalable solution for anchoring data to public blockchains.\",\n  \"version\": \"1.2.0\",\n  \"main\": \"server.js\",\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"eslint-check\": \"eslint --print-config . | eslint-config-prettier-check\",\n    \"test\": \"mocha tests/*.js\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"linters\": {\n      \"*.js\": [\n        \"eslint --fix\",\n        \"git add\"\n      ],\n      \"*.{json,css,md}\": [\n        \"prettier --write\",\n        \"git add\"\n      ]\n    }\n  },\n  \"keywords\": [\n    \"Chainpoint\",\n    \"bitcoin\",\n    \"lightning\",\n    \"Tierion\",\n    \"node\",\n    \"hash\",\n    \"blockchain\",\n    \"crypto\",\n    \"cryptography\",\n    \"sha256\"\n  ],\n  \"author\": \"Jason Bukowski <jason@tierion.com> (https://tierion.com)\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"chai\": \"^4.2.0\",\n    \"eslint\": \"^5.4.0\",\n    \"eslint-config-prettier\": \"^3.0.1\",\n    \"eslint-plugin-prettier\": \"^2.6.2\",\n    \"eslint-plugin-react\": \"^7.11.1\",\n    \"husky\": \"^1.3.1\",\n    \"lint-staged\": \"^8.1.0\",\n    \"mocha\": \"^5.2.0\",\n    \"prettier\": \"^1.14.2\",\n    \"rimraf\": \"^2.6.3\",\n    \"supertest\": \"^3.4.2\"\n  },\n  \"dependencies\": {\n    \"async-await-parallel\": \"^1.0.0\",\n    \"async-retry\": \"^1.2.3\",\n    \"blake2s-js\": \"^1.3.0\",\n    \"bluebird\": \"^3.5.5\",\n    \"chainpoint-binary\": \"^5.1.1\",\n    \"chainpoint-parse\": \"^5.0.1\",\n    \"chalk\": \"^2.4.2\",\n    \"dotenv\": \"^8.2.0\",\n    \"envalid\": \"^4.2.0\",\n    \"executive\": \"^1.6.3\",\n    \"generate-password\": \"^1.4.2\",\n    \"id128\": \"^1.6.6\",\n    \"ip\": \"^1.1.5\",\n    \"jmespath\": \"^0.15.0\",\n    \"js-binary\": \"^1.2.0\",\n    \"level-rocksdb\": \"^4.0.0\",\n    \"lnrpc-node-client\": \"^1.1.2\",\n    \"lodash\": \"^4.17.11\",\n    \"lsat-js\": \"^2.0.0\",\n    \"merkle-tools\": \"^1.4.0\",\n    \"request\": \"^2.88.0\",\n    \"request-promise-native\": \"^1.0.7\",\n    \"restify\": \"^8.3.3\",\n    \"restify-cors-middleware\": \"^1.1.1\",\n    \"restify-errors\": \"^6.1.1\",\n    \"universal-analytics\": \"^0.4.23\",\n    \"uuid\": \"^3.3.2\",\n    \"uuid-time\": \"^1.0.0\",\n    \"uuid-validate\": \"^0.0.3\",\n    \"validator\": \"^10.11.0\",\n    \"winston\": \"^3.2.1\",\n    \"winston-papertrail\": \"^1.0.5\"\n  }\n}\n"
  },
  {
    "path": "scripts/install_deps.sh",
    "content": "#!/bin/bash\n\nif [ -x \"$(command -v docker)\" ]; then\n    echo \"Docker already installed\"\nelse\n    echo \"Install docker\"\n    curl -fsSL https://get.docker.com -o get-docker.sh\n    bash get-docker.sh\n    sudo usermod -aG docker $USER\nfi\n\nif [[ \"$OSTYPE\" == \"linux-gnu\" ]]; then\n    sudo apt-get -qq update -y\n    sudo apt-get -qq install -y apt-utils\n    sudo apt-get -qq install -y git make jq openssl\n    curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -\n    sudo apt-get install -y nodejs\n    curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -\n    echo \"deb https://dl.yarnpkg.com/debian/ stable main\" | sudo tee /etc/apt/sources.list.d/yarn.list\n    sudo apt-get update && sudo apt-get install yarn\n    sudo curl -L \"https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n    sudo chmod +x /usr/local/bin/docker-compose\n    sudo ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose || echo Binary is not at usual location or is already linked\nelif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n    ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n    brew install caskroom/cask/brew-cask\n    brew cask install docker-toolbox\n    brew install jq\n    brew install homebrew/core/make\n    brew install git\n    brew install node\n    brew install yarn\n    brew install openssl\nfi\n\nyarn"
  },
  {
    "path": "scripts/prod_secrets_expand.sh",
    "content": "#!/bin/sh\n\n: ${ENV_SECRETS_DIR:=/run/secrets}\n\nfunction env_secret_debug() {\n    if [ ! -z \"$ENV_SECRETS_DEBUG\" ]; then\n        echo -e \"\\033[1m$@\\033[0m\"\n    fi\n}\n\n# usage: env_secret_expand VAR\n#    ie: env_secret_expand 'XYZ_DB_PASSWORD'\n# (will check for \"$XYZ_DB_PASSWORD\" variable value for a placeholder that defines the\n#  name of the docker secret to use instead of the original value. For example:\n# XYZ_DB_PASSWORD=DOCKER-SECRET->my-db.secret\nenv_secret_expand() {\n    var=\"$1\"\n    eval val=\\$$var\n    if secret_name=$(expr match \"$val\" \"DOCKER-SECRET->\\([^}]\\+\\)$\"); then\n        secret=\"${ENV_SECRETS_DIR}/${secret_name}\"\n        env_secret_debug \"Secret file for $var: $secret\"\n        if [ -f \"$secret\" ]; then\n            val=$(cat \"${secret}\")\n            export \"$var\"=\"$val\"\n            env_secret_debug \"Expanded variable: $var=$val\"\n        else\n            env_secret_debug \"Secret file does not exist! $secret\"\n        fi\n    fi\n}\n\nenv_secrets_expand() {\n    for env_var in $(printenv | cut -f1 -d\"=\")\n    do\n        env_secret_expand $env_var\n    done\n\n    if [ ! -z \"$ENV_SECRETS_DEBUG\" ]; then\n        echo -e \"\\n\\033[1mExpanded environment variables\\033[0m\"\n        printenv\n    fi\n}\n\nenv_secrets_expand"
  },
  {
    "path": "scripts/run_prod.sh",
    "content": "#!/bin/bash\ncd $(dirname $0)\nsource ./prod_secrets_expand.sh\nyarn start"
  },
  {
    "path": "server.js",
    "content": "/**\n * Copyright 2019 Tierion\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *     http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// load environment variables\nconst env = require('./lib/parse-env.js').env\n\nconst apiServer = require('./lib/api-server.js')\nconst aggregator = require('./lib/aggregator.js')\nconst { version } = require('./package.json')\nconst rocksDB = require('./lib/models/RocksDB.js')\nconst cachedProofs = require('./lib/cached-proofs.js')\nconst cores = require('./lib/cores.js')\nconst logger = require('./lib/logger.js')\n\n// establish a connection with the database\nasync function openStorageConnectionAsync() {\n  await rocksDB.openConnectionAsync()\n}\n\n// process all steps need to start the application\nasync function startAsync() {\n  try {\n    logger.info(`App : Startup : Version ${version}`)\n    // display NETWORK value\n    logger.info(`App : Startup : Network : ${env.NETWORK}`)\n\n    // establish a connection with the database\n    await openStorageConnectionAsync()\n    logger.info(`App : Startup : Storage Connection Opened`)\n    \n    // connect to the Cores listed in .env and check/open lightning connections\n    await cores.connectAsync()\n    logger.info(`App : Startup : Cores Connected`)\n\n    // start API server\n    await apiServer.startAsync(cores.getLn())\n    logger.info(`App : Startup : API Started`)\n\n    // start the interval processes for refreshing the IP blocklist\n    apiServer.startIPBlacklistRefreshInterval()\n\n    // start the interval processes for aggregating and submitting hashes to Core\n    aggregator.startAggInterval()\n\n    // start the interval processes for pruning expired proof state data from RocksDB\n    rocksDB.startPruningInterval()\n\n    // start the interval processes for pruning cached proof data from memory\n    cachedProofs.startPruneExpiredItemsInterval()\n\n    // start the interval processes for pruning cached transaction data from memory\n    cores.startPruneExpiredItemsInterval()\n\n    // start monitoring health of peer/channel connections\n    cores.startConnectionMonitoringInterval()\n\n    logger.info(`App : Startup : Complete`)\n  } catch (err) {\n    logger.error(`App : Startup : ${err.message}`)\n    // Unrecoverable Error : Exit cleanly (!), so Docker Compose `on-failure` policy\n    // won't force a restart since this situation will not resolve itself.\n    process.exit(0)\n  }\n}\n\n// get the whole show started\nstartAsync()\n"
  },
  {
    "path": "swarm-compose.yaml",
    "content": "version: \"3.7\"\n\nnetworks:\n  chainpoint-gateway:\n\nsecrets:\n  HOT_WALLET_PASS:\n    external: true\n  HOT_WALLET_ADDRESS:\n    external: true\n\nservices:\n  chainpoint-gateway:\n    restart: on-failure\n    entrypoint: /home/node/app/scripts/run_prod.sh\n    volumes:\n      - ./ip-blacklist.txt:/home/node/app/ip-blacklist.txt:ro\n      - ~/.chainpoint/gateway/data/rocksdb:/root/.chainpoint/gateway/data/rocksdb\n      - ./.env:/home/node/app/.env\n      - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro\n    image: gcr.io/chainpoint-registry/github_chainpoint_chainpoint-gateway:${DOCKER_TAG:-latest}\n    user: ${USERID}:${GROUPID}\n    build: .\n    deploy:\n      mode: global\n      placement:\n        constraints: [node.role==manager]\n      restart_policy:\n        condition: any\n        delay: 5s\n        max_attempts: 15\n        window: 90s\n    depends_on:\n      - lnd\n    ports:\n      - target: 8080\n        published: 80\n        protocol: tcp\n        mode: host\n    networks:\n      - chainpoint-gateway\n    secrets:\n      - HOT_WALLET_PASS\n      - HOT_WALLET_ADDRESS\n    environment:\n      HOME: /root\n      HOT_WALLET_PASS: DOCKER-SECRET->HOT_WALLET_PASS\n      HOT_WALLET_ADDRESS: DOCKER-SECRET->HOT_WALLET_ADDRESS\n      LND_SOCKET: ${LND_SOCKET}\n      CHAINPOINT_CORE_CONNECT_IP_LIST: \"${CHAINPOINT_CORE_CONNECT_IP_LIST}\"\n      AGGREGATION_INTERVAL_SECONDS: \"${AGGREGATION_INTERVAL_SECONDS}\"\n      PROOF_EXPIRE_MINUTES: \"${PROOF_EXPIRE_MINUTES}\"\n      POST_HASHES_MAX: \"${POST_HASHES_MAX}\"\n      POST_VERIFY_PROOFS_MAX: \"${POST_VERIFY_PROOFS_MAX}\"\n      GET_PROOFS_MAX: \"${GET_PROOFS_MAX}\"\n      MAX_SATOSHI_PER_HASH: \"${MAX_SATOSHI_PER_HASH}\"\n      NETWORK: ${NETWORK}\n      NODE_ENV: ${NODE_ENV}\n      CHANNEL_AMOUNT: ${CHANNEL_AMOUNT}\n      FUND_AMOUNT: ${FUND_AMOUNT}\n      NO_LSAT_CORE_WHITELIST: ${NO_LSAT_CORE_WHITELIST}\n      GOOGLE_UA_ID: ${GOOGLE_UA_ID}\n      PUBLIC_IP: ${LND_PUBLIC_IP}\n    tty: true\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '1g'\n        max-file: '5'\n\n  # Lightning node\n  lnd:\n    image: tierion/lnd:${NETWORK:-testnet}-v0.14.1\n    user: ${USERID}:${GROUPID}\n    entrypoint: \"./start-lnd.sh\"\n    ports:\n    - target: 8080\n      published: 8080\n      protocol: tcp\n      mode: host\n    - target: 9735\n      published: 9735\n      protocol: tcp\n      mode: host\n    - target: 10009\n      published: 10009\n      protocol: tcp\n      mode: host\n    deploy:\n      restart_policy:\n        condition: any\n        delay: 5s\n        max_attempts: 15\n        window: 90s\n      endpoint_mode: dnsrr\n    environment:\n      - PUBLICIP=${LND_PUBLIC_IP}\n      - RPCUSER\n      - RPCPASS\n      - NETWORK=${NETWORK:-testnet}\n      - CHAIN\n      - DEBUG=info\n      - BACKEND=neutrino\n      - NEUTRINO=faucet.lightning.community:18333\n      - LND_REST_PORT\n      - LND_RPC_PORT\n      - TLSPATH\n      - TLSEXTRADOMAIN=lnd\n    volumes:\n      - ~/.chainpoint/gateway/.lnd:/root/.lnd:z\n    networks:\n      - chainpoint-gateway\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '1g'\n        max-file: '5'\n\n"
  },
  {
    "path": "tests/RocksDB.js",
    "content": "/* global describe, it, before, after */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst rocksDB = require('../lib/models/RocksDB.js')\nconst rmrf = require('rimraf')\nconst uuidv1 = require('uuid/v1')\nconst crypto = require('crypto')\n\nconst TEST_ROCKS_DIR = './test_db'\n\nlet insertedProofStateHashIdNodes = null\n\ndescribe('RocksDB Methods', () => {\n  let db = null\n  before(async () => {\n    db = await rocksDB.openConnectionAsync(TEST_ROCKS_DIR)\n    expect(db).to.be.a('object')\n  })\n  after(() => {\n    db.close(() => {\n      rmrf.sync(TEST_ROCKS_DIR)\n    })\n  })\n\n  describe('Proof State Functions', () => {\n    it('should return the same data that was inserted', async () => {\n      let sampleData = generateSampleProofStateData(100)\n      await rocksDB.saveProofStatesBatchAsync(sampleData.state)\n      let queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(sampleData.proofIdNodes)\n      insertedProofStateHashIdNodes = sampleData.proofIdNodes\n      queriedState = convertStateBackToBinaryForm(queriedState)\n      expect(queriedState).to.deep.equal(sampleData.state)\n    })\n  })\n\n  describe('Incoming Hash Functions', () => {\n    let delOps = []\n    it('should return the same data that was inserted', async () => {\n      let sampleData = generateSampleHashObjects(100)\n      await rocksDB.queueIncomingHashObjectsAsync(sampleData)\n      let getResults = await rocksDB.getIncomingHashesUpToAsync(Date.now)\n      let queriedHashes = getResults[0]\n      delOps = getResults[1]\n      expect(queriedHashes).to.deep.equal(sampleData)\n    })\n    after(async () => {\n      await db.batch(delOps)\n    })\n  })\n\n  describe('Generic key/value Functions', () => {\n    it('should return the same value that was inserted', async () => {\n      let keys = []\n      let values = []\n      for (let x = 0; x < 100; x++) {\n        keys.push(`testKey${x}${crypto.randomBytes(8).toString('hex')}`)\n        values.push(crypto.randomBytes(8).toString('hex'))\n      }\n      for (let x = 0; x < 100; x++) {\n        await rocksDB.setAsync(keys[x], values[x])\n      }\n\n      let getValues = []\n      for (let x = 0; x < 100; x++) {\n        getValues.push(await rocksDB.getAsync(keys[x]))\n      }\n      expect(getValues).to.deep.equal(values)\n\n      let delOps = []\n      for (let key of keys) {\n        delOps.push({ type: 'del', key: key })\n      }\n      rocksDB.deleteBatchAsync(delOps).then(async () => {\n        let getValues = []\n        for (let x = 0; x < 100; x++) {\n          getValues.push(await rocksDB.getAsync(keys[x]))\n        }\n        expect(getValues).to.deep.equal(values)\n      })\n    })\n  })\n\n  describe('Delete/Prune Functions', () => {\n    before(() => {\n      rocksDB.setENV({\n        PROOF_EXPIRE_MINUTES: 0\n      })\n    })\n    it('should batch delete as expected', async () => {\n      let keys = []\n      let values = []\n      for (let x = 0; x < 100; x++) {\n        keys.push(`testKey${x}${crypto.randomBytes(8).toString('hex')}`)\n        values.push(crypto.randomBytes(8).toString('hex'))\n      }\n      for (let x = 0; x < 100; x++) {\n        await rocksDB.setAsync(keys[x], values[x])\n      }\n      let getValues = []\n      for (let x = 0; x < 100; x++) {\n        getValues.push(await rocksDB.getAsync(keys[x]))\n      }\n      expect(getValues).to.deep.equal(values)\n\n      let delOps = []\n      for (let key of keys) {\n        delOps.push({ type: 'del', key: `custom_key:${key}` })\n      }\n      await rocksDB.deleteBatchAsync(delOps)\n      getValues = []\n      for (let x = 0; x < 100; x++) {\n        let getResult = await rocksDB.getAsync(keys[x])\n        expect(getResult).to.equal(null)\n      }\n    })\n\n    it('should initiate prune interval as expected', async () => {\n      let interval = rocksDB.startPruningInterval()\n      expect(interval).to.be.a('object')\n      clearInterval(interval)\n    })\n\n    it('should prune proof state data as expected', async () => {\n      // retrieve inserted proof state, confirm it still exists\n      let queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(insertedProofStateHashIdNodes)\n      expect(queriedState).to.be.a('array')\n      expect(queriedState.length).to.be.greaterThan(0)\n      for (let x = 0; x < queriedState.length; x++) {\n        expect(queriedState[x]).to.have.property('hash')\n        expect(queriedState[x].hash).to.be.a('string')\n      }\n\n      // prune all proof state data (0 minute expiration)\n      await rocksDB.pruneOldProofStateDataAsync()\n\n      // retrieve inserted proof state, confirm it has all beed pruned\n      queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(insertedProofStateHashIdNodes)\n      expect(queriedState).to.be.a('array')\n      expect(queriedState.length).to.be.greaterThan(0)\n      for (let x = 0; x < queriedState.length; x++) {\n        expect(queriedState[x]).to.have.property('hash')\n        expect(queriedState[x].hash).to.equal(null)\n      }\n    })\n  })\n\n  describe('Other Functions', () => {\n    it('hexToUUIDv1 should return null with invalid hex value', done => {\n      let result = rocksDB.hexToUUIDv1('deadbeefcafe')\n      expect(result).to.equal(null)\n      done()\n    })\n    it('hexToUUIDv1 should return the expected result with proper hex value', done => {\n      let result = rocksDB.hexToUUIDv1('ed60c311ede60102689f66a9e98feab6')\n      expect(result)\n        .to.be.a('string')\n        .and.to.equal('ed60c311-ede6-0102-689f-66a9e98feab6')\n      done()\n    })\n  })\n})\n\n// support functions\n\nfunction generateSampleProofStateData(batchSize) {\n  let results = {}\n  results.state = []\n  results.proofIdNodes = []\n\n  for (let x = 0; x < batchSize; x++) {\n    let newHashIdNode = uuidv1()\n    let submitId = uuidv1()\n    results.state.push({\n      proofId: newHashIdNode,\n      hash: crypto.randomBytes(32).toString('hex'),\n      proofState: [Buffer.from(Math.round(Math.random()) ? '00' : '01', 'hex'), crypto.randomBytes(32)],\n      submission: {\n        submitId: submitId,\n        cores: [\n          { ip: '65.1.12.122', proofId: uuidv1() },\n          { ip: '65.1.12.123', proofId: uuidv1() },\n          { ip: '65.1.12.124', proofId: uuidv1() }\n        ]\n      }\n    })\n    results.proofIdNodes.push(newHashIdNode)\n  }\n\n  return results\n}\n\nfunction convertStateBackToBinaryForm(queriedState) {\n  for (let stateItem of queriedState) {\n    let binState\n    for (let psItem of stateItem.proofState) {\n      binState = []\n      if (psItem.left) {\n        binState.push(Buffer.from('00', 'hex'))\n        binState.push(Buffer.from(psItem.left, 'hex'))\n      } else {\n        binState.push(Buffer.from('01', 'hex'))\n        binState.push(Buffer.from(psItem.right, 'hex'))\n      }\n    }\n    stateItem.proofState = binState\n  }\n  return queriedState\n}\n\nfunction generateSampleHashObjects(batchSize) {\n  let results = []\n\n  for (let x = 0; x < batchSize; x++) {\n    results.push({\n      proof_id: uuidv1(),\n      hash: crypto.randomBytes(32).toString('hex')\n    })\n  }\n\n  return results\n}\n"
  },
  {
    "path": "tests/aggregator.js",
    "content": "/* global describe, it, before, after */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst aggregator = require('../lib/aggregator.js')\nconst uuidv1 = require('uuid/v1')\nconst crypto = require('crypto')\nconst BLAKE2s = require('blake2s-js')\nconst MerkleTools = require('merkle-tools')\n\ndescribe('Aggregator Methods', () => {\n  describe('startAggInterval', () => {\n    it('should initiate interval as expected', async () => {\n      let interval = aggregator.startAggInterval()\n      expect(interval).to.be.a('object')\n      clearInterval(interval)\n    })\n  })\n\n  describe('aggregateSubmitAndPersistAsync with 0 hashes', () => {\n    let hashCount = 0\n    let IncomingHashes = generateIncomingHashData(hashCount)\n    let ProofStateData = []\n    before(() => {\n      aggregator.setRocksDB({\n        getIncomingHashesUpToAsync: async () => {\n          let delOps = IncomingHashes.map(item => {\n            return { type: 'del', key: item.proof_id }\n          })\n          return [IncomingHashes, delOps]\n        }\n      })\n    })\n    after(() => {})\n    it('should complete successfully', async () => {\n      expect(IncomingHashes.length).to.equal(hashCount)\n      await aggregator.aggregateSubmitAndPersistAsync()\n      expect(IncomingHashes.length).to.equal(0)\n      expect(ProofStateData.length).to.equal(hashCount)\n    })\n  })\n\n  describe('aggregateSubmitAndPersistAsync with 100 hashes', () => {\n    let hashCount = 100\n    let IncomingHashes = generateIncomingHashData(hashCount)\n    let newHashIdCore1 = null\n    let newHashIdCore2 = null\n    let ProofStateData = null\n    let ip1 = '65.21.21.122'\n    let ip2 = '65.21.21.123'\n    before(() => {\n      aggregator.setRocksDB({\n        getIncomingHashesUpToAsync: async () => {\n          let delOps = IncomingHashes.map(item => {\n            return { type: 'del', key: item.proof_id }\n          })\n          return [IncomingHashes, delOps]\n        },\n        deleteBatchAsync: async delOps => {\n          let delHashIds = delOps.map(item => item.key)\n          IncomingHashes = IncomingHashes.filter(item => !delHashIds.includes(item.proof_id))\n        },\n        saveProofStatesBatchAsync: async items => {\n          ProofStateData = items\n        }\n      })\n      aggregator.setCores({\n        submitHashAsync: async () => {\n          let hash = crypto.randomBytes(32).toString('hex')\n          newHashIdCore1 = generateBlakeEmbeddedUUID(hash)\n          newHashIdCore2 = generateBlakeEmbeddedUUID(hash)\n          return [\n            { ip: ip1, response: { hash_id: newHashIdCore1, hash: hash, processing_hints: 'hints' } },\n            { ip: ip2, response: { hash_id: newHashIdCore2, hash: hash, processing_hints: 'hints' } }\n          ]\n        }\n      })\n    })\n    after(() => {})\n    it('should complete successfully', async () => {\n      var merkleTools = new MerkleTools()\n      expect(IncomingHashes.length).to.equal(hashCount)\n      let aggRoot = await aggregator.aggregateSubmitAndPersistAsync()\n      expect(IncomingHashes.length).to.equal(0)\n      expect(ProofStateData.length).to.equal(hashCount)\n      for (let x = 0; x < hashCount; x++) {\n        expect(ProofStateData[x])\n          .to.have.property('proofId')\n          .and.and.be.a('string')\n        expect(ProofStateData[x])\n          .to.have.property('hash')\n          .and.and.be.a('string')\n        expect(ProofStateData[x])\n          .to.have.property('proofState')\n          .and.and.be.a('array')\n        // add the additional nodeId operation to get final leaf values\n        let proofIdBuffer = Buffer.from(`node_id:${ProofStateData[x].proofId}`, 'utf8')\n        let hashBuffer = Buffer.from(ProofStateData[x].hash, 'hex')\n        ProofStateData[x].hash = crypto\n          .createHash('sha256')\n          .update(Buffer.concat([proofIdBuffer, hashBuffer]))\n          .digest()\n        // convert from binary\n        let proofState = []\n        for (let y = 0; y < ProofStateData[x].proofState.length; y += 2) {\n          let operand = ProofStateData[x].proofState[y + 1].toString('hex')\n          let isLeft = ProofStateData[x].proofState[y].toString('hex') === '00'\n          let fullOp\n          if (isLeft) {\n            fullOp = { left: operand }\n          } else {\n            fullOp = { right: operand }\n          }\n          proofState.push(fullOp)\n        }\n        expect(merkleTools.validateProof(proofState, ProofStateData[x].hash, aggRoot)).to.equal(true)\n        expect(ProofStateData[x]).to.have.property('submission')\n        expect(ProofStateData[x].submission).to.be.a('object')\n        expect(ProofStateData[x].submission)\n          .to.have.property('submitId')\n          .and.and.be.a('string')\n        expect(ProofStateData[x].submission).to.have.property('cores')\n        expect(ProofStateData[x].submission.cores).to.to.a('array')\n        expect(ProofStateData[x].submission.cores.length).to.equal(2)\n        expect(ProofStateData[x].submission.cores[0]).to.be.a('object')\n        expect(ProofStateData[x].submission.cores[0])\n          .to.have.property('ip')\n          .and.and.be.a('string')\n          .and.to.equal(ip1)\n        expect(ProofStateData[x].submission.cores[0])\n          .to.have.property('proofId')\n          .and.and.be.a('string')\n          .and.to.equal(newHashIdCore1)\n        expect(ProofStateData[x].submission.cores[1]).to.be.a('object')\n        expect(ProofStateData[x].submission.cores[1])\n          .to.have.property('ip')\n          .and.and.be.a('string')\n          .and.to.equal(ip2)\n        expect(ProofStateData[x].submission.cores[1])\n          .to.have.property('proofId')\n          .and.and.be.a('string')\n          .and.to.equal(newHashIdCore2)\n      }\n    })\n  })\n})\n\n// support functions\nfunction generateIncomingHashData(batchSize) {\n  let hashes = []\n\n  for (let x = 0; x < batchSize; x++) {\n    let newHashIdNode = uuidv1()\n    hashes.push({\n      proof_id: newHashIdNode,\n      hash: crypto.randomBytes(32).toString('hex')\n    })\n  }\n\n  return hashes\n}\n\nfunction generateBlakeEmbeddedUUID(hash) {\n  let timestampDate = new Date()\n  let timestampMS = timestampDate.getTime()\n  // 5 byte length BLAKE2s hash w/ personalization\n  let h = new BLAKE2s(5, { personalization: Buffer.from('CHAINPNT') })\n  let hashStr = [timestampMS.toString(), timestampMS.toString().length, hash, hash.length].join(':')\n\n  h.update(Buffer.from(hashStr))\n\n  return uuidv1({\n    msecs: timestampMS,\n    node: Buffer.concat([Buffer.from([0x01]), h.digest()])\n  })\n}\n"
  },
  {
    "path": "tests/api-server.js",
    "content": "/* global describe, it, beforeEach, before */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst apiServer = require('../lib/api-server.js')\n\nlet rocksData = {}\nconst TOR_IPS_KEY = 'blacklist:tor:ips'\n\ndescribe('API Server Methods', () => {\n  beforeEach(() => {\n    apiServer.setRocksDB({\n      getAsync: async key => rocksData[key],\n      setAsync: async (key, value) => {\n        rocksData[key] = value.toString()\n      }\n    })\n  })\n\n  describe('startIPBlacklistRefreshInterval', () => {\n    it('should initiate interval as expected', async () => {\n      let interval = apiServer.startIPBlacklistRefreshInterval()\n      expect(interval).to.be.a('object')\n      clearInterval(interval)\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request failure, cache read failure', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        throw 'bad'\n      })\n      apiServer.setRocksDB(null)\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(0)\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request failure, empty cache', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        throw 'bad'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(0)\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request failure, cache present', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        throw 'bad'\n      })\n      rocksData[TOR_IPS_KEY] = '65.1.1.1,202.10.0.12'\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('65.1.1.1')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('202.10.0.12')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, cache write failure', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setRocksDB(null)\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, cache write success', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, no local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => false\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, empty local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => ''\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, malformatted local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => 'these arent IPs!'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, semi-malformatted local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '162.247.74.202\\ninvalid'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(3)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n      expect(ips[2])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.202')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, semi-duplicate local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '162.247.74.201\\ninvalid'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(2)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, semi-commented local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '162.247.74.204\\n#67.1.1.1'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(3)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n      expect(ips[2])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.204')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with tor request success, IPv6 local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '::ffff:172.18.0.1'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(3)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n      expect(ips[2])\n        .to.be.a('string')\n        .and.to.equal('::ffff:172.18.0.1')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with full tor request success, good local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\\nPublished 2019-02-17 23:51:28\\nLastStatus 2019-02-18 01:18:34\\nExitAddress 162.247.74.201 2019-02-18 01:22:36\\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\\nPublished 2019-02-18 14:56:19\\nLastStatus 2019-02-18 16:02:35\\nExitAddress 104.218.63.73 2019-02-18 16:07:38'\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '65.1.1.1\\n65.2.2.2\\n65.3.3.3'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(5)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('162.247.74.201')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('104.218.63.73')\n      expect(ips[2])\n        .to.be.a('string')\n        .and.to.equal('65.1.1.1')\n      expect(ips[3])\n        .to.be.a('string')\n        .and.to.equal('65.2.2.2')\n      expect(ips[4])\n        .to.be.a('string')\n        .and.to.equal('65.3.3.3')\n    })\n  })\n\n  describe('refreshIPBlacklistAsync with empty tor request success, good local list', () => {\n    before(() => {\n      apiServer.setRP(async () => {\n        return ''\n      })\n      apiServer.setFS({\n        existsSync: () => true,\n        readFileSync: () => '65.1.1.1\\n65.2.2.2\\n65.3.3.3'\n      })\n    })\n    it('should return expected value', async () => {\n      let ips = await apiServer.refreshIPBlacklistAsync()\n      expect(ips).to.be.a('array')\n      expect(ips.length).to.equal(3)\n      expect(ips[0])\n        .to.be.a('string')\n        .and.to.equal('65.1.1.1')\n      expect(ips[1])\n        .to.be.a('string')\n        .and.to.equal('65.2.2.2')\n      expect(ips[2])\n        .to.be.a('string')\n        .and.to.equal('65.3.3.3')\n    })\n  })\n})\n"
  },
  {
    "path": "tests/cached-proofs.js",
    "content": "/* global describe, it, before */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst fs = require('fs')\nconst cachedProofs = require('../lib/cached-proofs.js')\n\ndescribe('Cached Proofs Methods', () => {\n  describe('startPruneExpiredItemsInterval', () => {\n    it('should initiate interval as expected', async () => {\n      let interval = cachedProofs.startPruneExpiredItemsInterval()\n      expect(interval).to.be.a('object')\n      clearInterval(interval)\n    })\n  })\n\n  describe('getPruneExpiredIntervalSeconds', () => {\n    it('should return expected value', async () => {\n      let seconds = cachedProofs.getPruneExpiredIntervalSeconds()\n      expect(seconds)\n        .to.be.a('number')\n        .and.to.equal(10)\n    })\n  })\n\n  describe('pruneExpiredItems', () => {\n    it('should prune no entries with all new items', done => {\n      let in15Minutes = Date.now() + 15 * 60 * 1000\n      cachedProofs.setCoreProofCache({\n        '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: in15Minutes },\n        '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: in15Minutes }\n      })\n      let cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      cachedProofs.pruneExpiredItems()\n      cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      done()\n    })\n\n    it('should prune one of two entries with new and old items', done => {\n      let in15Minutes = Date.now() + 15 * 60 * 1000\n      let ago15Minutes = Date.now() - 15 * 60 * 1000\n      cachedProofs.setCoreProofCache({\n        '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: in15Minutes },\n        '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: ago15Minutes }\n      })\n      let cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      cachedProofs.pruneExpiredItems()\n      cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.not.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      done()\n    })\n\n    it('should prune all entries with old items', done => {\n      let ago15Minutes = Date.now() - 15 * 60 * 1000\n      cachedProofs.setCoreProofCache({\n        '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: ago15Minutes },\n        '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: ago15Minutes }\n      })\n      let cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      cachedProofs.pruneExpiredItems()\n      cache = cachedProofs.getCoreProofCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.not.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789')\n      expect(cache).to.not.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa')\n      done()\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with unknown hash_ids', () => {\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: () => [{ hash_id: proofId1, proof: null }, { hash_id: proofId2, proof: null }]\n      })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(results[0]).to.not.have.property('anchorsComplete')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(results[1]).to.not.have.property('anchorsComplete')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.equal(null)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 1.5 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.equal(null)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 1.1 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, cached hash_ids  - mainnet', () => {\n    let in15Minutes = Date.now() + 15 * 60 * 1000\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    let cacheContents = {\n      [submitId1]: { coreProof: proofObj1, expiresAt: in15Minutes },\n      [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes }\n    }\n    before(() => {\n      cachedProofs.setCoreProofCache(cacheContents)\n      cachedProofs.setCores({\n        getProofsAsync: () => {\n          throw 'Do not call!'\n        }\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.deep.equal(cacheContents)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, cached hash_ids  - testnet', () => {\n    let in15Minutes = Date.now() + 15 * 60 * 1000\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-tcal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-tbtc-proof.chp.json'))\n    let cacheContents = {\n      [submitId1]: { coreProof: proofObj1, expiresAt: in15Minutes },\n      [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes }\n    }\n    before(() => {\n      cachedProofs.setCoreProofCache(cacheContents)\n      cachedProofs.setCores({\n        getProofsAsync: () => {\n          throw 'Do not call!'\n        }\n      })\n      cachedProofs.setENV({ NETWORK: 'testnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('tcal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('tcal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('tbtc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.deep.equal(cacheContents)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, non-cached hash_ids', () => {\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: () => [{ hash_id: proofId1, proof: proofObj1 }, { hash_id: proofId2, proof: proofObj2 }]\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.deep.equal(proofObj1)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 16 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, cached and non-cached hash_ids, cache a null result', () => {\n    let in15Minutes = Date.now() + 15 * 60 * 100\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let proofId3 = '66bd6380-f4e7-11e7-895d-0176dc2220ff'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId3 = '77bd6380-f4e7-11e7-895d-0176dc2220ff'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let submission3 = {\n      submitId: submitId3,\n      cores: [{ ip: ip, proofId: proofId3 }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    let cacheContents = {\n      [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes }\n    }\n    before(() => {\n      cachedProofs.setCoreProofCache(cacheContents)\n      cachedProofs.setCores({\n        getProofsAsync: () => [{ hash_id: proofId1, proof: proofObj1 }, { hash_id: proofId3, proof: null }]\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2, submission3])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(3)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(results[2]).to.be.a('object')\n      expect(results[2])\n        .to.have.property('submitId')\n        .and.to.equal(submitId3)\n      expect(results[2])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.deep.equal(proofObj1)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 16 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt).to.equal(in15Minutes)\n      expect(cache).to.have.property(submitId3)\n      expect(cache[submitId3]).to.be.a('object')\n      expect(cache[submitId3]).to.have.property('coreProof')\n      expect(cache[submitId3].coreProof).to.equal(null)\n      expect(cache[submitId3]).to.have.property('expiresAt')\n      expect(cache[submitId3].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 1.1 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with mixed, cached and unknown hash_ids, cache a null result', () => {\n    let in15Minutes = Date.now() + 15 * 60 * 1000\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    let cacheContents = {\n      [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes }\n    }\n    before(() => {\n      cachedProofs.setCoreProofCache(cacheContents)\n      cachedProofs.setCores({\n        getProofsAsync: () => [{ hash_id: proofId1, proof: null }]\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(results[0]).to.not.have.property('anchorsComplete')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt).to.equal(in15Minutes)\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.equal(null)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 1.5 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with mixed, non-cached and unknown hash_ids', () => {\n    let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip = '65.1.1.1'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip, proofId: proofId1 }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip, proofId: proofId2 }]\n    }\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: () => [{ hash_id: proofId1, proof: null }, { hash_id: proofId2, proof: proofObj2 }]\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(results[0]).to.not.have.property('anchorsComplete')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.equal(null)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 1.1 * 60 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, first IP bad', () => {\n    let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId1b = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip1 = '65.1.1.1'\n    let ip2 = '65.2.2.2'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip1, proofId: proofId1a }, { ip: ip2, proofId: proofId1b }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip1, proofId: proofId2a }, { ip: ip2, proofId: proofId2b }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: ip => {\n          if (ip === ip1) throw new Error('Bad IP')\n          return [{ hash_id: proofId1b, proof: proofObj1 }, { hash_id: proofId2b, proof: proofObj2 }]\n        }\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.deep.equal(proofObj1)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 16 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, IP bad, different sub counts', () => {\n    let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip1 = '65.1.1.1'\n    let ip2 = '65.2.2.2'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip1, proofId: proofId1a }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip1, proofId: proofId2a }, { ip: ip2, proofId: proofId2b }]\n    }\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: ip => {\n          if (ip === ip1) throw new Error('Bad IP')\n          return [{ hash_id: proofId2b, proof: proofObj2 }]\n        }\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.equal(null)\n      expect(results[0]).to.not.have.property('anchorsComplete')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.not.have.property(submitId1)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000)\n    })\n  })\n\n  describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, two IPs bad, different sub counts and IPs', () => {\n    let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId1b = '66a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let proofId2c = '77bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let submitId1 = '88a34bd0-f4e7-11e7-a52b-016a36a9d789'\n    let submitId2 = '88bd6380-f4e7-11e7-895d-0176dc2220aa'\n    let ip1a = '65.1.1.1'\n    let ip1b = '65.2.2.2'\n    let ip2a = '65.3.3.3'\n    let ip2b = '65.4.4.4'\n    let ip2c = '65.5.5.5'\n    let submission1 = {\n      submitId: submitId1,\n      cores: [{ ip: ip1a, proofId: proofId1a }, { ip: ip1b, proofId: proofId1b }]\n    }\n    let submission2 = {\n      submitId: submitId2,\n      cores: [{ ip: ip2a, proofId: proofId2a }, { ip: ip2b, proofId: proofId2b }, { ip: ip2c, proofId: proofId2c }]\n    }\n    let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json'))\n    let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json'))\n    before(() => {\n      cachedProofs.setCoreProofCache({})\n      cachedProofs.setCores({\n        getProofsAsync: ip => {\n          if (ip === ip1a || ip == ip2a || ip == ip2b) throw new Error('Bad IP')\n          if (ip == ip1b) return [{ hash_id: proofId1b, proof: proofObj1 }]\n          if (ip == ip2c) return [{ hash_id: proofId2c, proof: proofObj2 }]\n        }\n      })\n      cachedProofs.setENV({ NETWORK: 'mainnet' })\n    })\n    it('should return expected value', async () => {\n      let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2])\n      let cache = cachedProofs.getCoreProofCache()\n      expect(results).to.be.a('array')\n      expect(results.length).to.equal(2)\n      expect(results[0]).to.be.a('object')\n      expect(results[0])\n        .to.have.property('submitId')\n        .and.to.equal(submitId1)\n      expect(results[0])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj1)\n      expect(results[0])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[0].anchorsComplete.length).to.equal(1)\n      expect(results[0].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1]).to.be.a('object')\n      expect(results[1])\n        .to.have.property('submitId')\n        .and.to.equal(submitId2)\n      expect(results[1])\n        .to.have.property('proof')\n        .and.to.deep.equal(proofObj2)\n      expect(results[1])\n        .to.have.property('anchorsComplete')\n        .and.to.be.a('array')\n      expect(results[1].anchorsComplete.length).to.equal(2)\n      expect(results[1].anchorsComplete[0])\n        .to.be.a('string')\n        .and.to.equal('cal')\n      expect(results[1].anchorsComplete[1])\n        .to.be.a('string')\n        .and.to.equal('btc')\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property(submitId1)\n      expect(cache[submitId1]).to.be.a('object')\n      expect(cache[submitId1]).to.have.property('coreProof')\n      expect(cache[submitId1].coreProof).to.deep.equal(proofObj1)\n      expect(cache[submitId1]).to.have.property('expiresAt')\n      expect(cache[submitId1].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 16 * 60 * 1000)\n      expect(cache).to.have.property(submitId2)\n      expect(cache[submitId2]).to.be.a('object')\n      expect(cache[submitId2]).to.have.property('coreProof')\n      expect(cache[submitId2].coreProof).to.deep.equal(proofObj2)\n      expect(cache[submitId2]).to.have.property('expiresAt')\n      expect(cache[submitId2].expiresAt)\n        .to.be.a('number')\n        .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000)\n        .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000)\n    })\n  })\n})\n"
  },
  {
    "path": "tests/calendar.js",
    "content": "/* global describe, it beforeEach, afterEach */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst request = require('supertest')\n\nconst app = require('../lib/api-server.js')\nconst calendar = require('../lib/endpoints/calendar.js')\n\ndescribe('Calendar Controller', () => {\n  let txIdKnown = '52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50'\n  let txKnownData = { tx: { data: 'data!' } }\n  let insecureServer = null\n  beforeEach(async () => {\n    insecureServer = await app.startInsecureRestifyServerAsync()\n    calendar.setCores({\n      getCachedTransactionAsync: txId => {\n        if (txId === txIdKnown) return txKnownData\n        return null\n      }\n    })\n  })\n  afterEach(() => {\n    insecureServer.close()\n  })\n\n  describe('GET /calendar/:txId/data', () => {\n    it('should return the proper error with non hex txId', done => {\n      request(insecureServer)\n        .get('/calendar/nothex/data')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, invalid txId present')\n          done()\n        })\n    })\n\n    it('should return the proper error with hex txId -- short', done => {\n      request(insecureServer)\n        .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d/data')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, invalid txId present')\n          done()\n        })\n    })\n\n    it('should return the proper error with hex txId -- long', done => {\n      request(insecureServer)\n        .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d5050/data')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, invalid txId present')\n          done()\n        })\n    })\n\n    it('should return the proper error with valid, not found', done => {\n      request(insecureServer)\n        .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d51/data')\n        .expect('Content-type', /json/)\n        .expect(404)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('NotFound')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('')\n          done()\n        })\n    })\n\n    it('should return the proper result on success', done => {\n      request(insecureServer)\n        .get('/calendar/' + txIdKnown + '/data')\n        .expect('Content-type', /text/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.text).to.equal(txKnownData.tx.data)\n          done()\n        })\n    })\n  })\n})\n"
  },
  {
    "path": "tests/config.js",
    "content": "/* global describe, it, beforeEach, afterEach */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst request = require('supertest')\n\nconst app = require('../lib/api-server.js')\n\nconst { version } = require('../package.json')\n\ndescribe('Config Controller', () => {\n  let insecureServer = null\n  beforeEach(async () => {\n    insecureServer = await app.startInsecureRestifyServerAsync()\n  })\n  afterEach(() => {\n    insecureServer.close()\n  })\n\n  describe('GET /config', () => {\n    it('should return a valid config object', done => {\n      request(insecureServer)\n        .get('/config')\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(Object.keys(res.body).length).to.equal(2)\n          expect(res.body)\n            .to.have.property('version')\n            .and.to.be.a('string')\n            .and.to.equal(version)\n          expect(res.body)\n            .to.have.property('time')\n            .and.to.be.a('string')\n          done()\n        })\n    })\n  })\n})\n"
  },
  {
    "path": "tests/cores.js",
    "content": "/* global describe, it, before, beforeEach, afterEach */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst { Lsat } = require('lsat-js')\n\nconst cores = require('../lib/cores.js')\nconst data = require('./sample-data/lsat-data.json')\nconst { version } = require('../package.json')\n\ndescribe.only('Cores Methods', function() {\n  this.timeout(5000)\n\n  describe('startPruneExpiredItemsInterval', () => {\n    it('should initiate interval as expected', async () => {\n      let interval = cores.startPruneExpiredItemsInterval()\n      expect(interval).to.be.a('object')\n      clearInterval(interval)\n    })\n  })\n\n  describe('getPruneExpiredIntervalSeconds', () => {\n    it('should return expected value', async () => {\n      let seconds = cores.getPruneExpiredIntervalSeconds()\n      expect(seconds)\n        .to.be.a('number')\n        .and.to.equal(10)\n    })\n  })\n\n  describe('pruneExpiredItems', () => {\n    it('should prune no entries with all new items', done => {\n      let in15Minutes = Date.now() + 15 * 60 * 1000\n      cores.setCoreTxCache({\n        '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: in15Minutes },\n        '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: in15Minutes }\n      })\n      let cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      cores.pruneExpiredItems()\n      cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      done()\n    })\n\n    it('should prune one of two entries with new and old items', done => {\n      let in15Minutes = Date.now() + 15 * 60 * 1000\n      let ago15Minutes = Date.now() - 15 * 60 * 1000\n      cores.setCoreTxCache({\n        '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: in15Minutes },\n        '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: ago15Minutes }\n      })\n      let cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      cores.pruneExpiredItems()\n      cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(in15Minutes)\n      expect(cache).to.not.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      done()\n    })\n\n    it('should prune all entries with old items', done => {\n      let ago15Minutes = Date.now() - 15 * 60 * 1000\n      cores.setCoreTxCache({\n        '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: ago15Minutes },\n        '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: ago15Minutes }\n      })\n      let cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'])\n        .to.be.a('object')\n        .and.to.have.property('expiresAt')\n      expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt)\n        .to.be.a('number')\n        .and.to.equal(ago15Minutes)\n      cores.pruneExpiredItems()\n      cache = cores.getCoreTxCache()\n      expect(cache).to.be.a('object')\n      expect(cache).to.not.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b')\n      expect(cache).to.not.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257')\n      done()\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' })\n      cores.setRP(async () => {\n        throw 'Bad IP'\n      })\n    })\n    it('should not connect and throw error with IP list and Bad IP', async () => {\n      let coreConnectionCount = 2\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' })\n      cores.setRP(async () => {\n        return { body: { network: 'testnet', sync_info: { catching_up: true } } }\n      })\n    })\n    it('should not connect and throw error with IP list and non-synced Core', async () => {\n      let coreConnectionCount = 2\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' })\n      cores.setRP(async () => {\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should not connect and throw error with IP list and synced Core, insufficient count', async () => {\n      let coreConnectionCount = 2\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(1)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' })\n      cores.setRP(async () => {\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should connect with IP list and synced Core, sufficient count 1', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err\n      }\n      expect(errResult).to.equal(null)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(1)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({\n        CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.2.2.2', '65.3.3.3'],\n        NETWORK: 'testnet'\n      })\n      let counter = 1\n      cores.setRP(async () => {\n        return {\n          body: { network: 'testnet', sync_info: { catching_up: counter++ % 2 ? false : true } }\n        }\n      })\n    })\n    it('should connect with IP list and mixed-synced Core, sufficient count 2', async () => {\n      let coreConnectionCount = 2\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err\n      }\n      expect(errResult).to.equal(null)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(2)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({\n        CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.2.2.2', '65.3.3.3'],\n        NETWORK: 'testnet'\n      })\n      cores.setRP(async () => {\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should connect with IP list and synced Core, sufficient count 3', async () => {\n      let coreConnectionCount = 3\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err\n      }\n      expect(errResult).to.equal(null)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(3)\n    })\n  })\n\n  describe('connectAsync', () => {\n    let options = null\n    before(() => {\n      cores.setENV({\n        CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'],\n        NETWORK: 'testnet'\n      })\n      cores.setRP(async o => {\n        options = o\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should use proper headers on Core requests', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      await cores.connectAsync()\n      expect(options).to.be.a('object')\n      expect(options).to.have.property('headers')\n      expect(options.headers).to.have.property('X-Node-Version')\n      expect(options.headers['X-Node-Version']).to.equal(version)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setRP(async () => {\n        throw 'Bad IP'\n      })\n    })\n    it('should not connect and throw error with Core discovery and bad discovery', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setRP(async opts => {\n        if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] }\n        throw 'Bad IP'\n      })\n    })\n    it('should not connect and throw error with Core discovery and bad IP returned', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setRP(async opts => {\n        if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] }\n        return { body: { sync_info: { catching_up: true } } }\n      })\n    })\n    it('should not connect and throw error with Core discovery and unsynced returned', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err.message\n      }\n      expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ NETWORK: 'mainnet' })\n      cores.setRP(async opts => {\n        if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] }\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should not connect with Core discovery and synced IP, network mismatch', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err\n      }\n      expect(errResult.message).to.equal('Unable to connect to 1 Core(s) as required')\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(0)\n    })\n  })\n\n  describe('connectAsync', () => {\n    before(() => {\n      cores.setENV({ NETWORK: 'testnet' })\n      cores.setRP(async opts => {\n        if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] }\n        return { body: { network: 'testnet', sync_info: { catching_up: false } } }\n      })\n    })\n    it('should connect with Core discovery and synced IP', async () => {\n      let coreConnectionCount = 1\n      cores.setCoreConnectionCount(coreConnectionCount)\n      let errResult = null\n      try {\n        await cores.connectAsync()\n      } catch (err) {\n        errResult = err\n      }\n      expect(errResult).to.equal(null)\n      let connectedIPs = cores.getCoreConnectedIPs()\n      expect(connectedIPs.length).to.equal(1)\n    })\n  })\n\n  describe('parse402Response', () => {\n    let lsat, challenge, response\n    before(() => {\n      challenge = data.challenge1000\n      lsat = Lsat.fromChallenge(challenge)\n      response = {\n        statusCode: 402,\n        headers: {\n          'www-authenticate': challenge\n        }\n      }\n    })\n\n    it('should throw if no LSAT challenge present in response or not a 402', () => {\n      const parseWrongStatusCode = () => cores.parse402Response({ ...response, statusCode: 401 })\n      const parseMissingHeader = () => cores.parse402Response({ statusCode: 402 })\n      expect(parseWrongStatusCode).to.throw()\n      expect(parseMissingHeader).to.throw()\n    })\n\n    it('should should return an LSAT with invoice information', () => {\n      const lsatFromResponse = cores.parse402Response(response)\n      expect(lsatFromResponse.invoice).to.exist\n      expect(lsatFromResponse.invoice).to.equal(lsat.invoice)\n    })\n  })\n\n  describe('submitHashAsync', () => {\n    let challengeResponse, env, coreList\n\n    beforeEach(() => {\n      coreList = ['65.1.1.1', '65.2.2.2', '65.3.3.3']\n      env = { MAX_SATOSHI_PER_HASH: 10, CHAINPOINT_CORE_CONNECT_IP_LIST: [coreList[0]] }\n      cores.setENV(env)\n      cores.setLN({\n        callMethodAsync: async (s, m) => {\n          if (m === 'sendPayment') return { on: (n, func) => func('ok'), end: () => null, write: () => {} }\n          return {}\n        }\n      })\n      challengeResponse = {\n        statusCode: 402,\n        response: {\n          statusCode: 402,\n          headers: {\n            'www-authenticate': data.challenge10\n          },\n          body: {\n            error: {\n              message: 'Payment Required.'\n            }\n          }\n        }\n      }\n    })\n\n    afterEach(() => {\n      cores.setENV({})\n      cores.setLN({})\n      cores.setRP(() => {})\n    })\n\n    it('should return [] on 1 of 1 invoice amount to high failure', async () => {\n      cores.setENV({ ...env, MAX_SATOSHI_PER_HASH: 5 })\n      cores.setRP(async () => {\n        throw challengeResponse\n      })\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(0)\n    })\n\n    it('should return [] on 1 of 1 submit failure', async () => {\n      let counter = 0\n      cores.setRP(async () => {\n        if (++counter === 1) throw 'Bad Submit'\n        throw challengeResponse\n      })\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(0)\n    })\n\n    it('should succeed on 1 of 1 item submitted', async () => {\n      cores.setRP(options => {\n        if (options.headers['Authorization']) return { body: 'ok' }\n        throw challengeResponse\n      })\n\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(1)\n      expect(result[0]).to.be.a('object')\n      expect(result[0]).to.have.property('ip')\n      expect(result[0].ip).to.equal('65.1.1.1')\n      expect(result[0]).to.have.property('response')\n      expect(result[0].response).to.equal('ok')\n    })\n\n    it('should succeed on 2 of 3 item submitted, one bad IP', async () => {\n      cores.setRP(async options => {\n        if (options.uri.includes(coreList[1])) throw 'Bad IP!'\n        if (options.headers['Authorization']) return { body: 'ok' }\n        throw challengeResponse\n      })\n      cores.setENV({\n        ...env,\n        CHAINPOINT_CORE_CONNECT_IP_LIST: coreList\n      })\n\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(2)\n      expect(result[0]).to.be.a('object')\n      expect(result[0]).to.have.property('ip')\n      expect(result[0].ip).to.equal(coreList[0])\n      expect(result[0]).to.have.property('response')\n      expect(result[0].response).to.equal('ok')\n      expect(result[1]).to.be.a('object')\n      expect(result[1]).to.have.property('ip')\n      expect(result[1].ip).to.equal(coreList[2])\n      expect(result[1]).to.have.property('response')\n      expect(result[1].response).to.equal('ok')\n    })\n\n    it('should succeed on 2 of 3 item submitted, one invoice amount too high', async () => {\n      cores.setENV({\n        ...env,\n        CHAINPOINT_CORE_CONNECT_IP_LIST: coreList\n      })\n      cores.setRP(async options => {\n        if (options.uri.includes(coreList[1])) {\n          let response = {\n            statusCode: 402,\n            response: {\n              statusCode: 402,\n              headers: {\n                'www-authenticate': data.challenge1000\n              },\n              body: {\n                error: {\n                  message: 'Payment Required.'\n                }\n              }\n            }\n          }\n          throw response\n        }\n        if (options.headers['Authorization']) return { body: 'ok' }\n        throw challengeResponse\n      })\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(2)\n      expect(result[0]).to.be.a('object')\n      expect(result[0]).to.have.property('ip')\n      expect(result[0].ip).to.equal(coreList[0])\n      expect(result[0]).to.have.property('response')\n      expect(result[0].response).to.equal('ok')\n      expect(result[1]).to.be.a('object')\n      expect(result[1]).to.have.property('ip')\n      expect(result[1].ip).to.equal(coreList[2])\n      expect(result[1]).to.have.property('response')\n      expect(result[1].response).to.equal('ok')\n    })\n\n    it('should succeed on 3 of 3 item submitted', async () => {\n      cores.setENV({\n        ...env,\n        CHAINPOINT_CORE_CONNECT_IP_LIST: coreList\n      })\n      cores.setRP(async options => {\n        if (options.headers['Authorization']) return { body: 'ok' }\n        throw challengeResponse\n      })\n      let result = await cores.submitHashAsync('deadbeefcafe')\n      expect(result).to.be.a('array')\n      expect(result.length).to.equal(3)\n      expect(result[0]).to.be.a('object')\n      expect(result[0]).to.have.property('ip')\n      expect(result[0].ip).to.equal('65.1.1.1')\n      expect(result[0]).to.have.property('response')\n      expect(result[0].response).to.equal('ok')\n      expect(result[1]).to.be.a('object')\n      expect(result[1]).to.have.property('ip')\n      expect(result[1].ip).to.equal('65.2.2.2')\n      expect(result[1]).to.have.property('response')\n      expect(result[1].response).to.equal('ok')\n      expect(result[2]).to.be.a('object')\n      expect(result[2]).to.have.property('ip')\n      expect(result[2].ip).to.equal('65.3.3.3')\n      expect(result[2]).to.have.property('response')\n      expect(result[2].response).to.equal('ok')\n    })\n  })\n\n  describe('getProofsAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        throw { message: 'Bad IP!!!!!', statusCode: 500 }\n      })\n    })\n    it('should throw error with status code', async () => {\n      let errResponse = null\n      try {\n        await cores.getProofsAsync('', [])\n      } catch (err) {\n        errResponse = err\n      }\n      expect(errResponse.message).to.equal('Invalid response on GET proof : 500 : Bad IP!!!!!')\n    })\n  })\n\n  describe('getProofsAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        throw 'Error!'\n      })\n    })\n    it('should throw error no status code', async () => {\n      let errResponse = null\n      try {\n        await cores.getProofsAsync('', [])\n      } catch (err) {\n        errResponse = err\n      }\n      expect(errResponse.message).to.equal('Invalid response received on GET proof : Error!')\n    })\n  })\n\n  describe('getProofsAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        return { body: 'ok' }\n      })\n    })\n    it('should return success', async () => {\n      let response = await cores.getProofsAsync('', [])\n      expect(response).to.equal('ok')\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        throw { message: 'Bad IP!!!!!', statusCode: 500 }\n      })\n    })\n    it('should throw error with status code', async () => {\n      let errResponse = null\n      try {\n        await cores.getLatestCalBlockInfoAsync()\n      } catch (err) {\n        errResponse = err\n      }\n      expect(errResponse.message).to.equal('Invalid response on GET status : 500')\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        throw 'Error!'\n      })\n    })\n    it('should throw error no status code', async () => {\n      let errResponse = null\n      try {\n        await cores.getLatestCalBlockInfoAsync()\n      } catch (err) {\n        errResponse = err\n      }\n      expect(errResponse.message).to.equal('Invalid response received on GET status')\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        return { body: { sync_info: { catching_up: false } } }\n      })\n    })\n    it('should return success with one good IP', async () => {\n      let response = await cores.getLatestCalBlockInfoAsync()\n      expect(response).to.be.a('object')\n      expect(response).to.have.property('catching_up')\n      expect(response.catching_up).to.equal(false)\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    let attempts = 0\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.1.1.2'] })\n      cores.setRP(async () => {\n        attempts++\n        if (attempts > 1) return { body: { sync_info: { catching_up: false } } }\n        throw 'Error!'\n      })\n    })\n    it('should return success with one bad and one good IP', async () => {\n      let response = await cores.getLatestCalBlockInfoAsync()\n      expect(response).to.be.a('object')\n      expect(response).to.have.property('catching_up')\n      expect(response.catching_up).to.equal(false)\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setRP(async () => {\n        return { body: { catching_up: true } }\n      })\n    })\n    it('should throw error no status code when not synced', async () => {\n      let errResponse = null\n      try {\n        await cores.getLatestCalBlockInfoAsync()\n      } catch (err) {\n        errResponse = err\n      }\n      expect(errResponse.message).to.equal('Invalid response received on GET status')\n    })\n  })\n\n  describe('getLatestCalBlockInfoAsync', () => {\n    let attempts = 0\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.1.1.2'] })\n      cores.setRP(async () => {\n        attempts++\n        if (attempts > 1) return { body: { sync_info: { catching_up: false } } }\n        return { body: { sync_info: { catching_up: false } } }\n      })\n    })\n    it('should return success with one unsynced and one good IP', async () => {\n      let response = await cores.getLatestCalBlockInfoAsync()\n      expect(response).to.be.a('object')\n      expect(response).to.have.property('catching_up')\n      expect(response.catching_up).to.equal(false)\n    })\n  })\n\n  describe('getCachedTransactionAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setCoreTxCache({ a: { transaction: '1' } })\n      cores.setRP(async () => {\n        throw 'Dont call!'\n      })\n    })\n    it('should return value from cache', async () => {\n      let response = await cores.getCachedTransactionAsync('a')\n      expect(response).to.equal('1')\n    })\n  })\n\n  describe('getCachedTransactionAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setCoreTxCache({})\n      cores.setRP(async () => {\n        throw 'Bad IP!'\n      })\n    })\n    it('should return null from bad IPs, no cache', async () => {\n      let response = await cores.getCachedTransactionAsync('a')\n      let cacheResult = cores.getCoreTxCache()\n      expect(response).to.equal(null)\n      expect(cacheResult).to.deep.equal({})\n    })\n  })\n\n  describe('getCachedTransactionAsync', () => {\n    before(() => {\n      cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] })\n      cores.setCoreTxCache({})\n      cores.setRP(async () => {\n        return { body: 'result' }\n      })\n    })\n    it('should return new tx and add to cache', async () => {\n      let response = await cores.getCachedTransactionAsync('a')\n      let cacheResult = cores.getCoreTxCache()\n      expect(response).to.equal('result')\n      expect(cacheResult).to.be.a('object')\n      expect(cacheResult).to.have.property('a')\n      expect(cacheResult.a).to.be.a('object')\n      expect(cacheResult.a).to.have.property('transaction')\n      expect(cacheResult.a.transaction).to.equal('result')\n      expect(cacheResult.a).to.have.property('expiresAt')\n      expect(cacheResult.a.expiresAt).to.be.a('number')\n    })\n  })\n})\n"
  },
  {
    "path": "tests/hashes.js",
    "content": "/* global describe, it beforeEach, afterEach, before, after */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst request = require('supertest')\n\nconst app = require('../lib/api-server.js')\nconst hashes = require('../lib/endpoints/hashes.js')\n\ndescribe('Hashes Controller', () => {\n  let insecureServer = null\n  beforeEach(async () => {\n    insecureServer = await app.startInsecureRestifyServerAsync()\n    hashes.setRocksDB({\n      queueIncomingHashObjectsAsync: async () => {}\n    })\n    hashes.setENV({ POST_HASHES_MAX: 1, AGGREGATION_INTERVAL_SECONDS: 60 })\n  })\n  afterEach(() => {\n    insecureServer.close()\n  })\n\n  describe('POST /hashes', () => {\n    before(() => {\n      app.setAcceptingHashes(false)\n    })\n    after(() => {\n      app.setAcceptingHashes(true)\n    })\n    it('should return the proper error when not accepting hashes', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'text/plain')\n        .expect('Content-type', /json/)\n        .expect(503)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('ServiceUnavailable')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('Service is not currently accepting hashes')\n          done()\n        })\n    })\n  })\n\n  describe('POST /hashes', () => {\n    it('should return the proper error with bad content type', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'text/plain')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid content type')\n          done()\n        })\n    })\n\n    it('should return the proper error with missing hashes property', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, missing hashes')\n          done()\n        })\n    })\n\n    it('should return the proper error with hashes not an array', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .send({ hashes: 'notarray' })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, hashes is not an Array')\n          done()\n        })\n    })\n\n    it('should return the proper error with empty hashes array', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .send({ hashes: [] })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid JSON body, hashes Array is empty')\n          done()\n        })\n    })\n\n    it('should return the proper error with max hashes exceeded', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .send({ hashes: ['a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1', 'b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1'] })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal(`invalid JSON body, hashes Array max size of 1 exceeded`)\n          done()\n        })\n    })\n\n    it('should return the proper error with invalid hashes', done => {\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .send({ hashes: ['invalid'] })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal(`invalid JSON body, invalid hashes present`)\n          done()\n        })\n    })\n\n    it('should return the proper result on success', done => {\n      let hash = 'a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'\n      request(insecureServer)\n        .post('/hashes')\n        .set('Content-type', 'application/json')\n        .send({ hashes: [hash] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body).to.have.property('meta')\n          expect(res.body.meta)\n            .to.have.property('submitted_at')\n            .and.to.be.a('string')\n          expect(res.body.meta).to.have.property('processing_hints')\n          expect(res.body.meta.processing_hints)\n            .to.have.property('cal')\n            .and.to.be.a('string')\n          expect(res.body.meta.processing_hints)\n            .to.have.property('btc')\n            .and.to.be.a('string')\n          expect(res.body)\n            .to.have.property('hashes')\n            .and.to.be.a('array')\n          expect(res.body.hashes).to.have.length(1)\n          expect(Object.keys(res.body.hashes[0]).length).to.equal(2)\n          expect(res.body.hashes[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n          expect(res.body.hashes[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(hash)\n          done()\n        })\n    })\n  })\n})\n"
  },
  {
    "path": "tests/parse-env.js",
    "content": "/* global describe, it */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst parseEnv = require('../lib/parse-env.js')\n\ndescribe('Environment variables', () => {\n  describe('valBase64', () => {\n    it('should throw error with number', done => {\n      expect(() => {\n        parseEnv.valBase64(234)\n      }).to.throw('Expected string but received a number')\n      done()\n    })\n    it('should throw error with boolean', done => {\n      expect(() => {\n        parseEnv.valBase64(true)\n      }).to.throw('Expected string but received a boolean')\n      done()\n    })\n    it('should return error on empty string', done => {\n      expect(() => {\n        parseEnv.valBase64('')\n      }).to.throw('The supplied value must be a valid Base 64 encoded string')\n      done()\n    })\n    it('should return error on non Base 64 encoded string', done => {\n      expect(() => {\n        parseEnv.valBase64('Not base 64 encoded')\n      }).to.throw('The supplied value must be a valid Base 64 encoded string')\n      done()\n    })\n    it('should return value with proper base 64 encoded string', done => {\n      let result = parseEnv.valBase64('cXdlCg==')\n      expect(result).to.equal('cXdlCg==')\n      done()\n    })\n  })\n\n  describe('valSocket', () => {\n    it('should throw error with number', done => {\n      expect(() => {\n        parseEnv.valSocket(234)\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should throw error with boolean', done => {\n      expect(() => {\n        parseEnv.valSocket(true)\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on empty string', done => {\n      expect(() => {\n        parseEnv.valSocket('')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on single segment string', done => {\n      expect(() => {\n        parseEnv.valSocket('127.0.0.1')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on 3+ segment string', done => {\n      expect(() => {\n        parseEnv.valSocket('127.0.0.1:2342:sdfs')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on bad host', done => {\n      expect(() => {\n        parseEnv.valSocket('badhost:2342')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on bad port string', done => {\n      expect(() => {\n        parseEnv.valSocket('goodhost.com:badport')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return error on invalid port number', done => {\n      expect(() => {\n        parseEnv.valSocket('goodhost.com:345345345345')\n      }).to.throw('The supplied value must be a valid <host>:<port> string')\n      done()\n    })\n    it('should return value with host:port string', done => {\n      let result = parseEnv.valSocket('goodhost.com:10009')\n      expect(result).to.equal('goodhost.com:10009')\n      done()\n    })\n  })\n\n  describe('valCoreIPList', () => {\n    it('should return success with empty string', done => {\n      let result = parseEnv.valCoreIPList('')\n      expect(result).to.equal('')\n      done()\n    })\n    it('should throw error with bad single IP', done => {\n      expect(() => {\n        parseEnv.valCoreIPList('234234.234234.234234.23434')\n      }).to.throw('The Core IP list contains an invalid entry')\n      done()\n    })\n    it('should return true with valid v4 IP', done => {\n      let result = parseEnv.valCoreIPList('65.1.1.1')\n      expect(result).to.deep.equal(['65.1.1.1'])\n      done()\n    })\n    it('should return success with valid v6 IP', done => {\n      let result = parseEnv.valCoreIPList('FE80:0000:0000:0000:0202:B3FF:FE1E:8329')\n      expect(result).to.deep.equal(['FE80:0000:0000:0000:0202:B3FF:FE1E:8329'])\n      done()\n    })\n    it('should return success with valid collapsed v6 IP', done => {\n      let result = parseEnv.valCoreIPList('FE80::0202:B3FF:FE1E:8329')\n      expect(result).to.deep.equal(['FE80::0202:B3FF:FE1E:8329'])\n      done()\n    })\n    it('should return success with hybrid v6 IP', done => {\n      let result = parseEnv.valCoreIPList('::ffff:65.1.1.1')\n      expect(result).to.deep.equal(['::ffff:65.1.1.1'])\n      done()\n    })\n    it('should throw error with bad IP in group', done => {\n      expect(() => {\n        parseEnv.valCoreIPList('65.1.1.1,10.165.32.31,234234.234234.234234.23434')\n      }).to.throw('The Core IP list contains an invalid entry')\n      done()\n    })\n    it('should throw error with missing IP in group', done => {\n      expect(() => {\n        parseEnv.valCoreIPList('65.1.1.1,,10.165.32.31')\n      }).to.throw('The Core IP list contains an invalid entry')\n      done()\n    })\n    it('should throw error with duplicate IP in group', done => {\n      expect(() => {\n        parseEnv.valCoreIPList('65.1.1.1,65.1.1.1,10.165.32.31')\n      }).to.throw('The Core IP list cannot contain duplicates')\n      done()\n    })\n    it('should return success with valid IP list', done => {\n      let result = parseEnv.valCoreIPList('65.1.1.1,FE80::0202:B3FF:FE1E:8329,10.165.32.31')\n      expect(result).to.deep.equal(['65.1.1.1', 'FE80::0202:B3FF:FE1E:8329', '10.165.32.31'])\n      done()\n    })\n  })\n\n  describe('valNetwork', () => {\n    it('should throw error with number', done => {\n      expect(() => {\n        parseEnv.valNetwork(234)\n      }).to.throw('The NETWORK value is invalid')\n      done()\n    })\n    it('should throw error with boolean', done => {\n      expect(() => {\n        parseEnv.valNetwork(true)\n      }).to.throw('The NETWORK value is invalid')\n      done()\n    })\n    it('should return mainnet on empty', done => {\n      let result = parseEnv.valNetwork('')\n      expect(result).to.equal('mainnet')\n      done()\n    })\n    it('should return mainnet on mainnet', done => {\n      let result = parseEnv.valNetwork('mainnet')\n      expect(result).to.equal('mainnet')\n      done()\n    })\n    it('should return testnet on testnet', done => {\n      let result = parseEnv.valNetwork('testnet')\n      expect(result).to.equal('testnet')\n      done()\n    })\n  })\n})\n"
  },
  {
    "path": "tests/proofs.js",
    "content": "/* global describe, it beforeEach, afterEach */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst request = require('supertest')\nconst fs = require('fs')\n\nconst app = require('../lib/api-server.js')\nconst proofs = require('../lib/endpoints/proofs.js')\n\ndescribe('Proofs Controller', () => {\n  let insecureServer = null\n  beforeEach(async () => {\n    insecureServer = await app.startInsecureRestifyServerAsync()\n    proofs.setRocksDB({\n      getProofStatesBatchByProofIdsAsync: async proofIds => {\n        switch (proofIds[0]) {\n          case 'bbb27662-2e21-11e9-b210-d663bd873d93':\n            return [\n              {\n                proofId: proofIds[0],\n                hash: '18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784',\n                proofState: [],\n                submission: {\n                  submitId: 'e4c59c50-37cd-11e9-b270-d778f1c6df42',\n                  cores: [{ ip: '65.1.1.1', proofId: '000139a0-2e5c-11e9-bec9-01115ea738e6' }]\n                }\n              }\n            ]\n          default:\n            return [\n              {\n                proofId: proofIds[0],\n                hash: null,\n                proofState: null,\n                submission: null\n              }\n            ]\n        }\n      }\n    })\n    proofs.setCachedProofs({\n      getCachedCoreProofsAsync: async submissionData => {\n        if (submissionData.length === 0) return []\n        switch (submissionData[0].submitId) {\n          case 'e4c59c50-37cd-11e9-b270-d778f1c6df42': {\n            let proofJSON = fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')\n            return [\n              {\n                submitId: submissionData[0].submitId,\n                proof: JSON.parse(proofJSON),\n                anchorsComplete: ['cal', 'btc']\n              }\n            ]\n          }\n          default:\n            return []\n        }\n      }\n    })\n    proofs.setENV({ GET_PROOFS_MAX: 1 })\n  })\n  afterEach(() => {\n    insecureServer.close()\n  })\n\n  describe('GET /proofs', () => {\n    it('should return the proper error with bad hash_id in uri', done => {\n      request(insecureServer)\n        .get('/proofs/badproofid')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid request, bad hash_id')\n          done()\n        })\n    })\n\n    it('should return the proper error with no hash ids', done => {\n      request(insecureServer)\n        .get('/proofs')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid request, at least one hash id required')\n          done()\n        })\n    })\n\n    it('should return the proper error with too many hash_ids', done => {\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', 'a3127662-2e21-11e9-b210-d663bd873d93,a3127662-2e21-11e9-b210-d663bd873d99')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid request, too many hash ids (1 max)')\n          done()\n        })\n    })\n\n    it('should return the proper error with invalid hash_id in header', done => {\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', 'invalid')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('invalid request, bad hash_id')\n          done()\n        })\n    })\n\n    it('should return the proper empty result with unknown hash_id', done => {\n      let proofId = 'a3127662-2e21-11e9-b210-d663bd873d93'\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', proofId)\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body).to.be.a('array')\n          expect(res.body).to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(proofId)\n          expect(res.body[0])\n            .to.have.property('proof')\n            .and.to.equal(null)\n          expect(res.body[0])\n            .to.have.property('anchors_complete')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors_complete).to.have.length(0)\n          done()\n        })\n    })\n\n    it('should return successfully with a base64 proof with no Accept setting', done => {\n      let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93'\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', proofId)\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body).to.be.a('array')\n          expect(res.body).to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(proofId)\n          expect(res.body[0])\n            .to.have.property('proof')\n            .and.to.be.a('string')\n          expect(res.body[0])\n            .to.have.property('anchors_complete')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors_complete).to.have.length(2)\n          expect(res.body[0].anchors_complete[0])\n            .to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors_complete[1])\n            .to.be.a('string')\n            .and.to.equal('btc')\n          done()\n        })\n    })\n\n    it('should return successfully with a base64 proof with Accept Base64 setting', done => {\n      let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93'\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', proofId)\n        .set('Accept', 'application/vnd.chainpoint.json+base64')\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body).to.be.a('array')\n          expect(res.body).to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(proofId)\n          expect(res.body[0])\n            .to.have.property('proof')\n            .and.to.be.a('string')\n          expect(res.body[0])\n            .to.have.property('anchors_complete')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors_complete).to.have.length(2)\n          expect(res.body[0].anchors_complete[0])\n            .to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors_complete[1])\n            .to.be.a('string')\n            .and.to.equal('btc')\n          done()\n        })\n    })\n\n    it('should return successfully with a JSON proof with Accept JSON setting', done => {\n      let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93'\n      request(insecureServer)\n        .get('/proofs')\n        .set('proofids', proofId)\n        .set('Accept', 'application/vnd.chainpoint.ld+json')\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body).to.be.a('array')\n          expect(res.body).to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(proofId)\n          expect(res.body[0]).to.have.property('proof')\n          expect(res.body[0].proof)\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal('18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784')\n          expect(res.body[0].proof)\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(proofId)\n          expect(res.body[0].proof)\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal('000139a0-2e5c-11e9-bec9-01115ea738e6')\n          expect(res.body[0])\n            .to.have.property('anchors_complete')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors_complete).to.have.length(2)\n          expect(res.body[0].anchors_complete[0])\n            .to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors_complete[1])\n            .to.be.a('string')\n            .and.to.equal('btc')\n          done()\n        })\n    })\n  })\n})\n"
  },
  {
    "path": "tests/sample-data/btc-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"cal\",\n              \"anchor_id\": \"9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d\",\n              \"uris\": [\n                \"https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"branches\": [\n        {\n          \"label\": \"btc_anchor_branch\",\n          \"ops\": [\n            {\n              \"l\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20\"\n            },\n            {\n              \"r\": \"ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"anchors\": [\n                {\n                  \"type\": \"btc\",\n                  \"anchor_id\": \"549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e\",\n                  \"uris\": [\n                    \"https://a.chainpoint.org/calendar/549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e/data\"\n                  ]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/cal-proof-l.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"cal\",\n              \"anchor_id\": \"985635\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/985635/hash\"]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/cal-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"cal\",\n              \"anchor_id\": \"9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d\",\n              \"uris\": [\n                \"https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data\"\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/core-btc-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784\",\n  \"proof_id\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_node_at\": \"2019-02-12T00:20:28Z\",\n  \"hash_id_core\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_core_at\": \"2019-02-12T00:20:28Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"core_id:000139a0-2e5c-11e9-bec9-01115ea738e6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"2697833:1549930833:1:https://a.chainpoint.org:cal:2697833\"\n        },\n        {\n          \"r\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"cal\",\n              \"anchor_id\": \"2697833\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/2697833/hash\"]\n            }\n          ]\n        }\n      ],\n      \"branches\": [\n        {\n          \"label\": \"btc_anchor_branch\",\n          \"ops\": [\n            {\n              \"l\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"4b414a5f4e67f4255027e37e9889b6a1f7f94b9285dfe6c45dc9cc70ea83238b\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"19e96821c648366829f9b4656f439497bf8109a59bd211f7dea5f7bc38faca52\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"3a22c564ca8be07c0d51a67874e5ef104ca37dc255b9c5e8043beecd2137343a\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"56bb7aa1d380608cfc7328310020d79d90dadf8c8b93a4dc0c887ef080b30208\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"2d52f99e44e3a6aed97e7a4c2b2580094f3df598e71ad60ce3e4c80b83289fc0\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"2e1e454f149402f668e940b095e01303f2976b0c851de896c54d77d1b0ab7e76\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"62726a2392223ad6793414fca1c87e2932421315690f4ebb9094243b4d75fc65\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"0659c270d03e0ed74b183966f7a8ce35b31b51b861cb44a2d8a9e4c2144d7b11\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"0100000001f74c607aec4e8552e97f5c6705331bd40971fee4615463e44dc237909eda8561010000006b4830450221009dea9d7cfef167627ae1d9fe5fae19ea23c4d262d35c951d15f282089a25f71502204e8ab4c0778e206e06a9512c2fbfa5092d5dbfe79394b6bd7523e6b17482b6b10121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20\"\n            },\n            {\n              \"r\": \"cb1ee800000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"6530dedcdef49f2ba8ffad45cc2a3da9163c545cc25293fbdd38243cff32c856\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"3eb37791f8e63616aacc0c0a305bcb527696e7bac90f2f7e503b506a4c43faa6\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"5c32a9aa6c2d51cdf40a6abb84e310a915710a2eb68505f01e50f5f1a7f25d1b\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"52d7ead6f09051315f6508b5691fa6213643510ced81066e3ca6717151ebc6d4\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"2007ea7ac74528f25264e50f5ba848675c7a2f28e7dd196e33874a50c05107b5\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"755908c4d69007fc1f0df3e6211d1041850a81afe32b262f9a9a7ed9c2abf0a4\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"e75bb3c9dccd54b626579f08d1e517b458716cc308a6257a7fbe2697c749bc96\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"98e2e5e17003189b4ef1a1fdf8de94af1a0bbb7b9e9d6310ad953719385f4a1e\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a7af3e0d38da4c2b3bc958c63176d349a53379f1467c4be65a86ef5e3f91747b\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"a2a69c02444bc5f396d87537be7e15ee29dada648c95f9ce731a9d0db285dc91\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"1b3c4074444b6926f5ac2fc5c9d4a7cb91bdb6dcd90356abf2c3fda5d20b655a\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"25bfc69c9c2a6026eaef5b528c9b354dfaf6f313f614dd41f88d8d4f0c046aa8\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"anchors\": [\n                {\n                  \"type\": \"btc\",\n                  \"anchor_id\": \"562658\",\n                  \"uris\": [\"https://a.chainpoint.org/calendar/2698364/data\"]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/core-cal-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784\",\n  \"proof_id\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_node_at\": \"2019-02-12T00:20:28Z\",\n  \"hash_id_core\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_core_at\": \"2019-02-12T00:20:28Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"core_id:000139a0-2e5c-11e9-bec9-01115ea738e6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"2697833:1549930833:1:https://a.chainpoint.org:cal:2697833\"\n        },\n        {\n          \"r\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"cal\",\n              \"anchor_id\": \"2697833\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/2697833/hash\"]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/core-tbtc-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784\",\n  \"proof_id\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_node_at\": \"2019-02-12T00:20:28Z\",\n  \"hash_id_core\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_core_at\": \"2019-02-12T00:20:28Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"core_id:000139a0-2e5c-11e9-bec9-01115ea738e6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"2697833:1549930833:1:https://a.chainpoint.org:cal:2697833\"\n        },\n        {\n          \"r\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"tcal\",\n              \"anchor_id\": \"2697833\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/2697833/hash\"]\n            }\n          ]\n        }\n      ],\n      \"branches\": [\n        {\n          \"label\": \"btc_anchor_branch\",\n          \"ops\": [\n            {\n              \"l\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"4b414a5f4e67f4255027e37e9889b6a1f7f94b9285dfe6c45dc9cc70ea83238b\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"19e96821c648366829f9b4656f439497bf8109a59bd211f7dea5f7bc38faca52\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"3a22c564ca8be07c0d51a67874e5ef104ca37dc255b9c5e8043beecd2137343a\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"56bb7aa1d380608cfc7328310020d79d90dadf8c8b93a4dc0c887ef080b30208\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"2d52f99e44e3a6aed97e7a4c2b2580094f3df598e71ad60ce3e4c80b83289fc0\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"2e1e454f149402f668e940b095e01303f2976b0c851de896c54d77d1b0ab7e76\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"62726a2392223ad6793414fca1c87e2932421315690f4ebb9094243b4d75fc65\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"0659c270d03e0ed74b183966f7a8ce35b31b51b861cb44a2d8a9e4c2144d7b11\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"0100000001f74c607aec4e8552e97f5c6705331bd40971fee4615463e44dc237909eda8561010000006b4830450221009dea9d7cfef167627ae1d9fe5fae19ea23c4d262d35c951d15f282089a25f71502204e8ab4c0778e206e06a9512c2fbfa5092d5dbfe79394b6bd7523e6b17482b6b10121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20\"\n            },\n            {\n              \"r\": \"cb1ee800000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"6530dedcdef49f2ba8ffad45cc2a3da9163c545cc25293fbdd38243cff32c856\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"3eb37791f8e63616aacc0c0a305bcb527696e7bac90f2f7e503b506a4c43faa6\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"5c32a9aa6c2d51cdf40a6abb84e310a915710a2eb68505f01e50f5f1a7f25d1b\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"52d7ead6f09051315f6508b5691fa6213643510ced81066e3ca6717151ebc6d4\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"2007ea7ac74528f25264e50f5ba848675c7a2f28e7dd196e33874a50c05107b5\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"755908c4d69007fc1f0df3e6211d1041850a81afe32b262f9a9a7ed9c2abf0a4\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"e75bb3c9dccd54b626579f08d1e517b458716cc308a6257a7fbe2697c749bc96\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"98e2e5e17003189b4ef1a1fdf8de94af1a0bbb7b9e9d6310ad953719385f4a1e\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a7af3e0d38da4c2b3bc958c63176d349a53379f1467c4be65a86ef5e3f91747b\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"a2a69c02444bc5f396d87537be7e15ee29dada648c95f9ce731a9d0db285dc91\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"1b3c4074444b6926f5ac2fc5c9d4a7cb91bdb6dcd90356abf2c3fda5d20b655a\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"25bfc69c9c2a6026eaef5b528c9b354dfaf6f313f614dd41f88d8d4f0c046aa8\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"anchors\": [\n                {\n                  \"type\": \"tbtc\",\n                  \"anchor_id\": \"562658\",\n                  \"uris\": [\"https://a.chainpoint.org/calendar/2698364/data\"]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/core-tcal-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784\",\n  \"proof_id\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_node_at\": \"2019-02-12T00:20:28Z\",\n  \"hash_id_core\": \"000139a0-2e5c-11e9-bec9-01115ea738e6\",\n  \"hash_submitted_core_at\": \"2019-02-12T00:20:28Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"core_id:000139a0-2e5c-11e9-bec9-01115ea738e6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"2697833:1549930833:1:https://a.chainpoint.org:cal:2697833\"\n        },\n        {\n          \"r\": \"456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"tcal\",\n              \"anchor_id\": \"2697833\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/2697833/hash\"]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/lsat-data.json",
    "content": "{\n  \"challenge1000\": \"LSAT macaroon=\\\"MDAxY2xvY2F0aW9uIDEyNy4wLjAuMTo4MDgwCjAwOTRpZGVudGlmaWVyIDAwMDA3Y2VmOTNmMmM1MWFhNjUyMDhiZWMxNDQ3ZmMzOGZjNThkOWJjZTEzNzVhNTMyZWRiMGRjZDI5MGEyYzMzMGFlMmNhOTMxYTFjMzZiNDhmNTQ5NDhiODk4YTI3MWE1M2VkOTFmZjdkMDA4MTkzOWE1ZmE1MTEyNDllODFjYmE1YwowMDJmc2lnbmF0dXJlIFAvS7iENFK0Z7Hc0GBM3wLOu0zB5Ino6DoXosjg4cpcCg\\\", invoice=\\\"lntb10u1pw7kfm8pp50nhe8uk9r2n9yz97c9z8lsu0ckxehnsnwkjn9mdsmnffpgkrxzhqdq5w3jhxapqd9h8vmmfvdjscqzpgllq2qvdlgkllc27kpd87lz8pdfsfmtteyc3kwq734jpwnvqt96e4nuy0yauzdrtkumxsvawgda8dlljxu3nnjlhs6w75390wy7ukj6cpfmygah\\\"\",\n  \"challenge10\": \"LSAT macaroon=\\\"MDAzMmxvY2F0aW9uIGh0dHBzOi8vbHNhdC1wbGF5Z3JvdW5kLmJ1Y2tvLm5vdy5zaAowMDk0aWRlbnRpZmllciAwMDAwYjE3NTUyMWQ5MWRhYTFjNmMzYTQ1ODdhYmQ2ZDc1MTk0ZDlmYjcxY2ExMDA2ZTM4ZjRhNjZiZjhlZGFmOTY3Y2RmMDI1ZDg5NjllMTFhZGZhMTlmODBlY2E3MjZhNDlmMDk4ZjBkNGIxZTliMjQ0NWExMDM1ODlhMDU2OGJkNWMKMDAyZnNpZ25hdHVyZSCbqEd6onEZANYxldTicJGq5k5esN0S3bR8ijHoS5UoZAo\\\", invoice=\\\"lntb100n1p0zy4xqpp5k964y8v3m2sudsaytpat6mt4r9xeldcu5yqxuw855e4l3md0je7qdquw3jhxapqwa5hg6pqxyczqumpw3escqzpgxqyz5vq0gl98qcw2t8vsqtvwhz3vjn6hmadq97g4hwllwd5rshraxpeeppnkldcg7y8et5qk7yn647lhflyt8j8wvln9xl9d2chdhahc240n8gqs83x93\\\"\",\n  \"challenge5\": \"LSAT macaroon=\\\"MDAzMmxvY2F0aW9uIGh0dHBzOi8vbHNhdC1wbGF5Z3JvdW5kLmJ1Y2tvLm5vdy5zaAowMDk0aWRlbnRpZmllciAwMDAwYzliNjFkOTBjZGNlM2QwN2UzNGY1OTI4MmQzMjk3NjI3NWQ5YWUxYTcwMzcyMDE4MDcxYjM2MGMwZGI3MDA2MTQ0OWUwMDNiOGZjNGNmOWNhNzg1NzhmMzZiZTgyN2RmNzgyZTJiMDk2ODk5MGM0MWUzMDkzOTM3N2ExYTA0MTYKMDAyZnNpZ25hdHVyZSALMsWsLgJvJFIs6ewLvLcYTp6sRq_sYg9-mlBWDQr6Kgo\\\", invoice=\\\"lntb50n1p0zy4gjpp5exmpmyxdec7s0c60ty5z6v5hvf6ants6wqmjqxq8rvmqcrdhqpssdq6w3jhxapqwa5hg6pqx5s8xct5wvcqzpgxqyz5vq54j6jvy6qwmynt0n5h8ewpwwj4trg2pz0ammz20ww6fyqathmrdk94e2hly296n7ag3cstcagjnjhxu7esml32fkteh3lmaz973p9ngqz8ak65\\\"\"\n}\n"
  },
  {
    "path": "tests/sample-data/tbtc-proof-l.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"tcal\",\n              \"anchor_id\": \"985635\",\n              \"uris\": [\"https://a.chainpoint.org/calendar/985635/hash\"]\n            }\n          ]\n        }\n      ],\n      \"branches\": [\n        {\n          \"label\": \"btc_anchor_branch\",\n          \"ops\": [\n            {\n              \"l\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20\"\n            },\n            {\n              \"r\": \"ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"anchors\": [\n                {\n                  \"type\": \"tbtc\",\n                  \"anchor_id\": \"503275\",\n                  \"uris\": [\"https://a.chainpoint.org/calendar/985814/data\"]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/tbtc-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"tcal\",\n              \"anchor_id\": \"9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d\",\n              \"uris\": [\n                \"https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"branches\": [\n        {\n          \"label\": \"btc_anchor_branch\",\n          \"ops\": [\n            {\n              \"l\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"r\": \"ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab\"\n            },\n            {\n              \"op\": \"sha-256\"\n            },\n            {\n              \"l\": \"01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20\"\n            },\n            {\n              \"r\": \"ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"l\": \"a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"r\": \"af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d\"\n            },\n            {\n              \"op\": \"sha-256-x2\"\n            },\n            {\n              \"anchors\": [\n                {\n                  \"type\": \"tbtc\",\n                  \"anchor_id\": \"549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e\",\n                  \"uris\": [\n                    \"https://a.chainpoint.org/calendar/549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e/data\"\n                  ]\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/sample-data/tcal-proof.chp.json",
    "content": "{\n  \"@context\": \"https://w3id.org/chainpoint/v3\",\n  \"type\": \"Chainpoint\",\n  \"hash\": \"ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391\",\n  \"proof_id\": \"66a34bd0-f4e7-11e7-a52b-016a36a9d789\",\n  \"hash_submitted_node_at\": \"2018-01-09T02:47:15Z\",\n  \"hash_id_core\": \"66bd6380-f4e7-11e7-895d-0176dc2220aa\",\n  \"hash_submitted_core_at\": \"2018-01-09T02:47:15Z\",\n  \"branches\": [\n    {\n      \"label\": \"cal_anchor_branch\",\n      \"ops\": [\n        {\n          \"l\": \"node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"r\": \"c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"l\": \"985635:1515466042:1:https://a.chainpoint.org:cal:985635\"\n        },\n        {\n          \"r\": \"0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837\"\n        },\n        {\n          \"op\": \"sha-256\"\n        },\n        {\n          \"anchors\": [\n            {\n              \"type\": \"tcal\",\n              \"anchor_id\": \"9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d\",\n              \"uris\": [\n                \"https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data\"\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/utils.js",
    "content": "/* global describe, it */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\n\nconst fs = require('fs')\nconst app = require('../lib/utils.js')\n\ndescribe('Utils Methods', () => {\n  describe('Sleep function', () => {\n    it('should sleep for 100ms', done => {\n      let amount = 100\n      let startMS = Date.now()\n      app.sleepAsync(amount).then(() => {\n        let elapsedMS = Date.now() - startMS\n        expect(elapsedMS).to.be.greaterThan(amount - 1)\n        expect(elapsedMS).to.be.lessThan(amount + 25)\n        done()\n      })\n    })\n    it('should sleep for 1000ms', done => {\n      let amount = 1000\n      let startMS = Date.now()\n      app.sleepAsync(amount).then(() => {\n        let elapsedMS = Date.now() - startMS\n        expect(elapsedMS).to.be.greaterThan(amount - 1)\n        expect(elapsedMS).to.be.lessThan(amount + 25)\n        done()\n      })\n    })\n  })\n\n  describe('Date functions', () => {\n    it('addSeconds should return correct result', done => {\n      let addend = 55\n      let startDate = new Date(2019, 0, 1, 0, 0, 0, 0)\n      let expectedDate = new Date(2019, 0, 1, 0, 0, addend, 0)\n      let calculatedDate = app.addSeconds(startDate, addend)\n      expect(calculatedDate.getTime()).to.equal(expectedDate.getTime())\n      done()\n    })\n    it('addMinutes should return correct result', done => {\n      let addend = 55\n      let startDate = new Date(2019, 0, 1, 0, 0, 0, 0)\n      let expectedDate = new Date(2019, 0, 1, 0, addend, 0, 0)\n      let calculatedDate = app.addMinutes(startDate, addend)\n      expect(calculatedDate.getTime()).to.equal(expectedDate.getTime())\n      done()\n    })\n    it('formatDateISO8601NoMs should return correct result', done => {\n      let startDate = new Date('2019-02-06T18:14:35.576Z')\n      let expectedDate = '2019-02-06T18:14:35Z'\n      let calculatedDate = app.formatDateISO8601NoMs(startDate)\n      expect(calculatedDate.toString()).to.equal(expectedDate)\n      done()\n    })\n  })\n\n  describe('Hash format function', () => {\n    it('lowerCaseHashes should return correct result', done => {\n      let startHashes = ['A1b2, ABCDef010101Cd']\n      let expectedHashes = ['a1b2, abcdef010101cd']\n      let calculatedHashes = app.lowerCaseHashes(startHashes)\n      expect(calculatedHashes.length).to.equal(expectedHashes.length)\n      expect(calculatedHashes[0]).to.equal(expectedHashes[0])\n      expect(calculatedHashes[1]).to.equal(expectedHashes[1])\n      done()\n    })\n  })\n\n  describe('Proof parsing function - mainnet', () => {\n    it('parseAnchorsComplete should return correct result for cal proof', done => {\n      let proofJSON = fs.readFileSync('./tests/sample-data/cal-proof.chp.json')\n      let proofObj = JSON.parse(proofJSON)\n      let res = app.parseAnchorsComplete(proofObj, 'mainnet')\n      expect(res.length).to.equal(1)\n      expect(res[0]).to.equal('cal')\n      done()\n    })\n    it('parseAnchorsComplete should return correct result for btc proof', done => {\n      let proofJSON = fs.readFileSync('./tests/sample-data/btc-proof.chp.json')\n      let proofObj = JSON.parse(proofJSON)\n      let res = app.parseAnchorsComplete(proofObj, 'mainnet')\n      expect(res.length).to.equal(2)\n      expect(res[0]).to.equal('cal')\n      expect(res[1]).to.equal('btc')\n      done()\n    })\n  })\n  describe('Proof parsing function - testnet', () => {\n    it('parseAnchorsComplete should return correct result for tcal proof', done => {\n      let proofJSON = fs.readFileSync('./tests/sample-data/tcal-proof.chp.json')\n      let proofObj = JSON.parse(proofJSON)\n      let res = app.parseAnchorsComplete(proofObj, 'testnet')\n      expect(res.length).to.equal(1)\n      expect(res[0]).to.equal('tcal')\n      done()\n    })\n    it('parseAnchorsComplete should return correct result for tbtc proof', done => {\n      let proofJSON = fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json')\n      let proofObj = JSON.parse(proofJSON)\n      let res = app.parseAnchorsComplete(proofObj, 'testnet')\n      expect(res.length).to.equal(2)\n      expect(res[0]).to.equal('tcal')\n      expect(res[1]).to.equal('tbtc')\n      done()\n    })\n  })\n\n  describe('Hex validation function', () => {\n    it('isHex should return false for non hex value', done => {\n      let val = 'nonhex'\n      let res = app.isHex(val)\n      expect(res).to.equal(false)\n      done()\n    })\n    it('isHex should return false for non hex value', done => {\n      let val = 'deadbeefcafe'\n      let res = app.isHex(val)\n      expect(res).to.equal(true)\n      done()\n    })\n  })\n\n  describe('Random number function', () => {\n    it('randomIntFromInterval should produce random numbers within the specified range', done => {\n      let iterations = 10000\n      for (let i = 0; i < iterations; i++) {\n        let min = Math.floor(Math.random() * 100)\n        let max = min * Math.ceil(Math.random() * 99)\n        let rnd = app.randomIntFromInterval(min, max)\n        expect(rnd).to.be.gte(min)\n        expect(rnd).to.be.lte(max)\n      }\n      // and test if bounds are inclusive\n      let rnd = app.randomIntFromInterval(10, 10)\n      expect(rnd).to.be.gte(10)\n      expect(rnd).to.be.lte(10)\n      done()\n    })\n  })\n\n  describe('UI password check function', () => {\n    it('should return false when value is false', done => {\n      let val = false\n      let res = app.nodeUIPasswordBooleanCheck(val)\n      expect(res).to.equal(false)\n      done()\n    })\n    it(\"should return false when value is 'false'\", done => {\n      let val = 'false'\n      let res = app.nodeUIPasswordBooleanCheck(val)\n      expect(res).to.equal(false)\n      done()\n    })\n    it(\"should return false when value is 'FALSE'\", done => {\n      let val = 'FALSE'\n      let res = app.nodeUIPasswordBooleanCheck(val)\n      expect(res).to.equal(false)\n      done()\n    })\n    it(\"should return false when value is 'False'\", done => {\n      let val = 'False'\n      let res = app.nodeUIPasswordBooleanCheck(val)\n      expect(res).to.equal(false)\n      done()\n    })\n    it('should return password if not any variation of false', done => {\n      let val = 'not false'\n      let res = app.nodeUIPasswordBooleanCheck(val)\n      expect(res).to.equal(val)\n      done()\n    })\n  })\n})\n"
  },
  {
    "path": "tests/verify.js",
    "content": "/* global describe, it beforeEach, afterEach */\n\nprocess.env.NODE_ENV = 'test'\n\n// test related packages\nconst expect = require('chai').expect\nconst request = require('supertest')\nconst fs = require('fs')\n\nconst app = require('../lib/api-server.js')\nconst verify = require('../lib/endpoints/verify.js')\nconst cpb = require('chainpoint-binary')\n\ndescribe('Verify Controller', () => {\n  let insecureServer = null\n  beforeEach(async () => {\n    insecureServer = await app.startInsecureRestifyServerAsync()\n    beforeEach(() => {\n      verify.setENV({ POST_VERIFY_PROOFS_MAX: 1 })\n    })\n    verify.setCores({\n      getCachedTransactionAsync: async txId => {\n        switch (txId) {\n          case '9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d': {\n            return { tx: { data: '4690932f928fb7f7ce6e6c49ee95851742231709360be28b7ce2af7b92cfa95b' } }\n          }\n          case '549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e': {\n            return { tx: { data: 'c617f5faca34474bea7020d75c39cb8427a32145f9646586ecb9184002131ad9' } }\n          }\n          default: {\n            return { tx: { data: '' } }\n          }\n        }\n      }\n    })\n  })\n  afterEach(() => {\n    insecureServer.close()\n  })\n\n  describe('POST /verify mainnet', () => {\n    beforeEach(() => {\n      verify.setENV({ POST_VERIFY_PROOFS_MAX: 1, NETWORK: 'mainnet' })\n    })\n    it('should return the proper error with bad content type', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'text/plain')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('Invalid content type')\n          done()\n        })\n    })\n\n    it('should return the proper error with missing proofs property', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('Invalid JSON body, missing proofs')\n          done()\n        })\n    })\n\n    it('should return the proper error with proofs not an array', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: 'notarray' })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('Invalid JSON body, proofs is not an Array')\n          done()\n        })\n    })\n\n    it('should return the proper error with empty proofs array', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [] })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal('Invalid JSON body, proofs Array is empty')\n          done()\n        })\n    })\n\n    it('should return the proper error with max proofs exceeded', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: ['p1', 'p2'] })\n        .expect('Content-type', /json/)\n        .expect(409)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.have.property('code')\n            .and.to.be.a('string')\n            .and.to.equal('InvalidArgument')\n          expect(res.body)\n            .to.have.property('message')\n            .and.to.be.a('string')\n            .and.to.equal(`Invalid JSON body, proofs Array max size of 1 exceeded`)\n          done()\n        })\n    })\n\n    it('should return successful result with malformed proof', done => {\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: ['p1'] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('malformed')\n          done()\n        })\n    })\n\n    it('should return successful result with bad network proof', done => {\n      let tcalProof = JSON.parse(fs.readFileSync('./tests/sample-data/tcal-proof.chp.json'))\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [tcalProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal(\n              `This is a 'mainnet' Node supporting 'cal' and 'btc' anchor types. Cannot verify 'tcal' anchors.`\n            )\n          done()\n        })\n    })\n\n    it('should return successful result with invalid cal proof (json) and legacy anchor', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof-l.chp.json'))\n      calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal(`Cannot verify legacy anchors.`)\n          done()\n        })\n    })\n\n    it('should return successful result with invalid cal proof (json)', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json'))\n      calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(1)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('invalid')\n          done()\n        })\n    })\n\n    it('should return successful result with invalid btc proof (json)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      btcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('invalid')\n          done()\n        })\n    })\n\n    it('should return successful result with valid cal proof (json)', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json'))\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(1)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('verified')\n          done()\n        })\n    })\n\n    it('should return successful result with valid btc proof (json)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('verified')\n          done()\n        })\n    })\n\n    it('should return successful result with mixed (cal ok, btc bad) btc proof (json)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      btcProof.branches[0].branches[0].ops[0].l = 'bad0cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('mixed')\n          done()\n        })\n    })\n\n    it('should return successful result with invalid cal proof (b64)', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json'))\n      calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      let calProofB64 = cpb.objectToBase64Sync(calProof)\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProofB64] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(1)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('invalid')\n          done()\n        })\n    })\n\n    it('should return successful result with invalid btc proof (b64)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      btcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      let btcProofB64 = cpb.objectToBase64Sync(btcProof)\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProofB64] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('invalid')\n          done()\n        })\n    })\n\n    it('should return successful result with valid cal proof (b64)', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json'))\n      let calProofB64 = cpb.objectToBase64Sync(calProof)\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProofB64] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(calProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(1)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('verified')\n          done()\n        })\n    })\n\n    it('should return successful result with valid btc proof (b64)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      let btcProofB64 = cpb.objectToBase64Sync(btcProof)\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProofB64] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('verified')\n          done()\n        })\n    })\n\n    it('should return successful result with mixed (cal ok, btc bad) btc proof (b64)', done => {\n      let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json'))\n      btcProof.branches[0].branches[0].ops[0].l = 'bad0cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837'\n      let btcProofB64 = cpb.objectToBase64Sync(btcProof)\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [btcProofB64] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(btcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('cal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('btc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('mixed')\n          done()\n        })\n    })\n  })\n\n  describe('POST /verify testnet', () => {\n    beforeEach(() => {\n      verify.setENV({ POST_VERIFY_PROOFS_MAX: 1, NETWORK: 'testnet' })\n    })\n\n    it('should return successful result with bad network proof', done => {\n      let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json'))\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [calProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal(\n              `This is a 'testnet' Node supporting 'tcal' and 'tbtc' anchor types. Cannot verify 'cal' anchors.`\n            )\n          done()\n        })\n    })\n\n    it('should return successful result with invalid tbtc proof (json) and legacy anchor', done => {\n      let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof-l.chp.json'))\n      tbtcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [tbtcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal(`Cannot verify legacy anchors.`)\n          done()\n        })\n    })\n\n    it('should return successful result with invalid tbtc proof (json)', done => {\n      let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json'))\n      tbtcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391'\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [tbtcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('tcal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('tbtc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(false)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('invalid')\n          done()\n        })\n    })\n\n    it('should return successful result with valid tbtc proof (json)', done => {\n      let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json'))\n      request(insecureServer)\n        .post('/verify')\n        .set('Content-type', 'application/json')\n        .send({ proofs: [tbtcProof] })\n        .expect('Content-type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          expect(err).to.equal(null)\n          expect(res.body)\n            .to.be.a('array')\n            .and.to.have.length(1)\n          expect(res.body[0])\n            .to.have.property('proof_index')\n            .and.to.be.a('number')\n            .and.to.equal(0)\n          expect(res.body[0])\n            .to.have.property('hash')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash)\n          expect(res.body[0])\n            .to.have.property('proof_id')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.proof_id)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_node_at')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_submitted_node_at)\n          expect(res.body[0])\n            .to.have.property('hash_id_core')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_id_core)\n          expect(res.body[0])\n            .to.have.property('hash_submitted_core_at')\n            .and.to.be.a('string')\n            .and.to.equal(tbtcProof.hash_submitted_core_at)\n          expect(res.body[0])\n            .to.have.property('anchors')\n            .and.to.be.a('array')\n          expect(res.body[0].anchors).to.have.length(2)\n          expect(res.body[0].anchors[0]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3)\n          expect(res.body[0].anchors[0])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('cal_anchor_branch')\n          expect(res.body[0].anchors[0])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('tcal')\n          expect(res.body[0].anchors[0])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[0])\n          expect(res.body[0].anchors[1]).to.be.a('object')\n          expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3)\n          expect(res.body[0].anchors[1])\n            .to.have.property('branch')\n            .and.to.be.a('string')\n            .and.to.equal('btc_anchor_branch')\n          expect(res.body[0].anchors[1])\n            .to.have.property('type')\n            .and.to.be.a('string')\n            .and.to.equal('tbtc')\n          expect(res.body[0].anchors[1])\n            .to.have.property('valid')\n            .and.to.be.a('boolean')\n            .and.to.equal(true)\n          expect(res.body[0].anchors[1])\n          expect(res.body[0])\n            .to.have.property('status')\n            .and.to.be.a('string')\n            .and.to.equal('verified')\n          done()\n        })\n    })\n  })\n})\n"
  }
]