diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac63fc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +dist/** +!dist/setup.sh +!dist/sr.sh diff --git a/README.md b/README.md index 6e901f9..224a5f0 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,78 @@ # cyphernode -Modular Bitcoin full-node microservices API server architecture and utilities toolkit to build scalable, secure and featureful apps and services without trusted third parties. +Modular Bitcoin full-node microservices API server architecture and utilities toolkit to build scalable, secure and featureful apps and services without trusted third parties. # What is cyphernode? -An open-source self-hosted API which allows you to spawn and call your encrypted overlay network of dockerized Bitcoin and crypto software projects (virtual machines). +Cyphernode is a Free open-source alternative to hosted services and commercial Bitcoin APIs such as Blockchain.info, Bitpay, Coinbase, BlockCypher, Bitgo, etc. You can use it to build Bitcoin services and applications using your own Bitcoin and Lightning Network full nodes. It is a substitute for the Bitcore and Insight software projects. -You can use it to build Bitcoin services and applications using your own Bitcoin and Lightning Network full nodes. +It implements a self-hosted API which allows you to spawn and call your encrypted overlay network of dockerized Bitcoin and crypto software projects (virtual machines). The Docker containers used in this project are hosted at www.bitcoindockers.com. -It is an alternative to hosted services and commercial Bitcoin APIs such as Blockchain.info, Bitpay, Coinbase, Blocypher, Bitgo, etc. - -It is a substitute for the Bitcore and Insight software projects. - -If aims to offer all advanced features and utilities necessary to operate entreprise grade Bitcoin services. We provide a curated list of functions using multiple software, but you can build your own private ones or add yours as a default option in the project. - -It is designed to be deployed on virtual machines with launch scripts, but with efficiency and minimalism in mind so that it can also run on multiple Rasberry Pi with very low computing ressources (and extremely low if installing pre-synchronized blockchain and pruned). Because of the modular architecture, heavier modules like blockchain indexers are optional (and not needed for most commercial use-cases). For a full-node and all modules, prepare 350GB of space and 2GB of RAM. - -Hardware wallets (ColdCard, Trezor) will be utilized for key generation and signing (using PSBT BIP174), as well as for connecting to self-hosted web user interfaces. +It aims to offer all advanced features and utilities necessary to operate entreprise-grade Bitcoin services. It includes a curated list of functions using multiple software, but you can build your own private ones or add yours as a default option in the project. It is currently in production by Bylls.com, Canada's first and largest Bitcoin payment processor, as well as Bitcoin Outlet, a fixed-rate Bitcoin exchange service alternative to Coinbase which allows Canadians to purchase bitcoins sent directly to their own Bitcoin wallet. -The docker containers used in this project are hosted at www.bitcoindockers.com +The project is in **heavy development** - we are currently looking for reviews, new features, user feedback and contributors to our roadmap. -The project is in **heavy development** - we are currently looking for review, new features, user feedback and contributors to our roadmap. +![image](doc/CN-Arch.jpg) + +# Low requirements, efficient use of resources + +Cyphernode is designed to be deployed on virtual machines with launch scripts, but with efficiency and minimalism in mind so that it can also run on multiple Rasberry Pi with very low computing ressources (and extremely low if installing pre-synchronized blockchain and pruned). Because of the modular architecture, heavier modules like blockchain indexers are optional (and not needed for most commercial use-cases). + +* For a full-node and all modules: + * 350 GB of storage, 2GB of RAM. +* Hardware wallets (ColdCard, Trezor) for key generation and signing (using PSBT BIP174), as well as for connecting to self-hosted web user interfaces. + +# Cyphernode Architecture +Cyphernode is an assembly of Docker containers being called by a request dispatcher. + +The request dispatcher (requesthandler.sh) is the HTTP entry point. +The request dispatcher is stateful: it keeps some data to be more effective on next calls. +The request dispatcher is where Cyphernode scales with new features: add your switch, dispatch requests to your stuff. +We are trying to construct each container so that it can be used separately, as a standalone reusable component. + +Important to us: + +Be as optimized as possible, using Alpine when possible and having the smallest Docker image size possible +Reuse existing software: built-in shell commands, well-established pieces of software, etc. +Use open-source software +Don't reinvent the wheel +Expose the less possible surface +Center element: proxy_docker +The proxy_docker is the container receiving and dispatching calls from clients. When adding a feature to Cyphernode, it is the first part to be modified to integrate the new feature. + +proxy_docker/app/script/requesthandler.sh +You will find in there the switch statement used to dispatch the requests. Just add a case block with your command, using other cases as examples for POST or GET requests. + +proxy_docker/app/config +You will find there config files. config.properties should be used to centralize configs. spender and watcher properties are used to obfuscate credentials on curl calls. + +proxy_docker/app/data +watching.sql contains the data model. Called "watching" because in the beginning of the project, it was only used for watching addresses. Now could be used to index the blockchain (build an explorer) and add more features. + +cron_docker +If you have jobs to be scheduled, use this container. Just add an executable and add it to the crontab. + +Currently used to make sure callbacks have been called for missed transactions. # About this project -- Created and maintained by www.satoshiportal.com -- Dedicated full-time developer @kexkey -- Project manager @FrancisPouliot -- Disclaimer: as of release on Sept. 23 2018 the project is still it its early stages (Alpha) and many of the features have yet to be implemented. The core architecture and basic wallet operations and blockchain query functions are fully functional. +* Created and maintained by www.satoshiportal.com +* Dedicated full-time developer @kexkey +* Project manager @FrancisPouliot +* Contributor: @\_\_escapee\_\_ +* Disclaimer: as of release on Sept. 23 2018 the project is still it its early stages (Alpha) and many of the features have yet to be implemented. The core architecture and basic wallet operations and blockchain query functions are fully functional. # How to use cyphernode? The core component of cyphernode is a request handler which exposes HTTP endpoints via REST API, acting as an absctration layer between your apps and the open-source Bitcoin sofware you want to interact with. -## DOCS +## Documentation -Read the API docs here: https://github.com/SatoshiPortal/cyphernode/blob/master/doc/API.md - -Installation documentation: https://github.com/SatoshiPortal/cyphernode/blob/master/doc/INSTALL.md - -Step-by-step manual install: https://github.com/SatoshiPortal/cyphernode/blob/master/doc/INSTALL-MANUAL-STEPS.md +* Read the API docs here: https://github.com/SatoshiPortal/cyphernode/blob/master/doc/API.md +* Installation documentation: https://github.com/SatoshiPortal/cyphernode/blob/master/doc/INSTALL.md +* Step-by-step manual install (deprecated): https://github.com/SatoshiPortal/cyphernode/blob/master/doc/INSTALL-MANUAL-STEPS.md ## When calling a cyphernode endpoint, you are either diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 6a534f6..ab22ed9 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -11,12 +11,14 @@ RUN apk add --update --no-cache \ su-exec COPY auth.sh /etc/nginx/conf.d/ -COPY default-ssl.conf /etc/nginx/conf.d/default.conf -COPY statuspage.html /etc/nginx/conf.d/status/ +COPY default.conf /etc/nginx/conf.d/default.conf COPY entrypoint.sh entrypoint.sh COPY trace.sh /etc/nginx/conf.d/ COPY tests.sh /etc/nginx/conf.d/ RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh +RUN touch /var/log/gatekeeper.log +RUN chmod a+rw /var/log/gatekeeper.log + ENTRYPOINT ["./entrypoint.sh"] diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties index f78e34b..4d59d81 100644 --- a/api_auth_docker/api-sample.properties +++ b/api_auth_docker/api-sample.properties @@ -1,8 +1,22 @@ # The file api.properties generated by the installer should look like this. -# Watcher can: +# Stats can: +action_helloworld=stats +action_getblockchaininfo=stats +action_installation_info=stats +action_getmempoolinfo=stats +action_getblockhash=stats + +# Watcher can do what the stats can do, plus: action_watch=watcher action_unwatch=watcher +action_watchxpub=watcher +action_unwatchxpubbyxpub=watcher +action_unwatchxpubbylabel=watcher +action_getactivewatchesbyxpub=watcher +action_getactivewatchesbylabel=watcher +action_getactivexpubwatches=watcher +action_watchtxid=watcher action_getactivewatches=watcher action_getbestblockhash=watcher action_getbestblockinfo=watcher @@ -10,11 +24,16 @@ action_getblockinfo=watcher action_gettransaction=watcher action_ln_getinfo=watcher action_ln_create_invoice=watcher +action_ln_getconnectionstring=watcher +action_ln_decodebolt11=watcher # Spender can do what the watcher can do, plus: action_getbalance=spender +action_getbalancebyxpub=spender +action_getbalancebyxpublabel=spender action_getnewaddress=spender action_spend=spender +action_bumpfee=spender action_addtobatch=spender action_batchspend=spender action_deriveindex=spender @@ -23,11 +42,15 @@ action_ln_pay=spender action_ln_newaddr=spender action_ots_stamp=spender action_ots_getfile=spender +action_ln_getinvoice=spender +action_ln_decodebolt11=spender +action_ln_connectfund=spender # Admin can do what the spender can do, plus: # Should be called from inside the Docker network only: action_conf=internal +action_newblock=internal action_executecallbacks=internal action_ots_backoffice=internal diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh old mode 100644 new mode 100755 index 8604f3a..7a543b9 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -16,20 +16,19 @@ . ./trace.sh -verify_sign() -{ +verify_sign() { local returncode - local header64=$(echo ${1} | cut -sd '.' -f1) - local payload64=$(echo ${1} | cut -sd '.' -f2) - local signature=$(echo ${1} | cut -sd '.' -f3) + local header64=$(echo "${1}" | cut -sd '.' -f1) + local payload64=$(echo "${1}" | cut -sd '.' -f2) + local signature=$(echo "${1}" | cut -sd '.' -f3) trace "[verify_sign] header64=${header64}" trace "[verify_sign] payload64=${payload64}" trace "[verify_sign] signature=${signature}" - local payload=$(echo -n ${payload64} | base64 -d) - local exp=$(echo ${payload} | jq ".exp") + local payload=$(echo -n "${payload64}" | base64 -d) + local exp=$(echo "${payload}" | jq ".exp") local current=$(date +"%s") trace "[verify_sign] payload=${payload}" @@ -38,7 +37,7 @@ verify_sign() if [ ${exp} -gt ${current} ]; then trace "[verify_sign] Not expired, let's validate signature" - local id=$(echo ${payload} | jq ".id" | tr -d '"') + local id=$(echo "${payload}" | jq -r ".id") trace "[verify_sign] id=${id}" # Check for code injection @@ -82,38 +81,47 @@ verify_sign() return 1 } -verify_group() -{ +verify_group() { trace "[verify_group] Verifying group..." local id=${1} # REQUEST_URI should look like this: /v0/watch/2blablabla + local context=$(echo "${REQUEST_URI#\/}" | cut -d '/' -f1) local action=$(echo "${REQUEST_URI#\/}" | cut -d '/' -f2) - trace "[verify_group] action=${action}" + trace "[verify_group] context=${context} action=${action}" # Check for code injection # action can be alphanum... and _ and - but nothing else - local actiontoinspect=$(echo "$action" | tr -d '_-') + local actiontoinspect=$(echo "$action" | tr -d '_-') case $actiontoinspect in (*[![:alnum:]]*|"") trace "[verify_group] Potential code injection, exiting" return 1 esac - # It is so much faster to include the keys here instead of grep'ing the file for key. - . ./api.properties - local needed_group local ugroups - eval needed_group='$action_'${action} - trace "[verify_group] needed_group=${needed_group}" - eval ugroups='$ugroups_'$id trace "[verify_group] user groups=${ugroups}" - case "${ugroups}" in - *${needed_group}*) trace "[verify_group] Access granted"; return 0 ;; - esac + if [ ${context} = "s" ]; then + # static files only accessible by a certain group + needed_group=${action} + elif [ ${context} = "v0" ]; then + # actual api calls + # It is so much faster to include the keys here instead of grep'ing the file for key. + . ./api.properties + eval needed_group='$action_'${action} + fi + + trace "[verify_group] needed_group=${needed_group}" + + # If needed_group is empty, the action was not found in api.propeties. + if [ -n "${needed_group}" ]; then + case "${ugroups}" in + *${needed_group}*) trace "[verify_group] Access granted"; return 0 ;; + esac + fi trace "[verify_group] Access NOT granted" return 1 diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index d9da37a..f8b4193 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -1,12 +1,25 @@ server { - listen 80; + listen 443 ssl; server_name localhost; - #include /etc/nginx/conf.d/ip-whitelist.conf; + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + location /s/ { + auth_request /auth; + root /etc/nginx/conf.d; + } location /v0/ { auth_request /auth; proxy_pass http://proxy:8888/; + + # Up default 60 second timeout for 3 minutes (OTS stamping can take time) + proxy_connect_timeout 180; + proxy_send_timeout 180; + proxy_read_timeout 180; + send_timeout 180; + } location /auth { diff --git a/api_auth_docker/keys-sample.properties b/api_auth_docker/keys-sample.properties index 637f0a3..12b3b81 100644 --- a/api_auth_docker/keys-sample.properties +++ b/api_auth_docker/keys-sample.properties @@ -1,9 +1,10 @@ # The file keys.properties generated by the installer should look like this. # kapi_id="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact -kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="000";kapi_key="eff1eeea370eacdc5cf7e96c2d82340d1568079a5d4d87006ec8788a98883b36";kapi_groups="stats";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="stats,watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="stats,watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="stats,watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="stats,watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="stats,watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="stats,watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} diff --git a/api_auth_docker/statuspage.html b/api_auth_docker/statuspage.html deleted file mode 100644 index dadca7b..0000000 --- a/api_auth_docker/statuspage.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - -
-

Hello World from Cyphernode!

-

If you are here, it means you successfully deployed Cyphernode. Congratulations, fellow Cyphernode Operator!

-
-
-
-

The following files have been encrypted with your configuration passphrase and your client keys passphrase, respectively:

- -
-
-

This is the status of Cyphernode's installation and running components

-

-  
- - diff --git a/api_auth_docker/trace.sh b/api_auth_docker/trace.sh old mode 100644 new mode 100755 index c2c46ed..a76f0f4 --- a/api_auth_docker/trace.sh +++ b/api_auth_docker/trace.sh @@ -3,13 +3,6 @@ trace() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" 1>&2 - fi -} - -trace_rc() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" 1>&2 + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] $$ $*" 2>>/var/log/gatekeeper.log 1>&2 fi } diff --git a/build.sh b/build.sh index a03dd62..84a1157 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,18 @@ TRACING=1 +# CYPHERNODE VERSION "v0.2.4" +CONF_VERSION="v0.2.4-local" +GATEKEEPER_VERSION="v0.2.4-local" +PROXY_VERSION="v0.2.4-local" +NOTIFIER_VERSION="v0.2.4-local" +PROXYCRON_VERSION="v0.2.4-local" +OTSCLIENT_VERSION="v0.2.4-local" +PYCOIN_VERSION="v0.2.4-local" +BITCOIN_VERSION="v0.18.0" +LIGHTNING_VERSION="v0.7.1" +GRAFANA_VERSION="v0.2.4-local" + trace() { if [ -n "${TRACING}" ]; then @@ -16,60 +28,19 @@ trace_rc() fi } - -build_docker_image() { - - local dockerfile="Dockerfile" - - if [[ ""$3 != "" ]]; then - dockerfile=$3 - fi - - trace "building docker image: $2" - #docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null - docker build $1 -f $1/$dockerfile -t $2 - -} - build_docker_images() { trace "Updating SatoshiPortal repos" - git submodule update --recursive --remote - - local bitcoin_dockerfile=Dockerfile.amd64 - local clightning_dockerfile=Dockerfile.amd64 - local proxy_dockerfile=Dockerfile.amd64 - - # compat mode for SatoshiPortal repo - # TODO: add more mappings? - if [[ $(uname -m) == 'armv7l' ]]; then - bitcoin_dockerfile="Dockerfile.arm32v6" - clightning_dockerfile="Dockerfile.arm32v6" - proxy_dockerfile="Dockerfile.arm32v6" - fi trace "Creating cyphernodeconf image" - build_docker_image install/ cyphernode/cyphernodeconf:$CN_VERSION - - trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BC_VERSION $bitcoin_dockerfile - build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$CL_VERSION $clightning_dockerfile + docker build cyphernodeconf_docker/ -t cyphernode/cyphernodeconf:$CONF_VERSION trace "Creating cyphernode images" - build_docker_image api_auth_docker/ cyphernode/gatekeeper:$CN_VERSION - build_docker_image proxy_docker/ cyphernode/proxy:$CN_VERSION $proxy_dockerfile - build_docker_image cron_docker/ cyphernode/proxycron:$CN_VERSION - build_docker_image pycoin_docker/ cyphernode/pycoin:$CN_VERSION - build_docker_image otsclient_docker/ cyphernode/otsclient:$CN_VERSION - + docker build api_auth_docker/ -t cyphernode/gatekeeper:$GATEKEEPER_VERSION \ + && docker build proxy_docker/ -t cyphernode/proxy:$PROXY_VERSION \ + && docker build notifier_docker/ -t cyphernode/notifier:$NOTIFIER_VERSION \ + && docker build cron_docker/ -t cyphernode/proxycron:$PROXYCRON_VERSION \ + && docker build pycoin_docker/ -t cyphernode/pycoin:$PYCOIN_VERSION \ + && docker build otsclient_docker/ -t cyphernode/otsclient:$OTSCLIENT_VERSION } -# CYPHERNODE VERSION -GATEKEEPER_VERSION="latest" -PROXY_VERSION="latest" -PROXYCRON_VERSION="latest" -OTSCLIENT_VERSION="latest" -PYCOIN_VERSION="latest" -BITCOIN_VERSION="latest" -LIGHTNING_VERSION="latest" - build_docker_images diff --git a/cyphernodeconf_docker/.dockerignore b/cyphernodeconf_docker/.dockerignore new file mode 100644 index 0000000..e178f23 --- /dev/null +++ b/cyphernodeconf_docker/.dockerignore @@ -0,0 +1,12 @@ +node_modules +Dockerfile +.dockerignore +.eslintignore +.eslintrc.json +.gitignore +jest.config.js +LICENSE +build.sh +run.sh +jest.config.js +test diff --git a/cyphernodeconf_docker/.eslintignore b/cyphernodeconf_docker/.eslintignore new file mode 100644 index 0000000..f177a5b --- /dev/null +++ b/cyphernodeconf_docker/.eslintignore @@ -0,0 +1,4 @@ +usr +scripts +config +node_modules diff --git a/cyphernodeconf_docker/.eslintrc.json b/cyphernodeconf_docker/.eslintrc.json new file mode 100644 index 0000000..7b62f1b --- /dev/null +++ b/cyphernodeconf_docker/.eslintrc.json @@ -0,0 +1,67 @@ +{ + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2017, + "sourceType": "module" + }, + "rules": { + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": "function", + "next": "*" + } + ], + "newline-per-chained-call": "error", + "no-tabs": "error", + "no-nested-ternary": "error", + "no-trailing-spaces": "error", + "no-multiple-empty-lines": "error", + "no-whitespace-before-property": "error", + "new-cap": "error", + "lines-around-comment": "error", + "key-spacing": "error", + "comma-style": "error", + "brace-style": "error", + "camelcase": "error", + "handle-callback-err": "error", + "no-new-require": "warn", + "no-sync": "warn", + "no-mixed-requires": "error", + "global-require": "error", + "comma-spacing": "error", + "eqeqeq": "error", + "curly": "error", + "keyword-spacing": "error", + "object-curly-spacing": [ + "warn", + "always" + ], + "object-curly-newline": [ + "warn", + "always" + ], + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/cyphernodeconf_docker/.gitignore b/cyphernodeconf_docker/.gitignore new file mode 100644 index 0000000..f41bfce --- /dev/null +++ b/cyphernodeconf_docker/.gitignore @@ -0,0 +1,8 @@ +*~ +*.log +*.sw[a-z] +DEADJOE +node_modules +coverage +.DS_Store +.idea diff --git a/cyphernodeconf_docker/Dockerfile b/cyphernodeconf_docker/Dockerfile new file mode 100644 index 0000000..e51c158 --- /dev/null +++ b/cyphernodeconf_docker/Dockerfile @@ -0,0 +1,16 @@ +FROM node:12.2.0-alpine + +ENV EDITOR=/usr/bin/nano + +COPY . /app +WORKDIR /app + +RUN mkdir /data && \ + apk add --update su-exec p7zip openssl nano apache2-utils git && \ + rm -rf /var/cache/apk/* && \ + npm ci --production + +WORKDIR /app + +ENTRYPOINT ["/sbin/su-exec"] + diff --git a/cyphernodeconf_docker/build.sh b/cyphernodeconf_docker/build.sh new file mode 100755 index 0000000..a05fdd8 --- /dev/null +++ b/cyphernodeconf_docker/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +VERSION=v0.2.4 + +docker build . -t cyphernode/cyphernodeconf:${VERSION} diff --git a/install/generator-cyphernode/generators/app/features.json b/cyphernodeconf_docker/features.json similarity index 100% rename from install/generator-cyphernode/generators/app/features.json rename to cyphernodeconf_docker/features.json diff --git a/install/generator-cyphernode/generators/app/help.json b/cyphernodeconf_docker/help.json similarity index 86% rename from install/generator-cyphernode/generators/app/help.json rename to cyphernodeconf_docker/help.json index 36032e7..c321031 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/cyphernodeconf_docker/help.json @@ -17,7 +17,9 @@ "gatekeeper_edit_apiproperties": "If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization. (Not recommended)", "gatekeeper_apiproperties": "You are about to edit the api.properties file. The format of the file is pretty simple: for each action, you will find what access group can access it. Admin group can do what Spender group can, and Spender group can do what Watcher group can. Internal group is for the endpoints accessible only within the Docker network, like the backoffice tasks used by the Cron container. The access groups for each API id/key are found in the keys.properties file.", "gatekeeper_cns": "I use domain names and/or IP addresses to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, enter cyphernodehost, 192.168.7.44 as a possible domains. 127.0.0.1, localhost, gatekeeper will be automatically added to your list. Make sure the provided domain names are in your DNS or client's hosts file and is reachable.", - "bitcoin_mode": "Cyphernode can spawn a new Bitcoin Core full node for its own use. But if you already have a Bitcoin Core node running, Cyphernode can use that.", + "traefik_datapath": "The Traefik's files will be stored in a container's mounted directory. Please provide the local mounted path to that directory. If running on OSX, check mountable directories in Docker's File Sharing configs.", + "traefik_datapath_custom": "Provide the full path name where the Traefik's files will be saved.", + "bitcoin_mode": "Cyphernode will spawn a new Bitcoin Core full node for its own use. If you already have Bitcoin Core node data, you can use the directory containing that data directly or copy the contents of it to a new directory to be used by cyphernode. Be aware that the files might change ownership, if you run cyphernode as a different user. In case you want to move the blockchain data to another node you might need to change the owner to fit the configuration of that node.", "bitcoin_node_ip": "Cyphernode uses Bitcoin Core RPC interface for its tasks. Please provide the IP address of your current Bitcoin Core node.", "bitcoin_rpcuser": "Bitcoin Core's RPC username used by Cyphernode when calling the node.", "bitcoin_rpcpassword": "Bitcoin Core's RPC password used by Cyphernode when calling the node.", @@ -28,6 +30,7 @@ "bitcoin_datapath_custom": " ", "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", + "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: In case you are running Cyphernode at home. This option won't make your router forward needed LN ports; you still need to configure and manage that part yourself in your router configuration.", "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container. If running on OSX, check mountable directories in Docker's File Sharing configs.", diff --git a/cyphernodeconf_docker/index.js b/cyphernodeconf_docker/index.js new file mode 100644 index 0000000..18a7d31 --- /dev/null +++ b/cyphernodeconf_docker/index.js @@ -0,0 +1,12 @@ +const App = require( './lib/app.js' ); + +const main = async ( args ) => { + const app = new App(); + const noWizard = args.indexOf('recreate') !== -1; + await app.start( { + noWizard: noWizard, + noSplashScreen: noWizard + } ); +}; + +main( process.argv.slice( 2, process.argv.length ) ); diff --git a/cyphernodeconf_docker/jest.config.js b/cyphernodeconf_docker/jest.config.js new file mode 100644 index 0000000..5d2efb8 --- /dev/null +++ b/cyphernodeconf_docker/jest.config.js @@ -0,0 +1,185 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // Respect "browser" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/fb/q84grm8d0w3gz0y0yx_bvx700000gn/T/jest_dx", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + coverageReporters: [ + //"json", + "text", + //"lcov", + //"clover" + ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // A path to a custom dependency extractor + // dependencyExtractor: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files usin a array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: null, + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: null, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/cyphernodeconf_docker/lib/ansi.js b/cyphernodeconf_docker/lib/ansi.js new file mode 100644 index 0000000..4d1d1e0 --- /dev/null +++ b/cyphernodeconf_docker/lib/ansi.js @@ -0,0 +1,4 @@ +module.exports = { + reset: '\u001B8\u001B[u', + clear: '\u001Bc' +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/lib/apikey.js b/cyphernodeconf_docker/lib/apikey.js similarity index 76% rename from install/generator-cyphernode/generators/app/lib/apikey.js rename to cyphernodeconf_docker/lib/apikey.js index 602d1ed..a5aa699 100644 --- a/install/generator-cyphernode/generators/app/lib/apikey.js +++ b/cyphernodeconf_docker/lib/apikey.js @@ -1,13 +1,24 @@ const spawn = require('child_process').spawn; +const configEntryRegexp = /^kapi_id="(\w+)";kapi_key="(\w+)";kapi_groups="(.+)";(.+)$/; module.exports = class ApiKey { constructor( id, groups, key, script ) { - this.setId(id || '001'); - this.setGroups(groups || ['admin'] ); + this.setId(id || '000'); + this.setGroups(groups || ['stats'] ); this.setScript(script || 'eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}' ); this.setKey(key); } + setFromConfigEntry(configEntry ) { + const match = configEntryRegexp.exec( configEntry ); + if( match ) { + this.setId( match[1] ); + this.setKey( match[2] ); + this.setGroups( match[3].split(',').map( (e)=>e.trim() ) ) + this.setScript( match[4] ); + } + } + setGroups( groups ) { this.groups = groups; } @@ -44,15 +55,13 @@ module.exports = class ApiKey { }); dd.stdout.on('error', function(err) { - console.log(err); reject(err); }) }); this.key = result; } catch( err ) { - console.log( err ); - return; + reject(err); } } @@ -71,6 +80,4 @@ module.exports = class ApiKey { return `${this.id}=${this.key}`; } -} - -//dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 \ No newline at end of file +}; diff --git a/cyphernodeconf_docker/lib/app.js b/cyphernodeconf_docker/lib/app.js new file mode 100644 index 0000000..e9cb672 --- /dev/null +++ b/cyphernodeconf_docker/lib/app.js @@ -0,0 +1,646 @@ +const {promisify} = require('util'); + +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const wrap = require('wrap-ansi'); +const validator = require('validator'); +const coinstring = require('coinstring'); +const inquirer = require('inquirer'); +const colorsys = require('colorsys'); +const ejs = require( 'ejs' ); +const ejsRenderFileAsync = promisify( ejs.renderFile ).bind( ejs ); + +const html2ansi = require('./html2ansi.js'); +const name = require('./name.js'); +const Archive = require('./archive.js'); +const ApiKey = require('./apikey.js'); +const Cert = require('./cert.js'); +const htpasswd = require( './htpasswd.js'); +const Config = require('./config.js'); +const SplashScreen = require( './splashScreen.js' ); +const ansi = require( './ansi.js' ); + +const features = require('../features.json'); + +const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars +const userRegexp = /^[a-zA-Z0-9\._\-]+$/; +const maxWidth = 82; + +const keyIds = { + '000': ['stats'], + '001': ['stats','watcher'], + '002': ['stats','watcher','spender'], + '003': ['stats','watcher','spender','admin'] +}; + +const configArchiveFileName = 'config.7z'; +const keyArchiveFileName = 'client.7z'; +const destinationDirName = '.cyphernodeconf'; + +const prefix = () => { + return chalk.green('Cyphernode')+': '; +}; + +const randomColor = () => { + const hex = colorsys.hslToHex( { h: (Math.random()*360)<<0, s: 50, l: 50 } ); + return hex.substr(1); +}; + +let prompters = []; +fs.readdirSync(path.join(__dirname, '..','prompters')).forEach((file) => { + prompters.push(require(path.join(__dirname, '..','prompters',file))); +}); + +module.exports = class App { + + constructor() { + this.features = features; + + if( fs.existsSync(path.join('/data', destinationDirName, 'exitStatus.sh')) ) { + fs.unlinkSync(path.join('/data', destinationDirName, 'exitStatus.sh')); + } + + this.splash = new SplashScreen( { + frameDir: path.join(__dirname, '..', 'splash' ), + enableFortune: false, + fortuneChalk: chalk.default.bold, + width: maxWidth, + fortuneSpacing: 3 + } ); + } + + async start( options ) { + + options = options || {}; + + this.sessionData = { + defaultDataDirBase: process.env.DEFAULT_DATADIR_BASE || process.env.HOME, + setupDir: process.env.SETUP_DIR || path.join( process.env.HOME, 'cyphernode' ), + default_username: process.env.DEFAULT_USER || '', + gatekeeper_version: process.env.GATEKEEPER_VERSION, + gatekeeper_cns: process.env.DEFAULT_CERT_HOSTNAME, + proxy_version: process.env.PROXY_VERSION, + proxycron_version: process.env.PROXYCRON_VERSION, + pycoin_version: process.env.PYCOIN_VERSION, + otsclient_version: process.env.OTSCLIENT_VERSION, + bitcoin_version: process.env.BITCOIN_VERSION, + lightning_version: process.env.LIGHTNING_VERSION, + notifier_version: process.env.NOTIFIER_VERSION, + setup_version: process.env.SETUP_VERSION, + noWizard: !!options.noWizard, + noSplashScreen: !!options.noSplashScreen, + lightning_nodename: name.generate(), + lightning_nodecolor: randomColor(), + installer_cleanup: false, + devmode: process.env.DEVMODE || false + }; + + await this.setupConfigArchive(); + + if( !this.sessionData.noSplashScreen ) { + await this.splash.show(); + } + + let missingProperties = []; + + if( this.config.validateErrors && this.config.validateErrors.length ) { + for( let error of this.config.validateErrors ) { + if( error.keyword === 'required' && error.params && error.params.missingProperty ) { + missingProperties.push( error.params.missingProperty ); + } + } + } + + if( this.sessionData.noWizard && missingProperties.length && this.config.isLoaded ) { + console.log(chalk.bold.red('Unable to migrate client.7z non-interactively. Rerun without the -r option') ); + process.exit(1); + } + + if( !this.sessionData.noWizard ) { + // save gatekeeper key password to check if it changed + this.sessionData.gatekeeper_clientkeyspassword = this.config.data.gatekeeper_clientkeyspassword; + if( missingProperties.length && this.config.isLoaded ) { + this.sessionData.markProperties = missingProperties; + } + await this.startWizard(); + } + + await this.processProps(); + await this.writeFiles(); + } + + async setupConfigArchive() { + + this.config = new Config( { + setup_version: this.sessionData.setup_version, + docker_versions: { + 'cyphernode/bitcoin': this.sessionData.bitcoin_version, + 'cyphernode/gatekeeper': this.sessionData.gatekeeper_version, + 'cyphernode/proxy': this.sessionData.proxy_version, + 'cyphernode/proxycron': this.sessionData.proxycron_version, + 'cyphernode/pycoin': this.sessionData.pycoin_version, + 'cyphernode/otsclient': this.sessionData.otsclient_version, + 'cyphernode/clightning': this.sessionData.lightning_version, + 'cyphernode/notifier': this.sessionData.notifier_version, + 'traefik': 'v1.7.9-alpine', + 'eclipse-mosquitto': '1.6' + } + } ); + + if( !fs.existsSync(this.destinationPath(configArchiveFileName)) ) { + let r = {}; + process.stdout.write(ansi.clear+ansi.reset); + while( !r.password0 || !r.password1 || r.password0 !== r.password1 ) { + + if( r.password0 && r.password1 && r.password0 !== r.password1 ) { + console.log(chalk.bold.red('Passwords do not match')+'\n'); + } + + r = await this.prompt([{ + type: 'password', + name: 'password0', + message: prefix()+chalk.bold.blue('Choose your configuration password'), + filter: this.trimFilter + }, + { + type: 'password', + name: 'password1', + message: prefix()+chalk.bold.blue('Confirm your configuration password'), + filter: this.trimFilter + }]); + } + this.sessionData.configurationPassword = r.password0; + } else { + try { + let r = {}; + if( process.env.CFG_PASSWORD ) { + this.sessionData.configurationPassword = process.env.CFG_PASSWORD; + } else { + process.stdout.write(ansi.reset); + while( !r.password ) { + r = await this.prompt([{ + type: 'password', + name: 'password', + message: prefix()+chalk.bold.blue('Enter your configuration password?'), + filter: this.trimFilter + }]); + } + this.sessionData.configurationPassword = r.password; + } + try { + await this.config.deserialize( + this.destinationPath(configArchiveFileName), + this.sessionData.configurationPassword, + ); + + // store clientkeyspassword in sessionData so it can be retrieved by getDefault + // and a simple return will not result in a password mismatch + if( this.config.data.hasOwnProperty('gatekeeper_clientkeyspassword') ) { + this.sessionData.gatekeeper_clientkeyspassword_c = + this.config.data.gatekeeper_clientkeyspassword; + } + + } catch (e) { + console.log(chalk.bold.red(e)); + process.exit(); + } + } catch( err ) { + console.log(chalk.bold.red('config archive is corrupt.')); + process.exit(1); + } + } + + this.config.data.adminhash = await htpasswd(this.sessionData.configurationPassword); + + for( let feature of this.features ) { + feature.checked = this.isChecked( 'features', feature.value ); + } + + } + + async startWizard() { + let r = await this.prompt([{ + type: 'confirm', + name: 'enablehelp', + message: prefix()+'Enable help?', + default: this.getDefault( 'enablehelp' ), + }]); + + this.config.data.enablehelp = r.enablehelp; + + if( this.config.data.enablehelp ) { + this.help = require('../help.json'); + } + + let prompts = []; + + for( let m of prompters ) { + let newPrompts = m.prompts(this); + + if( this.sessionData.markProperties && + this.sessionData.markProperties.length && + this.config.isLoaded ) { + + for( let prompt of newPrompts ) { + if( this.sessionData.markProperties.indexOf(prompt.name) !== -1 ) { + prompt.message = prompt.message+' '+chalk.bgGreen('new option'); + } + } + } + + prompts = prompts.concat(newPrompts); + } + + const props = await this.prompt(prompts); + + this.config.data = Object.assign(this.config.data, props); + } + + async processProps() { + + // creates keys if they don't exist or we say so. + if( this.config.data.gatekeeper_recreatekeys || + this.config.data.gatekeeper_keys.configEntries.length===0 ) { + + delete this.config.data.gatekeeper_recreatekeys; + + let configEntries = []; + let clientInformation = []; + + for( let keyId in keyIds ) { + const apikey = await this.createRandomKey( keyId, keyIds[keyId] ); + configEntries.push(apikey.getConfigEntry()); + clientInformation.push(apikey.getClientInformation()); + } + + this.config.data.gatekeeper_keys = { + configEntries: configEntries, + clientInformation: clientInformation + } + + } + + const cert = new Cert(); + this.sessionData.cns = cert.cns(this.config.data.gatekeeper_cns); + + // create certs if they don't exist or we say so. + if( this.config.data.gatekeeper_recreatecert || + !this.config.data.gatekeeper_sslcert || + !this.config.data.gatekeeper_sslkey ) { + delete this.config.data.gatekeeper_recreatecert; + const cert = new Cert(); + console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); + try { + const result = await cert.create(this.sessionData.cns); + if( result.code === 0 ) { + this.config.data.gatekeeper_sslkey = result.key.toString(); + this.config.data.gatekeeper_sslcert = result.cert.toString(); + } else { + console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); + } + } catch( err ) { + console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); + } + } + } + + async createRandomKey( id, groups ) { + if( !id || !groups || !groups.length ) { + return; + } + const apikey = new ApiKey(); + apikey.setId(id); + apikey.setGroups(groups); + await apikey.randomiseKey(); + return apikey + } + + async writeFiles() { + + console.log( chalk.green( ' create' )+' '+configArchiveFileName ); + if( !this.config.serialize( + this.destinationPath(configArchiveFileName), + this.sessionData.configurationPassword + ) ) { + console.log(chalk.bold.red( 'error! Config archive was not written' )); + } + + const pathProps = [ + 'gatekeeper_datapath', + 'traefik_datapath', + 'proxy_datapath', + 'bitcoin_datapath', + 'lightning_datapath', + 'otsclient_datapath' + ]; + + for( let pathProp of pathProps ) { + if( this.config.data[pathProp] === '_custom' ) { + this.config.data[pathProp] = this.config.data[pathProp+'_custom'] || ''; + } + } + + this.sessionData.installationInfo = this.installationInfo(); + + for( let m of prompters ) { + const name = m.name(); + for( let t of m.templates(this.config.data) ) { + const p = path.join(name,t); + const destFile = this.destinationPath( path.join( destinationDirName, p ) ); + const targetDir = path.dirname( destFile ); + + if( !fs.existsSync(targetDir) ) { + fs.mkdirSync(targetDir, { recursive: true }); + } + const result = await ejsRenderFileAsync( this.templatePath(p), Object.assign({}, this.sessionData, this.config.data), {} ); + + console.log( chalk.green( ' create' )+' '+p ); + fs.writeFileSync( destFile, result ); + + } + } + + console.log( chalk.green( ' create' )+' '+keyArchiveFileName ); + + if( this.config.data.gatekeeper_keys && this.config.data.gatekeeper_keys.clientInformation ) { + + if( this.sessionData.gatekeeper_clientkeyspassword !== this.config.data.gatekeeper_clientkeyspassword && + fs.existsSync(this.destinationPath(keyArchiveFileName)) ) { + fs.unlinkSync( this.destinationPath(keyArchiveFileName) ); + } + + const archive = new Archive( this.destinationPath(keyArchiveFileName), this.config.data.gatekeeper_clientkeyspassword ); + if( !await archive.writeEntry( 'keys.txt', this.config.data.gatekeeper_keys.clientInformation.join('\n') ) ) { + console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); + } + if( !await archive.writeEntry( 'cacert.pem', this.config.data.gatekeeper_sslcert ) ) { + console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); + } + } + + fs.writeFileSync(path.join('/data', destinationDirName, 'exitStatus.sh'), 'EXIT_STATUS=0'); + + } + + installationInfo() { + + for( let feature of this.features ) { + feature.checked = this.isChecked( 'features', feature.value ); + } + + const cert = new Cert(); + const gatekeeper_cns = cert.cns( this.config.data.gatekeeper_cns ); + + const features = [ + { + name: 'Bitcoin core node', + label: 'bitcoin', + host: 'bitcoin', + networks: ['cyphernodenet'], + docker: 'cyphernode/bitcoin:'+this.config.docker_versions['cyphernode/bitcoin'], + extra: { + prune: this.config.data.bitcoin_prune, + prune_size: this.config.data.bitcoin_prune_size, + expose: this.config.data.bitcoin_expose, + uacomment: this.config.data.bitcoin_uacomment + } + }, + { + name: 'Gatekeeper', + label: 'gatekeeper', + host: 'gatekeeper', + networks: ['cyphernodenet'], + docker: 'cyphernode/gatekeeper:'+this.config.docker_versions['cyphernode/gatekeeper'], + extra: { + port: this.config.data.gatekeeper_port, + cns: gatekeeper_cns + } + }, + { + name: 'Proxy', + label: 'proxy', + host: 'proxy', + networks: ['cyphernodenet'], + docker: 'cyphernode/proxy:'+this.config.docker_versions['cyphernode/proxy'] + }, + { + name: 'Proxy cron', + label: 'proxycron', + host: 'proxycron', + networks: ['cyphernodenet'], + docker: 'cyphernode/proxycron:'+this.config.docker_versions['cyphernode/proxycron'] + }, + { + name: 'Pycoin', + label: 'pycoin', + host: 'pycoin', + networks: ['cyphernodenet'], + docker: 'cyphernode/pycoin:'+this.config.docker_versions['cyphernode/pycoin'] + }, + { + name: 'Notifier', + label: 'notifier', + host: 'notifier', + networks: ['cyphernodenet', 'cyphernodeappsnet'], + docker: 'cyphernode/notifier:'+this.config.docker_versions['cyphernode/notifier'] + }, + { + name: 'MQ broker', + label: 'broker', + host: 'broker', + networks: ['cyphernodenet'], + docker: 'eclipse-mosquitto:'+this.config.docker_versions['eclipse-mosquitto'] + } + + ]; + + const optional_features = []; + + const optional_features_data = { + otsclient: { + docker: "cyphernode/otsclient:" + this.config.docker_versions['cyphernode/otsclient'] + }, + lightning: { + docker: "cyphernode/clightning:"+this.config.docker_versions['cyphernode/clightning'], + extra: { + nodename: this.config.data.lightning_nodename, + nodecolor: this.config.data.lightning_nodecolor, + expose: this.config.data.lightning_expose, + external_ip: this.config.data.lightning_external_ip, + implementation: this.config.data.lightning_implementation + } + } + } + + for( let feature of this.features ) { + const f = { + active: feature.checked, + name: feature.name, + label: feature.value, + host: feature.value, + networks: ['cyphernodenet'], + docker: optional_features_data[feature.value].docker + }; + + if( feature.checked ) { + f.extra = optional_features_data[feature.value].extra + } + + optional_features.push( f ); + } + + let bitcoin_version = this.config.docker_versions['cyphernode/bitcoin']; + + if( bitcoin_version[0] === 'v' ) { + bitcoin_version = bitcoin_version.substr(1); + } + + const ii = { + api_versions: ['v0'], + setup_version: this.config.setup_version, + bitcoin_version: bitcoin_version, + features: features, + optional_features: optional_features, + devmode: this.sessionData.devmode, + + }; + + return ii; + } + + async prompt( questions ) { + if( !questions ) { + return {}; + } + + const r = await inquirer.prompt( questions ); + return r; + } + + /* some utils */ + + destinationPath( relPath ) { + return path.join( '/data', relPath ); + } + + templatePath( relPath ) { + return path.join(__dirname, '..','templates',relPath ); + } + + isChecked(name, value ) { + return this.config.data && this.config.data[name] && this.config.data[name].indexOf(value) != -1 ; + } + + getDefault(name) { + + if( this.config && this.config.data && this.config.data.hasOwnProperty(name) ) { + return this.config.data[name]; + } + + if( this.sessionData && this.sessionData.hasOwnProperty(name) ) { + return this.sessionData[name]; + } + + } + + optional(input, validator) { + if( input === undefined || + input === null || + input === '' ) { + return true; + } + return validator(input); + } + + ipOrFQDNValidator(host ) { + host = (host+"").trim(); + if( !(validator.isIP(host) || + validator.isFQDN(host)) ) { + throw new Error( 'No IP address or fully qualified domain name' ) + } + return true; + } + + xkeyValidator(xpub ) { + // TOOD: check for version + if( !coinstring.isValid( xpub ) ) { + throw new Error('Not an extended key.'); + } + return true; + } + + pathValidator(p ) { + return true; + } + + derivationPathValidator(path ) { + return true; + } + + colorValidator(color) { + if( !validator.isHexadecimal(color) ) { + throw new Error('Not a hex color.'); + } + return true; + } + + lightningNodeNameValidator(name) { + if( !name || name.length > 32 ) { + throw new Error('Please enter anything shorter than 32 characters'); + } + return true; + } + + notEmptyValidator(path ) { + if( !path ) { + throw new Error('Please enter something'); + } + return true; + } + + usernameValidator(user ) { + if( !userRegexp.test( user ) ) { + throw new Error('Choose a valid username'); + } + return true; + } + + UACommentValidator(comment ) { + if( !uaCommentRegexp.test( comment ) ) { + throw new Error('Unsafe characters in UA comment. Please use only a-z, A-Z, 0-9, SPACE and .,:_?@'); + } + return true; + } + + trimFilter(input ) { + return (input+"").trim(); + } + + featureChoices() { + return this.features; + } + + setupDir() { + return this.sessionData.setupDir; + } + + defaultDataDirBase() { + return this.sessionData.defaultDataDirBase; + } + + getHelp(topic ) { + if( !this.config.data.enablehelp || !this.help ) { + return ''; + } + + const helpText = this.help[topic] || this.help['__default__']; + + if( !helpText ||helpText === '' ) { + return ''; + } + + return "\n\n"+wrap( html2ansi(helpText),maxWidth )+"\n\n"; + } + +}; diff --git a/install/generator-cyphernode/generators/app/lib/archive.js b/cyphernodeconf_docker/lib/archive.js similarity index 95% rename from install/generator-cyphernode/generators/app/lib/archive.js rename to cyphernodeconf_docker/lib/archive.js index 4795d89..d2ff216 100644 --- a/install/generator-cyphernode/generators/app/lib/archive.js +++ b/cyphernodeconf_docker/lib/archive.js @@ -1,4 +1,3 @@ -const fs = require('fs'); const spawn = require('child_process').spawn; const stringio = require('@rauschma/stringio'); const defaultArgs = ['-t7z', '-ms=on', '-mhe=on']; @@ -16,7 +15,7 @@ module.exports = class Archive { } let args = defaultArgs.slice(); args.unshift('x'); - args.push( '-so' ); + args.push( '-so' ); if( this.password ) { args.push('-p'+this.password ); } @@ -41,7 +40,7 @@ module.exports = class Archive { if( this.password ) { args.push('-p'+this.password ); } - args.push( '-si'+entryName ); + args.push( '-si'+entryName ); args.push( this.file ) const archiver = spawn('7z', args, { stdio: ['pipe', 'ignore', 'ignore' ] } ); await stringio.streamWrite(archiver.stdin, content); diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/cyphernodeconf_docker/lib/cert.js similarity index 76% rename from install/generator-cyphernode/generators/app/lib/cert.js rename to cyphernodeconf_docker/lib/cert.js index 1a61c99..33a534b 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/cyphernodeconf_docker/lib/cert.js @@ -20,7 +20,9 @@ subjectAltName = @alt_names `; const domainTmpl = 'DNS.%#% = %DOMAIN%'; -const ipTmpl = 'IP.%#% = %IP%' +const ipTmpl = 'IP.%#% = %IP%'; + +const standardCNs = ['127.0.0.1','localhost','gatekeeper']; module.exports = class Cert { @@ -29,6 +31,18 @@ module.exports = class Cert { this.args = options.args || { days: 3650 }; } + cns( cnsString ) { + if( !cnsString ) { + return standardCNs; + } + const cns = cnsString.split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); + + if( cns.length ) { + return standardCNs.concat(cns); + } + return standardCNs; + } + buildConfig( cns ) { let ips = []; @@ -62,9 +76,9 @@ module.exports = class Cert { } async create( cns ) { - cns = cns || []; - - cns = cns.concat(['127.0.0.1','localhost','gatekeeper']); + if (!cns || !cns.length) { + throw 'No cns'; + } let args = defaultArgs.slice(); @@ -87,9 +101,9 @@ module.exports = class Cert { const conf = this.buildConfig( cns ); fs.writeFileSync( confFileTmp.name, conf ); - const openssl = spawn('openssl', args, { stdio: ['ignore', 'ignore', 'ignore'] } ); let code = await new Promise( function(resolve, reject) { + const openssl = spawn('openssl', args, { stdio: ['ignore', 'ignore', 'ignore'] } ); openssl.on('exit', (code) => { resolve(code); }); @@ -105,29 +119,7 @@ module.exports = class Cert { return { code: code, key: key, - cert: cert, - cns: cns + cert: cert } } - - getFullPath() { - return path.join( this.folder, this.filename ); - } - - async passwd( pw ) { - const openssl = spawn('openssl', [ "passwd", pw ], {stdio: ['ignore', 'pipe', 'ignore' ]}); - - const result = await new Promise( function(resolve, reject ) { - let result = ''; - openssl.stdout.on('data', (data) => { - result += data.toString(); - }); - - openssl.on('exit', (code) => { - resolve(result); - }); - }); - - return result; - } -} +}; diff --git a/cyphernodeconf_docker/lib/config.js b/cyphernodeconf_docker/lib/config.js new file mode 100644 index 0000000..f0dd512 --- /dev/null +++ b/cyphernodeconf_docker/lib/config.js @@ -0,0 +1,220 @@ +const Ajv = require('ajv'); +const fs = require('fs'); +const Archive = require('./archive.js'); +const ApiKey = require('./apikey.js'); +const name = require('./name.js'); +const colorsys = require( 'colorsys'); + + +const schemas = { + '0.1.0': require('../schema/config-v0.1.0.json'), + '0.2.0': require('../schema/config-v0.2.0.json'), + '0.2.2': require('../schema/config-v0.2.2.json') +}; + +const versionHistory = [ '0.1.0', '0.2.0', '0.2.2' ]; +const defaultSchemaVersion=versionHistory[0]; +const latestSchemaVersion=versionHistory[versionHistory.length-1]; + +module.exports = class Config { + + constructor( options ) { + + options = options || {}; + this.setup_version = options.setup_version; + this.docker_versions = options.docker_versions; + + const ajv = new Ajv({ + removeAdditional: true, + useDefaults: true, + coerceTypes: true, + allErrors: true + }); + + this.validators = {}; + + for( let v in schemas ) { + this.validators[v]=ajv.compile(schemas[v]); + } + + + this.migrations = { + '0.1.0->0.2.0': this.migrate_0_1_0_to_0_2_0, + '0.2.0->0.2.2': this.migrate_0_2_0_to_0_2_2 + }; + + this.setData( { schema_version: latestSchemaVersion } ); + this.isLoaded = false; + } + + generateMigrationPathToLatest( currentVersion ) { + if( currentVersion === latestSchemaVersion ) { + return; + } + const index = versionHistory.indexOf( currentVersion ); + if( index === -1 ) { + return; + } + + let path = []; + for( let i=index; i'+versionHistory[i+1]; + path.push( this.migrations[methodLabel] ); + } + + return path; + + } + + setData( data ) { + if( !data ) { + return; + } + this.data = data; + this.data.schema_version = this.data.schema_version || this.__version || defaultSchemaVersion; + this.data.setup_version = this.data.setup_version || this.setup_version; + this.data.docker_versions = this.data.docker_versions || this.docker_versions; + this.validate(); + } + + async serialize( path, password ) { + this.resolveConfigConflicts(); + this.validate(); + const configJsonString = JSON.stringify(this.data, null, 4); + const archive = new Archive( path, password ); + return archive.writeEntry( 'config.json', configJsonString ); + } + + async deserialize( path, password ) { + + if( fs.existsSync(path) ) { + + const archive = new Archive( path, password ); + + const r = await archive.readEntry('config.json'); + + if( r.error ) { + throw( 'Password is wrong. Have a nice day.' ); + } + + if( !r.value ) { + throw('config archive is corrupt.'); + } + + this.setData( JSON.parse(r.value) ); + this.isLoaded = true; + } + + //this.resolveConfigConflicts(); + const version = this.data.schema_version || this.data.__version || defaultSchemaVersion; + if( version !== latestSchemaVersion ) { + // migrate here + // create a copy of the old config + fs.copyFileSync( path, path+'-'+version ); + await this.migrateFrom(version); + // validate again to strip all illegal properties from config with latest version + this.validate(); + } + } + + resolveConfigConflicts() { + // TODO solve this in config schema + if( this.data.features && this.data.features.length && this.data.features.indexOf('lightning') !== -1 ) { + this.data.bitcoin_prune = false; + delete this.data.bitcoin_prune_size; + } + } + + validate() { + const version = this.data.schema_version || this.data.__version; + + if( !version || !this.validators[version] || Object.keys( schemas ).indexOf( version ) == -1 ) { + throw "Unknown version in data" + } + + // this will assign default values from the schema + this.valid = this.validators[version]( this.data ); + this.validateErrors = this.validators[version].errors; + + } + + async migrateFrom(sourceVersion) { + const migrations = this.generateMigrationPathToLatest(sourceVersion) + + if( !migrations ) { + return; + } + + for( let migration of migrations ) { + await migration.apply(this); + } + } + + // migration methods: + async migrate_0_1_0_to_0_2_0() { + const currentVersion = this.data.schema_version || this.data.__version; + if( currentVersion != '0.1.0' ) { + return; + } + + this.data.schema_version = '0.2.0'; + + // rewrite specific properties with incompatible content + + // gatekeeper_keys: add stats group to all keys and add a label containing only + // the stats group + + const gatekeeper_keys = this.data.gatekeeper_keys; + + for( let i=0; i { + + if( !password ) { + return null; + } + + password = password.replace(/'/g, `'\\''`); + + return await new Promise( (resolve) => { + exec('htpasswd -bnB admin \''+password+'\' | cut -sd \':\' -f2', (error, stdout, stderr) => { + if (error) { + return resolve(null); + } + // remove newline at the end + resolve(stdout.substr(0,stdout.length-1)); + }); + }); + + +}; + diff --git a/install/generator-cyphernode/generators/app/lib/name.js b/cyphernodeconf_docker/lib/name.js similarity index 99% rename from install/generator-cyphernode/generators/app/lib/name.js rename to cyphernodeconf_docker/lib/name.js index 7d7f187..92825e0 100644 --- a/install/generator-cyphernode/generators/app/lib/name.js +++ b/cyphernodeconf_docker/lib/name.js @@ -1,4 +1,4 @@ -const MAXLENGTH = 30; +const MAXLENGTH = 28; const ADJECTIVES = [ /*a*/ ["Abiding", diff --git a/cyphernodeconf_docker/lib/splashScreen.js b/cyphernodeconf_docker/lib/splashScreen.js new file mode 100644 index 0000000..3273e9b --- /dev/null +++ b/cyphernodeconf_docker/lib/splashScreen.js @@ -0,0 +1,135 @@ +const fs = require('fs'); +const path = require('path'); +const ansi = require( './ansi.js' ); +const lunicode = require('lunicode'); + +const easeOutCubic = (t, b, c, d) => { + return c*((t=t/d-1)*t*t+1)+b; +}; + +lunicode.tools.creepify.options.top = true; // add diacritics on top. Default: true +lunicode.tools.creepify.options.middle = true; // add diacritics in the middle. Default: true +lunicode.tools.creepify.options.bottom = true; // add diacritics on the bottom. Default: true +lunicode.tools.creepify.options.maxHeight = 15; // How many diacritic marks shall we put on top/bottom? Default: 15 +lunicode.tools.creepify.options.randomization = 100; // 0-100%. maxHeight 100 and randomization 20%: the height goes from 80 to 100. randomization 70%: height goes from 30 to 100. Default: 100 + +const fortunes = [ + 'Cause fuck central banking', + 'Not your keys, not your bitcoin', + 'Don\'t trust, verify', + 'Craig Wright is a fraud', + 'HODL!' +]; + +module.exports = class SplashScreen { + + constructor( options ) { + options = options || {}; + + if( !options.frameDir ) { + throw "no frame directory to load" + } + + this.width = options.width || 82; + this.fortuneEnabled = !!options.enableFortune; + this.fortuneSpacing = options.fortuneSpacing || 0; + this.fortuneChalk = options.fortuneChalk; + + this.loadFramesFromDir( options.frameDir ); + + if( this.fortuneEnabled ) { + + let fortune = this.fortune(); + if( fortune.length > this.width-2 ) { + fortune = fortune.substr(0,this.width-2); + } + fortune = this.center(fortune); + + let fortuneLines = []; + + + + + fortuneLines.push( this.creepify(fortune) )+'\n'; + + + + + for( let i=0; i { + this.frames.push(fs.readFileSync(path.join(__dirname,'..','splash',file))); + }); + } + + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + fortune() { + return fortunes[ Math.random()*fortunes.length << 0 ]; + } + + creepify( string ) { + if( this.fortuneChalk ) { + return this.fortuneChalk(lunicode.tools.creepify.encode( string )); + } + return lunicode.tools.creepify.encode( string ); + } + + center( string ) { + const offset = ((this.width - string.length)*0.5) << 0; + for( let i=0; i 1 ) { + await this.sleep(400); + + for( let frame of this.frames ) { + process.stdout.write(ansi.reset); + process.stdout.write(frame.toString()); + await this.sleep(33); + } + } + + await this.sleep(400); + process.stdout.write('\n'); + } + +}; \ No newline at end of file diff --git a/cyphernodeconf_docker/package-lock.json b/cyphernodeconf_docker/package-lock.json new file mode 100644 index 0000000..42a79a4 --- /dev/null +++ b/cyphernodeconf_docker/package-lock.json @@ -0,0 +1,5508 @@ +{ + "name": "cyphernodeconf", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.5.tgz", + "integrity": "sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helpers": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.4.5", + "@babel/types": "^7.4.4", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", + "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helpers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz", + "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==", + "dev": true, + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", + "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", + "dev": true + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", + "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/types": "^7.4.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + } + }, + "@babel/types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", + "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@cnakazawa/watch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", + "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "@jest/console": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", + "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", + "dev": true, + "requires": { + "@jest/source-map": "^24.3.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + } + }, + "@jest/core": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.8.0.tgz", + "integrity": "sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.8.0", + "jest-config": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve-dependencies": "^24.8.0", + "jest-runner": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "jest-watcher": "^24.8.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "strip-ansi": "^5.0.0" + } + }, + "@jest/environment": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.8.0.tgz", + "integrity": "sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0" + } + }, + "@jest/fake-timers": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.8.0.tgz", + "integrity": "sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-mock": "^24.8.0" + } + }, + "@jest/reporters": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.8.0.tgz", + "integrity": "sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.1.1", + "jest-haste-map": "^24.8.0", + "jest-resolve": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.2.1", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + } + }, + "@jest/source-map": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", + "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", + "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/types": "^24.8.0", + "@types/istanbul-lib-coverage": "^2.0.0" + } + }, + "@jest/test-sequencer": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz", + "integrity": "sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==", + "dev": true, + "requires": { + "@jest/test-result": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-runner": "^24.8.0", + "jest-runtime": "^24.8.0" + } + }, + "@jest/transform": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.8.0.tgz", + "integrity": "sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.8.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-util": "^24.8.0", + "micromatch": "^3.1.10", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + } + }, + "@jest/types": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", + "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^12.0.9" + } + }, + "@rauschma/stringio": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rauschma/stringio/-/stringio-1.4.0.tgz", + "integrity": "sha512-3uor2f/MXZkmX5RJf8r+OC3WvZVzpSme0yyL0rQDPEnatE02qRcqwEwnsgpgriEck0S/n4vWtUd6tTtrJwk45Q==", + "requires": { + "@types/node": "^10.0.3" + } + }, + "@types/babel__core": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", + "integrity": "sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.0.2.tgz", + "integrity": "sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.6.tgz", + "integrity": "sha512-XYVgHF2sQ0YblLRMLNPB3CkFMewzFmlDsH/TneZFHUXDlABQgh88uOxuez7ZcXxayLFrqLwtDH1t+FmlFwNZxw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/node": { + "version": "10.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.8.tgz", + "integrity": "sha512-I4+DbJEhLEg4/vIy/2gkWDvXBOOtPKV9EnLhYjMoqxcRW+TTZtUftkHktz/a8suoD5mUL7m6ReLrkPvSsCQQmw==" + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/yargs": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", + "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", + "dev": true + }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "dev": true + }, + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", + "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "babel-jest": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", + "integrity": "sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==", + "dev": true, + "requires": { + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.6.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz", + "integrity": "sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "babel-plugin-jest-hoist": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz", + "integrity": "sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz", + "integrity": "sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.6.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bs58": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-2.0.1.tgz", + "integrity": "sha1-VZCNWPGYKrogCPob7Y+RmYopv40=" + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "coinstring": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/coinstring/-/coinstring-2.3.0.tgz", + "integrity": "sha1-zbYzY6lhUCQEolr7gsLibV/2J6Q=", + "requires": { + "bs58": "^2.0.1", + "create-hash": "^1.1.1" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colorsys": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/colorsys/-/colorsys-1.0.22.tgz", + "integrity": "sha512-KCqF23oqkOD0IUCTLCl0obwGIMyeGFlNWuJ4oRRVKmawvKQeb3x5UvajVeH9AShZWU9hNaIhjXeTGw3iPNtl/Q==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true, + "optional": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "cssom": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", + "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", + "dev": true + }, + "cssstyle": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", + "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff-sequences": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", + "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", + "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.0.0" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "exec-sh": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", + "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expect": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", + "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-regex-util": "^24.3.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "^2.0.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", + "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", + "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.11", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "jest": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.8.0.tgz", + "integrity": "sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==", + "dev": true, + "requires": { + "import-local": "^2.0.0", + "jest-cli": "^24.8.0" + }, + "dependencies": { + "jest-cli": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.8.0.tgz", + "integrity": "sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==", + "dev": true, + "requires": { + "@jest/core": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^12.0.2" + } + } + } + }, + "jest-changed-files": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.8.0.tgz", + "integrity": "sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + } + }, + "jest-config": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.8.0.tgz", + "integrity": "sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.8.0", + "@jest/types": "^24.8.0", + "babel-jest": "^24.8.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.8.0", + "jest-environment-node": "^24.8.0", + "jest-get-type": "^24.8.0", + "jest-jasmine2": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.8.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", + "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.3.0", + "jest-get-type": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-docblock": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.3.0.tgz", + "integrity": "sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==", + "dev": true, + "requires": { + "detect-newline": "^2.1.0" + } + }, + "jest-each": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", + "integrity": "sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.8.0", + "jest-util": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-environment-jsdom": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz", + "integrity": "sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/fake-timers": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-util": "^24.8.0", + "jsdom": "^11.5.1" + } + }, + "jest-environment-node": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.8.0.tgz", + "integrity": "sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/fake-timers": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-util": "^24.8.0" + } + }, + "jest-get-type": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", + "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==", + "dev": true + }, + "jest-haste-map": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.8.0.tgz", + "integrity": "sha512-ZBPRGHdPt1rHajWelXdqygIDpJx8u3xOoLyUBWRW28r3tagrgoepPrzAozW7kW9HrQfhvmiv1tncsxqHJO1onQ==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.4.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", + "integrity": "sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.8.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "pretty-format": "^24.8.0", + "throat": "^4.0.0" + } + }, + "jest-leak-detector": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz", + "integrity": "sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==", + "dev": true, + "requires": { + "pretty-format": "^24.8.0" + } + }, + "jest-matcher-utils": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", + "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.8.0", + "jest-get-type": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-message-util": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", + "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.8.0.tgz", + "integrity": "sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", + "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==", + "dev": true + }, + "jest-resolve": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.8.0.tgz", + "integrity": "sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz", + "integrity": "sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.8.0" + } + }, + "jest-runner": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.8.0.tgz", + "integrity": "sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.8.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.8.0", + "jest-jasmine2": "^24.8.0", + "jest-leak-detector": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-resolve": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + } + }, + "jest-runtime": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.8.0.tgz", + "integrity": "sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.8.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/yargs": "^12.0.2", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^12.0.2" + } + }, + "jest-serializer": { + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.4.0.tgz", + "integrity": "sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==", + "dev": true + }, + "jest-snapshot": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.8.0.tgz", + "integrity": "sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "expect": "^24.8.0", + "jest-diff": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-resolve": "^24.8.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.8.0", + "semver": "^5.5.0" + } + }, + "jest-util": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.8.0.tgz", + "integrity": "sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/fake-timers": "^24.8.0", + "@jest/source-map": "^24.3.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + } + }, + "jest-validate": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.8.0.tgz", + "integrity": "sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "camelcase": "^5.0.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.8.0", + "leven": "^2.1.0", + "pretty-format": "^24.8.0" + } + }, + "jest-watcher": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.8.0.tgz", + "integrity": "sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==", + "dev": true, + "requires": { + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/yargs": "^12.0.9", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.8.0", + "string-length": "^2.0.0" + } + }, + "jest-worker": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", + "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", + "dev": true, + "requires": { + "merge-stream": "^1.0.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lunicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lunicode/-/lunicode-2.0.1.tgz", + "integrity": "sha512-tFXPCYVUH9NENfyAfsyj477g2oN/W0aJumjODy0QbIZ4c68dLoGzCc6Ovr46QpF+rMuHa8Qgku6nrMjK78WeDw==" + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", + "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", + "dev": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "pretty-format": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", + "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prompts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", + "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", + "dev": true, + "requires": { + "kleur": "^3.0.2", + "sisteransi": "^1.0.0" + } + }, + "psl": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "dev": true, + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", + "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rsvp": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.4.tgz", + "integrity": "sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==", + "dev": true + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sisteransi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz", + "integrity": "sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "table": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.0.tgz", + "integrity": "sha512-nHFDrxmbrkU7JAFKqKbDJXfzrX2UBsWmrieXFTGxiI5e4ncg3VqsZeI4EzNmX0ncp4XNGVeoxIWJXfCIXwrsvw==", + "dev": true, + "requires": { + "ajv": "^6.9.1", + "lodash": "^4.17.11", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "requires": { + "rimraf": "^2.6.3" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + }, + "dependencies": { + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/cyphernodeconf_docker/package.json b/cyphernodeconf_docker/package.json new file mode 100644 index 0000000..16bce4b --- /dev/null +++ b/cyphernodeconf_docker/package.json @@ -0,0 +1,37 @@ +{ + "name": "cyphernodeconf", + "version": "0.0.0", + "description": "", + "homepage": "", + "scripts": { + "test": "jest", + "coverageHtml": "jest --coverage --coverageReporters html", + "lint": "./node_modules/.bin/eslint lib/**/*.js", + "lintfix": "./node_modules/.bin/eslint lib/**/**.js --fix" + }, + "author": "jash ", + "main": "lib/index.js", + "keywords": [ + "cyphernode" + ], + "dependencies": { + "@rauschma/stringio": "^1.4.0", + "ajv": "^6.10.0", + "chalk": "^2.4.2", + "coinstring": "^2.3.0", + "colorsys": "^1.0.22", + "ejs": "^2.6.1", + "inquirer": "^6.3.1", + "lunicode": "^2.0.1", + "parse5": "^5.1.0", + "tmp": "^0.1.0", + "validator": "^10.11.0", + "wrap-ansi": "^5.1.0" + }, + "devDependencies": { + "eslint": "^5.16.0", + "jest": "^24.8.0" + }, + "repository": "git@github.com:schulterklopfer/cyphernodeconf.git", + "license": "MIT" +} diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/cyphernodeconf_docker/prompters/000_cyphernode.js similarity index 62% rename from install/generator-cyphernode/generators/app/prompters/000_cyphernode.js rename to cyphernodeconf_docker/prompters/000_cyphernode.js index 374b936..f5bde84 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/cyphernodeconf_docker/prompters/000_cyphernode.js @@ -20,27 +20,30 @@ module.exports = { // input, confirm, list, rawlist, expand, checkbox, password, editor type: 'checkbox', name: 'features', - message: prefix()+'What features do you want to add to your cyphernode?'+utils._getHelp('features'), - choices: utils._featureChoices() + message: prefix()+'What features do you want to add to your cyphernode?'+utils.getHelp('features'), + choices: utils.featureChoices() }, { type: 'list', name: 'net', - default: utils._getDefault( 'net' ), - message: prefix()+'What net do you want to run on?'+utils._getHelp('net'), + default: utils.getDefault( 'net' ), + message: prefix()+'What net do you want to run on?'+utils.getHelp('net'), choices: [{ name: "Testnet", value: "testnet" },{ name: "Mainnet", value: "mainnet" + },{ + name: "RegTest", + value: "regtest" }] }, { type: 'confirm', name: 'run_as_different_user', - default: utils._getDefault( 'run_as_different_user' ), - message: prefix()+'Run as different user?'+utils._getHelp('run_as_different_user') + default: utils.getDefault( 'run_as_different_user' ), + message: prefix()+'Run as different user?'+utils.getHelp('run_as_different_user') }, { when: function( props ) { @@ -48,16 +51,16 @@ module.exports = { }, type: 'input', name: 'username', - default: utils._getDefault( 'username' ), - message: prefix()+'What username will cyphernode run under?'+utils._getHelp('username'), - filter: utils._trimFilter, - validate: utils._usernameValidator + default: utils.getDefault( 'username' ), + message: prefix()+'What username will cyphernode run under?'+utils.getHelp('username'), + filter: utils.trimFilter, + validate: utils.usernameValidator }, { type: 'confirm', name: 'use_xpub', - default: utils._getDefault( 'use_xpub' )||false, - message: prefix()+'Use a default xpub key to watch or generate adresses?'+utils._getHelp('use_xpub'), + default: utils.getDefault( 'use_xpub' )||false, + message: prefix()+'Use a default xpub key to watch or generate adresses?'+utils.getHelp('use_xpub'), }, { when: function( props ) { @@ -65,10 +68,10 @@ module.exports = { }, type: 'input', name: 'xpub', - default: utils._getDefault( 'xpub' ), - message: prefix()+'What is your default xpub key?'+utils._getHelp('xpub'), - filter: utils._trimFilter, - validate: utils._xkeyValidator + default: utils.getDefault( 'xpub' ), + message: prefix()+'What is your default xpub key?'+utils.getHelp('xpub'), + filter: utils.trimFilter, + validate: utils.xkeyValidator }, { when: function( props ) { @@ -76,13 +79,13 @@ module.exports = { }, type: 'input', name: 'derivation_path', - default: utils._getDefault( 'derivation_path' ), - message: prefix()+'What is your default derivation path?'+utils._getHelp('derivation_path'), - filter: utils._trimFilter, - validate: utils._derivationPathValidator + default: utils.getDefault( 'derivation_path' ), + message: prefix()+'What is your default derivation path?'+utils.getHelp('derivation_path'), + filter: utils.trimFilter, + validate: utils.derivationPathValidator }]; }, templates: function( props ) { - return []; + return ['info.json']; } }; diff --git a/cyphernodeconf_docker/prompters/010_gatekeeper.js b/cyphernodeconf_docker/prompters/010_gatekeeper.js new file mode 100644 index 0000000..97d496b --- /dev/null +++ b/cyphernodeconf_docker/prompters/010_gatekeeper.js @@ -0,0 +1,95 @@ +const chalk = require('chalk'); + +const name = 'gatekeeper'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + +const hasAuthKeys = function( props ) { + return props && + props.gatekeeper_keys && + props.gatekeeper_keys.configEntries && + props.gatekeeper_keys.configEntries.length > 0; +} + +const hasCert = function( props ) { + return props && + props.gatekeeper_sslkey && + props.gatekeeper_sslcert +} + +let password = ''; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + // TODO: delete clientKeys archive when password chnages + return [{ + type: 'password', + name: 'gatekeeper_clientkeyspassword', + default: utils.getDefault( 'gatekeeper_clientkeyspassword' ), + message: prefix()+'Enter a password to protect your client keys with'+utils.getHelp('gatekeeper_clientkeyspassword'), + filter: utils.trimFilter, + validate: utils.notEmptyValidator + }, + { + when: function( props ) { + // hacky hack + password = props.gatekeeper_clientkeyspassword; + return true; + }, + type: 'password', + name: 'gatekeeper_clientkeyspassword_c', + default: utils.getDefault( 'gatekeeper_clientkeyspassword_c' ), + message: prefix()+'Confirm your client keys password.'+utils.getHelp('gatekeeper_clientkeyspassword_c'), + filter: utils.trimFilter, + validate: function( input ) { + if(input !== password) { + throw new Error( 'Client keys passwords do not match' ); + } + return true; + } + }, + { + type: 'input', + name: 'gatekeeper_port', + default: utils.getDefault( 'gatekeeper_port' ), + message: prefix()+'The port gatekeeper will listen on for requests'+utils.getHelp('gatekeeper_port'), + filter: utils.trimFilter, + validate: function( port ) { + return utils.notEmptyValidator( port ) && !isNaN( parseInt(port) ) + } + }, + { + when: function() { return hasAuthKeys( utils.props ); }, + type: 'confirm', + name: 'gatekeeper_recreatekeys', + default: false, + message: prefix()+'Recreate gatekeeper keys?'+utils.getHelp('gatekeeper_recreatekeys') + }, + { + when: function() { return hasCert( utils.props ); }, + type: 'confirm', + name: 'gatekeeper_recreatecert', + default: false, + message: prefix()+'Recreate gatekeeper certificate?'+utils.getHelp('gatekeeper_recreatecert') + }, + { + when: function(props) { return !hasCert( utils.props ) || props.gatekeeper_recreatecert }, + type: 'input', + name: 'gatekeeper_cns', + default: utils.getDefault( 'gatekeeper_cns' ), + message: prefix()+'Gatekeeper cert CNS (ips, domains, wildcard domains seperated by comma)?'+utils.getHelp('gatekeeper_cns') + }]; + }, + templates: function( props ) { + return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem', 'default.conf' ]; + } +}; diff --git a/cyphernodeconf_docker/prompters/030_traefik.js b/cyphernodeconf_docker/prompters/030_traefik.js new file mode 100644 index 0000000..086ae3b --- /dev/null +++ b/cyphernodeconf_docker/prompters/030_traefik.js @@ -0,0 +1,44 @@ +const chalk = require('chalk'); + +const name = 'traefik'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [ + { + type: 'input', + name: 'traefik_http_port', + default: utils.getDefault( 'traefik_http_port' ), + message: prefix()+'The HTTP port your apps will be accessible to the internet on.'+utils.getHelp('traefik_http_port'), + filter: utils.trimFilter, + validate: function( port ) { + return utils.notEmptyValidator( port ) && !isNaN( parseInt(port) ) + } + }, + { + type: 'input', + name: 'traefik_https_port', + default: utils.getDefault( 'traefik_https_port' ), + message: prefix()+'The HTTPS port your apps will be accessible to the internet on.'+utils.getHelp('traefik_https_port'), + filter: utils.trimFilter, + validate: function( port ) { + return utils.notEmptyValidator( port ) && !isNaN( parseInt(port) ) + } + } + ]; + }, + templates: function( props ) { + return [ 'acme.json', 'traefik.toml', 'htpasswd' ]; + } +}; diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/cyphernodeconf_docker/prompters/100_lightning.js similarity index 53% rename from install/generator-cyphernode/generators/app/prompters/200_lightning.js rename to cyphernodeconf_docker/prompters/100_lightning.js index 5da99a9..97a5c1d 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/cyphernodeconf_docker/prompters/100_lightning.js @@ -17,7 +17,7 @@ const featureCondition = function(props) { const templates = { 'lnd': [ path.join('lnd','lnd.conf') ], - 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf') ] + 'c-lightning': [ path.join('c-lightning','config') ] }; module.exports = { @@ -31,8 +31,8 @@ module.exports = { when: featureCondition, type: 'list', name: 'lightning_implementation', - default: utils._getDefault( 'lightning_implementation' ), - message: prefix()+'What lightning implementation do you want to use?'+utils._getHelp('lightning_implementation'), + default: utils.getDefault( 'lightning_implementation' ), + message: prefix()+'What lightning implementation do you want to use?'+utils.getHelp('lightning_implementation'), choices: [ { name: 'C-lightning', @@ -46,36 +46,52 @@ module.exports = { ] }, */ + { + when: featureCondition, + type: 'confirm', + name: 'lightning_announce', + default: utils.getDefault( 'lightning_announce' ), + message: prefix()+'Do you want to announce your lightning node?'+utils.getHelp('lightning_announce'), + }, + { + when: (props) => { return featureCondition(props) && props.lightning_announce }, + type: 'input', + name: 'lightning_external_ip', + default: utils.getDefault( 'lightning_external_ip' ), + filter: utils.trimFilter, + validate: utils.ipOrFQDNValidator, + message: prefix()+'What external ip does your lightning node have?'+utils.getHelp('lightning_external_ip'), + }, { when: featureCondition, type: 'input', name: 'lightning_nodename', - default: utils._getDefault( 'lightning_nodename' ), - filter: utils._trimFilter, + default: utils.getDefault( 'lightning_nodename' ), + filter: utils.trimFilter, validate: (input)=>{ if( !input.trim() ) { return true; } - return utils._lightningNodeNameValidator(input); + return utils.lightningNodeNameValidator(input); }, - message: prefix()+'What name has your lightning node?'+utils._getHelp('lightning_nodename'), + message: prefix()+'What name has your lightning node?'+utils.getHelp('lightning_nodename'), }, { when: featureCondition, type: 'input', name: 'lightning_nodecolor', - default: utils._getDefault( 'lightning_nodecolor' ), - filter: utils._trimFilter, + default: utils.getDefault( 'lightning_nodecolor' ), + filter: utils.trimFilter, validate: (input)=>{ if( !input.trim() ) { return true; } - return utils._colorValidator(input); + return utils.colorValidator(input); }, - message: prefix()+'What color has your lightning node?'+utils._getHelp('lightning_nodecolor'), + message: prefix()+'What color has your lightning node?'+utils.getHelp('lightning_nodecolor'), }]; }, templates: function( props ) { - return templates[props.lightning_implementation] + return featureCondition(props)?templates[props.lightning_implementation]:[]; } }; diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/cyphernodeconf_docker/prompters/900_bitcoin.js similarity index 54% rename from install/generator-cyphernode/generators/app/prompters/100_bitcoin.js rename to cyphernodeconf_docker/prompters/900_bitcoin.js index 62d7862..4dc2a44 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/cyphernodeconf_docker/prompters/900_bitcoin.js @@ -23,7 +23,7 @@ const bitcoinInternalAndPrune = function(props) { }; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -31,16 +31,12 @@ module.exports = { { type: 'list', name: 'bitcoin_mode', - default: utils._getDefault( 'bitcoin_mode' ), - message: prefix()+'Where is your bitcoin full node running?'+utils._getHelp('bitcoin_mode'), + default: utils.getDefault( 'bitcoin_mode' ), + message: prefix()+'Cyphernode will manage your bitcoin full node.'+utils.getHelp('bitcoin_mode'), choices: [ { - name: 'Nowhere! I want cyphernode to run one.', + name: 'Ok. That is awesome', value: 'internal' - }, - { - name: 'I have a full node running.', - value: 'external' } ] }, @@ -48,38 +44,42 @@ module.exports = { when: bitcoinExternal, type: 'input', name: 'bitcoin_node_ip', - default: utils._getDefault( 'bitcoin_node_ip' ), - filter: utils._trimFilter, - validate: utils._ipOrFQDNValidator, - message: prefix()+'What is your full node ip address?'+utils._getHelp('bitcoin_node_ip'), + default: utils.getDefault( 'bitcoin_node_ip' ), + filter: utils.trimFilter, + validate: utils.ipOrFQDNValidator, + message: prefix()+'What is your full node ip address?'+utils.getHelp('bitcoin_node_ip'), }, { type: 'input', name: 'bitcoin_rpcuser', - default: utils._getDefault( 'bitcoin_rpcuser' ), - message: prefix()+'Name of bitcoin rpc user?'+utils._getHelp('bitcoin_rpcuser'), - filter: utils._trimFilter, + default: utils.getDefault( 'bitcoin_rpcuser' ), + message: prefix()+'Name of bitcoin rpc user?'+utils.getHelp('bitcoin_rpcuser'), + filter: utils.trimFilter, }, { type: 'password', name: 'bitcoin_rpcpassword', - default: utils._getDefault( 'bitcoin_rpcpassword' ), - message: prefix()+'Password of bitcoin rpc user?'+utils._getHelp('bitcoin_rpcpassword'), - filter: utils._trimFilter, + default: utils.getDefault( 'bitcoin_rpcpassword' ), + message: prefix()+'Password of bitcoin rpc user?'+utils.getHelp('bitcoin_rpcpassword'), + filter: utils.trimFilter, }, { - when: bitcoinInternal, + when: function(props) { + return bitcoinInternal( props ) && props.features.indexOf('lightning') === -1; + }, type: 'confirm', name: 'bitcoin_prune', - default: utils._getDefault( 'bitcoin_prune' ), - message: prefix()+'Run bitcoin node in prune mode?'+utils._getHelp('bitcoin_prune'), + default: utils.getDefault( 'bitcoin_prune' ), + message: prefix()+'Run bitcoin node in prune mode?'+utils.getHelp('bitcoin_prune'), }, { - when: bitcoinInternalAndPrune, + when: function(props) { + return bitcoinInternalAndPrune( props ) && props.features.indexOf('lightning') === -1; + }, type: 'input', name: 'bitcoin_prune_size', - default: utils._getDefault( 'bitcoin_prune_size' ), - message: prefix()+'What is the maximum size of your blockchain data in megabytes?'+utils._getHelp('bitcoin_prune_size'), + default: utils.getDefault( 'bitcoin_prune_size' ), + message: prefix()+'What is the maximum size of your blockchain data in megabytes?'+utils.getHelp('bitcoin_prune_size'), validate: function( input ) { if( ! /^\d+$/.test(input) ) { throw new Error( "Not a number"); @@ -94,16 +94,16 @@ module.exports = { when: bitcoinInternal, type: 'input', name: 'bitcoin_uacomment', - default: utils._getDefault( 'bitcoin_uacomment' ), - message: prefix()+'Any UA comment?'+utils._getHelp('bitcoin_uacomment'), - filter: utils._trimFilter, - validate: (input)=> {return utils._optional(input,utils._UACommentValidator) } + default: utils.getDefault( 'bitcoin_uacomment' ), + message: prefix()+'Any UA comment?'+utils.getHelp('bitcoin_uacomment'), + filter: utils.trimFilter, + validate: (input)=> {return utils.optional(input,utils.UACommentValidator) } }]; }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1' }, templates: function( props ) { - return ['bitcoin.conf'] + return ['bitcoin.conf', 'bitcoin-client.conf'] } }; \ No newline at end of file diff --git a/cyphernodeconf_docker/prompters/999_installer.js b/cyphernodeconf_docker/prompters/999_installer.js new file mode 100644 index 0000000..1d3ca18 --- /dev/null +++ b/cyphernodeconf_docker/prompters/999_installer.js @@ -0,0 +1,309 @@ +const path = require('path'); +const chalk = require('chalk'); + +const name = 'installer'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + +const installerDocker = function(props) { + return props.installer_mode === 'docker' +}; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + type: 'list', + name: 'installer_mode', + default: utils.getDefault( 'installer_mode' ), + message: prefix()+chalk.red('Where do you want to install cyphernode?')+utils.getHelp('installer_mode'), + choices: [{ + name: "Docker", + value: "docker" + }] + }, + { + when: installerDocker, + type: 'list', + name: 'traefik_datapath', + default: utils.getDefault( 'traefik_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/traefik", + value: utils.setupDir()+"/cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/traefik", + value: utils.defaultDataDirBase()+"/cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/traefik", + value: utils.defaultDataDirBase()+"/.cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase()+"/traefik", + value: utils.defaultDataDirBase()+"/traefik" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your traefik data?'+utils.getHelp('traefik_datapath'), + }, + { + when: (props)=>{ return installerDocker(props) && (props.traefik_datapath === '_custom') }, + type: 'input', + name: 'traefik_datapath_custom', + default: utils.getDefault( 'traefik_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Custom path for traefik data?'+utils.getHelp('traefik_datapath_custom'), + }, + { + when: installerDocker, + type: 'list', + name: 'gatekeeper_datapath', + default: utils.getDefault( 'gatekeeper_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/gatekeeper", + value: utils.setupDir()+"/cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/gatekeeper", + value: utils.defaultDataDirBase()+"/cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/gatekeeper", + value: utils.defaultDataDirBase()+"/.cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase()+"/gatekeeper", + value: utils.defaultDataDirBase()+"/gatekeeper" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your gatekeeper data?'+utils.getHelp('gatekeeper_datapath'), + }, + { + when: (props)=>{ return installerDocker(props) && (props.gatekeeper_datapath === '_custom') }, + type: 'input', + name: 'gatekeeper_datapath_custom', + default: utils.getDefault( 'gatekeeper_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Custom path for gatekeeper data?'+utils.getHelp('gatekeeper_datapath_custom'), + }, + { + when: installerDocker, + type: 'list', + name: 'proxy_datapath', + default: utils.getDefault( 'proxy_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/proxy", + value: utils.setupDir()+"/cyphernode/proxy" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/proxy", + value: utils.defaultDataDirBase()+"/cyphernode/proxy" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/proxy", + value: utils.defaultDataDirBase()+"/.cyphernode/proxy" + }, + { + name: utils.defaultDataDirBase()+"/proxy", + value: utils.defaultDataDirBase()+"/proxy" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your proxy data?'+utils.getHelp('proxy_datapath'), + }, + { + when: (props)=>{ return installerDocker(props) && (props.proxy_datapath === '_custom') }, + type: 'input', + name: 'proxy_datapath_custom', + default: utils.getDefault( 'proxy_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Custom path for your proxy data?'+utils.getHelp('proxy_datapath_custom'), + }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, + type: 'list', + name: 'bitcoin_datapath', + default: utils.getDefault( 'bitcoin_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/bitcoin", + value: utils.setupDir()+"/cyphernode/bitcoin" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/bitcoin", + value: utils.defaultDataDirBase()+"/cyphernode/bitcoin" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/bitcoin", + value: utils.defaultDataDirBase()+"/.cyphernode/bitcoin" + }, + { + name: utils.defaultDataDirBase()+"/bitcoin", + value: utils.defaultDataDirBase()+"/bitcoin" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your bitcoin full node data?'+utils.getHelp('bitcoin_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' && props.bitcoin_datapath === '_custom' }, + type: 'input', + name: 'bitcoin_datapath_custom', + default: utils.getDefault( 'bitcoin_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Custom path for your bitcoin full node data?'+utils.getHelp('bitcoin_datapath_custom'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, + type: 'list', + name: 'lightning_datapath', + default: utils.getDefault( 'lightning_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/lightning", + value: utils.setupDir()+"/cyphernode/lightning" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/lightning", + value: utils.defaultDataDirBase()+"/cyphernode/lightning" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/lightning", + value: utils.defaultDataDirBase()+"/.cyphernode/lightning" + }, + { + name: utils.defaultDataDirBase()+"/lightning", + value: utils.defaultDataDirBase()+"/lightning" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your lightning node data?'+utils.getHelp('lightning_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 && props.lightning_datapath === '_custom'}, + type: 'input', + name: 'lightning_datapath_custom', + default: utils.getDefault( 'lightning_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Custom path for your lightning node data?'+utils.getHelp('lightning_datapath_custom'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 }, + type: 'list', + name: 'otsclient_datapath', + default: utils.getDefault( 'otsclient_datapath' ), + choices: [ + { + name: utils.setupDir()+"/cyphernode/otsclient", + value: utils.setupDir()+"/cyphernode/otsclient" + }, + { + name: utils.defaultDataDirBase()+"/cyphernode/otsclient", + value: utils.defaultDataDirBase()+"/cyphernode/otsclient" + }, + { + name: utils.defaultDataDirBase()+"/.cyphernode/otsclient", + value: utils.defaultDataDirBase()+"/.cyphernode/otsclient" + }, + { + name: utils.defaultDataDirBase()+"/otsclient", + value: utils.defaultDataDirBase()+"/otsclient" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your OTS data?'+utils.getHelp('otsclient_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 && props.otsclient_datapath === '_custom' }, + type: 'input', + name: 'otsclient_datapath_custom', + default: utils.getDefault( 'otsclient_datapath_custom' ), + filter: utils.trimFilter, + validate: utils.pathValidator, + message: prefix()+'Where is your otsclient data?'+utils.getHelp('otsclient_datapath_custom'), + }, + { + type: 'confirm', + name: 'gatekeeper_expose', + default: utils.getDefault( 'gatekeeper_expose' ), + message: prefix()+'Expose gatekeeper outside of the docker network?'+utils.getHelp('gatekeeper_expose'), + }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, + type: 'confirm', + name: 'bitcoin_expose', + default: utils.getDefault( 'bitcoin_expose' ), + message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils.getHelp('bitcoin_expose'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, + type: 'confirm', + name: 'lightning_expose', + default: utils.getDefault( 'lightning_expose' ), + message: prefix()+'Expose lightning node outside of the docker network?'+utils.getHelp('lightning_expose'), + }, + { + when: installerDocker, + type: 'list', + name: 'docker_mode', + default: utils.getDefault( 'docker_mode' ), + message: prefix()+'What docker mode: docker swarm or docker-compose?'+utils.getHelp('docker_mode'), + choices: [{ + name: "docker swarm", + value: "swarm" + }, + { + name: "docker-compose", + value: "compose" + }] + }, + { + type: 'confirm', + name: 'installer_cleanup', + default: utils.getDefault( 'installer_cleanup' ), + message: prefix()+'Cleanup installer after installation?'+utils.getHelp('installer_cleanup'), + }]; + }, + templates: function( props ) { + if( props.installer_mode === 'docker' ) { + return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh', 'testdeployment.sh', path.join('docker', 'docker-compose.yaml')]; + } + return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh', 'testdeployment.sh']; + } +}; diff --git a/cyphernodeconf_docker/run.sh b/cyphernodeconf_docker/run.sh new file mode 100755 index 0000000..0a9d49d --- /dev/null +++ b/cyphernodeconf_docker/run.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +export SETUP_DIR=$(pwd)/../dist +export DEFAULT_USER=$USER +export DEFAULT_CERT_HOSTNAME=disk0book.local +export PROXYCRON_VERSION=v0.2.4 +export PYCOIN_VERSION=v0.2.4 +export SETUP_VERSION=v0.2.4 +export BITCOIN_VERSION=v0.18.0 +export LIGHTNING_VERSION=v0.7.1 +export DEFAULT_DATADIR_BASE=$HOME +export GATEKEEPER_VERSION=v0.2.4 +export PROXY_VERSION=v0.2.4 +export OTSCLIENT_VERSION=v0.2.4 +export NOTIFIER_VERSION=v0.2.4 +export EDITOR=/usr/bin/nano + +user=$(id -u):$(id -g) + +if [ "${MODE}" = 'docker' ]; then + docker build . -t cyphernodeconf:local + docker run -v $(pwd)/testinst:/data \ + -e DEFAULT_USER=jash \ + -e DEFAULT_DATADIR_BASE=$HOME \ + -e SETUP_DIR=$SETUP_DIR \ + -e DEFAULT_CERT_HOSTNAME=$(hostname) \ + -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ + -e PROXY_VERSION=$PROXY_VERSION \ + -e NOTIFIER_VERSION=$NOTIFIER_VERSION \ + -e PROXYCRON_VERSION=$PROXYCRON_VERSION \ + -e OTSCLIENT_VERSION=$OTSCLIENT_VERSION \ + -e PYCOIN_VERSION=$PYCOIN_VERSION \ + -e BITCOIN_VERSION=$BITCOIN_VERSION \ + -e LIGHTNING_VERSION=$LIGHTNING_VERSION \ + -e SETUP_VERSION=$SETUP_VERSION \ + -e DEFAULT_USER=$DEFAULT_USER \ + --log-driver=none \ + --network none \ + --rm -it cyphernodeconf:local $user node index.js $@ +else + /usr/local/bin/node index.js $@ +fi diff --git a/cyphernodeconf_docker/schema/config-v0.1.0.json b/cyphernodeconf_docker/schema/config-v0.1.0.json new file mode 100644 index 0000000..044e2f8 --- /dev/null +++ b/cyphernodeconf_docker/schema/config-v0.1.0.json @@ -0,0 +1,466 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://cyphernode.io/config-v0.1.0.json", + "type": "object", + "title": "Cyphernode config file structure v0.1.0", + "additionalProperties": false, + "required": [ + "__version", + "features", + "net", + "xpub", + "derivation_path", + "installer_mode", + "run_as_different_user", + "username", + "docker_mode", + "bitcoin_rpcuser", + "bitcoin_rpcpassword", + "bitcoin_prune", + "bitcoin_datapath", + "bitcoin_node_ip", + "bitcoin_mode", + "bitcoin_expose", + "gatekeeper_apiproperties", + "gatekeeper_keys", + "gatekeeper_sslcert", + "gatekeeper_sslkey", + "gatekeeper_cns", + "gatekeeper_clientkeyspassword", + "gatekeeper_clientkeyspassword_c", + "gatekeeper_statuspw", + "gatekeeper_datapath", + "lightning_expose", + "lightning_external_ip", + "lightning_implementation", + "lightning_datapath", + "lightning_nodename", + "lightning_nodecolor", + "otsclient_datapath", + "proxy_datapath" + ], + "allOf": [ + { + "if": { + "properties": { + "run_as_different_user": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "username" + ] + } + }, + { + "if": { + "properties": { + "bitcoin_prune": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "bitcoin_prune_size" + ] + } + } + ], + "properties": { + "__version": { + "type": "string", + "enum": ["0.1.0"], + "default": "0.1.0", + "examples": ["0.1.0"] + }, + "features": { + "$id": "#/properties/features", + "type": "array", + "title": "The optional features of this cyphernode", + "default": [], + "items": { + "$id": "#/properties/features/items", + "type": "string", + "enum": [ + "lightning", + "otsclient" + ], + "title": "The feature", + "examples": [ + "lightning", + "otsclient" + ] + } + }, + "net": { + "$id": "#/properties/net", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ], + "title": "The net cyphernode is running on", + "default": "testnet", + "examples": [ + "testnet" + ] + }, + "xpub": { + "$id": "#/properties/xpub", + "type": "string", + "title": "Default xpub to derive addresses from", + "pattern": "^(\\w+)$" + }, + "derivation_path": { + "$id": "#/properties/derivation_path", + "type": "string", + "title": "Default derivation path", + "default": "0/n", + "examples": [ + "0/n" + ] + }, + "installer_mode": { + "$id": "#/properties/installer_mode", + "type": "string", + "enum": [ + "docker" + ], + "title": "Install mode", + "default": "docker", + "examples": [ + "docker" + ] + }, + "run_as_different_user": { + "$id": "#/properties/run_as_different_user", + "type": "boolean", + "title": "Run as different user", + "default": true, + "examples": [ + true + ] + }, + "username": { + "$id": "#/properties/username", + "type": "string", + "title": "Username to run under", + "default": "cyphernode", + "examples": [ + "cyphernode" + ] + }, + "docker_mode": { + "$id": "#/properties/docker_mode", + "type": "string", + "enum": [ + "swarm", + "compose" + ], + "title": "How to run the containers", + "default": "compose", + "examples": [ + "compose" + ] + }, + "bitcoin_rpcuser": { + "$id": "#/properties/bitcoin_rpcuser", + "type": "string", + "title": "Bitcoin rpc user", + "default": "bitcoin", + "examples": [ + "bitcoin" + ] + }, + "bitcoin_rpcpassword": { + "$id": "#/properties/bitcoin_rpcpassword", + "type": "string", + "title": "Bitcoin rpc password", + "default": "CHANGEME", + "examples": [ + "CHANGEME" + ] + }, + "bitcoin_uacomment": { + "$id": "#/properties/bitcoin_uacomment", + "type": "string", + "title": "Bitcoin user agent comment", + "examples": [ + "cyphernode" + ] + }, + "bitcoin_prune": { + "$id": "#/properties/bitcoin_prune", + "type": "boolean", + "title": "Bitcoin prune", + "default": false, + "examples": [ + "false" + ] + }, + "bitcoin_prune_size": { + "$id": "#/properties/bitcoin_prune_size", + "type": "integer", + "title": "Bitcoin prune size", + "default": 550, + "examples": [ + 550 + ] + }, + "bitcoin_datapath": { + "$id": "#/properties/bitcoin_datapath", + "type": "string", + "title": "Bitcoin datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "bitcoin_datapath_custom": { + "$id": "#/properties/bitcoin_datapath_custom", + "type": "string", + "title": "Bitcoin custom datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "lightning_datapath": { + "$id": "#/properties/lightning_datapath", + "type": "string", + "title": "Lightning datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "lightning_datapath_custom": { + "$id": "#/properties/lightning_datapath_custom", + "type": "string", + "title": "Lightning custom datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "proxy_datapath": { + "$id": "#/properties/proxy_datapath", + "type": "string", + "title": "Proxy datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "proxy_datapath_custom": { + "$id": "#/properties/proxy_datapath_custom", + "type": "string", + "title": "Proxy custom datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "otsclient_datapath": { + "$id": "#/properties/otsclient_datapath", + "type": "string", + "title": "OTS Client datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "otsclient_datapath_custom": { + "$id": "#/properties/otsclient_datapath_custom", + "type": "string", + "title": "OTS Client custom datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "bitcoin_node_ip": { + "$id": "#/properties/bitcoin_node_ip", + "type": "string", + "format": "ipv4", + "title": "Bitcoin node ip", + "examples": [ + "123.123.123.123" + ] + }, + "bitcoin_mode": { + "$id": "#/properties/bitcoin_mode", + "type": "string", + "enum": [ + "internal" + ], + "title": "Bitcoin mode", + "examples": [ + "internal" + ] + }, + "bitcoin_expose": { + "$id": "#/properties/bitcoin_expose", + "type": "boolean", + "title": "Expose bitcoin node", + "default": false, + "examples": [ + false + ] + }, + "lightning_expose": { + "$id": "#/properties/lightning_expose", + "type": "boolean", + "title": "Expose lightning node", + "default": false, + "examples": [ + false + ] + }, + "lightning_external_ip": { + "$id": "#/properties/lightning_external_ip", + "type": "string", + "format": "ipv4", + "title": "Lightning node reachable ip", + "examples": [ + "123.123.123.123" + ] + }, + "gatekeeper_datapath": { + "$id": "#/properties/gatekeeper_datapath", + "type": "string", + "title": "Gatekeeper datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_datapath_custom": { + "$id": "#/properties/gatekeeper_datapath_custom", + "type": "string", + "title": "Gatekeeper custom datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_apiproperties": { + "$id": "#/properties/gatekeeper_apiproperties", + "type": "string", + "title": "API properties", + "examples": [ + "# Stats can:\naction_getblockchaininfo=stats\n\n# Watcher can:\naction_watch=watcher\naction_unwatch=watcher\naction_watchxpub=watcher\naction_unwatchxpubbyxpub=watcher\naction_unwatchxpubbylabel=watcher\naction_getactivewatchesbyxpub=watcher\naction_getactivewatchesbylabel=watcher\naction_getactivexpubwatches=watcher\naction_watchtxid=watcher\naction_getactivewatches=watcher\naction_getbestblockhash=watcher\naction_getbestblockinfo=watcher\naction_getblockinfo=watcher\naction_gettransaction=watcher\naction_ln_getinfo=watcher\naction_ln_create_invoice=watcher\naction_ln_getconnectionstring=watcher\naction_ln_decodebolt11=watcher\n\n# Spender can do what the watcher can do, plus:\naction_getbalance=spender\naction_getbalancebyxpub=spender\naction_getbalancebyxpublabel=spender\naction_getnewaddress=spender\naction_spend=spender\naction_addtobatch=spender\naction_batchspend=spender\naction_deriveindex=spender\naction_derivepubpath=spender\naction_ln_pay=spender\naction_ln_newaddr=spender\naction_ots_stamp=spender\naction_ots_getfile=spender\naction_ln_getinvoice=spender\naction_ln_decodebolt11=spender\naction_ln_connectfund=spender\n\n# Admin can do what the spender can do, plus:\n\n\n# Should be called from inside the Docker network only:\naction_conf=internal\naction_newblock=internal\naction_executecallbacks=internal\naction_ots_backoffice=internal" + ] + }, + "gatekeeper_keys": { + "$id": "#/properties/gatekeeper_keys", + "type": "object", + "title": "Gatekeeper keys", + "default": { + "configEntries": [], + "clientInformation": [] + }, + "required": [ + "configEntries", + "clientInformation" + ], + "properties": { + "configEntries": { + "$id": "#/properties/gatekeeper_keys/configEntries", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/configEntries/entry", + "type": "string", + "pattern": "^kapi_id=\".+\";kapi_key=\".+\";kapi_groups=\".+\";.+$" + }, + "examples": [ + [ + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ] + ] + }, + "clientInformation": { + "$id": "#/properties/gatekeeper_keys/clientInformation", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/clientInformation/entry", + "type": "string", + "pattern": "^.+=.+$" + }, + "examples": [ + [ + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + ] + } + } + }, + "gatekeeper_clientkeyspassword": { + "$id": "#/properties/gatekeeper_clientkeyspassword", + "type": "string", + "title": "Password for the encrypted client keys archive" + }, + "gatekeeper_clientkeyspassword_c": { + "$id": "#/properties/gatekeeper_clientkeyspassword", + "type": "string", + "title": "Password for the encrypted client keys archive, verified" + }, + "gatekeeper_statuspw": { + "$id": "#/properties/adminhash", + "type": "string", + "title": "MD5 hash of admin password" + }, + "gatekeeper_sslcert": { + "$id": "#/properties/gatekeeper_sslcert", + "type": "string", + "title": "Gatekeeper SSL Cert" + }, + "gatekeeper_sslkey": { + "$id": "#/properties/gatekeeper_sslkey", + "type": "string", + "title": "Gatekeeper SSL Key" + }, + "gatekeeper_cns": { + "$id": "#/properties/gatekeeper_cns", + "type": "string", + "title": "Gatekeeper cns", + "examples": [ + "myhost.mydomain.com,*.myotherdomain.com,123.123.123.123" + ] + }, + "lightning_implementation": { + "$id": "#/properties/lightning_implementation", + "type": "string", + "enum": [ + "c-lightning" + ], + "title": "The lightning implementation", + "default": "c-lightning", + "examples": [ + "c-lightning" + ] + }, + "lightning_nodename": { + "$id": "#/properties/lightning_nodename", + "type": "string", + "title": "The lightning node name", + "examples": [ + "🚀 Mighty Moose 🚀" + ] + }, + "lightning_nodecolor": { + "$id": "#/properties/lightning_nodecolor", + "type": "string", + "pattern": "^[0-9A-Fa-f]{6}$", + "title": "The lightning node color", + "default": "ff0000", + "examples": [ + "ff0000", + "00ff00", + "00ffff" + ] + } + } +} \ No newline at end of file diff --git a/cyphernodeconf_docker/schema/config-v0.2.0.json b/cyphernodeconf_docker/schema/config-v0.2.0.json new file mode 100644 index 0000000..9bea159 --- /dev/null +++ b/cyphernodeconf_docker/schema/config-v0.2.0.json @@ -0,0 +1,515 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://cyphernode.io/config-v0.2.0.json", + "type": "object", + "title": "Cyphernode config file structure v0.2.0", + "additionalProperties": false, + "required": [ + "schema_version", + "setup_version", + "features", + "net", + "use_xpub", + "installer_mode", + "run_as_different_user", + "docker_mode", + "docker_versions", + "adminhash", + "bitcoin_rpcuser", + "bitcoin_rpcpassword", + "bitcoin_prune", + "bitcoin_datapath", + "bitcoin_mode", + "bitcoin_expose", + "gatekeeper_keys", + "gatekeeper_sslcert", + "gatekeeper_sslkey", + "gatekeeper_cns", + "gatekeeper_clientkeyspassword", + "gatekeeper_datapath", + "gatekeeper_port", + "lightning_expose", + "lightning_implementation", + "lightning_datapath", + "lightning_nodename", + "lightning_nodecolor", + "proxy_datapath", + "otsclient_datapath", + "traefik_datapath" + ], + "allOf": [ + { + "if": { + "properties": { + "run_as_different_user": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "username" + ] + } + }, + { + "if": { + "properties": { + "use_xpub": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "xpub", + "derivation_path" + ] + } + }, + { + "if": { + "properties": { + "bitcoin_prune": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "bitcoin_prune_size" + ] + } + } + ], + "properties": { + "schema_version": { + "type": "string", + "enum": [ + "0.2.0" + ], + "default": "0.2.0", + "examples": [ + "0.2.0" + ] + }, + "setup_version": { + "type": "string", + "examples": [ + "v0.2.0" + ] + }, + "docker_versions": { + "$id": "#/properties/dockerVersions", + "type": "object", + "title": "All versions of the docker containers", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "features": { + "$id": "#/properties/features", + "type": "array", + "title": "The optional features of this cyphernode", + "default": [], + "items": { + "$id": "#/properties/features/items", + "type": "string", + "enum": [ + "lightning", + "otsclient" + ], + "title": "The feature", + "default": "", + "examples": [ + "lightning", + "otsclient" + ] + } + }, + "net": { + "$id": "#/properties/net", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ], + "title": "The net cyphernode is running on", + "default": "testnet", + "examples": [ + "testnet" + ] + }, + "use_xpub": { + "$id": "#/properties/use_xpub", + "type": "boolean", + "title": "Use xpub key?", + "default": false, + "examples": [ + false + ] + }, + "xpub": { + "$id": "#/properties/xpub", + "type": "string", + "title": "Default xpub to derive addresses from", + "pattern": "^(\\w+)$" + }, + "derivation_path": { + "$id": "#/properties/derivation_path", + "type": "string", + "title": "Default derivation path", + "default": "0/n", + "examples": [ + "0/n" + ] + }, + "installer_mode": { + "$id": "#/properties/installer_mode", + "type": "string", + "enum": [ + "docker" + ], + "title": "Install mode", + "default": "docker", + "examples": [ + "docker" + ] + }, + "run_as_different_user": { + "$id": "#/properties/run_as_different_user", + "type": "boolean", + "title": "Run as different user", + "default": true, + "examples": [ + true + ] + }, + "username": { + "$id": "#/properties/username", + "type": "string", + "title": "Username to run under", + "default": "cyphernode", + "examples": [ + "cyphernode" + ] + }, + "docker_mode": { + "$id": "#/properties/docker_mode", + "type": "string", + "enum": [ + "swarm", + "compose" + ], + "title": "How to run the containers", + "default": "compose", + "examples": [ + "compose" + ] + }, + "bitcoin_rpcuser": { + "$id": "#/properties/bitcoin_rpcuser", + "type": "string", + "title": "Bitcoin rpc user", + "default": "bitcoin", + "examples": [ + "bitcoin" + ] + }, + "bitcoin_rpcpassword": { + "$id": "#/properties/bitcoin_rpcpassword", + "type": "string", + "title": "Bitcoin rpc password", + "default": "CHANGEME", + "examples": [ + "CHANGEME" + ] + }, + "bitcoin_uacomment": { + "$id": "#/properties/bitcoin_uacomment", + "type": "string", + "title": "Bitcoin user agent comment", + "examples": [ + "cyphernode" + ] + }, + "bitcoin_prune": { + "$id": "#/properties/bitcoin_prune", + "type": "boolean", + "title": "Bitcoin prune", + "default": false, + "examples": [ + "false" + ] + }, + "bitcoin_prune_size": { + "$id": "#/properties/bitcoin_prune_size", + "type": "integer", + "title": "Bitcoin prune size", + "default": 550, + "examples": [ + 550 + ] + }, + "bitcoin_datapath": { + "$id": "#/properties/bitcoin_datapath", + "type": "string", + "title": "Bitcoin datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "bitcoin_datapath_custom": { + "$id": "#/properties/bitcoin_datapath_custom", + "type": "string", + "title": "Bitcoin custom datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "lightning_datapath": { + "$id": "#/properties/lightning_datapath", + "type": "string", + "title": "Lightning datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "lightning_datapath_custom": { + "$id": "#/properties/lightning_datapath_custom", + "type": "string", + "title": "Lightning custom datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "proxy_datapath": { + "$id": "#/properties/proxy_datapath", + "type": "string", + "title": "Proxy datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "proxy_datapath_custom": { + "$id": "#/properties/proxy_datapath_custom", + "type": "string", + "title": "Proxy custom datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "otsclient_datapath": { + "$id": "#/properties/otsclient_datapath", + "type": "string", + "title": "OTS Client datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "otsclient_datapath_custom": { + "$id": "#/properties/otsclient_datapath_custom", + "type": "string", + "title": "OTS Client custom datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "traefik_datapath": { + "$id": "#/properties/traefik_datapath", + "type": "string", + "title": "Traefik datapath", + "examples": [ + "/tmp/cyphernode/traefik" + ] + }, + "traefik_datapath_custom": { + "$id": "#/properties/traefik_datapath_custom", + "type": "string", + "title": "Traefik custom datapath", + "examples": [ + "/tmp/cyphernode/traefik" + ] + }, + "lightning_external_ip": { + "$id": "#/properties/lightning_external_ip", + "type": "string", + "format": "ipv4", + "title": "External lightning node ip", + "examples": [ + "123.123.123.123" + ] + }, + "bitcoin_mode": { + "$id": "#/properties/bitcoin_mode", + "type": "string", + "enum": [ + "internal" + ], + "title": "Bitcoin mode", + "default": "internal", + "examples": [ + "internal" + ] + }, + "bitcoin_expose": { + "$id": "#/properties/bitcoin_expose", + "type": "boolean", + "title": "Expose bitcoin node", + "default": false, + "examples": [ + false + ] + }, + "lightning_expose": { + "$id": "#/properties/lightning_expose", + "type": "boolean", + "title": "Expose lightning node", + "default": false, + "examples": [ + false + ] + }, + "gatekeeper_datapath": { + "$id": "#/properties/gatekeeper_datapath", + "type": "string", + "title": "Gatekeeper datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_datapath_custom": { + "$id": "#/properties/gatekeeper_datapath_custom", + "type": "string", + "title": "Gatekeeper custom datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_port": { + "$id": "#/properties/gatekeeper_port", + "type": "integer", + "title": "Gatekeeper port", + "default": 2009, + "examples": [ + 2009 + ] + }, + "gatekeeper_keys": { + "$id": "#/properties/gatekeeper_keys", + "type": "object", + "title": "Gatekeeper keys", + "default": { + "configEntries": [], + "clientInformation": [] + }, + "required": [ + "configEntries", + "clientInformation" + ], + "properties": { + "configEntries": { + "$id": "#/properties/gatekeeper_keys/configEntries", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/configEntries/entry", + "type": "string", + "pattern": "^kapi_id=\".+\";kapi_key=\".+\";kapi_groups=\".+\";.+$" + }, + "examples": [ + [ + "kapi_id=\"000\";kapi_key=\"a27f9e73fdde6a5005879c259c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"stats\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"stats,watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"stats,watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"stats,watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ] + ] + }, + "clientInformation": { + "$id": "#/properties/gatekeeper_keys/clientInformation", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/clientInformation/entry", + "type": "string", + "pattern": "^.+=.+$" + }, + "examples": [ + [ + "000=a27f9e73fdde6a5005879c259c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + ] + } + } + }, + "gatekeeper_sslcert": { + "$id": "#/properties/gatekeeper_sslcert", + "type": "string", + "title": "Gatekeeper SSL Cert" + }, + "gatekeeper_sslkey": { + "$id": "#/properties/gatekeeper_sslkey", + "type": "string", + "title": "Gatekeeper SSL Key" + }, + "gatekeeper_cns": { + "$id": "#/properties/gatekeeper_cns", + "type": "string", + "title": "Gatekeeper cns", + "examples": [ + "myhost.mydomain.com,*.myotherdomain.com,123.123.123.123" + ] + }, + "gatekeeper_clientkeyspassword": { + "$id": "#/properties/gatekeeper_clientkeyspassword", + "type": "string", + "title": "Password for the encrypted client keys archive" + }, + "adminhash": { + "$id": "#/properties/adminhash", + "type": "string", + "title": "Bcrypted hash of admin password" + }, + "lightning_implementation": { + "$id": "#/properties/lightning_implementation", + "type": "string", + "enum": [ + "c-lightning" + ], + "title": "The lightning implementation", + "default": "c-lightning", + "examples": [ + "c-lightning" + ] + }, + "lightning_nodename": { + "$id": "#/properties/lightning_nodename", + "type": "string", + "title": "The lightning node name", + "examples": [ + "🚀 Mighty Moose 🚀" + ] + }, + "lightning_nodecolor": { + "$id": "#/properties/lightning_nodecolor", + "type": "string", + "pattern": "^[0-9A-Fa-f]{6}$", + "title": "The lightning node color", + "examples": [ + "ff0000", + "00ff00", + "00ffff" + ] + } + } +} \ No newline at end of file diff --git a/cyphernodeconf_docker/schema/config-v0.2.2.json b/cyphernodeconf_docker/schema/config-v0.2.2.json new file mode 100644 index 0000000..4dfcaa9 --- /dev/null +++ b/cyphernodeconf_docker/schema/config-v0.2.2.json @@ -0,0 +1,590 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://cyphernode.io/config-v0.2.2.json", + "type": "object", + "title": "Cyphernode config file structure v0.2.2", + "additionalProperties": false, + "required": [ + "schema_version", + "setup_version", + "features", + "net", + "use_xpub", + "installer_mode", + "run_as_different_user", + "docker_mode", + "docker_versions", + "adminhash", + "bitcoin_rpcuser", + "bitcoin_rpcpassword", + "bitcoin_prune", + "bitcoin_datapath", + "bitcoin_mode", + "bitcoin_expose", + "gatekeeper_expose", + "gatekeeper_keys", + "gatekeeper_sslcert", + "gatekeeper_sslkey", + "gatekeeper_cns", + "gatekeeper_clientkeyspassword", + "gatekeeper_datapath", + "gatekeeper_port", + "proxy_datapath", + "traefik_datapath", + "traefik_http_port", + "traefik_https_port" + ], + "allOf": [ + { + "if": { + "properties": { + "run_as_different_user": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "username" + ] + } + }, + { + "if": { + "properties": { + "use_xpub": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "xpub", + "derivation_path" + ] + } + }, + { + "if": { + "properties": { + "bitcoin_prune": { + "enum": [ + true + ] + } + } + }, + "then": { + "required": [ + "bitcoin_prune_size" + ] + } + }, + { + "if": { + "properties": { + "features": { + "contains": { + "enum": [ + "lightning" + ] + } + } + } + }, + "then": { + "required": [ + "lightning_announce", + "lightning_expose", + "lightning_implementation", + "lightning_datapath", + "lightning_nodename", + "lightning_nodecolor" + ] + } + }, + { + "if": { + "properties": { + "features": { + "contains": { + "enum": [ + "otsclient" + ] + } + } + } + }, + "then": { + "required": [ + "otsclient_datapath" + ] + } + } + ], + "properties": { + "schema_version": { + "type": "string", + "enum": [ + "0.2.2" + ], + "default": "0.3.0", + "examples": [ + "0.2.2" + ] + }, + "setup_version": { + "type": "string", + "examples": [ + "v0.2.0" + ] + }, + "docker_versions": { + "$id": "#/properties/dockerVersions", + "type": "object", + "title": "All versions of the docker containers", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "features": { + "$id": "#/properties/features", + "type": "array", + "title": "The optional features of this cyphernode", + "default": [], + "items": { + "$id": "#/properties/features/items", + "type": "string", + "enum": [ + "lightning", + "otsclient" + ], + "title": "The feature", + "default": "", + "examples": [ + "lightning", + "otsclient" + ] + } + }, + "net": { + "$id": "#/properties/net", + "type": "string", + "enum": [ + "testnet", + "mainnet", + "regtest" + ], + "title": "The net cyphernode is running on", + "default": "testnet", + "examples": [ + "testnet" + ] + }, + "use_xpub": { + "$id": "#/properties/use_xpub", + "type": "boolean", + "title": "Use xpub key?", + "default": false, + "examples": [ + false + ] + }, + "xpub": { + "$id": "#/properties/xpub", + "type": "string", + "title": "Default xpub to derive addresses from", + "pattern": "^(\\w+)$" + }, + "derivation_path": { + "$id": "#/properties/derivation_path", + "type": "string", + "title": "Default derivation path", + "default": "0/n", + "examples": [ + "0/n" + ] + }, + "installer_mode": { + "$id": "#/properties/installer_mode", + "type": "string", + "enum": [ + "docker" + ], + "title": "Install mode", + "default": "docker", + "examples": [ + "docker" + ] + }, + "run_as_different_user": { + "$id": "#/properties/run_as_different_user", + "type": "boolean", + "title": "Run as different user", + "default": true, + "examples": [ + true + ] + }, + "username": { + "$id": "#/properties/username", + "type": "string", + "title": "Username to run under", + "default": "cyphernode", + "examples": [ + "cyphernode" + ] + }, + "docker_mode": { + "$id": "#/properties/docker_mode", + "type": "string", + "enum": [ + "swarm", + "compose" + ], + "title": "How to run the containers", + "default": "compose", + "examples": [ + "compose" + ] + }, + "bitcoin_rpcuser": { + "$id": "#/properties/bitcoin_rpcuser", + "type": "string", + "title": "Bitcoin rpc user", + "default": "bitcoin", + "examples": [ + "bitcoin" + ] + }, + "bitcoin_rpcpassword": { + "$id": "#/properties/bitcoin_rpcpassword", + "type": "string", + "title": "Bitcoin rpc password", + "default": "CHANGEME", + "examples": [ + "CHANGEME" + ] + }, + "bitcoin_uacomment": { + "$id": "#/properties/bitcoin_uacomment", + "type": "string", + "title": "Bitcoin user agent comment", + "examples": [ + "cyphernode" + ] + }, + "bitcoin_prune": { + "$id": "#/properties/bitcoin_prune", + "type": "boolean", + "title": "Bitcoin prune", + "default": false, + "examples": [ + "false" + ] + }, + "bitcoin_prune_size": { + "$id": "#/properties/bitcoin_prune_size", + "type": "integer", + "title": "Bitcoin prune size", + "default": 550, + "examples": [ + 550 + ] + }, + "bitcoin_datapath": { + "$id": "#/properties/bitcoin_datapath", + "type": "string", + "title": "Bitcoin datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "bitcoin_datapath_custom": { + "$id": "#/properties/bitcoin_datapath_custom", + "type": "string", + "title": "Bitcoin custom datapath", + "examples": [ + "/tmp/cyphernode/bitcoin" + ] + }, + "lightning_datapath": { + "$id": "#/properties/lightning_datapath", + "type": "string", + "title": "Lightning datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "lightning_datapath_custom": { + "$id": "#/properties/lightning_datapath_custom", + "type": "string", + "title": "Lightning custom datapath", + "examples": [ + "/tmp/cyphernode/lightning" + ] + }, + "proxy_datapath": { + "$id": "#/properties/proxy_datapath", + "type": "string", + "title": "Proxy datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "proxy_datapath_custom": { + "$id": "#/properties/proxy_datapath_custom", + "type": "string", + "title": "Proxy custom datapath", + "examples": [ + "/tmp/cyphernode/proxy" + ] + }, + "otsclient_datapath": { + "$id": "#/properties/otsclient_datapath", + "type": "string", + "title": "OTS Client datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "otsclient_datapath_custom": { + "$id": "#/properties/otsclient_datapath_custom", + "type": "string", + "title": "OTS Client custom datapath", + "examples": [ + "/tmp/cyphernode/otsclient" + ] + }, + "traefik_http_port": { + "$id": "#/properties/traefik_port", + "type": "integer", + "title": "Traefik HTTP port", + "default": 80, + "examples": [ + 80 + ] + }, + "traefik_https_port": { + "$id": "#/properties/traefik_https_port", + "type": "integer", + "title": "Traefik HTTPS port", + "default": 443, + "examples": [ + 443 + ] + }, + "traefik_datapath": { + "$id": "#/properties/traefik_datapath", + "type": "string", + "title": "Traefik datapath", + "examples": [ + "/tmp/cyphernode/traefik" + ] + }, + "traefik_datapath_custom": { + "$id": "#/properties/traefik_datapath_custom", + "type": "string", + "title": "Traefik custom datapath", + "examples": [ + "/tmp/cyphernode/traefik" + ] + }, + "lightning_announce": { + "$id": "#/properties/lightning_announce", + "type": "boolean", + "title": "Announce lightning ip", + "default": false, + "examples": [ + false + ] + }, + "lightning_external_ip": { + "$id": "#/properties/lightning_external_ip", + "type": "string", + "format": "ipv4", + "title": "External lightning node ip", + "examples": [ + "123.123.123.123" + ] + }, + "bitcoin_mode": { + "$id": "#/properties/bitcoin_mode", + "type": "string", + "enum": [ + "internal" + ], + "title": "Bitcoin mode", + "default": "internal", + "examples": [ + "internal" + ] + }, + "bitcoin_expose": { + "$id": "#/properties/bitcoin_expose", + "type": "boolean", + "title": "Expose bitcoin node", + "default": false, + "examples": [ + false + ] + }, + "lightning_expose": { + "$id": "#/properties/lightning_expose", + "type": "boolean", + "title": "Expose lightning node", + "default": false, + "examples": [ + false + ] + }, + "gatekeeper_expose": { + "$id": "#/properties/gatekeeper_expose", + "type": "boolean", + "title": "Expose gatekeeper port", + "default": false, + "examples": [ + false + ] + }, + "gatekeeper_datapath": { + "$id": "#/properties/gatekeeper_datapath", + "type": "string", + "title": "Gatekeeper datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_datapath_custom": { + "$id": "#/properties/gatekeeper_datapath_custom", + "type": "string", + "title": "Gatekeeper custom datapath", + "examples": [ + "/tmp/cyphernode/gatekeeper" + ] + }, + "gatekeeper_port": { + "$id": "#/properties/gatekeeper_port", + "type": "integer", + "title": "Gatekeeper port", + "default": 2009, + "examples": [ + 2009 + ] + }, + "gatekeeper_keys": { + "$id": "#/properties/gatekeeper_keys", + "type": "object", + "title": "Gatekeeper keys", + "default": { + "configEntries": [], + "clientInformation": [] + }, + "required": [ + "configEntries", + "clientInformation" + ], + "properties": { + "configEntries": { + "$id": "#/properties/gatekeeper_keys/configEntries", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/configEntries/entry", + "type": "string", + "pattern": "^kapi_id=\".+\";kapi_key=\".+\";kapi_groups=\".+\";.+$" + }, + "examples": [ + [ + "kapi_id=\"000\";kapi_key=\"a27f9e73fdde6a5005879c259c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"stats\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"stats,watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"stats,watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"stats,watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ] + ] + }, + "clientInformation": { + "$id": "#/properties/gatekeeper_keys/clientInformation", + "type": "array", + "items": { + "$id": "#/properties/gatekeeper_keys/clientInformation/entry", + "type": "string", + "pattern": "^.+=.+$" + }, + "examples": [ + [ + "000=a27f9e73fdde6a5005879c259c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + ] + } + } + }, + "gatekeeper_sslcert": { + "$id": "#/properties/gatekeeper_sslcert", + "type": "string", + "title": "Gatekeeper SSL Cert" + }, + "gatekeeper_sslkey": { + "$id": "#/properties/gatekeeper_sslkey", + "type": "string", + "title": "Gatekeeper SSL Key" + }, + "gatekeeper_cns": { + "$id": "#/properties/gatekeeper_cns", + "type": "string", + "title": "Gatekeeper cns", + "examples": [ + "myhost.mydomain.com,*.myotherdomain.com,123.123.123.123" + ] + }, + "gatekeeper_clientkeyspassword": { + "$id": "#/properties/gatekeeper_clientkeyspassword", + "type": "string", + "title": "Password for the encrypted client keys archive" + }, + "adminhash": { + "$id": "#/properties/adminhash", + "type": "string", + "title": "Bcrypted hash of admin password" + }, + "lightning_implementation": { + "$id": "#/properties/lightning_implementation", + "type": "string", + "enum": [ + "c-lightning" + ], + "title": "The lightning implementation", + "default": "c-lightning", + "examples": [ + "c-lightning" + ] + }, + "lightning_nodename": { + "$id": "#/properties/lightning_nodename", + "type": "string", + "title": "The lightning node name", + "examples": [ + "🚀 Mighty Moose 🚀" + ] + }, + "lightning_nodecolor": { + "$id": "#/properties/lightning_nodecolor", + "type": "string", + "pattern": "^[0-9A-Fa-f]{6}$", + "title": "The lightning node color", + "examples": [ + "ff0000", + "00ff00", + "00ffff" + ] + } + } +} diff --git a/install/generator-cyphernode/generators/app/splash/01.png.txt b/cyphernodeconf_docker/splash/01.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/01.png.txt rename to cyphernodeconf_docker/splash/01.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/02.png.txt b/cyphernodeconf_docker/splash/02.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/02.png.txt rename to cyphernodeconf_docker/splash/02.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/03.png.txt b/cyphernodeconf_docker/splash/03.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/03.png.txt rename to cyphernodeconf_docker/splash/03.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/04.png.txt b/cyphernodeconf_docker/splash/04.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/04.png.txt rename to cyphernodeconf_docker/splash/04.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/05.png.txt b/cyphernodeconf_docker/splash/05.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/05.png.txt rename to cyphernodeconf_docker/splash/05.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/06.png.txt b/cyphernodeconf_docker/splash/06.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/06.png.txt rename to cyphernodeconf_docker/splash/06.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/07.png.txt b/cyphernodeconf_docker/splash/07.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/07.png.txt rename to cyphernodeconf_docker/splash/07.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/08.png.txt b/cyphernodeconf_docker/splash/08.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/08.png.txt rename to cyphernodeconf_docker/splash/08.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/09.png.txt b/cyphernodeconf_docker/splash/09.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/09.png.txt rename to cyphernodeconf_docker/splash/09.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/10.png.txt b/cyphernodeconf_docker/splash/10.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/10.png.txt rename to cyphernodeconf_docker/splash/10.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/11.png.txt b/cyphernodeconf_docker/splash/11.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/11.png.txt rename to cyphernodeconf_docker/splash/11.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/12.png.txt b/cyphernodeconf_docker/splash/12.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/12.png.txt rename to cyphernodeconf_docker/splash/12.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/13.png.txt b/cyphernodeconf_docker/splash/13.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/13.png.txt rename to cyphernodeconf_docker/splash/13.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/14.png.txt b/cyphernodeconf_docker/splash/14.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/14.png.txt rename to cyphernodeconf_docker/splash/14.png.txt diff --git a/install/generator-cyphernode/generators/app/splash/15.png.txt b/cyphernodeconf_docker/splash/15.png.txt similarity index 100% rename from install/generator-cyphernode/generators/app/splash/15.png.txt rename to cyphernodeconf_docker/splash/15.png.txt diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf b/cyphernodeconf_docker/templates/bitcoin/bitcoin-client.conf similarity index 77% rename from install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf rename to cyphernodeconf_docker/templates/bitcoin/bitcoin-client.conf index bed4730..e9402e8 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf +++ b/cyphernodeconf_docker/templates/bitcoin/bitcoin-client.conf @@ -1,6 +1,9 @@ <% if (net === 'testnet') { %> # testnet testnet=1 +<% } else if( net === 'regtest' ) { %> +# regtest +regtest=1 <% } %> rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/cyphernodeconf_docker/templates/bitcoin/bitcoin.conf similarity index 57% rename from install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf rename to cyphernodeconf_docker/templates/bitcoin/bitcoin.conf index c02e2fc..c54a581 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/cyphernodeconf_docker/templates/bitcoin/bitcoin.conf @@ -1,9 +1,12 @@ <% if (net === 'testnet') { %> # testnet testnet=1 +<% } else if( net === 'regtest' ) { %> +# regtest +regtest=1 <% } %> -<% if (bitcoin_prune) { %> +<% if (bitcoin_prune && bitcoin_mode === 'internal') { %> prune=<%= bitcoin_prune_size || 550 %> <% } else { %> txindex=1 @@ -28,16 +31,31 @@ server=1 <% if (net === 'testnet') { %> test.wallet=watching01.dat +test.wallet=xpubwatching01.dat test.wallet=spending01.dat test.wallet=ln01.dat +test.rpcbind=0.0.0.0 +<% } else if ( net === 'regtest' ) { %> +regtest.wallet=watching01.dat +regtest.wallet=xpubwatching01.dat +regtest.wallet=spending01.dat +regtest.wallet=ln01.dat +regtest.rpcbind=0.0.0.0 +regtest.rpcport=18443 <% } else { %> main.wallet=watching01.dat +main.wallet=xpubwatching01.dat main.wallet=spending01.dat main.wallet=ln01.dat +main.rpcbind=0.0.0.0 <% } %> -walletnotify=curl proxy:8888/conf/%s +walletnotify=/usr/bin/curl proxy:8888/conf/%s +blocknotify=/usr/bin/curl proxy:8888/newblock/%s <% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> uacomment=<%= bitcoin_uacomment %> <% } %> + +addresstype=bech32 +walletrbf=1 diff --git a/cyphernodeconf_docker/templates/cyphernode/info.json b/cyphernodeconf_docker/templates/cyphernode/info.json new file mode 100644 index 0000000..186bb31 --- /dev/null +++ b/cyphernodeconf_docker/templates/cyphernode/info.json @@ -0,0 +1 @@ +<%- JSON.stringify( installationInfo, null, 2 ) %> \ No newline at end of file diff --git a/cyphernodeconf_docker/templates/gatekeeper/api.properties b/cyphernodeconf_docker/templates/gatekeeper/api.properties new file mode 100644 index 0000000..f2f4739 --- /dev/null +++ b/cyphernodeconf_docker/templates/gatekeeper/api.properties @@ -0,0 +1,60 @@ + +# Watcher can do stuff +# Spender can do what the watcher can do plus more stuff +# Admin can do what the spender can do plus even more stuff + +# Stats can: +action_helloworld=stats +action_getblockchaininfo=stats +action_installation_info=stats +action_getmempoolinfo=stats + +# Watcher can: +action_watch=watcher +action_unwatch=watcher +action_watchxpub=watcher +action_unwatchxpubbyxpub=watcher +action_unwatchxpubbylabel=watcher +action_getactivewatchesbyxpub=watcher +action_getactivewatchesbylabel=watcher +action_getactivexpubwatches=watcher +action_watchtxid=watcher +action_getactivewatches=watcher +action_getbestblockhash=watcher +action_getbestblockinfo=watcher +action_getblockinfo=watcher +action_gettransaction=watcher +action_ots_verify=watcher +action_ots_info=watcher +action_ln_getinfo=watcher +action_ln_create_invoice=watcher +action_ln_getconnectionstring=watcher +action_ln_decodebolt11=watcher + +# Spender can do what the watcher can do, plus: +action_getbalance=spender +action_getbalancebyxpub=spender +action_getbalancebyxpublabel=spender +action_getnewaddress=spender +action_spend=spender +action_bumpfee=spender +action_addtobatch=spender +action_batchspend=spender +action_deriveindex=spender +action_derivepubpath=spender +action_ln_pay=spender +action_ln_newaddr=spender +action_ots_stamp=spender +action_ots_getfile=spender +action_ln_getinvoice=spender +action_ln_decodebolt11=spender +action_ln_connectfund=spender + +# Admin can do what the spender can do, plus: + + +# Should be called from inside the Docker network only: +action_conf=internal +action_newblock=internal +action_executecallbacks=internal +action_ots_backoffice=internal \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem b/cyphernodeconf_docker/templates/gatekeeper/cert.pem similarity index 100% rename from install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem rename to cyphernodeconf_docker/templates/gatekeeper/cert.pem diff --git a/api_auth_docker/default-ssl.conf b/cyphernodeconf_docker/templates/gatekeeper/default.conf similarity index 67% rename from api_auth_docker/default-ssl.conf rename to cyphernodeconf_docker/templates/gatekeeper/default.conf index 69c7dc1..b92d0d8 100644 --- a/api_auth_docker/default-ssl.conf +++ b/cyphernodeconf_docker/templates/gatekeeper/default.conf @@ -1,22 +1,25 @@ server { - listen 443 ssl; + listen <%= gatekeeper_port %> ssl; server_name localhost; - #include /etc/nginx/conf.d/ip-whitelist.conf; - ssl_certificate /etc/ssl/certs/cert.pem; ssl_certificate_key /etc/ssl/private/key.pem; - location /status { - auth_basic "status"; - auth_basic_user_file conf.d/status/htpasswd; + location /s/ { + auth_request /auth; root /etc/nginx/conf.d; - index statuspage.html; } location /v0/ { auth_request /auth; proxy_pass http://proxy:8888/; + + # Up default 60 second timeout for 3 minutes (OTS stamping can take time) + proxy_connect_timeout 180; + proxy_send_timeout 180; + proxy_read_timeout 180; + send_timeout 180; + } location /auth { diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem b/cyphernodeconf_docker/templates/gatekeeper/key.pem similarity index 100% rename from install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem rename to cyphernodeconf_docker/templates/gatekeeper/key.pem diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties b/cyphernodeconf_docker/templates/gatekeeper/keys.properties similarity index 100% rename from install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties rename to cyphernodeconf_docker/templates/gatekeeper/keys.properties diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/cyphernodeconf_docker/templates/installer/config.sh similarity index 86% rename from install/generator-cyphernode/generators/app/templates/installer/config.sh rename to cyphernodeconf_docker/templates/installer/config.sh index ddd9ca3..b4382f1 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/cyphernodeconf_docker/templates/installer/config.sh @@ -5,9 +5,12 @@ FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> +GATEKEEPER_PORT=<%= gatekeeper_port %> +TRAEFIK_DATAPATH=<%= traefik_datapath %> DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> +SHARED_HTPASSWD_PATH=<%= traefik_datapath %>/htpasswd <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> LIGHTNING_DATAPATH=<%= lightning_datapath %> <% } %> diff --git a/cyphernodeconf_docker/templates/installer/docker/docker-compose.yaml b/cyphernodeconf_docker/templates/installer/docker/docker-compose.yaml new file mode 100644 index 0000000..7971a87 --- /dev/null +++ b/cyphernodeconf_docker/templates/installer/docker/docker-compose.yaml @@ -0,0 +1,268 @@ +version: "3" + +services: + + <% if( bitcoin_mode === 'internal' ) { %> + ########################## + # BITCOIN # + ########################## + + bitcoin: + image: cyphernode/bitcoin:<%= bitcoin_version %> + command: $USER bitcoind + <% if( bitcoin_expose ) { %> + ports: + - "<%= (net === 'regtest') ? '18443:18443' : ((net === 'testnet') ? '18332:18332' : '8332:8332') %>" + - "<%= (net === 'regtest') ? '18444:18444' : ((net === 'testnet') ? '18333:18333' : '8333:8333') %>" + <% } %> + volumes: + - "<%= bitcoin_datapath %>:/.bitcoin" + - bitcoin_monitor:/bitcoin_monitor + healthcheck: + test: bitcoin-cli echo && touch /bitcoin_monitor/up || rm -f /bitcoin_monitor/up + interval: 20s + timeout: 5s + retries: 10 + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + <% } %> + + ########################## + # PROXY # + ########################## + + proxy: + image: cyphernode/proxy:<%= proxy_version %> + command: $USER ./startproxy.sh + environment: + - "TRACING=1" + - "WATCHER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal') ? 'bitcoin' : bitcoin_node_ip %>:<%= (net === 'regtest') ? '18443' : ((net === 'testnet') ? '18332' : '8332') %>/wallet" + - "WATCHER_BTC_NODE_DEFAULT_WALLET=watching01.dat" + - "WATCHER_BTC_NODE_XPUB_WALLET=xpubwatching01.dat" + - "WATCHER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + - "WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties" + - "SPENDER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal') ? 'bitcoin' : bitcoin_node_ip %>:<%= (net === 'regtest') ? '18443' : ((net === 'testnet') ? '18332' : '8332') %>/wallet" + - "SPENDER_BTC_NODE_DEFAULT_WALLET=spending01.dat" + - "SPENDER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" + - "PROXY_LISTENING_PORT=8888" + - "DB_PATH=/proxy/db" + - "DB_FILE=/proxy/db/proxydb" + - "PYCOIN_CONTAINER=pycoin:7777" + <% if ( use_xpub && xpub ) { %> + - "DERIVATION_PUB32=<%= xpub %>" + - "DERIVATION_PATH=<%= derivation_path %>" + <% } %> + - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune ? 'true' : 'false' %>" + - "OTSCLIENT_CONTAINER=otsclient:6666" + - "OTS_FILES=/proxy/otsfiles" + - "XPUB_DERIVATION_GAP=100" + <% if ( devmode ) { %> + ports: + - "8888:8888" + <% } %> + volumes: + - "<%= proxy_datapath %>:/proxy/db" + <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> + - "<%= lightning_datapath %>:/.lightning" + <% } %> + <% if ( features.indexOf('otsclient') !== -1 ) { %> + - "<%= otsclient_datapath %>:/proxy/otsfiles" + <% } %> + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # PROXYCRON # + ########################## + + proxycron: + image: cyphernode/proxycron:<%= proxycron_version %> + environment: + - "TX_CONF_URL=proxy:8888/executecallbacks" + - "OTS_URL=proxy:8888/ots_backoffice" + networks: + - cyphernodenet + restart: always + depends_on: + - proxy +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # BROKER # + ########################## + + broker: + image: eclipse-mosquitto:1.6 + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # NOTIFIER # + ########################## + + notifier: + image: cyphernode/notifier:<%= notifier_version %> + command: $USER ./startnotifier.sh + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always + depends_on: + - broker +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # PYCOIN # + ########################## + + pycoin: + image: cyphernode/pycoin:<%= pycoin_version %> + command: $USER ./startpycoin.sh + environment: + - "TRACING=1" + - "PYCOIN_LISTENING_PORT=7777" + <% if ( devmode ) { %> + ports: + - "7777:7777" + <% } %> + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + <% if ( features.indexOf('otsclient') !== -1 ) { %> + ########################## + # OTSCLIENT # + ########################## + + otsclient: + image: cyphernode/otsclient:<%= otsclient_version %> + command: $USER /script/startotsclient.sh + environment: + - "TRACING=1" + - "OTSCLIENT_LISTENING_PORT=6666" + <% if (net === 'testnet') { %> + - "TESTNET=1" + <% } %> + volumes: + - "<%= otsclient_datapath %>:/otsfiles" + - "<%= bitcoin_datapath %>/bitcoin-client.conf:/.bitcoin/bitcoin.conf" + command: $USER /script/startotsclient.sh + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + <% } %> + + ########################## + # GATEKEEPER # + ########################## + + gatekeeper: + # HTTP authentication API gate + image: cyphernode/gatekeeper:<%= gatekeeper_version %> + command: $USER + environment: + - "TRACING=1" + <% if( gatekeeper_expose ) { %> + ports: + - "<%= gatekeeper_port %>:<%= gatekeeper_port %>" + <% } %> + volumes: + - "<%= gatekeeper_datapath %>/certs:/etc/ssl/certs" + - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" + - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties" + - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" + - "<%= gatekeeper_datapath %>/default.conf:/etc/nginx/conf.d/default.conf" + - "<%= gatekeeper_datapath %>/htpasswd:/etc/nginx/conf.d/status/htpasswd" + - "<%= gatekeeper_datapath %>/installation.json:/etc/nginx/conf.d/s/stats/installation.json" + - "<%= gatekeeper_datapath %>/client.7z:/etc/nginx/conf.d/s/stats/client.7z" + - "<%= gatekeeper_datapath %>/config.7z:/etc/nginx/conf.d/s/stats/config.7z" + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always + depends_on: + - proxy +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # TRAEFIK # + ########################## + + traefik: + image: traefik:v1.7.9-alpine + ports: + - 80:80 + - 443:443 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" + - "<%= traefik_datapath%>/acme.json:/acme.json" + - "<%= traefik_datapath%>/htpasswd:/htpasswd/htpasswd" + networks: + - cyphernodeappsnet + restart: always + depends_on: + - gatekeeper +# deploy: +# placement: +# constraints: [node.hostname==dev] + + <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> + ########################## + # LIGHTNING # + ########################## + + lightning: + image: cyphernode/clightning:<%= lightning_version %> + command: $USER sh -c 'while [ ! -f "/bitcoin_monitor/up" ]; do echo "bitcoin not ready" ; sleep 10 ; done ; echo "bitcoin ready!" ; lightningd' + <% if( lightning_expose ) { %> + ports: + - "9735:9735" + <% } %> + volumes: + - "<%= lightning_datapath %>:/.lightning" + - "<%= bitcoin_datapath %>/bitcoin-client.conf:/.bitcoin/bitcoin.conf" + - bitcoin_monitor:/bitcoin_monitor:ro + networks: + - cyphernodenet + restart: always + depends_on: + - bitcoin +# deploy: +# placement: +# constraints: [node.hostname==dev] + <% } %> + +volumes: + bitcoin_monitor: + +networks: + cyphernodenet: + external: true + cyphernodeappsnet: + external: true diff --git a/cyphernodeconf_docker/templates/installer/start.sh b/cyphernodeconf_docker/templates/installer/start.sh new file mode 100644 index 0000000..dbc0b74 --- /dev/null +++ b/cyphernodeconf_docker/templates/installer/start.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +. ./.cyphernodeconf/installer/config.sh + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +start_apps() { + local SCRIPT_NAME="start.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + + for i in $current_path/apps/* + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then + APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + APP_ID=$(basename $APP_SCRIPT_PATH) + + if [ -f "$APP_START_SCRIPT_PATH" ]; then + . $APP_START_SCRIPT_PATH + elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then + export SHARED_HTPASSWD_PATH + export GATEKEEPER_DATAPATH + export GATEKEEPER_PORT + export LIGHTNING_DATAPATH + export BITCOIN_DATAPATH + export APP_SCRIPT_PATH + export APP_ID + export DOCKER_MODE + + if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID + elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans + fi + fi + fi + done +} + +<% if (run_as_different_user) { %> +OS=$(uname -s) +if [ "$OS" = "Darwin" ]; then + printf "\r\n\033[0;91m'Run as another user' feature is not supported on OSX. User <%= default_username %> will be used to run Cyphernode.\033[0m\r\n\r\n" + export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) +else + export USER=$(id -u <%= username %>):$(id -g <%= username %>) +fi +<% } else { %> +export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) +<% } %> + +export ARCH=$(uname -m) +current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" + +<% if (docker_mode == 'swarm') { %> +docker stack deploy -c $current_path/docker-compose.yaml cyphernode +<% } else if(docker_mode == 'compose') { %> +docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans +<% } %> + +start_apps + +. ./testdeployment.sh diff --git a/cyphernodeconf_docker/templates/installer/stop.sh b/cyphernodeconf_docker/templates/installer/stop.sh new file mode 100644 index 0000000..4e6649c --- /dev/null +++ b/cyphernodeconf_docker/templates/installer/stop.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" + + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +stop_apps() { + local SCRIPT_NAME="stop.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + + for i in $current_path/apps/* + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then + APP_STOP_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + APP_ID=$(basename $APP_SCRIPT_PATH) + + if [ -f "$APP_STOP_SCRIPT_PATH" ]; then + . $APP_STOP_SCRIPT_PATH + elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then + export SHARED_HTPASSWD_PATH + export GATEKEEPER_DATAPATH + export GATEKEEPER_PORT + export LIGHTNING_DATAPATH + export BITCOIN_DATAPATH + export APP_SCRIPT_PATH + export APP_ID + export DOCKER_MODE + + if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack rm $APP_ID + elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down + fi + + fi + fi + done +} + +. ./.cyphernodeconf/installer/config.sh +stop_apps + +<% if (docker_mode == 'swarm') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker stack rm cyphernode +<% } else if(docker_mode == 'compose') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker-compose -f $current_path/docker-compose.yaml down +<% } %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/cyphernodeconf_docker/templates/installer/testdeployment.sh similarity index 51% rename from install/generator-cyphernode/generators/app/templates/installer/start.sh rename to cyphernodeconf_docker/templates/installer/testdeployment.sh index 8fed59f..94dde14 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/cyphernodeconf_docker/templates/installer/testdeployment.sh @@ -1,9 +1,46 @@ #!/bin/sh +. ./.cyphernodeconf/installer/config.sh + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +test_apps() { + local SCRIPT_NAME="test.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + local returncode=0 + + for i in $current_path/apps/* + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d "$APP_SCRIPT_PATH" ]; then + APP_TEST_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + + if [ -f "$APP_TEST_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then + APP_ID=$(basename "$APP_SCRIPT_PATH") + printf "\r\n\e[1;36mTesting $APP_ID... \e[1;0m" + . $APP_TEST_SCRIPT_PATH + local rc=$? + + if [ ""$rc -eq "0" ]; then + printf "\e[1;36m$APP_ID rocks!\e[1;0m" + fi + returncode=$(($rc | ${returncode})) + echo "" + fi + fi + done + return $returncode +} + <% if (run_as_different_user) { %> OS=$(uname -s) if [ "$OS" = "Darwin" ]; then - printf "\r\n\033[0;91m'Run as another user' feature is not supported on OSX. User <%= default_username %> will be used to run Cyphernode.\033[0m\r\n\r\n" export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) else export USER=$(id -u <%= username %>):$(id -g <%= username %>) @@ -15,12 +52,6 @@ export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) export ARCH=$(uname -m) current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" -<% if (docker_mode == 'swarm') { %> -docker stack deploy -c $current_path/docker-compose.yaml cyphernode -<% } else if(docker_mode == 'compose') { %> -docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans -<% } %> - arch=$(uname -m) case "${arch}" in arm*) printf "\r\n\033[1;31mSince we're on a slow RPi, let's give Docker 60 more seconds before performing our tests...\033[0m\r\n\r\n" @@ -32,19 +63,26 @@ esac docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v <%= gatekeeper_datapath %>:/gatekeeper \ -v $current_path:/dist \ ---network cyphernodenet alpine:3.8 /testfeatures.sh +-v cyphernode_bitcoin_monitor:/bitcoin_monitor:ro \ +--network cyphernodenet eclipse-mosquitto:1.6.2 /testfeatures.sh if [ -f $current_path/exitStatus.sh ]; then . $current_path/exitStatus.sh rm -f $current_path/exitStatus.sh fi +test_apps + +EXIT_STATUS=$(($? | ${EXIT_STATUS})) + +printf "\r\n\e[1;32mTests finished.\e[0m\n" + if [ "$EXIT_STATUS" -ne "0" ]; then - printf "\r\n\033[1;31mThere was an error during cyphernode installation. Please see Docker's logs for more information. Run ./stop.sh to stop cyphernode.\r\n\r\n\033[0m" + printf "\r\n\033[1;31mThere was an error during cyphernode installation. Please see Docker's logs for more information. Run ./testdeployment.sh to rerun the tests. Run ./stop.sh to stop cyphernode.\r\n\r\n\033[0m" exit 1 fi printf "\r\n\033[0;92mDepending on your current location and DNS settings, point your favorite browser to one of the following URLs to access Cyphernode's status page:\r\n" printf "\r\n" -printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + '/status/\\r\\n') %><% }) %>\033[0m\r\n" +printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + '/welcome\\r\\n') %><% }) %>\033[0m\r\n" printf "\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n\r\n\033[0m" diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/cyphernodeconf_docker/templates/installer/testfeatures.sh similarity index 69% rename from install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh rename to cyphernodeconf_docker/templates/installer/testfeatures.sh index e9ef718..037e3f0 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/cyphernodeconf_docker/templates/installer/testfeatures.sh @@ -24,7 +24,7 @@ checkgatekeeper() { sleep 2 echo " Testing expired request... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getblockinfo) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper:<%= gatekeeper_port %>/v0/getblockinfo) [ "${rc}" -ne "403" ] && return 10 # Let's test authentication (signature) @@ -34,7 +34,7 @@ checkgatekeeper() { token="$h64.$p64.a$s" echo " Testing bad signature... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getblockinfo) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper:<%= gatekeeper_port %>/v0/getblockinfo) [ "${rc}" -ne "403" ] && return 30 # Let's test authorization (action access for groups) @@ -42,7 +42,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing watcher trying to do a spender action... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getbalance) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper:<%= gatekeeper_port %>/v0/getbalance) [ "${rc}" -ne "403" ] && return 40 id="002" @@ -52,7 +52,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing spender trying to do an internal action call... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/conf) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper:<%= gatekeeper_port %>/v0/conf) [ "${rc}" -ne "403" ] && return 50 @@ -63,7 +63,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing admin trying to do an internal action call... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/conf) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper:<%= gatekeeper_port %>/v0/conf) [ "${rc}" -ne "403" ] && return 60 echo -e "\e[1;36mGatekeeper rocks!" > /dev/console @@ -83,6 +83,34 @@ checkpycoin() { return 0 } +checkbroker() { + echo -en "\r\n\e[1;36mTesting Broker... " > /dev/console + local rc + + rc=$(mosquitto_pub -h broker -t "testtopic" -m "testbroker") + [ "$?" -ne "0" ] && return 110 + + echo -e "\e[1;36mBroker rocks!" > /dev/console + + return 0 +} + +checknotifier() { + echo -en "\r\n\e[1;36mTesting Notifier... " > /dev/console + local response + local returncode + + response=$(mosquitto_rr -h broker -W 15 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"http://proxy:8888/helloworld\"}") + returncode=$? + [ "${returncode}" -ne "0" ] && return 115 + http_code=$(echo "${response}" | jq -r ".http_code") + [ "${http_code}" -ge "400" ] && return 118 + + echo -e "\e[1;36mNotifier rocks!" > /dev/console + + return 0 +} + checkots() { echo -en "\r\n\e[1;36mTesting OTSclient... " > /dev/console local rc @@ -121,8 +149,8 @@ checklnnode() { } checkservice() { - local interval=10 - local totaltime=120 + local interval=15 + local totaltime=180 local outcome local returncode=0 local endtime=$(($(date +%s) + ${totaltime})) @@ -133,12 +161,12 @@ checkservice() { while : do outcome=0 - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do echo -e " \e[0;32mVerifying \e[0;33m${container}\e[0;32m..." > /dev/console (ping -c 10 ${container} 2> /dev/null | grep "0% packet loss" > /dev/null) & eval ${container}=$! done - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do eval wait '$'${container} ; returncode=$? ; outcome=$((${outcome} + ${returncode})) eval c_${container}=${returncode} done @@ -158,9 +186,9 @@ checkservice() { # { "name": "pycoin", "active":true }, # { "name": "otsclient", "active":true }, # { "name": "bitcoin", "active":true }, - # { "name": "lightning", "active":true } + # { "name": "lightning", "active":true }, # ] - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do [ -n "${result}" ] && result="${result}," result="${result}{\"name\":\"${container}\",\"active\":" eval "returncode=\$c_${container}" @@ -179,8 +207,8 @@ checkservice() { } timeout_feature() { - local interval=10 - local totaltime=60 + local interval=15 + local totaltime=120 local testwhat=${1} local returncode local endtime=$(($(date +%s) + ${totaltime})) @@ -218,14 +246,14 @@ feature_status() { # { "name": "pycoin", "active":true }, # { "name": "otsclient", "active":true }, # { "name": "bitcoin", "active":true }, -# { "name": "lightning", "active":true } +# { "name": "lightning", "active":true }, # ], # "features": [ # { "name": "gatekeeper", "working":true }, # { "name": "pycoin", "working":true }, # { "name": "otsclient", "working":true }, # { "name": "bitcoin", "working":true }, -# { "name": "lightning", "working":true } +# { "name": "lightning", "working":true }, # ] #} @@ -233,7 +261,11 @@ feature_status() { echo "EXIT_STATUS=1" > /dist/exitStatus.sh -brokenproxy="false" +############################# +# Ping containers and PROXY # +############################# + +workingproxy="true" containers=$(checkservice) returncode=$? finalreturncode=${returncode} @@ -242,7 +274,7 @@ if [ "${returncode}" -ne "0" ]; then status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"proxy\") | .active") if [ "${status}" = "false" ]; then echo -e "\e[1;31mThe Proxy, the main Cyphernode's component, is not responding. We will only test the gatekeeper if its container is up, but you'll see errors for the other components. Please check the logs." > /dev/console - brokenproxy="true" + workingproxy="false" fi else echo -e "\e[1;36mCyphernode seems to be correctly deployed. Let's run more thourough tests..." > /dev/console @@ -254,10 +286,14 @@ fi # { "name": "pycoin", "working":true }, # { "name": "otsclient", "working":true }, # { "name": "bitcoin", "working":true }, -# { "name": "lightning", "working":true } +# { "name": "lightning", "working":true }, # ] -result="${containers},\"features\":[{\"name\":\"gatekeeper\",\"working\":" +############################# +# GATEKEEPER # +############################# + +result="${containers},\"features\":[{\"coreFeature\":true, \"name\":\"proxy\",\"working\":${workingproxy}}, {\"coreFeature\":true, \"name\":\"gatekeeper\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"gatekeeper\") | .active") if [ "${status}" = "true" ]; then timeout_feature checkgatekeeper @@ -268,9 +304,43 @@ fi finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Gatekeeper error!')}" -result="${result},{\"name\":\"pycoin\",\"working\":" +############################# +# BROKER # +############################# + +result="${result},{\"coreFeature\":true, \"name\":\"broker\",\"working\":" +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"broker\") | .active") +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then + timeout_feature checkbroker + returncode=$? +else + returncode=1 +fi +finalreturncode=$((${returncode} | ${finalreturncode})) +result="${result}$(feature_status ${returncode} 'Broker error!')}" + +############################# +# NOTIFIER # +############################# + +result="${result},{\"coreFeature\":true, \"name\":\"notifier\",\"working\":" +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"notifier\") | .active") +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then + timeout_feature checknotifier + returncode=$? +else + returncode=1 +fi +finalreturncode=$((${returncode} | ${finalreturncode})) +result="${result}$(feature_status ${returncode} 'Notifier error!')}" + +############################# +# PYCOIN # +############################# + +result="${result},{\"coreFeature\":true, \"name\":\"pycoin\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"pycoin\") | .active") -if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then timeout_feature checkpycoin returncode=$? else @@ -280,9 +350,13 @@ finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Pycoin error!')}" <% if (features.indexOf('otsclient') != -1) { %> -result="${result},{\"name\":\"otsclient\",\"working\":" +############################# +# OTSCLIENT # +############################# + +result="${result},{\"coreFeature\":false, \"name\":\"otsclient\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"otsclient\") | .active") -if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then timeout_feature checkots returncode=$? else @@ -292,9 +366,16 @@ finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'OTSclient error!')}" <% } %> -result="${result},{\"name\":\"bitcoin\",\"working\":" +############################# +# BITCOIN # +############################# + +echo -e "\r\n\e[1;36mWaiting for Bitcoin Core to be ready... " > /dev/console +timeout_feature '[ -f "/bitcoin_monitor/up" ]' + +result="${result},{\"coreFeature\":true, \"name\":\"bitcoin\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"bitcoin\") | .active") -if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then timeout_feature checkbitcoinnode returncode=$? else @@ -304,9 +385,13 @@ finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Bitcoin error!')}" <% if (features.indexOf('lightning') != -1) { %> -result="${result},{\"name\":\"lightning\",\"working\":" +############################# +# LIGHTNING # +############################# + +result="${result},{\"coreFeature\":false, \"name\":\"lightning\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"lightning\") | .active") -if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then +if [[ "${workingproxy}" = "true" && "${status}" = "true" ]]; then timeout_feature checklnnode returncode=$? else @@ -314,12 +399,11 @@ else fi finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Lightning error!')}" + <% } %> result="{${result}]}" echo "${result}" > /gatekeeper/installation.json -echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console - echo "EXIT_STATUS=${finalreturncode}" > /dist/exitStatus.sh diff --git a/cyphernodeconf_docker/templates/lightning/c-lightning/README.md b/cyphernodeconf_docker/templates/lightning/c-lightning/README.md new file mode 100644 index 0000000..d2989c1 --- /dev/null +++ b/cyphernodeconf_docker/templates/lightning/c-lightning/README.md @@ -0,0 +1,5 @@ +# How to create the hmac for the cookie file: + +``` +# echo -n "access-key" | openssl dgst -hmac "cyphernode:sparkwallet" -sha256 -binary | base64 | sed 's/[\+\W]//g' +``` diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config b/cyphernodeconf_docker/templates/lightning/c-lightning/config similarity index 51% rename from install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config rename to cyphernodeconf_docker/templates/lightning/c-lightning/config index 2ced26c..feea5d9 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config +++ b/cyphernodeconf_docker/templates/lightning/c-lightning/config @@ -1,4 +1,7 @@ -<% if (net === 'testnet') { %> +<% if (net === 'regtest') { %> +# regtest +network=regtest +<% } else if (net === 'testnet') { %> # testnet network=testnet <% } else if (net === 'mainnet') { %> @@ -10,6 +13,9 @@ alias=<%= lightning_nodename %> <% if( lightning_nodecolor ) { %> rgb=<%= lightning_nodecolor %> <% } %> -bitcoin-rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> -bitcoin-rpcuser=<%= bitcoin_rpcuser %> -bitcoin-rpcpassword=<%= bitcoin_rpcpassword %> + +addr=0.0.0.0:9735 + +<% if( locals.lightning_external_ip ) { %> +announce-addr=<%= locals.lightning_external_ip %>:9735 +<% } %> diff --git a/cyphernodeconf_docker/templates/lightning/c-lightning/cookie b/cyphernodeconf_docker/templates/lightning/c-lightning/cookie new file mode 100644 index 0000000..830e6e6 --- /dev/null +++ b/cyphernodeconf_docker/templates/lightning/c-lightning/cookie @@ -0,0 +1 @@ +cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf b/cyphernodeconf_docker/templates/lightning/lnd/lnd.conf similarity index 100% rename from install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf rename to cyphernodeconf_docker/templates/lightning/lnd/lnd.conf diff --git a/cyphernodeconf_docker/templates/traefik/acme.json b/cyphernodeconf_docker/templates/traefik/acme.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/cyphernodeconf_docker/templates/traefik/acme.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/cyphernodeconf_docker/templates/traefik/htpasswd b/cyphernodeconf_docker/templates/traefik/htpasswd new file mode 100644 index 0000000..2f17faa --- /dev/null +++ b/cyphernodeconf_docker/templates/traefik/htpasswd @@ -0,0 +1 @@ +admin:<%- adminhash %> diff --git a/cyphernodeconf_docker/templates/traefik/traefik.toml b/cyphernodeconf_docker/templates/traefik/traefik.toml new file mode 100644 index 0000000..66bda92 --- /dev/null +++ b/cyphernodeconf_docker/templates/traefik/traefik.toml @@ -0,0 +1,31 @@ +debug = false + +logLevel = "ERROR" +defaultEntryPoints = ["https","http"] + +[entryPoints] + [entryPoints.http] + address = ":<%= traefik_http_port %>" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":<%= traefik_https_port %>" + [entryPoints.https.tls] + +[retry] + +[docker] +endpoint = "unix:///var/run/docker.sock" +domain = "cyphernode.localhost" +watch = true +exposedByDefault = false + +[acme] +email = "letsencrypt@yourdomain.com" +storage = "acme.json" +entryPoint = "https" +onHostRule = true +[acme.httpChallenge] +entryPoint = "http" +[[acme.domains]] + main = "cyphernode.yourdomain.com" diff --git a/cyphernodeconf_docker/test/apikey.test.js b/cyphernodeconf_docker/test/apikey.test.js new file mode 100644 index 0000000..9edf40e --- /dev/null +++ b/cyphernodeconf_docker/test/apikey.test.js @@ -0,0 +1,42 @@ +const ApiKey = require('../lib/apikey.js'); + + +test( 'Create ApiKey instance', ()=>{ + const apiKey = new ApiKey('testId',['group1','group2']); + expect( apiKey ).not.toBe( undefined ); + expect( apiKey.id ).toEqual( 'testId' ); + expect( apiKey.groups ).toEqual( ['group1','group2'] ); + expect( apiKey.key ).toBe( undefined ); + expect( apiKey.script ).toEqual( 'eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}' ); +}); + +test( 'Create ApiKey instance and randomise it', async ()=>{ + const apiKey = new ApiKey('testId',['group1','group2']); + await apiKey.randomiseKey(); + expect( apiKey ).not.toBe( undefined ); + expect( apiKey.id ).toEqual( 'testId' ); + expect( apiKey.groups ).toEqual( ['group1','group2'] ); + expect( apiKey.key ).not.toBe( undefined ); + expect( apiKey.script ).toEqual( 'eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}' ); +}); + +test( 'Create ApiKey instance, randomise it and use getters', async ()=>{ + const apiKey = new ApiKey('testId',['group1','group2']); + await apiKey.randomiseKey(); + const keyString = apiKey.getKey(); + const script = apiKey.script; + expect( keyString ).not.toBe( undefined ); + expect( apiKey.id ).toEqual( 'testId' ); + expect( apiKey.getClientInformation() ).toEqual( 'testId='+keyString ); + expect( apiKey.getConfigEntry() ).toEqual( `kapi_id="testId";kapi_key="${keyString}";kapi_groups="group1,group2";${script}` ); +}); + +test( 'Set properties of ApiKey instance from config entry', async () => { + const configEntry = 'kapi_id="000";kapi_key="b1fdc782037609f8ecc063ac192e92d57544263a950c637ed6b7d79cc9eb9f95";kapi_groups="stats";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}'; + const apiKey = new ApiKey(); + apiKey.setFromConfigEntry(configEntry); + expect( apiKey.id ).toEqual('000'); + expect( apiKey.groups ).toEqual(['stats']); + expect( apiKey.key ).toEqual('b1fdc782037609f8ecc063ac192e92d57544263a950c637ed6b7d79cc9eb9f95'); + expect( apiKey.script ).toEqual('eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}') +}) \ No newline at end of file diff --git a/cyphernodeconf_docker/test/archive.test.js b/cyphernodeconf_docker/test/archive.test.js new file mode 100644 index 0000000..386609a --- /dev/null +++ b/cyphernodeconf_docker/test/archive.test.js @@ -0,0 +1,26 @@ +const Archive = require('../lib/archive.js'); +const tmp = require('tmp'); +const path = require('path'); + +test( 'Create Archive instance', ()=>{ + new Archive( '/tmp/testArchive.7z', 'test123' ); +}); + +test( 'Write, Read, Delete', async ()=>{ + const tmpDir = tmp.dirSync(); + const archive = new Archive( path.join(tmpDir.name,'archive.7z'), 'test123' ); + + await archive.writeEntry('testEntry', 'testContent' ); + + const c0 = await archive.readEntry( 'testEntry' ); + + expect( c0.value ).toEqual( 'testContent' ); + + await archive.deleteEntry('testEntry'); + + const c1 = await archive.readEntry( 'testEntry' ); + + expect( c1.value ).toBe( '' ); + + tmpDir.removeCallback(); +}); diff --git a/cyphernodeconf_docker/test/cert.test.js b/cyphernodeconf_docker/test/cert.test.js new file mode 100644 index 0000000..9b1a937 --- /dev/null +++ b/cyphernodeconf_docker/test/cert.test.js @@ -0,0 +1,60 @@ +const Cert = require('../lib/cert.js'); + +test( 'Create Cert instance', ()=>{ + const cert = new Cert(); + expect( cert.args.days ).toBe( 3650 ); +}); + +test( 'buildConfig', ()=>{ + const cert = new Cert(); + const conf = cert.buildConfig(['127.0.0.1','localhost','gatekeeper']); + expect( conf ).toEqual(` +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +prompt = no +[req_distinguished_name] +CN = localhost +[v3_ca] +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +DNS.2 = gatekeeper +IP.1 = 127.0.0.1 +`); +}); + + +test( 'cns', () => { + const cert = new Cert(); + const cns = cert.cns(' abc, cde' ); + expect( cns ).toEqual([ + '127.0.0.1', + 'localhost', + 'gatekeeper', + 'abc', + 'cde' + ]); +}); + +test( 'create', async ()=>{ + jest.setTimeout(999999); + const cert = new Cert(); + const cns = cert.cns('abc,cde' ); + const r = await cert.create( cns ); + + expect( r.code ).toBe(0); + expect( r.key ).not.toBe(undefined); + expect( r.cert ).not.toBe(undefined); +}); + +test( 'create throws', async ()=>{ + const cert = new Cert(); + let err; + try { + await cert.create(); + } catch( e ) { + err = e; + } + expect( err ).not.toBe(undefined); +}); \ No newline at end of file diff --git a/cyphernodeconf_docker/test/config.test.js b/cyphernodeconf_docker/test/config.test.js new file mode 100644 index 0000000..e067705 --- /dev/null +++ b/cyphernodeconf_docker/test/config.test.js @@ -0,0 +1,151 @@ +const Config = require('../lib/config.js'); +const ApiKey = require('../lib/apikey.js'); +const fs = require('fs'); +const { promisify } = require( 'util' ); +const existsAsync = promisify( fs.exists ); +const configV010 = require('./data/config.0.1.0.json'); +const configV020 = require('./data/config.0.2.0.json'); +const configV022 = require('./data/config.0.2.2.json'); + + + +const expect020 = async (data) => { + const gatekeeper_keys = JSON.parse(JSON.stringify(configV010.gatekeeper_keys)); + expect( data.gatekeeper_keys.configEntries.length ).toBe( 4 ); + for( let i=0; i<3; i++ ) { + const configEntry = data.gatekeeper_keys.configEntries[i+1]; + const oldConfigEntry = gatekeeper_keys.configEntries[i]; + + const key = new ApiKey(); + key.setFromConfigEntry( configEntry ) + + const oldKey = new ApiKey(); + oldKey.setFromConfigEntry( oldConfigEntry ); + + expect( key.id ).toEqual( oldKey.id ); + expect( key.key ).toEqual( oldKey.key ); + expect( key.script ).toEqual( oldKey.script ); + + for( let oldGroup of oldKey.groups ) { + expect( key.groups ).toContain(oldGroup); + } + + expect( key.groups ).toContain('stats'); + + } +}; + +const expect022 = async (data) => { + expect( data.lightning_announce ).not.toBe(undefined); + expect( data.gatekeeper_expose ).not.toBe(undefined); +}; + +let configFileName; + +beforeAll(() => { + configFileName = '/tmp/config'+Math.round(Math.random()*100000000)+'.7z'; +}); + +test( 'create config v0.1.0', () => { + new Config(configV010); +}); + + +test( 'create config v0.2.0', () => { + new Config(configV020); +}); + + +test( 'validate config v0.1.0', () => { + const config = new Config(JSON.parse(JSON.stringify(configV010))); + config.data.foo = "bar"; + config.data.bar = "foo"; + config.validate(); + expect( config.data.foo ).toBe( undefined ); + expect( config.data.bar ).toBe( undefined ); +}); + + +test( 'validate config v0.2.0', () => { + const config = new Config(JSON.parse(JSON.stringify(configV020))); + config.data.foo = "bar"; + config.data.bar = "foo"; + config.validate(); + expect( config.data.foo ).toBe( undefined ); + expect( config.data.bar ).toBe( undefined ); +}); + +test( 'validate config v0.2.2', () => { + const config = new Config(JSON.parse(JSON.stringify(configV022))); + config.data.foo = "bar"; + config.data.bar = "foo"; + config.validate(); + expect( config.data.foo ).toBe( undefined ); + expect( config.data.bar ).toBe( undefined ); +}); + +test( 'generateMigrationPathToLatest from 0.1.0', async () => { + const config = new Config(); + const path = config.generateMigrationPathToLatest('0.1.0'); + expect( path ).toEqual( [config.migrate_0_1_0_to_0_2_0, config.migrate_0_2_0_to_0_2_2] ); +}); + +test( 'generateMigrationPathToLatest from 0.2.0', async () => { + const config = new Config(); + const path = config.generateMigrationPathToLatest('0.2.0'); + expect( path ).toEqual( [config.migrate_0_2_0_to_0_2_2] ); +}); + +test( 'generateMigrationPathToLatest from 0.2.2', async () => { + const config = new Config(); + const path = config.generateMigrationPathToLatest('0.2.2'); + expect( path ).toBe( undefined ); +}); + +test( 'migrate 0.1.0 -> 0.2.0', async () => { + const config = new Config(); + config.setData( JSON.parse(JSON.stringify(configV010)) ); + // deep clone gatekeeper_keys + await config.migrate_0_1_0_to_0_2_0(); + expect020( config.data ); + +}); + +test( 'migrate 0.2.0 -> 0.2.2', async () => { + const config = new Config(); + config.setData( JSON.parse(JSON.stringify(configV020)) ); + await config.migrate_0_2_0_to_0_2_2(); + expect022(config.data); + +}); + +test( 'migrateFrom 0.1.0', async () => { + const config = new Config(); + config.setData( JSON.parse(JSON.stringify(configV010)) ); + await config.migrateFrom('0.1.0'); + config.validate(); + expect020(config.data); + expect022(config.data); + expect( config.data.traefik_http_port ).toEqual( 80 ); + expect( config.data.traefik_https_port ).toEqual( 443 ); +}); + + + +test( 'serialise', async () => { + const config = new Config(); + config.setData( JSON.parse(JSON.stringify(configV022)) ) + const success = await config.serialize(configFileName,'test123' ); + const exists = await existsAsync(configFileName); + expect( success ).toEqual( true ); + expect( exists ).toEqual( true ); +}); + +test( 'deserialise', async () => { + const config = new Config( { + setup_version: 'setup_version' + } ); + await config.deserialize(configFileName,'test123' ); + expect( config.data ).toEqual( configV022 ); +}); + diff --git a/cyphernodeconf_docker/test/data/config.0.1.0.json b/cyphernodeconf_docker/test/data/config.0.1.0.json new file mode 100644 index 0000000..a54c194 --- /dev/null +++ b/cyphernodeconf_docker/test/data/config.0.1.0.json @@ -0,0 +1,55 @@ +{ + "features": [ + "lightning", + "otsclient" + ], + "net": "testnet", + "use_xpub": true, + "installer_mode": "docker", + "run_as_different_user": true, + "username": "cyphernode", + "docker_mode": "compose", + "bitcoin_rpcuser": "bitcoin", + "bitcoin_rpcpassword": "test123", + "bitcoin_uacomment": "", + "bitcoin_prune": false, + "bitcoin_prune_size": 550, + "bitcoin_node_ip": "123.123.123.123", + "bitcoin_datapath": "/Users/jash/.cyphernode/bitcoin", + "bitcoin_mode": "internal", + "bitcoin_expose": false, + "lightning_expose": true, + "gatekeeper_apiproperties": "GKAP", + "gatekeeper_keys": { + "configEntries": [ + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ], + "clientInformation": [ + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + }, + "gatekeeper_sslcert": "-----BEGIN CERTIFICATE-----\nMIIE/jCCAuagAwIBAgIJAIBv4aiI2NRtMA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNV\nBAMME2Rpc2swYm9vay5mcml0ei5ib3gwHhcNMTkwMTE3MTcwMDA5WhcNMjkwMTE0\nMTcwMDA5WjAeMRwwGgYDVQQDDBNkaXNrMGJvb2suZnJpdHouYm94MIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyVKNTGlPfd4QX9HaDc9a6prbc9il4jtR\niChSlMf3/6UfAMcS+xVgR2iR8FK/DQuFzxn+6BybpoiD333rjDr7zR7y9px3Xph2\nbmsjZy0hv9SIBbx0DJvvwODTlWTAH8qgU2DN6xWc7vjgeGi5uTpnmwWrkH6BjtVr\nwoBkF0JmfH7KiLS/QjWqPKeI6o/GpvCP9meD131Sq/ReoOTrJ4F5aNdhAril4nU5\n6e7Y+Iyp35DZSLuU+pDJAhxEvkYGas1ted5RRxlho8ukaoABCbmaTeNmgsJxK2SC\nABjfUc38aAlNLuMbMMR7Q85Z84OTJiUqanVczwdSj1QHlNCWZK1McBPhj2m2Wdge\ngedrq5XcjQGChzTEozcFntU0qzY3ja1+DOE8UaMaTrDH4saUXCMZk3W1m5mmiZW3\nmcB0cKGdeg6K6USg1BwBTU9qolUusxz5T0tNxjcMlXU93P17d4s5IXfliXhMNr/6\n4fl78Ey3FNprTix4alW7hBAp/eA/LhS55s3jwdoVzJl4RELC0284pahj5exYQwU6\nzjLedMxzC+7veQYwWfZOs9jVCTP0YStuT0j9xD3ausLZyB1Egbsajyy71IeoYOf1\n9S6dFIXE5LHAw2j3D3bh5wb019I8V5szGbeemdBpb3m+bzT8qjLSNranuT41CIHd\nIYjq85vDEJsCAwEAAaM/MD0wOwYDVR0RBDQwMoITZGlzazBib29rLmZyaXR6LmJv\neIIJbG9jYWxob3N0ggpnYXRla2VlcGVyhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IC\nAQBrE4bJsIMwSRPng94PcqR5F6Cux0bkwezALJCHpjHTuqok/wHHE5dZsAXcSsYc\n5givuBESih6CpY5h21Od0TBugyv3FCRY8OoaBXtlO6FYlEnVeJ8AOexJTb3qcbBS\nHU8MBWEydUh5HFA3PRKAG0Y4cvUK4WXJZ42Et3td0NkGFOv6bxdtVGB4Vz7FGn+3\nqd9fpmFCdQYDp6RSZDDz4B8XLsVuTeTES5GbUMSQAGanP7jxMr04wQ3MuoZrRODN\nFatifOJfq0fZddsBjJbrTLxArIqaPh3J4xzwiNE5du4CQDQrbbHXG22kuvbr5foA\ncixLnuyWMq0a5a70mSNS6TZ3nq4ATXNNa0cZ8fBxHqHGTLM8gQisW8vTaZfIFh/i\nhnFcGxtpo1ryi7JG9HCWsh0x20677iag5MuZfv2s4TbK71Ol6WV4FravCqU0qgbn\nTTl+BnYw3H67FO/a6RD4ISlFWK+8EVEQdMgvPoRuw323YznT0Nd8Q/Gq8raYF2wa\nz9T9OXu6TcVGtfPAgX+AM/+hDqWGxyiFR9ZtLpGOHGP8f+TZA5uCawc8Zry4yN6L\nE0yPIx96pJz59T3k8XbRHTQCaPsSUGRAZIY9LpJj0fIG7zCr9eCBpp2qyzmpyNfx\negN3ILYy1Y8JbJj73HWyP0F3Am7i76tkCWB7tQeFOb5FMg==\n-----END CERTIFICATE-----\n", + "gatekeeper_sslkey": "-----BEGIN PRIVATE KEY-----\nMIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDJUo1MaU993hBf\n0doNz1rqmttz2KXiO1GIKFKUx/f/pR8AxxL7FWBHaJHwUr8NC4XPGf7oHJumiIPf\nfeuMOvvNHvL2nHdemHZuayNnLSG/1IgFvHQMm+/A4NOVZMAfyqBTYM3rFZzu+OB4\naLm5OmebBauQfoGO1WvCgGQXQmZ8fsqItL9CNao8p4jqj8am8I/2Z4PXfVKr9F6g\n5OsngXlo12ECuKXidTnp7tj4jKnfkNlIu5T6kMkCHES+RgZqzW153lFHGWGjy6Rq\ngAEJuZpN42aCwnErZIIAGN9RzfxoCU0u4xswxHtDzlnzg5MmJSpqdVzPB1KPVAeU\n0JZkrUxwE+GPabZZ2B6B52urldyNAYKHNMSjNwWe1TSrNjeNrX4M4TxRoxpOsMfi\nxpRcIxmTdbWbmaaJlbeZwHRwoZ16DorpRKDUHAFNT2qiVS6zHPlPS03GNwyVdT3c\n/Xt3izkhd+WJeEw2v/rh+XvwTLcU2mtOLHhqVbuEECn94D8uFLnmzePB2hXMmXhE\nQsLTbzilqGPl7FhDBTrOMt50zHML7u95BjBZ9k6z2NUJM/RhK25PSP3EPdq6wtnI\nHUSBuxqPLLvUh6hg5/X1Lp0UhcTkscDDaPcPduHnBvTX0jxXmzMZt56Z0Glveb5v\nNPyqMtI2tqe5PjUIgd0hiOrzm8MQmwIDAQABAoICAQCI5uA7M+ngd9++qR+VAIqc\nus28y3iSjS/2XSU7E3irmYepqbZYk8KzDIMhX8OXhVxq5wyWns2hw3eZxTEmXP3a\nEM+7r87kvtzaXXTntqMapdYRwINSB8BT8w8uqiKT++Bmko+06y+auhc7Ckwxj2vg\n2Uw/qCdGEA+FZnWp83dp9XaY3ACrb37iXDMY/shhwXjEYMQhB5HuaPDojIL0jHEZ\nQE0x4oq7omfNkqRs8IqcAw4fDaBTe52VF9APa+L1QdjOZMX0iWgCUHrwCTere1FY\n4ehVxw/aKDDXDBLguCiKPrkDx2A4G4SPKYW1uKWZ7PAZENIZ3qrf2I6HPgjnUYmG\nAHQiR3JcwsXFZZAMW/kbqzRCS7CrvNnrzcUL9JAlpFmMDeAFIlbVkFED+kOtVioR\nPAcDWKtlWOWbX3Kn218FCblH86XdzB9H/pgbHxf8cXFcnaqVApxC1zv+uIaV526p\nU9maF8CMVX5bZ21e+dpP1BQ3DDRn3DCQno/QrGGMxK66EBVrHrllHBYpUppcj4w8\nCn5RDhp2KJjYsgX9zjuXif1gdP2jqBCDWCog+YwsoQP1Qp613D6rl5TKaa/rfZmX\nfG5Q98/wfAHwLjIDfwCXWqKOFoGdMzxg9hxk0bSNn64m1UY6OYB4yJs/o6spqGXq\nZRaX2LStSq5fhvB+tjl3AQKCAQEA7wBS3t2dHOVLZGVycxSq1LRoeW44KxZaNSiv\nXj5Xrw/jAnnAEAcVFrCGFY40MC4SNM//VUWHs4zKXxAhK/vFOgSvmnNh6mWDd/sO\nFyzo17fhwhi4u8BIjvchHgwg5aMi4uCLFM8RBZjI5MpMyNBwxT5VLKKKv3N9YtUK\n5JPhXkZJnhOhKj3vTaCeJFBMvcknoqqNcUcEEMo0d+YqUAYh9+8qpjBSKBV+8/VZ\nbQwyjd+EN5ajqRLIjKOy1GrbRwBcxadGdZzqMDYlSVBdAxAssPiUqhTLeHAuGfbK\nB0iF0DLgwl0N/6qx0WtLTA3MY7NcQJ/cjUioJKqbnvouEP2f2wKCAQEA16Qulg5I\nyDbJsiFzmeLNpKSXeh9y1q/hvqizd8R9GAUh9TVSgAnoiCpucguvMYBsXeGU3srr\no9AvxmkmrMWVNZbolaMRv0p+nXPR6uF5tFQJ/jYm1H/jI3ieF2ZXJveQBkqjRsOD\nsWI4HNuarGnsOo8rqV0ybYGFks1dhuvBZp7RemZbXqaFnk/D8FtkmHRsWUdEQifd\njHCATRbUHjAm8tk7HjdJbhYIMCZkbN1HRkx044pk+os37Eqi4Ok+s/MbQ7g2lY+R\nt1sxrJo4dsqxwflz9U6U5ECwH2hxuzpowGXwfcqtPLvVJT1p+TUxIHNb2eUvJg7R\ntsx4BzC4FbaOQQKCAQBMaHYV+hO4oTWxKx2j3P+gcOzVpX8fh03foNov7w5pUVLp\n/7J/bUQ4tMapLYVRwejgKX8f69KufFxWd/mi7iLnoYfigPDU1w9o1EJ09k6aaJcs\nTmsA20BXNHrJ+GasA7OrhM3yISD4ARh4zJQvvzPOW1cvpedlva5gYmvRF9X9Jctz\nViM4NgTDdI4aXfGq2xxozM3bYTDTjVGo41SzsMI7WaLw7pHVbsnfhJjKXBPtd3/1\npZs8+lxTWiJZ9q+Ty3HdXtUP1NKqByV0gtS5nforuc0Ncwh5wKN8eYGtQmpFXX6t\npMJM8m1W3k58Sg0F8tmTb4g7Qvc+gayu7py7odnxAoIBAQC5pJqGjF2UH7acJ7hB\nrsOjDh9p/1D6Cgip/soiPYID/8cwNmuD1wPc1cqnW+/DCfBBEkb7Vm5uZHf8s+Gb\ns620qOoqiGxq5lMCcgcx0lLYL9E6jJv5LO/6RYi0VGKLJW1UPUU7Um45c3kjPtt1\nuuqnr0HDWHxlUbAQpcPyt1uUYP2uBhh675jwpXLlpYiAxxnP8k6NNYzBrsiFlTWf\nl4ywXNtMNAR/RXBfI24pWoZVutSWXzp3hwrp3YdDYQmeGZhLQHedYi/sThIBqfMa\nMX2+pGZztObxac81+tCOgsZTfG8BnE5vjrT8jlaBOI3Ghgl5GJjyhqd8W6KpbgPM\njZEBAoIBAQCuI/dv6opNniMmOY+iI1Oh6kyNnctKe/VF2SqwfeK3I6fCfcNEm46u\nQHnJWqVCIQ1ZsU8b/Pn0+mJYB/n+OYh585DsDR2YEJPXE+qrVHG1qIEEyTgcTb30\n+nehJ6d5SYMej4VMdepgMD7HeG7Nly1wwu0VYTUTLP3Z812NUdnBYmb5QSRnsHrk\nOVlAkJAaunAk1O3rwwdyGwMmNVIgzf0foa/c8uFQCbUHocQUwr//vN+U+Nhi4VQC\nnRiPT36rWxLqwyn8sa7dcE9A6Hp2KDBc97DJTDNvA3StF1JHAf/EoiTq3mWvVNhR\nmPN+iWirFH2RN5KkSaTzEtrXAgbLvic9\n-----END PRIVATE KEY-----\n", + "gatekeeper_cns": "disk0book.fritz.box", + "proxy_datapath": "/Users/jash/.cyphernode/proxy", + "lightning_implementation": "c-lightning", + "lightning_datapath": "/Users/jash/.cyphernode/lightning", + "lightning_nodename": "🚀 Disciplined Dormouse 🚀", + "lightning_nodecolor": "ff0000", + "otsclient_datapath": "/Users/jash/.cyphernode/otsclient", + "adminhash": "BsJFlh7q4JmwI\n", + "use_xpub": true, + "xpub": "abc", + "derivation_path": "da", + "gatekeeper_clientkeyspassword": "test123", + "gatekeeper_clientkeyspassword_c": "test123", + "gatekeeper_statuspw": "dasfdssad", + "gatekeeper_datapath": "/Users/jash/.cyphernode/gatekeeper", + "traefik_datapath": "foo", + "lightning_external_ip": "123.123.123.123" + +} \ No newline at end of file diff --git a/cyphernodeconf_docker/test/data/config.0.2.0.json b/cyphernodeconf_docker/test/data/config.0.2.0.json new file mode 100644 index 0000000..df63db7 --- /dev/null +++ b/cyphernodeconf_docker/test/data/config.0.2.0.json @@ -0,0 +1,52 @@ +{ + "schema_version": "0.2.0", + "features": [ + "lightning", + "otsclient" + ], + "net": "testnet", + "use_xpub": true, + "installer_mode": "docker", + "run_as_different_user": true, + "username": "cyphernode", + "docker_mode": "compose", + "bitcoin_rpcuser": "bitcoin", + "bitcoin_rpcpassword": "test123", + "bitcoin_uacomment": "", + "bitcoin_prune": false, + "bitcoin_prune_size": 550, + "bitcoin_datapath": "/Users/jash/.cyphernode/bitcoin", + "bitcoin_mode": "internal", + "bitcoin_expose": false, + "lightning_expose": true, + "gatekeeper_keys": { + "configEntries": [ + "kapi_id=\"000\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ], + "clientInformation": [ + "000=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + }, + "gatekeeper_sslcert": "-----BEGIN CERTIFICATE-----\nMIIE/jCCAuagAwIBAgIJAIBv4aiI2NRtMA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNV\nBAMME2Rpc2swYm9vay5mcml0ei5ib3gwHhcNMTkwMTE3MTcwMDA5WhcNMjkwMTE0\nMTcwMDA5WjAeMRwwGgYDVQQDDBNkaXNrMGJvb2suZnJpdHouYm94MIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyVKNTGlPfd4QX9HaDc9a6prbc9il4jtR\niChSlMf3/6UfAMcS+xVgR2iR8FK/DQuFzxn+6BybpoiD333rjDr7zR7y9px3Xph2\nbmsjZy0hv9SIBbx0DJvvwODTlWTAH8qgU2DN6xWc7vjgeGi5uTpnmwWrkH6BjtVr\nwoBkF0JmfH7KiLS/QjWqPKeI6o/GpvCP9meD131Sq/ReoOTrJ4F5aNdhAril4nU5\n6e7Y+Iyp35DZSLuU+pDJAhxEvkYGas1ted5RRxlho8ukaoABCbmaTeNmgsJxK2SC\nABjfUc38aAlNLuMbMMR7Q85Z84OTJiUqanVczwdSj1QHlNCWZK1McBPhj2m2Wdge\ngedrq5XcjQGChzTEozcFntU0qzY3ja1+DOE8UaMaTrDH4saUXCMZk3W1m5mmiZW3\nmcB0cKGdeg6K6USg1BwBTU9qolUusxz5T0tNxjcMlXU93P17d4s5IXfliXhMNr/6\n4fl78Ey3FNprTix4alW7hBAp/eA/LhS55s3jwdoVzJl4RELC0284pahj5exYQwU6\nzjLedMxzC+7veQYwWfZOs9jVCTP0YStuT0j9xD3ausLZyB1Egbsajyy71IeoYOf1\n9S6dFIXE5LHAw2j3D3bh5wb019I8V5szGbeemdBpb3m+bzT8qjLSNranuT41CIHd\nIYjq85vDEJsCAwEAAaM/MD0wOwYDVR0RBDQwMoITZGlzazBib29rLmZyaXR6LmJv\neIIJbG9jYWxob3N0ggpnYXRla2VlcGVyhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IC\nAQBrE4bJsIMwSRPng94PcqR5F6Cux0bkwezALJCHpjHTuqok/wHHE5dZsAXcSsYc\n5givuBESih6CpY5h21Od0TBugyv3FCRY8OoaBXtlO6FYlEnVeJ8AOexJTb3qcbBS\nHU8MBWEydUh5HFA3PRKAG0Y4cvUK4WXJZ42Et3td0NkGFOv6bxdtVGB4Vz7FGn+3\nqd9fpmFCdQYDp6RSZDDz4B8XLsVuTeTES5GbUMSQAGanP7jxMr04wQ3MuoZrRODN\nFatifOJfq0fZddsBjJbrTLxArIqaPh3J4xzwiNE5du4CQDQrbbHXG22kuvbr5foA\ncixLnuyWMq0a5a70mSNS6TZ3nq4ATXNNa0cZ8fBxHqHGTLM8gQisW8vTaZfIFh/i\nhnFcGxtpo1ryi7JG9HCWsh0x20677iag5MuZfv2s4TbK71Ol6WV4FravCqU0qgbn\nTTl+BnYw3H67FO/a6RD4ISlFWK+8EVEQdMgvPoRuw323YznT0Nd8Q/Gq8raYF2wa\nz9T9OXu6TcVGtfPAgX+AM/+hDqWGxyiFR9ZtLpGOHGP8f+TZA5uCawc8Zry4yN6L\nE0yPIx96pJz59T3k8XbRHTQCaPsSUGRAZIY9LpJj0fIG7zCr9eCBpp2qyzmpyNfx\negN3ILYy1Y8JbJj73HWyP0F3Am7i76tkCWB7tQeFOb5FMg==\n-----END CERTIFICATE-----\n", + "gatekeeper_sslkey": "-----BEGIN PRIVATE KEY-----\nMIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDJUo1MaU993hBf\n0doNz1rqmttz2KXiO1GIKFKUx/f/pR8AxxL7FWBHaJHwUr8NC4XPGf7oHJumiIPf\nfeuMOvvNHvL2nHdemHZuayNnLSG/1IgFvHQMm+/A4NOVZMAfyqBTYM3rFZzu+OB4\naLm5OmebBauQfoGO1WvCgGQXQmZ8fsqItL9CNao8p4jqj8am8I/2Z4PXfVKr9F6g\n5OsngXlo12ECuKXidTnp7tj4jKnfkNlIu5T6kMkCHES+RgZqzW153lFHGWGjy6Rq\ngAEJuZpN42aCwnErZIIAGN9RzfxoCU0u4xswxHtDzlnzg5MmJSpqdVzPB1KPVAeU\n0JZkrUxwE+GPabZZ2B6B52urldyNAYKHNMSjNwWe1TSrNjeNrX4M4TxRoxpOsMfi\nxpRcIxmTdbWbmaaJlbeZwHRwoZ16DorpRKDUHAFNT2qiVS6zHPlPS03GNwyVdT3c\n/Xt3izkhd+WJeEw2v/rh+XvwTLcU2mtOLHhqVbuEECn94D8uFLnmzePB2hXMmXhE\nQsLTbzilqGPl7FhDBTrOMt50zHML7u95BjBZ9k6z2NUJM/RhK25PSP3EPdq6wtnI\nHUSBuxqPLLvUh6hg5/X1Lp0UhcTkscDDaPcPduHnBvTX0jxXmzMZt56Z0Glveb5v\nNPyqMtI2tqe5PjUIgd0hiOrzm8MQmwIDAQABAoICAQCI5uA7M+ngd9++qR+VAIqc\nus28y3iSjS/2XSU7E3irmYepqbZYk8KzDIMhX8OXhVxq5wyWns2hw3eZxTEmXP3a\nEM+7r87kvtzaXXTntqMapdYRwINSB8BT8w8uqiKT++Bmko+06y+auhc7Ckwxj2vg\n2Uw/qCdGEA+FZnWp83dp9XaY3ACrb37iXDMY/shhwXjEYMQhB5HuaPDojIL0jHEZ\nQE0x4oq7omfNkqRs8IqcAw4fDaBTe52VF9APa+L1QdjOZMX0iWgCUHrwCTere1FY\n4ehVxw/aKDDXDBLguCiKPrkDx2A4G4SPKYW1uKWZ7PAZENIZ3qrf2I6HPgjnUYmG\nAHQiR3JcwsXFZZAMW/kbqzRCS7CrvNnrzcUL9JAlpFmMDeAFIlbVkFED+kOtVioR\nPAcDWKtlWOWbX3Kn218FCblH86XdzB9H/pgbHxf8cXFcnaqVApxC1zv+uIaV526p\nU9maF8CMVX5bZ21e+dpP1BQ3DDRn3DCQno/QrGGMxK66EBVrHrllHBYpUppcj4w8\nCn5RDhp2KJjYsgX9zjuXif1gdP2jqBCDWCog+YwsoQP1Qp613D6rl5TKaa/rfZmX\nfG5Q98/wfAHwLjIDfwCXWqKOFoGdMzxg9hxk0bSNn64m1UY6OYB4yJs/o6spqGXq\nZRaX2LStSq5fhvB+tjl3AQKCAQEA7wBS3t2dHOVLZGVycxSq1LRoeW44KxZaNSiv\nXj5Xrw/jAnnAEAcVFrCGFY40MC4SNM//VUWHs4zKXxAhK/vFOgSvmnNh6mWDd/sO\nFyzo17fhwhi4u8BIjvchHgwg5aMi4uCLFM8RBZjI5MpMyNBwxT5VLKKKv3N9YtUK\n5JPhXkZJnhOhKj3vTaCeJFBMvcknoqqNcUcEEMo0d+YqUAYh9+8qpjBSKBV+8/VZ\nbQwyjd+EN5ajqRLIjKOy1GrbRwBcxadGdZzqMDYlSVBdAxAssPiUqhTLeHAuGfbK\nB0iF0DLgwl0N/6qx0WtLTA3MY7NcQJ/cjUioJKqbnvouEP2f2wKCAQEA16Qulg5I\nyDbJsiFzmeLNpKSXeh9y1q/hvqizd8R9GAUh9TVSgAnoiCpucguvMYBsXeGU3srr\no9AvxmkmrMWVNZbolaMRv0p+nXPR6uF5tFQJ/jYm1H/jI3ieF2ZXJveQBkqjRsOD\nsWI4HNuarGnsOo8rqV0ybYGFks1dhuvBZp7RemZbXqaFnk/D8FtkmHRsWUdEQifd\njHCATRbUHjAm8tk7HjdJbhYIMCZkbN1HRkx044pk+os37Eqi4Ok+s/MbQ7g2lY+R\nt1sxrJo4dsqxwflz9U6U5ECwH2hxuzpowGXwfcqtPLvVJT1p+TUxIHNb2eUvJg7R\ntsx4BzC4FbaOQQKCAQBMaHYV+hO4oTWxKx2j3P+gcOzVpX8fh03foNov7w5pUVLp\n/7J/bUQ4tMapLYVRwejgKX8f69KufFxWd/mi7iLnoYfigPDU1w9o1EJ09k6aaJcs\nTmsA20BXNHrJ+GasA7OrhM3yISD4ARh4zJQvvzPOW1cvpedlva5gYmvRF9X9Jctz\nViM4NgTDdI4aXfGq2xxozM3bYTDTjVGo41SzsMI7WaLw7pHVbsnfhJjKXBPtd3/1\npZs8+lxTWiJZ9q+Ty3HdXtUP1NKqByV0gtS5nforuc0Ncwh5wKN8eYGtQmpFXX6t\npMJM8m1W3k58Sg0F8tmTb4g7Qvc+gayu7py7odnxAoIBAQC5pJqGjF2UH7acJ7hB\nrsOjDh9p/1D6Cgip/soiPYID/8cwNmuD1wPc1cqnW+/DCfBBEkb7Vm5uZHf8s+Gb\ns620qOoqiGxq5lMCcgcx0lLYL9E6jJv5LO/6RYi0VGKLJW1UPUU7Um45c3kjPtt1\nuuqnr0HDWHxlUbAQpcPyt1uUYP2uBhh675jwpXLlpYiAxxnP8k6NNYzBrsiFlTWf\nl4ywXNtMNAR/RXBfI24pWoZVutSWXzp3hwrp3YdDYQmeGZhLQHedYi/sThIBqfMa\nMX2+pGZztObxac81+tCOgsZTfG8BnE5vjrT8jlaBOI3Ghgl5GJjyhqd8W6KpbgPM\njZEBAoIBAQCuI/dv6opNniMmOY+iI1Oh6kyNnctKe/VF2SqwfeK3I6fCfcNEm46u\nQHnJWqVCIQ1ZsU8b/Pn0+mJYB/n+OYh585DsDR2YEJPXE+qrVHG1qIEEyTgcTb30\n+nehJ6d5SYMej4VMdepgMD7HeG7Nly1wwu0VYTUTLP3Z812NUdnBYmb5QSRnsHrk\nOVlAkJAaunAk1O3rwwdyGwMmNVIgzf0foa/c8uFQCbUHocQUwr//vN+U+Nhi4VQC\nnRiPT36rWxLqwyn8sa7dcE9A6Hp2KDBc97DJTDNvA3StF1JHAf/EoiTq3mWvVNhR\nmPN+iWirFH2RN5KkSaTzEtrXAgbLvic9\n-----END PRIVATE KEY-----\n", + "gatekeeper_cns": "disk0book.fritz.box", + "proxy_datapath": "/Users/jash/.cyphernode/proxy", + "lightning_implementation": "c-lightning", + "lightning_datapath": "/Users/jash/.cyphernode/lightning", + "lightning_nodename": "🚀 Disciplined Dormouse 🚀", + "lightning_nodecolor": "ff0000", + "otsclient_datapath": "/Users/jash/.cyphernode/otsclient", + "adminhash": "BsJFlh7q4JmwI\n", + "use_xpub": true, + "xpub": "abc", + "derivation_path": "da", + "gatekeeper_clientkeyspassword": "test123", + "gatekeeper_datapath": "/Users/jash/.cyphernode/gatekeeper", + "traefik_datapath": "foo" +} \ No newline at end of file diff --git a/cyphernodeconf_docker/test/data/config.0.2.2.json b/cyphernodeconf_docker/test/data/config.0.2.2.json new file mode 100644 index 0000000..51926d9 --- /dev/null +++ b/cyphernodeconf_docker/test/data/config.0.2.2.json @@ -0,0 +1,59 @@ +{ + "schema_version": "0.2.2", + "setup_version": "setup_version", + "docker_versions": {}, + "features": [ + "lightning", + "otsclient" + ], + "net": "testnet", + "use_xpub": true, + "installer_mode": "docker", + "run_as_different_user": true, + "username": "cyphernode", + "docker_mode": "compose", + "bitcoin_rpcuser": "bitcoin", + "bitcoin_rpcpassword": "test123", + "bitcoin_uacomment": "", + "bitcoin_prune": false, + "bitcoin_prune_size": 550, + "bitcoin_datapath": "/Users/jash/.cyphernode/bitcoin", + "bitcoin_mode": "internal", + "bitcoin_expose": false, + "lightning_expose": true, + "gatekeeper_port": 2009, + "gatekeeper_keys": { + "configEntries": [ + "kapi_id=\"000\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"001\";kapi_key=\"a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a\";kapi_groups=\"watcher\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"002\";kapi_key=\"fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d\";kapi_groups=\"watcher,spender\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}", + "kapi_id=\"003\";kapi_key=\"f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417\";kapi_groups=\"watcher,spender,admin\";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}" + ], + "clientInformation": [ + "000=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "001=a27f9e73fdde6a5005879c273c9aea5e8d917eec77bbdfd73272c0af9b4c6b7a", + "002=fe58ddbb66d7302a7087af3242a98b6326c51a257f5eab1c06bb8cc02e25890d", + "003=f0b8bb52f4c7007938757bcdfc73b452d6ce08cc0c660ce57c5464ae95f35417" + ] + }, + "gatekeeper_sslcert": "-----BEGIN CERTIFICATE-----\nMIIE/jCCAuagAwIBAgIJAIBv4aiI2NRtMA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNV\nBAMME2Rpc2swYm9vay5mcml0ei5ib3gwHhcNMTkwMTE3MTcwMDA5WhcNMjkwMTE0\nMTcwMDA5WjAeMRwwGgYDVQQDDBNkaXNrMGJvb2suZnJpdHouYm94MIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyVKNTGlPfd4QX9HaDc9a6prbc9il4jtR\niChSlMf3/6UfAMcS+xVgR2iR8FK/DQuFzxn+6BybpoiD333rjDr7zR7y9px3Xph2\nbmsjZy0hv9SIBbx0DJvvwODTlWTAH8qgU2DN6xWc7vjgeGi5uTpnmwWrkH6BjtVr\nwoBkF0JmfH7KiLS/QjWqPKeI6o/GpvCP9meD131Sq/ReoOTrJ4F5aNdhAril4nU5\n6e7Y+Iyp35DZSLuU+pDJAhxEvkYGas1ted5RRxlho8ukaoABCbmaTeNmgsJxK2SC\nABjfUc38aAlNLuMbMMR7Q85Z84OTJiUqanVczwdSj1QHlNCWZK1McBPhj2m2Wdge\ngedrq5XcjQGChzTEozcFntU0qzY3ja1+DOE8UaMaTrDH4saUXCMZk3W1m5mmiZW3\nmcB0cKGdeg6K6USg1BwBTU9qolUusxz5T0tNxjcMlXU93P17d4s5IXfliXhMNr/6\n4fl78Ey3FNprTix4alW7hBAp/eA/LhS55s3jwdoVzJl4RELC0284pahj5exYQwU6\nzjLedMxzC+7veQYwWfZOs9jVCTP0YStuT0j9xD3ausLZyB1Egbsajyy71IeoYOf1\n9S6dFIXE5LHAw2j3D3bh5wb019I8V5szGbeemdBpb3m+bzT8qjLSNranuT41CIHd\nIYjq85vDEJsCAwEAAaM/MD0wOwYDVR0RBDQwMoITZGlzazBib29rLmZyaXR6LmJv\neIIJbG9jYWxob3N0ggpnYXRla2VlcGVyhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IC\nAQBrE4bJsIMwSRPng94PcqR5F6Cux0bkwezALJCHpjHTuqok/wHHE5dZsAXcSsYc\n5givuBESih6CpY5h21Od0TBugyv3FCRY8OoaBXtlO6FYlEnVeJ8AOexJTb3qcbBS\nHU8MBWEydUh5HFA3PRKAG0Y4cvUK4WXJZ42Et3td0NkGFOv6bxdtVGB4Vz7FGn+3\nqd9fpmFCdQYDp6RSZDDz4B8XLsVuTeTES5GbUMSQAGanP7jxMr04wQ3MuoZrRODN\nFatifOJfq0fZddsBjJbrTLxArIqaPh3J4xzwiNE5du4CQDQrbbHXG22kuvbr5foA\ncixLnuyWMq0a5a70mSNS6TZ3nq4ATXNNa0cZ8fBxHqHGTLM8gQisW8vTaZfIFh/i\nhnFcGxtpo1ryi7JG9HCWsh0x20677iag5MuZfv2s4TbK71Ol6WV4FravCqU0qgbn\nTTl+BnYw3H67FO/a6RD4ISlFWK+8EVEQdMgvPoRuw323YznT0Nd8Q/Gq8raYF2wa\nz9T9OXu6TcVGtfPAgX+AM/+hDqWGxyiFR9ZtLpGOHGP8f+TZA5uCawc8Zry4yN6L\nE0yPIx96pJz59T3k8XbRHTQCaPsSUGRAZIY9LpJj0fIG7zCr9eCBpp2qyzmpyNfx\negN3ILYy1Y8JbJj73HWyP0F3Am7i76tkCWB7tQeFOb5FMg==\n-----END CERTIFICATE-----\n", + "gatekeeper_sslkey": "-----BEGIN PRIVATE KEY-----\nMIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDJUo1MaU993hBf\n0doNz1rqmttz2KXiO1GIKFKUx/f/pR8AxxL7FWBHaJHwUr8NC4XPGf7oHJumiIPf\nfeuMOvvNHvL2nHdemHZuayNnLSG/1IgFvHQMm+/A4NOVZMAfyqBTYM3rFZzu+OB4\naLm5OmebBauQfoGO1WvCgGQXQmZ8fsqItL9CNao8p4jqj8am8I/2Z4PXfVKr9F6g\n5OsngXlo12ECuKXidTnp7tj4jKnfkNlIu5T6kMkCHES+RgZqzW153lFHGWGjy6Rq\ngAEJuZpN42aCwnErZIIAGN9RzfxoCU0u4xswxHtDzlnzg5MmJSpqdVzPB1KPVAeU\n0JZkrUxwE+GPabZZ2B6B52urldyNAYKHNMSjNwWe1TSrNjeNrX4M4TxRoxpOsMfi\nxpRcIxmTdbWbmaaJlbeZwHRwoZ16DorpRKDUHAFNT2qiVS6zHPlPS03GNwyVdT3c\n/Xt3izkhd+WJeEw2v/rh+XvwTLcU2mtOLHhqVbuEECn94D8uFLnmzePB2hXMmXhE\nQsLTbzilqGPl7FhDBTrOMt50zHML7u95BjBZ9k6z2NUJM/RhK25PSP3EPdq6wtnI\nHUSBuxqPLLvUh6hg5/X1Lp0UhcTkscDDaPcPduHnBvTX0jxXmzMZt56Z0Glveb5v\nNPyqMtI2tqe5PjUIgd0hiOrzm8MQmwIDAQABAoICAQCI5uA7M+ngd9++qR+VAIqc\nus28y3iSjS/2XSU7E3irmYepqbZYk8KzDIMhX8OXhVxq5wyWns2hw3eZxTEmXP3a\nEM+7r87kvtzaXXTntqMapdYRwINSB8BT8w8uqiKT++Bmko+06y+auhc7Ckwxj2vg\n2Uw/qCdGEA+FZnWp83dp9XaY3ACrb37iXDMY/shhwXjEYMQhB5HuaPDojIL0jHEZ\nQE0x4oq7omfNkqRs8IqcAw4fDaBTe52VF9APa+L1QdjOZMX0iWgCUHrwCTere1FY\n4ehVxw/aKDDXDBLguCiKPrkDx2A4G4SPKYW1uKWZ7PAZENIZ3qrf2I6HPgjnUYmG\nAHQiR3JcwsXFZZAMW/kbqzRCS7CrvNnrzcUL9JAlpFmMDeAFIlbVkFED+kOtVioR\nPAcDWKtlWOWbX3Kn218FCblH86XdzB9H/pgbHxf8cXFcnaqVApxC1zv+uIaV526p\nU9maF8CMVX5bZ21e+dpP1BQ3DDRn3DCQno/QrGGMxK66EBVrHrllHBYpUppcj4w8\nCn5RDhp2KJjYsgX9zjuXif1gdP2jqBCDWCog+YwsoQP1Qp613D6rl5TKaa/rfZmX\nfG5Q98/wfAHwLjIDfwCXWqKOFoGdMzxg9hxk0bSNn64m1UY6OYB4yJs/o6spqGXq\nZRaX2LStSq5fhvB+tjl3AQKCAQEA7wBS3t2dHOVLZGVycxSq1LRoeW44KxZaNSiv\nXj5Xrw/jAnnAEAcVFrCGFY40MC4SNM//VUWHs4zKXxAhK/vFOgSvmnNh6mWDd/sO\nFyzo17fhwhi4u8BIjvchHgwg5aMi4uCLFM8RBZjI5MpMyNBwxT5VLKKKv3N9YtUK\n5JPhXkZJnhOhKj3vTaCeJFBMvcknoqqNcUcEEMo0d+YqUAYh9+8qpjBSKBV+8/VZ\nbQwyjd+EN5ajqRLIjKOy1GrbRwBcxadGdZzqMDYlSVBdAxAssPiUqhTLeHAuGfbK\nB0iF0DLgwl0N/6qx0WtLTA3MY7NcQJ/cjUioJKqbnvouEP2f2wKCAQEA16Qulg5I\nyDbJsiFzmeLNpKSXeh9y1q/hvqizd8R9GAUh9TVSgAnoiCpucguvMYBsXeGU3srr\no9AvxmkmrMWVNZbolaMRv0p+nXPR6uF5tFQJ/jYm1H/jI3ieF2ZXJveQBkqjRsOD\nsWI4HNuarGnsOo8rqV0ybYGFks1dhuvBZp7RemZbXqaFnk/D8FtkmHRsWUdEQifd\njHCATRbUHjAm8tk7HjdJbhYIMCZkbN1HRkx044pk+os37Eqi4Ok+s/MbQ7g2lY+R\nt1sxrJo4dsqxwflz9U6U5ECwH2hxuzpowGXwfcqtPLvVJT1p+TUxIHNb2eUvJg7R\ntsx4BzC4FbaOQQKCAQBMaHYV+hO4oTWxKx2j3P+gcOzVpX8fh03foNov7w5pUVLp\n/7J/bUQ4tMapLYVRwejgKX8f69KufFxWd/mi7iLnoYfigPDU1w9o1EJ09k6aaJcs\nTmsA20BXNHrJ+GasA7OrhM3yISD4ARh4zJQvvzPOW1cvpedlva5gYmvRF9X9Jctz\nViM4NgTDdI4aXfGq2xxozM3bYTDTjVGo41SzsMI7WaLw7pHVbsnfhJjKXBPtd3/1\npZs8+lxTWiJZ9q+Ty3HdXtUP1NKqByV0gtS5nforuc0Ncwh5wKN8eYGtQmpFXX6t\npMJM8m1W3k58Sg0F8tmTb4g7Qvc+gayu7py7odnxAoIBAQC5pJqGjF2UH7acJ7hB\nrsOjDh9p/1D6Cgip/soiPYID/8cwNmuD1wPc1cqnW+/DCfBBEkb7Vm5uZHf8s+Gb\ns620qOoqiGxq5lMCcgcx0lLYL9E6jJv5LO/6RYi0VGKLJW1UPUU7Um45c3kjPtt1\nuuqnr0HDWHxlUbAQpcPyt1uUYP2uBhh675jwpXLlpYiAxxnP8k6NNYzBrsiFlTWf\nl4ywXNtMNAR/RXBfI24pWoZVutSWXzp3hwrp3YdDYQmeGZhLQHedYi/sThIBqfMa\nMX2+pGZztObxac81+tCOgsZTfG8BnE5vjrT8jlaBOI3Ghgl5GJjyhqd8W6KpbgPM\njZEBAoIBAQCuI/dv6opNniMmOY+iI1Oh6kyNnctKe/VF2SqwfeK3I6fCfcNEm46u\nQHnJWqVCIQ1ZsU8b/Pn0+mJYB/n+OYh585DsDR2YEJPXE+qrVHG1qIEEyTgcTb30\n+nehJ6d5SYMej4VMdepgMD7HeG7Nly1wwu0VYTUTLP3Z812NUdnBYmb5QSRnsHrk\nOVlAkJAaunAk1O3rwwdyGwMmNVIgzf0foa/c8uFQCbUHocQUwr//vN+U+Nhi4VQC\nnRiPT36rWxLqwyn8sa7dcE9A6Hp2KDBc97DJTDNvA3StF1JHAf/EoiTq3mWvVNhR\nmPN+iWirFH2RN5KkSaTzEtrXAgbLvic9\n-----END PRIVATE KEY-----\n", + "gatekeeper_cns": "disk0book.fritz.box", + "proxy_datapath": "/Users/jash/.cyphernode/proxy", + "lightning_implementation": "c-lightning", + "lightning_datapath": "/Users/jash/.cyphernode/lightning", + "lightning_nodename": "🚀 Disciplined Dormouse 🚀", + "lightning_nodecolor": "ff0000", + "otsclient_datapath": "/Users/jash/.cyphernode/otsclient", + "adminhash": "BsJFlh7q4JmwI\n", + "use_xpub": true, + "xpub": "abc", + "derivation_path": "da", + "gatekeeper_clientkeyspassword": "test123", + "gatekeeper_datapath": "/Users/jash/.cyphernode/gatekeeper", + "traefik_datapath": "foo", + "lightning_announce": true, + "gatekeeper_expose": false, + "traefik_http_port": 80, + "traefik_https_port": 443 +} \ No newline at end of file diff --git a/cyphernodeconf_docker/test/htpasswd.test.js b/cyphernodeconf_docker/test/htpasswd.test.js new file mode 100644 index 0000000..9490db0 --- /dev/null +++ b/cyphernodeconf_docker/test/htpasswd.test.js @@ -0,0 +1,6 @@ +const htpasswd = require('../lib/htpasswd.js'); + +test( 'generate htpasswd', async ()=>{ + const pw = await htpasswd( 'test123' ); + expect( pw ).not.toBe( undefined ); +}); \ No newline at end of file diff --git a/cyphernodeconf_docker/test/name.test.js b/cyphernodeconf_docker/test/name.test.js new file mode 100644 index 0000000..59cead5 --- /dev/null +++ b/cyphernodeconf_docker/test/name.test.js @@ -0,0 +1,7 @@ +const name = require('../lib/name.js'); + + +test( 'Create new random name', ()=>{ + const n = name.generate(); + expect( n ).not.toBe( undefined ) +}); \ No newline at end of file diff --git a/dist/config.json.sample b/dist/config.json.sample deleted file mode 100644 index c0abc27..0000000 --- a/dist/config.json.sample +++ /dev/null @@ -1,26 +0,0 @@ -{ - "derivation_path": "0/n", - "installer": "docker", - "features": [ - "lightning", - "otsclient", - "electrum" - ], - "net": "testnet", - "xpub": "upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb", - "bitcoin_mode": "internal", - "bitcoin_rpcuser": "user", - "bitcoin_rpcpassword": "password", - "bitcoin_prune": false, - "bitcoin_uacomment": "", - "lightning_implementation": "c-lightning", - "lightning_nodename": "SatoshiPortal", - "lightning_nodecolor": "ff00ff", - "electrum_implementation": "eps", - "installer_mode": "docker", - "proxy_datapath": "/tmp/p", - "bitcoin_datapath": "/tmp/b", - "lightning_datapath": "/tmp/l", - "bitcoin_expose": false, - "devmode": true -} diff --git a/dist/setup.sh b/dist/setup.sh index 95e6fea..dee2b03 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -110,7 +110,7 @@ sudo_if_required() { } modify_permissions() { - local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml" "$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml" "traefik" "$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH" "$TRAEFIK_DATAPATH") for d in "${directories[@]}" do if [[ -e $d ]]; then @@ -122,7 +122,7 @@ modify_permissions() { } modify_owner() { - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH" "$TRAEFIK_DATAPATH") local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) for d in "${directories[@]}" do @@ -139,7 +139,7 @@ configure() { local recreate="" if [[ $1 == 1 ]]; then - recreate="recreate" + recreate=" recreate" fi @@ -151,8 +151,6 @@ configure() { if [[ -t 1 ]]; then interactive=' -it' - else - gen_options=' --force 2' fi if [[ $CFG_PASSWORD ]]; then @@ -183,21 +181,24 @@ configure() { # configure features of cyphernode docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ + -e DEFAULT_DATADIR_BASE=$HOME \ + -e SETUP_DIR=$SETUP_DIR \ -e DEFAULT_CERT_HOSTNAME=$(hostname) \ - -e VERSION_OVERRIDE=$VERSION_OVERRIDE \ -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ -e PROXY_VERSION=$PROXY_VERSION \ + -e NOTIFIER_VERSION=$NOTIFIER_VERSION \ -e PROXYCRON_VERSION=$PROXYCRON_VERSION \ -e OTSCLIENT_VERSION=$OTSCLIENT_VERSION \ -e PYCOIN_VERSION=$PYCOIN_VERSION \ -e BITCOIN_VERSION=$BITCOIN_VERSION \ -e LIGHTNING_VERSION=$LIGHTNING_VERSION \ + -e SETUP_VERSION=$SETUP_VERSION \ --log-driver=none$pw_env \ --network none \ - --rm$interactive cyphernode/cyphernodeconf:$CONF_VERSION $user yo --no-insight cyphernode$gen_options $recreate - if [[ -f $current_path/exitStatus.sh ]]; then - . $current_path/exitStatus.sh - rm $current_path/exitStatus.sh + --rm$interactive cyphernode/cyphernodeconf:$CONF_VERSION $user node index.js$recreate + if [[ -f $cyphernodeconf_filepath/exitStatus.sh ]]; then + . $cyphernodeconf_filepath/exitStatus.sh + rm $cyphernodeconf_filepath/exitStatus.sh fi if [[ ! $EXIT_STATUS == 0 ]]; then @@ -348,7 +349,6 @@ compare_bitcoinconf() { } install_docker() { - local archpath=$(uname -m) # compat mode for SatoshiPortal repo @@ -363,36 +363,48 @@ install_docker() { next fi - if [ -d $GATEKEEPER_DATAPATH ]; then - if [[ ! -f $GATEKEEPER_DATAPATH/installation.json ]]; then - # prevent mounting installation.json as a directory - sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json - fi - - if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then - sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 - fi - - if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then - sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 - fi - - copy_file $current_path/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED - copy_file $current_path/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED - copy_file $current_path/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED + if [[ ! -f $GATEKEEPER_DATAPATH/installation.json ]]; then + # prevent mounting installation.json as a directory + sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json fi + if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 + fi + + if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 + fi + + copy_file $cyphernodeconf_filepath/gatekeeper/default.conf $GATEKEEPER_DATAPATH/default.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED + copy_file $current_path/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED + copy_file $current_path/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/traefik/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED + + + if [ ! -d $TRAEFIK_DATAPATH ]; then + step " create $TRAEFIK_DATAPATH" + sudo_if_required mkdir -p $TRAEFIK_DATAPATH + next + fi + + copy_file $cyphernodeconf_filepath/traefik/acme.json $TRAEFIK_DATAPATH/acme.json 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/traefik/traefik.toml $TRAEFIK_DATAPATH/traefik.toml 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/traefik/htpasswd $TRAEFIK_DATAPATH/htpasswd 1 $SUDO_REQUIRED + + if [ ! -d $PROXY_DATAPATH ]; then step " create $PROXY_DATAPATH" sudo_if_required mkdir -p $PROXY_DATAPATH next fi - copy_file $current_path/installer/config.sh $PROXY_DATAPATH/config.sh 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/installer/config.sh $PROXY_DATAPATH/config.sh 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/cyphernode/info.json $PROXY_DATAPATH/info.json 1 $SUDO_REQUIRED if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then @@ -402,18 +414,22 @@ install_docker() { fi if [ -d $BITCOIN_DATAPATH ]; then - local cmpStatus=$(compare_bitcoinconf $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) + local cmpStatus=$(compare_bitcoinconf $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) if [[ $cmpStatus == 'dataloss' ]]; then if [[ $ALWAYSYES == 1 ]]; then - copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin-client.conf $BITCOIN_DATAPATH/bitcoin-client.conf 1 $SUDO_REQUIRED else while true; do echo " Really copy bitcoin.conf with pruning option?" read -p " This will discard some blockchain data. (yn) " yn case $yn in - [Yy]* ) copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED; break;; - [Nn]* ) copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + [Yy]* ) copy_file $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin-client.conf $BITCOIN_DATAPATH/bitcoin-client.conf 1 $SUDO_REQUIRED + break;; + [Nn]* ) copy_file $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin-client.conf $BITCOIN_DATAPATH/bitcoin-client.conf.cyphernode 0 $SUDO_REQUIRED echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually."; break;; @@ -422,7 +438,8 @@ install_docker() { done fi elif [[ $cmpStatus == 'incompatible' ]]; then - copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin-client.conf $BITCOIN_DATAPATH/bitcoin-client.conf.cyphernode 0 $SUDO_REQUIRED echo " Blockchain data is not compatible, due to misconfigured nets." echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually." @@ -430,7 +447,8 @@ install_docker() { if [[ $cmpStatus == 'reindex' ]]; then echo " Warning Reindexing will take some time." fi - copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $cyphernodeconf_filepath/bitcoin/bitcoin-client.conf $BITCOIN_DATAPATH/bitcoin-client.conf 1 $SUDO_REQUIRED fi fi fi @@ -441,15 +459,15 @@ install_docker() { if [[ $archpath == "rpi" ]]; then dockerfile="Dockerfile-alpine" fi + if [ ! -d $LIGHTNING_DATAPATH ]; then step " create $LIGHTNING_DATAPATH" sudo_if_required mkdir -p $LIGHTNING_DATAPATH next fi - if [ -d $LIGHTNING_DATAPATH ]; then - copy_file $current_path/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED - copy_file $current_path/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED - fi + + copy_file $cyphernodeconf_filepath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED + fi fi @@ -496,10 +514,37 @@ install_docker() { fi fi - copy_file $current_path/installer/docker/docker-compose.yaml $current_path/docker-compose.yaml - copy_file $current_path/installer/testfeatures.sh $current_path/testfeatures.sh 0 - copy_file $current_path/installer/start.sh $current_path/start.sh 0 - copy_file $current_path/installer/stop.sh $current_path/stop.sh 0 + local appsnet_entry=$(docker network ls | grep cyphernodeappsnet); + + if [[ $appsnet_entry =~ 'cyphernodeappsnet' ]]; then + if [[ $appsnet_entry =~ 'local' && $DOCKER_MODE == 'swarm' ]]; then + step " recreate cyphernode apps network" + try docker network rm cyphernodeappsnet > /dev/null 2>&1 + try docker network create -d overlay --attachable --opt encrypted cyphernodeappsnet > /dev/null 2>&1 + next + elif [[ $appsnet_entry =~ 'swarm' && $DOCKER_MODE == 'compose' ]]; then + step " recreate cyphernode apps network" + try docker network rm cyphernodeappsnet > /dev/null 2>&1 + try docker network create cyphernodeappsnet > /dev/null 2>&1 + next + fi + else + if [[ $DOCKER_MODE == 'swarm' ]]; then + step " create cyphernode apps network" + try docker network create -d overlay --attachable --opt encrypted cyphernodeappsnet > /dev/null 2>&1 + next + elif [[ $DOCKER_MODE == 'compose' ]]; then + step " create cyphernode apps network" + try docker network create cyphernodeappsnet > /dev/null 2>&1 + next + fi + fi + + copy_file $cyphernodeconf_filepath/installer/docker/docker-compose.yaml $current_path/docker-compose.yaml + copy_file $cyphernodeconf_filepath/installer/testfeatures.sh $current_path/testfeatures.sh 0 + copy_file $cyphernodeconf_filepath/installer/start.sh $current_path/start.sh 0 + copy_file $cyphernodeconf_filepath/installer/stop.sh $current_path/stop.sh 0 + copy_file $cyphernodeconf_filepath/installer/testdeployment.sh $current_path/testdeployment.sh 0 if [[ ! -x $current_path/start.sh ]]; then step " make start.sh executable" @@ -518,14 +563,24 @@ install_docker() { try chmod +x $current_path/testfeatures.sh next fi + + if [[ ! -x $current_path/testdeployment.sh ]]; then + step " make testdeployment.sh executable" + try chmod +x $current_path/testdeployment.sh + next + fi } check_directory_owner() { # if one directory does not have access rights for $RUN_AS_USER, we echo 1, else we echo 0 - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$TRAEFIK_DATAPATH") local status=0 for d in "${directories[@]}" do + if [[ ''$d == '' ]]; then + continue + fi + d=$(realpath $d) if [[ -e $d ]]; then # is it mine and does it have rw ? # don't care about group rights @@ -551,19 +606,38 @@ check_bitcoind() { echo 0 } -sanity_checks() { +realpath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} - echo " check requirements." +check_docker() { if ! [ -x "$(command -v docker)" ]; then echo " docker is not installed on your system. Please check https://www.docker.com/get-started." exit fi +} - if [[ $DOCKER_MODE == 'compose' && ! -x "$(command -v docker-compose)" ]]; then +check_docker_compose() { + if ! [ -x "$(command -v docker-compose)" ]; then echo " docker-compose is not installed on your system. Please check https://docs.docker.com/compose/install/." exit fi +} + +sanity_checks_pre_config() { + echo " check requirements for configuration step." + check_docker +} + +sanity_checks_pre_install() { + + echo " check requirements for installation step." + + check_docker + if [[ $DOCKER_MODE == 'compose' ]]; then + check_docker_compose + fi local OS=$(uname -s) @@ -603,7 +677,7 @@ sanity_checks() { if [[ $sudo_reason == 'directories' ]]; then echo " or check your data volumes if they have the right owner." echo " The owner of the following folders should be '$RUN_AS_USER':" - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$TRAEFIK_DATAPATH") local status=0 for d in "${directories[@]}" do @@ -622,6 +696,14 @@ sanity_checks() { fi } +install_apps() { + if [ ! -d "$current_path/apps" ]; then + local apps_repo="https://github.com/SatoshiPortal/cypherapps.git" + echo " clone $apps_repo into apps" + docker run --rm -v "$current_path":/git --entrypoint git cyphernode/cyphernodeconf:$CONF_VERSION clone --single-branch -b ${CYPHERAPPS_VERSION} "$apps_repo" /git/apps > /dev/null 2>&1 + fi +} + install() { if [[ ''$INSTALLER_MODE == 'none' ]]; then echo "Skipping installation phase" @@ -638,16 +720,20 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 -# CYPHERNODE VERSION "v0.1.0-rc.2" -VERSION_OVERRIDE="true" -CONF_VERSION="v0.1-rc.2" -GATEKEEPER_VERSION="v0.1-rc.2" -PROXY_VERSION="v0.1-rc.2" -PROXYCRON_VERSION="v0.1-rc.2" -OTSCLIENT_VERSION="v0.1-rc.2" -PYCOIN_VERSION="v0.1-rc.2" -BITCOIN_VERSION="v0.17.0" -LIGHTNING_VERSION="v0.6.2" +# CYPHERNODE VERSION "v0.2.4" +SETUP_VERSION="v0.2.4" +CONF_VERSION="v0.2.4" +GATEKEEPER_VERSION="v0.2.4" +PROXY_VERSION="v0.2.4" +NOTIFIER_VERSION="v0.2.4" +PROXYCRON_VERSION="v0.2.4" +OTSCLIENT_VERSION="v0.2.4" +PYCOIN_VERSION="v0.2.4" +CYPHERAPPS_VERSION="v0.2.2" +BITCOIN_VERSION="v0.18.0" +LIGHTNING_VERSION="v0.7.1" + +SETUP_DIR=$(dirname $(realpath $0)) # trap ctrl-c and call ctrl_c() trap ctrl_c INT @@ -658,6 +744,7 @@ function ctrl_c() { } export current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +export cyphernodeconf_filepath="$current_path/.cyphernodeconf" while getopts ":cirhys" opt; do case $opt in @@ -690,17 +777,34 @@ while getopts ":cirhys" opt; do esac done +nbbuiltimgs=$(docker images --filter=reference='cyphernode/*:*-local' | wc -l) +if [[ $nbbuiltimgs -gt 1 ]]; then + read -p "Locally built Cyphernode images found! Do you want to use them? [yn] " -n 1 -r + + if [[ $REPLY =~ ^[Yy]$ ]]; then + CONF_VERSION="$CONF_VERSION-local" + GATEKEEPER_VERSION="$GATEKEEPER_VERSION-local" + PROXY_VERSION="$PROXY_VERSION-local" + NOTIFIER_VERSION="$NOTIFIER_VERSION-local" + PROXYCRON_VERSION="$PROXYCRON_VERSION-local" + OTSCLIENT_VERSION="$OTSCLIENT_VERSION-local" + PYCOIN_VERSION="$PYCOIN_VERSION-local" + fi +fi + if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then CONFIGURE=1 INSTALL=1 fi if [[ $CONFIGURE == 1 ]]; then + sanity_checks_pre_config configure $RECREATE fi -if [[ -f $current_path/installer/config.sh ]]; then - . $current_path/installer/config.sh + +if [[ -f "$cyphernodeconf_filepath/installer/config.sh" ]]; then + . "$cyphernodeconf_filepath/installer/config.sh" fi if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then @@ -710,15 +814,18 @@ if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cypherno fi if [[ $INSTALL == 1 ]]; then - sanity_checks + sanity_checks_pre_install create_user install modify_owner modify_permissions + install_apps + if [[ ! $AUTOSTART == 1 ]]; then + cowsay + fi + fi if [[ $AUTOSTART == 1 ]]; then exec $current_path/start.sh -else - cowsay fi diff --git a/doc/API.md b/doc/API.v0.md similarity index 50% rename from doc/API.md rename to doc/API.v0.md index af24a4e..d62a1a2 100644 --- a/doc/API.md +++ b/doc/API.v0.md @@ -71,6 +71,169 @@ Proxy response: } ``` +### Watch a Bitcoin xpub/ypub/zpub/tpub/upub/vpub extended public key (called by application) + +Used to watch the transactions related to an xpub. It will first derive 100 addresses using the provided xpub, derivation path and index information. It will add those addresses to the watching DB table and add those addresses to the Watching-by-xpub wallet. The watching process will take care of calling the provided callbacks when a transaction occurs. When a transaction is seen, Cyphernode will derive and start watching new addresses related to the xpub, keeping a 100 address gap between the last used address in a transaction and the last watched address of that xpub. The label can be used later, instead of the whole xpub, with unwatchxpub* and and getactivewatchesby*. + +```http +POST http://cyphernode:8888/watchxpub +with body... +{"label":"4421","pub32":"upub57Wa4MvRPNyAhxr578mQUdPr6MHwpg3Su875hj8K75AeUVZLXtFeiP52BrhNqDg93gjALU1MMh5UPRiiQPrwiTiuBBBRHzeyBMgrbwkmmkq","path":"0/1/n","nstart":109,"unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"} +``` + +Proxy response: + +```json +{ + "id":"5", + "event":"watchxpub", + "pub32":"upub57Wa4MvRPNyAhxr578mQUdPr6MHwpg3Su875hj8K75AeUVZLXtFeiP52BrhNqDg93gjALU1MMh5UPRiiQPrwiTiuBBBRHzeyBMgrbwkmmkq", + "label":"2219", + "path":"0/1/n", + "nstart":"109", + "unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf", + "confirmedCallbackURL":"192.168.111.233:1111/callback1conf" +} +``` + +### Un-watch a previously watched Bitcoin xpub by providing the xpub (called by application) + +Updates the watched address rows in DB so that callbacks won't be called on tx confirmations for the provided xpub and related addresses. + +```http +GET http://cyphernode:8888/unwatchxpubbyxpub/upub57Wa4MvRPNyAhxr578mQUdPr6MHwpg3Su875hj8K75AeUVZLXtFeiP52BrhNqDg93gjALU1MMh5UPRiiQPrwiTiuBBBRHzeyBMgrbwkmmkq +``` + +Proxy response: + +```json +{ + "event":"unwatchxpubbyxpub", + "pub32":"upub57Wa4MvRPNyAhxr578mQUdPr6MHwpg3Su875hj8K75AeUVZLXtFeiP52BrhNqDg93gjALU1MMh5UPRiiQPrwiTiuBBBRHzeyBMgrbwkmmkq" +} +``` + +### Un-watch a previously watched Bitcoin xpub by providing the label (called by application) + +Updates the watched address rows in DB so that callbacks won't be called on tx confirmations for the provided xpub and related addresses. + +```http +GET http://cyphernode:8888/unwatchxpubbylabel/4421 +``` + +Proxy response: + +```json +{ + "event":"unwatchxpubbylabel", + "label":"4421" +} +``` + +### Watch a TXID (called by application) + +Used to watch a transaction. Will call the 1-conf callback url after the transaction has been mined. Will call the x-conf callback url after the transaction has x confirmations. + +```http +POST http://cyphernode:8888/watchtxid +with body... +{"txid":"b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387","confirmedCallbackURL":"192.168.111.233:1111/callback1conf","xconfCallbackURL":"192.168.111.233:1111/callbackXconf","nbxconf":6} +``` + +Proxy response: + +```json +{ + "id":"5", + "event":"watchtxid", + "inserted":"1", + "txid":"b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387", + "confirmedCallbackURL":"192.168.111.233:1111/callback1conf", + "xconfCallbackURL":"192.168.111.233:1111/callbackXconf", + "nbxconf":6 +} +``` + +### Get a list of Bitcoin xpub being watched (called by application) + +Returns the list of currently watched xpub and callback information. + +```http +GET http://cyphernode:8888/getactivexpubwatches +``` + +Proxy response: + +```json +{ + "watches": [ + { + "id":"291", + "pub32":"upub57Wa4MvRPNyAhxr578mQUdPr6MHwpg3Su875hj8K75AeUVZLXtFeiP52BrhNqDg93gjALU1MMh5UPRiiQPrwiTiuBBBRHzeyBMgrbwkmmkq", + "label":"2217", + "derivation_path":"1/3/n", + "last_imported_n":"121", + "unconfirmedCallbackURL":"192.168.133.233:1111/callback0conf", + "confirmedCallbackURL":"192.168.133.233:1111/callback1conf", + "watching_since":"2018-09-06 21:14:03"} + ] +} +``` + +### Get a list of Bitcoin addresses being watched by provided xpub (called by application) + +Returns the list of currently watched addresses related to the provided xpub and callback information. + +```http +GET http://cyphernode:8888/getactivewatchesbyxpub/tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk +``` + +Proxy response: + +```json + +{ + "watches": [ + { + "id":"291", + "address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp", + "imported":"1", + "unconfirmedCallbackURL":"192.168.133.233:1111/callback0conf", + "confirmedCallbackURL":"192.168.133.233:1111/callback1conf", + "watching_since":"2018-09-06 21:14:03", + "derivation_path":"1/0/n", + "pub32_index":"44"} + ] +} +``` + +### Get a list of Bitcoin addresses being watched by provided xpub label (called by application) + +Returns the list of currently watched addresses related to the provided xpub label and callback information. + +```http +GET http://cyphernode:8888/getactivewatchesbylabel/2219 +``` + +Proxy response: + +```json + +{ + "watches": [ + { + "id":"291", + "address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp", + "imported":"1", + "unconfirmedCallbackURL":"192.168.133.233:1111/callback0conf", + "confirmedCallbackURL":"192.168.133.233:1111/callback1conf", + "watching_since":"2018-09-06 21:14:03", + "derivation_path":"1/0/n", + "pub32_index":"44"} + ] +} +``` + ### Confirm a Transaction on Watched Address (called by Bitcoin node on transaction confirmations) Confirms a transaction on an imported address. The Watching Bitcoin node will notify Cyphernode (thanks to walletnotify in bitcoin.conf) by calling this endpoint with txid when a tx is new or updated on an address. If address is still being watched (flag in DB), the corresponding callbacks will be called. @@ -129,6 +292,108 @@ When cyphernode receives a transaction confirmation (/conf endpoint) on a watche } ``` +### Get mempool information + +Returns the mempool information of the Bitcoin node. +```http +GET http://cyphernode:8888/getmempoolinfo +``` + +Proxy response: + +```json +{ + "size": 25, + "bytes": 5462, + "usage": 34736, + "maxmempool": 64000000, + "mempoolminfee": 1e-05, + "minrelaytxfee": 1e-05 +} +``` + +### Get the blockchain information (called by application) + +Returns the blockchain information of the Bitcoin node. Used for example by the welcome app to get syncing progression. + +```http +GET http://cyphernode:8888/getblockchaininfo +``` + +Proxy response: + +```json +{ + "chain": "test", + "blocks": 1486864, + "headers": 1486864, + "bestblockhash": "000000000000002fb99d683e64bbfc2b7ad16f9a425cf7be77b481fb1afa363b", + "difficulty": 13971064.71015782, + "mediantime": 1554149114, + "verificationprogress": 0.9999994536561675, + "initialblockdownload": false, + "chainwork": "000000000000000000000000000000000000000000000103ceb57a5896f347ce", + "size_on_disk": 23647567017, + "pruned": false, + "softforks": [ + { + "id": "bip34", + "version": 2, + "reject": { + "status": true + } + }, + { + "id": "bip66", + "version": 3, + "reject": { + "status": true + } + }, + { + "id": "bip65", + "version": 4, + "reject": { + "status": true + } + } + ], + "bip9_softforks": { + "csv": { + "status": "active", + "startTime": 1456790400, + "timeout": 1493596800, + "since": 770112 + }, + "segwit": { + "status": "active", + "startTime": 1462060800, + "timeout": 1493596800, + "since": 834624 + } + }, + "warnings": "Warning: unknown new rules activated (versionbit 28)" +} +``` + +### Get the Block Hash from Height (called by application) + +Returns the best block hash matching height provided. + +```http +GET http://cyphernode:8888/getblockhash/593104 +``` + +Proxy response: + +```json +{ + "result":"00000000000000000005dc459f0575b17413dbe7685e3e0fd382ed521f1be68b", + "error":null, + "id":null +} +``` + ### Get the Best Block Hash (called by application) Returns the best block hash of the watching Bitcoin node. @@ -325,12 +590,17 @@ Proxy response: } ``` + + ### Get a new Bitcoin address from spending wallet (called by application) -Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor). +Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor). Will derive the default address type (set in your bitcoin.conf file, p2sh-segwit if not specified) or you can supply the address type like the following examples. ```http GET http://cyphernode:8888/getnewaddress +GET http://cyphernode:8888/getnewaddress/bech32 +GET http://cyphernode:8888/getnewaddress/legacy +GET http://cyphernode:8888/getnewaddress/p2sh-segwit ``` Proxy response: @@ -341,6 +611,12 @@ Proxy response: } ``` +```json +{ + "address":"tb1ql7yvh3lmajxmaljsnsu3w8lhwczu963tvjfzpj" +} +``` + ### Spend coins from spending wallet (called by application) Calls sendtoaddress RPC on the spending wallet with supplied info. @@ -360,6 +636,29 @@ Proxy response: } ``` +### Bump transaction's fees (called by application) + +Calls bumpfee RPC on the spending wallet with supplied info. + +```http +POST http://cyphernode:8888/bumpfee +with body... +{"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648","confTarget":4} +or... +{"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"} +``` + +Proxy response: + +```json +{ + "txid": "7c048f43af90315e201ff4dada8796605a2505c7ab54054ba0e9e05cadd9079b", + "origfee": 0.00041221, + "fee": 0.00068112, + "errors": [ "Blabla don't do that" ] +} +``` + ### Add an output to the next batched transaction (called by application) Inserts output information in the DB. Used when batchspend is called later. @@ -555,3 +854,231 @@ Proxy response: "address": "tb1q9n8jfwe9qvlgczfxa5n4pe7haarqflzerqfhk9" } ``` + +### Get your Lightning Network Node connection string, for others to connect to you + +Returns a string containing your LN node connection information. + +```http +GET http://cyphernode:8888/ln_getconnectionstring +``` + +Proxy response: + +```json +{ + "connectstring": "02ffb6242a744143d427cf0962fce182d426609b80034a297ea5879c8c64d326ab@24.25.26.27:9735" +} +``` + +### Connect to a LN node and fund a channel with it + +First, it will connect your LN node to the supplied LN node. Then, it will fund a channel of the provided amount between you two. Cyphernode will call the supplied callback URL when the channel is ready to be used. + +```http +POST http://cyphernode:8888/ln_connectfund +with body... +{"peer":"nodeId@ip:port","msatoshi":"100000","callbackUrl":"https://callbackUrl/?channelReady=f3y2c3cvm4uzg2gq"} +``` + +Proxy response: + +```json +{ + "result": "success", + "txid": "85b8e69733202e126620e7745be9e23a6b544b758145d86848f3e513e6e1ca42", + "channel_id": "a459352219deb8e1b6bdc4a3515888569adad8a3023f8b57edeb0bc4d1f77b74" +} +``` + +```json +{ + "result": "failed", + "message": "Failed at watching txid" +} +``` + +### Get a previously created Lightning Network invoice by its label + +Returns the invoice corresponding to the supplied label. + +```http +GET http://cyphernode:8888/ln_getinvoice/label +GET http://cyphernode:8888/ln_getinvoice/koNCcrSvhX3dmyFhW +``` + +Proxy response: + +```json +{ + "invoices": [ + { + "label": "koNCcrSvhX3dmyFhW", + "bolt11": "lntb10n1pw92fk9pp56p6g7nnuhcj63j6wpyquy67wc7xfanhc20d49ta2dzrge2mj3s5qdq9vscnzcqp2rzjqvdcvlvavcc6zdfvcrehhn4ff024s75dfaqyzmzvuxsj2yd3u684v93ylqqqq0sqqqqqqqqpqqqqqzsqqcu6jamf7du64nxtj99x5t6hvy4hlfv8fc8m5j39g8kyzpk5r89s28f93x5jsfnzl8mhtkhqvx2qxehns4ltw7w5h8h7ppdcw8t0uz0wcptztsqg", + "payment_hash": "d0748f4e7cbe25a8cb4e0901c26bcec78c9ecef853db52afaa68868cab728c28", + "msatoshi": 1000, + "status": "paid", + "pay_index": 10, + "msatoshi_received": 1002, + "paid_at": 1549084373, + "description": "d11", + "expires_at": 1549087957 + } + ] +} +``` + +### Delete a previously created Lightning Network invoice by its label + +Deletes the invoice corresponding to the supplied label if status is unpaid, so that no payment comes in. Returns the invoice corresponding to the supplied label. + +```http +GET http://cyphernode:8888/ln_delinvoice/label +GET http://cyphernode:8888/ln_delinvoice/koNCcrSvhX3dmyFhW +``` + +Proxy response: + +```json +{ + "label": "koNCcrSvhX3dmyFhW", + "bolt11": "lntb10n1pw92fk9pp56p6g7nnuhcj63j6wpyquy67wc7xfanhc20d49ta2dzrge2mj3s5qdq9vscnzcqp2rzjqvdcvlvavcc6zdfvcrehhn4ff024s75dfaqyzmzvuxsj2yd3u684v93ylqqqq0sqqqqqqqqpqqqqqzsqqcu6jamf7du64nxtj99x5t6hvy4hlfv8fc8m5j39g8kyzpk5r89s28f93x5jsfnzl8mhtkhqvx2qxehns4ltw7w5h8h7ppdcw8t0uz0wcptztsqg", + "payment_hash": "d0748f4e7cbe25a8cb4e0901c26bcec78c9ecef853db52afaa68868cab728c28", + "msatoshi": 1000, + "status": "unpaid", + "description": "d11", + "expires_at": 1549087957 +} +``` + +### Decodes the BOLT11 string of a Lightning Network invoice + +Returns the detailed information of a BOLT11 string of a Lightning Network invoice. + +```http +GET http://cyphernode:8888/ln_decodebolt11/bolt11 +GET http://cyphernode:8888/ln_decodebolt11/lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3 +``` + +Proxy response: + +```json +{ + "currency": "tb", + "created_at": 1536073035, + "expiry": 3600, + "payee": "03bb990f43e6a6eccb223288d32fcb91209b12370c0a8bf5cdf4ad7bc11e33f253", + "description": "Bitcoin Outlet order #7082", + "min_final_cltv_expiry": 10, + "payment_hash": "430fb9d240fcb4612c335d475f285820dc9891588e19baf048520189c8af49e2", + "signature": "30440220445a7d39cd4b086c0bd6c67cd8cd5a6a86e8b9345883b56e89fa851decfb876202206b1e6c5122a46c5fbba4ccb4a77ef1bb20078f0aeea4059d5ca3f773db85c920" +} +``` + +### Stamp a hash on the Bitcoin blockchain using OTS (called by application) + +Will stamp the supplied hash to the Bitcoin blockchain using OTS. Cyphernode will curl the callback when the OTS stamping is complete. + +```http +POST http://cyphernode:8888/ots_stamp +with body... +{"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","callbackUrl":"192.168.111.233:1111/otscallback?id=1234567"} +``` + +Proxy response: + +```json +{ + "method": "ots_stamp", + "hash": "1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7", + "id": "422", + "result": "success" +} +``` + +```json +{ + "method": "ots_stamp", + "hash": "1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7", + "id": "422", + "result": "error", + "error": "Error message from OTS client." +} +``` + +### Get the OTS file of the supplied hash + +Returns the binary OTS file of the supplied hash. If stamp is complete, will return a complete OTS file, or an incomplete OTS file otherwise. + +```http +GET http://cyphernode:8888/ots_getfile/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 +``` + +Proxy response: + +Binary application/octet-stream content type. + +### Verify an OTS file + +Will verify the supplied OTS file, or verify the local OTS file named after the supplied hash suffixed with .ots. + +```http +POST http://cyphernode:8888/ots_verify +with body... +{"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7"} +or +{"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} +``` + +Proxy response: + +```json +{ + "method": "ots_verify", + "hash": "1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7", + "result": "success", + "message": "Message from OTS client." +} +``` + +```json +{ + "method": "ots_verify", + "hash": "1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7", + "result": "pending", + "message": "Message from OTS client." +} +``` + +```json +{ + "method": "ots_verify", + "hash": "1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7", + "result": "error", + "message": "Error message from OTS client." +} +``` + +### Get info an OTS file + +Will return the base64 string of the detailed information of the supplied OTS file, or of the local OTS file named after the supplied hash suffixed with .ots. + +```http +POST http://cyphernode:8888/ots_info +with body... +{"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7"} +or +{"base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} +or +{"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} +``` + +Proxy response: + +```json +{ + "method": "ots_info", + "result": "success", + "message": "Base64 string of the information text" +} +``` diff --git a/doc/API.v1.md b/doc/API.v1.md new file mode 100644 index 0000000..3029824 --- /dev/null +++ b/doc/API.v1.md @@ -0,0 +1,179 @@ +# Cyphernode + +## API v1 (RESTful) + +### Collections + +#### watchedAddresses + +| Request | Descripton | +|---------|------------| +| `POST /v1/watchedAddresses` | Create new address watch | +| `GET /v1/watchedAddresses` | Get list of watched addresses | +| `GET /v1/watchedAddresses/
` | Get details of watched address | +| `DELETE /v1/watchedAddresses/
` | Remove watched address | + +##### POST /v1/watchedAddresses + +Request body +``` +{ + "address": , + "callback": +} +``` + +Response body - 200 - OK +``` +{ + "id": , + "address": , + "callback": ", + "estimatesmartfee2blocks": , + "estimatesmartfee6blocks": , + "estimatesmartfee36blocks": , + "estimatesmartfee144blocks": +} +``` + +Response body - 503 - Resource temporarily unavailable +``` +{ + "reason": +} +``` + +Response body - 403 - Forbidden +``` +{ +} +``` + +##### GET /v1/watchedAddresses + +Response body - 200 - OK +``` +[ + { + "id": , + "address": , + "imported": , + "callback": , + "watching_since": + }, + ... +] +``` + +Response body - 503 - Resource temporarily unavailable +``` +{ + "reason": +} +``` + +Response body - 403 - Forbidden +``` +{ +} +``` + +##### GET /v1/watchedAddresses/\ + +Response body - 200 - OK +``` +{ + "id": , + "address": , + "imported": , + "callback": , + "watching_since": +} +``` + +Response body - 503 - Resource temporarily unavailable +``` +{ + "reason": +} +``` + +Response body - 403 - Forbidden +``` +{ +} +``` + +Response body - 404 - Not found +``` +{ +} +``` + + +##### DELETE /v1/watchedAddresses/\ + +Response body - 200 - OK +``` +{ + "address": "
", + "imported": , + "callback": , + "watching_since": +} +``` + +Response body - 503 - Resource temporarily unavailable +``` +{ + "reason": +} +``` + +Response body - 403 - Forbidden +``` +{ +} +``` + +Response body - 404 - Not found +``` +{ +} +``` + +##### Asynchronous callbacks + +Request body +``` +{ + "id": , + "address": , + "hash": , + "vout_n": , + "sent_amount": , + "confirmations": , + "received": , + "size": , + "vsize": , + "fees": , + "is_replaceable": , + "blockhash": , + "blocktime": , + "blockheight": +} +``` + +Response body - 200 - OK +``` +{ +} +``` + +Response body - 503 - Resource temporarily unavailable +``` +{ + "reason": +} +``` + diff --git a/doc/CN-Arch.jpg b/doc/CN-Arch.jpg new file mode 100644 index 0000000..3bb4e42 Binary files /dev/null and b/doc/CN-Arch.jpg differ diff --git a/doc/CYPHERAPPS.md b/doc/CYPHERAPPS.md new file mode 100644 index 0000000..e60a62f --- /dev/null +++ b/doc/CYPHERAPPS.md @@ -0,0 +1,17 @@ +# Cyphernode Apps + +We are providing one default Cyphernode application: The Cyphernode Welcome App. It is a simple Golang application that uses the Cyphernode API to get some information about it: the Bitcoin Core syncing progression, the installed components, a link to download the encrypted config file, a link to download the encrypted API ID/keys file and a link to Spark Wallet, if LN is installed. + +We are also providing Spark Wallet as a Cyphernode application. It is a hybrid application, directly using the c-lightning directory instead of only using the Cyphernode API. + +## Concept + +As you already know it, we want Cyphernode to be modular and decoupled. That's why we created a completely separated repository for the Cyphernode Apps: https://github.com/SatoshiPortal/cypherapps + +Cypherapps acts as an indirection layer between Cyphernode and the actual applications. The repo is cloned into the Cyphernode directory during setup, depending on the selected optional features. The corresponding docker images are taken from the Docker hub repositories. + +Separating Cypherapps from Cyphernode allows us to add applications without changing Cyphernode. + +## Examples + +Welcome App: https://github.com/SatoshiPortal/cyphernode_welcome diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 307f438..5f00e36 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -1,6 +1,6 @@ # This README file can be used if you want to install manually. This is the old documentation before there was the installer. -# Here are the exact steps I did to install cyphernode on a debian server running on x86 arch, as user debian. +# Here are the exact steps I did to install cyphernode on a Debian GNU/Linux server running on x86 arch, as user debian. ## Update server and install git @@ -11,10 +11,10 @@ sudo apt-get update ; sudo apt-get upgrade ; sudo apt-get install git ## Docker installation: https://docs.docker.com/install/linux/docker-ce/debian/ ```shell -sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common +sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88 -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian \ +sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian \ $(lsb_release -cs) \ stable" sudo apt-get update @@ -142,9 +142,9 @@ sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f ## Test the deployment ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/ots_stamp ``` If you need the authorization header to copy/paste in another tool: @@ -157,5 +157,5 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - -docker exec -it `docker ps -q -f name=cyphernodestack_cyphernode` curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" proxy:8888/derivepubpath +docker exec -it `docker ps -q -f name=cyphernode_proxy` curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" proxy:8888/derivepubpath ``` diff --git a/doc/INSTALL-MANUALLY.md b/doc/INSTALL-MANUALLY.md index 475bf53..ac62da7 100644 --- a/doc/INSTALL-MANUALLY.md +++ b/doc/INSTALL-MANUALLY.md @@ -124,9 +124,9 @@ pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode ## Test deployment from outside of the Swarm ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/ots_stamp ``` If you need the authorization header to copy/paste in another tool: diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 75fc6bb..6e4bf8e 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -20,8 +20,28 @@ Or you can simply run this magic command to start setup and installation: curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/master/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh ``` +Note that you can replace "master" in the URL by "dev" or any existing git branch/tag you actually want to install. + +### Build cyphernode yourself + +You can build cyphernode images yourself. The images will have the same name than the ones in the docker hub, with the suffix -local. + +```shell +git clone https://github.com/SatoshiPortal/cyphernode.git +cd cyphernode +./build.sh +cd dist +./setup.sh +``` + +`setup.sh` will detect locally built images (with suffix `-local`) and ask you if you want to use them when installing cyphernode. + +For full paranoia mode, you can also build yourself all images used by cyphernode but in external repositories by using the `build.sh` script in each of the repo. You can see a list of images cyphernode uses here: https://cloud.docker.com/u/cyphernode/repository/list + ## Upgrading +To upgrade to the most recent version, just get and run the most recent version of the setup.sh file as described in the previous section. Migration should be taken care by the script. + Your proxy's database won't be lost. Migration scripts are taking care of automatically migrating the database when starting the proxy. ``` @@ -39,9 +59,9 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech Directly using curl on command line, put your API ID (id=) and API key (k=) in the following commands: ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/ots_stamp ``` ## Manually test your installation directly on the Proxy: diff --git a/doc/README.md b/doc/README.md index a97c123..5d491a2 100644 --- a/doc/README.md +++ b/doc/README.md @@ -25,14 +25,11 @@ We are providing an installer to help you setup Cyphernode. #### See [Instructions for installation](INSTALL.md) for automatic install instructions -All the Docker images used by Cyphernode have been prebuilt for x86 and ARM (RPi) architectures and are hosted on the Docker hub public registry, Cyphernode repository (https://hub.docker.com/u/cyphernode/). +All the Docker images used by Cyphernode have been prebuilt for x86, ARM (RPi) and aarch64 (pine64/NODL) architectures and are hosted on the Docker hub public registry, Cyphernode repository (https://hub.docker.com/u/cyphernode/). ### Build from sources -However, it is possible for you to build from sources. In that case, please refer to the files INSTALL-MANUALLY.md and INSTALL-MANUAL-STEPS.md. - -#### See [Instructions for manual installation](INSTALL-MANUALLY.md) for manual build and install instructions -#### See [Step-by-step detailed instructions](INSTALL-MANUAL-STEPS.md) for real-world copy-paste standard install instructions +However, it is possible for you to build from sources. In that case, please refer to the `build.sh` scripts in each of the repositories used by cyphernode (https://cloud.docker.com/u/cyphernode/repository/list). See [Instructions for installation](INSTALL.md). # For Your Information @@ -42,6 +39,8 @@ Current components in Cyphernode: - Proxy: request handler. Well dispatch authenticated and authorized requests to the right component. Use a SQLite3 database for its tasks. - Proxy Cron: scheduler. Can call the proxy on regular interval for asynchronous tasks like payment notifications on watches, callbacks when OTS files are ready, etc. - Pycoin: Bitcoin keys and addresses tool. Used by Cyphernode to derive addresses from an xPub and a derivation path. +- notifier: Handling callbacks used by watchers as well as OTS stamping. +- broker: pub/sub mechanism is taken care by the broker to which all subscribers and publishers should register. - Bitcoin: Bitcoin Core node. Cyphernode uses a watching wallet for watchers (no funds) and a spending wallet for spending. Mandatory component, but optionally part of Cyphernode installation, as we can use an already running Bitcoin Core node. - Lightning: optional. C-Lightning node. The LN node will use the Bitcoin node for its tasks. - OTSclient: optional. Used to stamp hashes on the Bitcoin blockchain. @@ -61,18 +60,18 @@ If you decide to have a prune Bitcoin Core node, the fee calculation on incoming ## Lightning Network -Currently, the LN functionalities of Cyphernode are very limited. Maybe even hard to use. You can: +Currently, basic LN functionalities is offered by Cyphernode. You can: - Get information on your LN node: ln_getinfo - Get a Bitcoin address where to send your funds to be used by your LN node: ln_newaddr - Create an invoice, so people can send you payment; the burden of creating a channel/route to you is on the payer: ln_create_invoice - Pay an invoice. You have to have the invoice and your LN node must already be connected to the network: ln_pay - -Basic and crucial functionalities that's missing (you have to manually use lightning-cli on your LN node): - +- Decode a BOLT11 string. +- Delete a created invoice to make sure cancelled payments are not accepted. +- Get a previously created invoice. +- Connect + fund: connects to a peer and fund a channel, all in one call. A callback can be provided to let you know when the channel is ready to use. +- Get connection string: to let your user know how to connect to your LN node. - Be notified when a LN payment is received -- Connect your node to the LN network -- Open/close channels ## Manually test your installation through the Gatekeeper @@ -85,9 +84,9 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech Directly using curl on command line, put your API ID (id=) and API key (k=) in the following commands: ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1:2009/v0/ots_stamp ``` diff --git a/doc/UPGRADE.md b/doc/UPGRADE.md new file mode 100644 index 0000000..8e2f3ae --- /dev/null +++ b/doc/UPGRADE.md @@ -0,0 +1,29 @@ +# Upgrade notes from 0.1 to 0.2, to upgrade manually + +Usually no need to do this since it will be done during setup.sh v0.2. + +1. cd currentInstallation, where setup.sh is located +2. ./stop.sh current running cyphernode +3. Execute: + +``` +docker run --rm -it -v "$PWD:/conf" alpine:3.8 +apk add --no-cache --update jq curl p7zip +cd conf +7z e config.7z +``` + + + +``` +k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\\"000\\\";kapi_key=\\\"$k\\\";kapi_groups=\\\"stats\\\";eval ugroups_\${kapi_id}=\${kapi_groups};eval ukey_\${kapi_id}=\${kapi_key}" && cat config.json | sed 's/kapi_groups=\\"/kapi_groups=\\"stats,/g' | jq ".gatekeeper_keys.configEntries = [\"$l\"] + .gatekeeper_keys.configEntries" | jq ".gatekeeper_keys.clientInformation = [\"000=$k\"] + .gatekeeper_keys.clientInformation" | jq ".gatekeeper_apiproperties = \"$(curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0/api_auth_docker/api-sample.properties | paste -s -d '\n')\"" > config.json + +7z u config.7z config.json +``` + + + + +``` +curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh +``` diff --git a/doc/openapi/openapi-generator-cli.sh b/doc/openapi/openapi-generator-cli.sh new file mode 100755 index 0000000..0a7f8c8 --- /dev/null +++ b/doc/openapi/openapi-generator-cli.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + + +generators="ada \ +ada-server \ +android \ +apache2 \ +apex \ +aspnetcore \ +bash \ +c \ +clojure \ +cwiki \ +cpp-qt5-client \ +cpp-qt5-qhttpengine-server \ +cpp-pistache-server \ +cpp-restbed-server \ +cpp-restsdk \ +cpp-tizen \ +csharp \ +csharp-refactor \ +csharp-dotnet2 \ +csharp-nancyfx \ +dart \ +dart-jaguar \ +eiffel \ +elixir \ +elm \ +erlang-client \ +erlang-proper \ +erlang-server \ +flash \ +scala-finch \ +go \ +go-server \ +go-gin-server \ +graphql-schema \ +graphql-nodejs-express-server \ +groovy \ +kotlin \ +kotlin-server \ +kotlin-spring \ +haskell-http-client \ +haskell \ +java \ +jaxrs-cxf-client \ +java-inflector \ +java-msf4j \ +java-pkmst \ +java-play-framework \ +java-undertow-server \ +java-vertx \ +jaxrs-cxf \ +jaxrs-cxf-cdi \ +jaxrs-jersey \ +jaxrs-resteasy \ +jaxrs-resteasy-eap \ +jaxrs-spec \ +javascript \ +javascript-flowtyped \ +javascript-closure-angular \ +jmeter \ +lua \ +mysql-schema \ +nodejs-server \ +objc \ +openapi \ +openapi-yaml \ +perl \ +php \ +php-laravel \ +php-lumen \ +php-slim \ +php-silex \ +php-symfony \ +php-ze-ph \ +powershell \ +python \ +python-flask \ +python-aiohttp \ +r \ +ruby \ +ruby-on-rails \ +ruby-sinatra \ +rust \ +rust-server \ +scalatra \ +scala-akka \ +scala-httpclient \ +scala-gatling \ +scala-lagom-server \ +scalaz \ +spring \ +dynamic-html \ +html \ +html2 \ +swift2-deprecated \ +swift3-deprecated \ +swift4 \ +typescript-angular \ +typescript-angularjs \ +typescript-aurelia \ +typescript-axios \ +typescript-fetch \ +typescript-inversify \ +typescript-jquery \ +typescript-node \ +typescript-rxjs" + +[[ $generators =~ (^|[[:space:]])$2($|[[:space:]]) ]] && echo 'Generator found' || (echo "No such generator" && exit) + + +if [[ ! -e $1 ]]; then + mkdir `pwd`/$2 +fi + +docker run --rm -v `pwd`:/generator openapitools/openapi-generator-cli generate -o /generator/$2 -i /generator/$1 -g $2 \ No newline at end of file diff --git a/doc/openapi/v0/cyphernode-api.yaml b/doc/openapi/v0/cyphernode-api.yaml new file mode 100644 index 0000000..c71a4fb --- /dev/null +++ b/doc/openapi/v0/cyphernode-api.yaml @@ -0,0 +1,1907 @@ +openapi: 3.0.0 +info: + description: "Cyphernode API v0" + version: "0.1.0" + title: "Cyphernode API" +externalDocs: + description: "Find out more about Swagger" + url: "http://swagger.io" +servers: + - url: /v0 + description: authoring + - url: http://localhost:8888/v0 + description: local cyphernode +security: + - BearerAuth: [] +tags: + - name: "cyphernode" + description: "Everything bitcoin" + externalDocs: + description: "Find out more" + url: "http://cyphernode.io" + - name: "openapi generator" + externalDocs: + description: "Find out more" + url: "https://github.com/OpenAPITools/openapi-generator" + - name: "watching addresses" + - name: "block" + - name: "transaction" + - name: "spending wallet" + - name: "addresses" + - name: "lightning" + - name: "open timestamps client" + - name: "core features" + - name: "optional features" +paths: + /watch: + post: + tags: + - "watching addresses" + - "core features" + summary: "Add a new Bitcoin address to be watched" + description: "Inserts the Bitcoin address and callbacks in the DB and imports the address to the Watching wallet." + operationId: "addWatchedAddress" + requestBody: + description: "Bitcoin address that needs to be watched" + required: true + content: + application/json: + schema: + type: "object" + required: + - "address" + - "confirmedCallbackURL" + - "unconfirmedCallbackURL" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + unconfirmedCallbackURL: + type: "string" + format: "url" + confirmedCallbackURL: + type: "string" + format: "url" + responses: + '200': + description: "successfully created" + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + '400': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getactivewatches: + get: + tags: + - "watching addresses" + - "core features" + summary: "Get list of watched Bitcoin addresses" + description: "Returns the list of currently watched Bitcoin addresses and callback information." + operationId: "listWatchedAddress" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "watches" + properties: + watches: + type: "array" + items: + $ref: '#/components/schemas/WatchedAddress' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /unwatch/{address}: + get: + parameters: + - in: "path" + name: "address" + description: "Address" + required: true + schema: + $ref: '#/components/schemas/TypeAddressString' + tags: + - "watching addresses" + - "core features" + summary: "Stop watching a Bitcoin address" + description: "Updates the watched Bitcoin address row in DB so that callbacks won't be called on tx confirmations for that address." + operationId: "deleteWatchedAddress" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + event: + type: "string" + address: + $ref: '#/components/schemas/TypeAddressString' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /watchxpub: + post: + tags: + - "watching addresses" + - "core features" + summary: "Add a new xpub to be watched" + description: "Inserts the xpub's Bitcoin addresses and callbacks in the DB and imports the addresses to the xpub Watching wallet." + operationId: "addXpubWatch" + requestBody: + description: "Bitcoin xpub that needs to be watched" + required: true + content: + application/json: + schema: + type: "object" + required: + - "label" + - "pub32" + - "path" + - "nstart" + - "unconfirmedCallbackURL" + - "confirmedCallbackURL" + properties: + label: + description: "Label for that xpub. Can be used, instead for xpub, for future references in xpub-related endpoints." + type: "string" + pub32: + $ref: '#/components/schemas/TypeXpubString' + path: + description: "Derivation path to use when deriving addresses from xpub to be watched. Must use variable n in the path for the index." + type: "string" + nstart: + description: "Index number to start watching from." + type: "integer" + unconfirmedCallbackURL: + type: "string" + format: "url" + confirmedCallbackURL: + type: "string" + format: "url" + responses: + '200': + description: "successfully created" + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + '400': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /unwatchxpubbyxpub/{xpub}: + get: + parameters: + - in: "path" + name: "xpub" + description: "Xpub address" + required: true + schema: + $ref: '#/components/schemas/TypeXpubString' + tags: + - "watching addresses" + - "core features" + summary: "Stop watching a Bitcoin xpub address" + description: "Updates the watched xpub Bitcoin address row in DB so that callbacks won't be called on tx confirmations for the provided xpub addresses." + operationId: "deleteWatchedXpubByXpub" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + event: + type: "string" + xpub: + $ref: '#/components/schemas/TypeXpubString' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /unwatchxpubbylabel/{label}: + get: + parameters: + - in: "path" + name: "label" + description: "Xpub label" + required: true + schema: + type: "string" + tags: + - "watching addresses" + - "core features" + summary: "Stop watching a Bitcoin xpub address" + description: "Updates the watched xpub Bitcoin address row in DB so that callbacks won't be called on tx confirmations for the provided xpub addresses." + operationId: "deleteWatchedXpubByLabel" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + event: + type: "string" + label: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getactivewatchesbyxpub/{xpub}: + get: + parameters: + - in: "path" + name: "xpub" + description: "Xpub address" + required: true + schema: + $ref: '#/components/schemas/TypeXpubString' + tags: + - "watching addresses" + - "core features" + summary: "Get list of watched-by-xpub Bitcoin addresses" + description: "Returns the list of currently watched-by-xpub Bitcoin addresses and callback information." + operationId: "listWatchedByXpubAddressByXpub" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "watches" + properties: + watches: + type: "array" + items: + $ref: '#/components/schemas/WatchedByXpubAddress' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getactivewatchesbylabel/{label}: + get: + parameters: + - in: "path" + name: "label" + description: "Xpub label" + required: true + schema: + type: "string" + tags: + - "watching addresses" + - "core features" + summary: "Get list of watched-by-xpub Bitcoin addresses by label" + description: "Returns the list of currently watched-by-xpub Bitcoin addresses, by label, and callback information." + operationId: "listWatchedByXpubAddressByLabel" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "watches" + properties: + watches: + type: "array" + items: + $ref: '#/components/schemas/WatchedByXpubAddress' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getactivexpubwatches: + get: + tags: + - "watching addresses" + - "core features" + summary: "Get list of watched-by-xpub Bitcoin xpub" + description: "Returns the list of currently watched xpub Bitcoin addresses and callback information." + operationId: "listWatchedByXpubAddresses" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "id" + - "pub32" + - "label" + - "derivation_path" + - "last_imported_n" + - "unconfirmedCallbackURL" + - "confirmedCallbackURL" + - "watching_since" + properties: + watches: + type: "array" + items: + $ref: '#/components/schemas/WatchedXpub' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /watchtxid: + post: + tags: + - "watching addresses" + - "core features" + summary: "Add a new txid to be watched" + description: "Inserts the transaction id and callbacks in the DB and check number of confirmations when a new block is added. Will eventually execute callbacks when enough confirmations for the transaction. Useful when we want to be notified when a LN channel is ready to use." + operationId: "addWatchedTxid" + requestBody: + description: "Bitcoin txid that needs to be watched" + required: true + content: + application/json: + schema: + type: "object" + required: + - "txid" + - "confirmedCallbackURL" + - "xconfCallbackURL" + - "nbxconf" + properties: + txid: + $ref: '#/components/schemas/TypeHashString' + unconfirmedCallbackURL: + type: "string" + format: "url" + xconfCallbackURL: + type: "string" + format: "url" + nbxconf: + type: "integer" + responses: + '200': + description: "successfully created" + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + '400': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + + /getblockhash/{height}: + get: + parameters: + - in: "path" + name: "height" + description: "Blockhash" + required: true + schema: + type: "integer" + tags: + - "block" + - "core features" + summary: "Get block hash matching height provided." + description: "Returns the block matching the height provided of the watching Bitcoin node." + operationId: "getBlockHash" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "result" + properties: + result: + $ref: '#/components/schemas/TypeHashString' + error: + type: "string" + id: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getbestblockhash: + get: + tags: + - "block" + - "core features" + summary: "Get the best block hash." + description: "Returns the best block hash of the watching Bitcoin node." + operationId: "getBestBlockHash" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + required: + - "result" + properties: + result: + $ref: '#/components/schemas/TypeHashString' + error: + type: "string" + id: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getblockchaininfo: + get: + tags: + - "stats" + - "core features" + summary: "Show blockchain info" + description: "Returns detailed blockchain information." + operationId: "getBlockchainInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/BlockchainInfo' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getinstallation_info: + get: + tags: + - "stats" + summary: "Show installation info" + description: "Returns detailed cyphernode installation information." + operationId: "getInstallationInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/InstallationInfo' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getblockinfo/{blockHash}: + get: + parameters: + - in: "path" + name: "blockHash" + description: "Blockhash" + required: true + schema: + $ref: '#/components/schemas/TypeHashString' + tags: + - "block" + - "core features" + summary: "Show block info" + description: "Returns detailed block information." + operationId: "getBlockInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + $ref: '#/components/schemas/Block' + error: + type: "string" + id: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getbestblockinfo: + get: + tags: + - "block" + - "core features" + summary: "Get the best block info" + description: "Returns detailed block information for current best block." + operationId: "getBestBlockInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + $ref: '#/components/schemas/Block' + error: + type: "string" + id: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /gettransaction/{transactionHash}: + get: + parameters: + - in: "path" + name: "transactionHash" + description: "Transaction id" + required: true + schema: + $ref: '#/components/schemas/TypeHashString' + tags: + - "transaction" + - "core features" + summary: "Get transaction info" + description: "Returns detailed transaction information." + operationId: "getTransactionInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + $ref: '#/components/schemas/Transaction' + error: + type: "string" + id: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getbalance: + get: + tags: + - "spending wallet" + - "core features" + summary: "Get the spending wallet balance" + description: "Returns the spending wallet balance, in BTC." + operationId: "getSpendingWalletBalance" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/Balance' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getbalancebyxpub/{xpub}: + get: + parameters: + - in: "path" + name: "xpub" + description: "Xpub address" + required: true + schema: + $ref: '#/components/schemas/TypeXpubString' + tags: + - "spending wallet" + - "core features" + summary: "Get the xpub balance" + description: "Returns the xpub balance, in BTC." + operationId: "getXpubBalanceByXpub" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/Balance' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getbalancebyxpublabel/{label}: + get: + parameters: + - in: "path" + name: "label" + description: "Xpub label" + required: true + schema: + type: "string" + tags: + - "spending wallet" + - "core features" + summary: "Get the xpub balance by its label" + description: "Returns the xpub balance by its label, in BTC." + operationId: "getXpubBalanceByLabel" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/Balance' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getnewaddress: + get: + tags: + - "spending wallet" + - "core features" + summary: "Generates a new address on the spending wallet" + description: "Generates a new address on the spending wallet. Useful to refill the spending wallet from cold wallet (ie Trezor)." + operationId: "getSpendingWalletNewAddress" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /spend: + post: + tags: + - "spending wallet" + - "core features" + summary: "Spend some amount to some address" + description: "Calls sendtoaddress RPC on the spending wallet with supplied info." + operationId: "spend" + requestBody: + description: "Address and amount" + required: true + content: + application/json: + schema: + type: "object" + required: + - "address" + - "amount" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + amount: + type: "number" + responses: + '200': + description: "operation successful" + content: + application/json: + schema: + type: "object" + required: + - "status" + - "hash" + properties: + status: + type: "string" + hash: + $ref: '#/components/schemas/TypeHashString' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /bumpfee: + post: + tags: + - "spending wallet" + - "core features" + summary: "Bump the fees of an unconfirmed transaction" + description: "Calls bumpfee RPC on the spending wallet with supplied info." + operationId: "bumpfee" + requestBody: + description: "txid and confirmation target" + required: true + content: + application/json: + schema: + type: "object" + required: + - "txid" + properties: + txid: + $ref: '#/components/schemas/TypeHashString' + confTarget: + type: "number" + responses: + '200': + description: "operation successful" + content: + application/json: + schema: + type: "object" + required: + - "txid" + - "origfee" + - "fee" + - "errors" + properties: + txid: + $ref: '#/components/schemas/TypeHashString' + origfee: + type: "number" + fee: + type: "number" + errors: + type: "array" + items: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /addtobatch: + post: + tags: + - "spending wallet" + - "core features" + summary: "Adds spending of some amount to some address to the next batch" + description: "Inserts output information in the DB. Used when batchspend is called later." + operationId: "spendInNextBatch" + requestBody: + description: "Address and amount" + required: true + content: + application/json: + schema: + type: "object" + required: + - "address" + - "amount" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + amount: + type: "number" + responses: + '200': + description: "operation successful" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /batchspend: + get: + tags: + - "spending wallet" + - "core features" + summary: "Spend previously amounts/addresses added with addtobatch" + description: "Creates a batched transaction whose outputs are the previously unspent addtobatch calls." + operationId: "batchSpend" + responses: + '200': + description: "operation successful" + content: + application/json: + schema: + type: "object" + required: + - "status" + - "hash" + properties: + status: + type: "string" + hash: + $ref: '#/components/schemas/TypeHashString' + '400': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /deriveindex/{indexSpecifier}: + get: + parameters: + - in: "path" + name: "indexSpecifier" + description: "Index specifier. Either an integer or a range in the form n-m" + required: true + schema: + type: "string" + tags: + - "addresses" + - "core features" + summary: "Derive address(es) using configured xpub key and derivation path" + description: "Derives address(es) for supplied index. Must be used with derivation.pub32 and derivation.path properties in config.properties." + operationId: "deriveIndex" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + addresses: + type: "array" + items: + type: "object" + required: + - "address" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /derivepubpath: + post: + tags: + - "addresses" + - "core features" + summary: "Derive address(es) using supplied xpub key and derivation path" + description: "Derives address(es) for supplied pub32 and path. config.properties' derivation.pub32 and derivation.path are not used." + operationId: "derivePubPath" + requestBody: + required: true + content: + application/json: + schema: + type: "object" + required: + - "pub32" + - "path" + properties: + pub32: + description: "xpub/ypub/zpub/tpub/upub/vpub used to derive address(es)" + type: "string" + path: + description: "Derivation path including the index, in the form n or n-m (ex.: 0/44'/86 or 0/44'/62-77)" + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + addresses: + type: "array" + items: + type: "object" + required: + - "address" + properties: + address: + $ref: '#/components/schemas/TypeAddressString' + '400': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getmempoolinfo: + get: + tags: + - "stats" + - "core features" + summary: "Show mempool info" + description: "Returns detailed mempool information." + operationId: "getmempoolinfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/MempoolInfo' + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_getinfo: + get: + tags: + - "lightning" + - "optional features" + summary: "Get lightning node info" + description: "Calls getinfo from lightningd. Useful to let your users know where to connect to." + operationId: "lightningGetNodeInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + id: + type: "string" + alias: + type: "string" + color: + $ref: '#/components/schemas/TypeInt32HexString' + version: + type: "string" + blockheight: + type: "integer" + network: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_create_invoice: + post: + tags: + - "lightning" + - "optional features" + summary: "Create lightning invoice" + description: "Returns a LN invoice. Label must be unique. Description will be used by your user for payment. Expiry is in seconds." + operationId: "lightningCreateInvoice" + requestBody: + description: "Information to create invoice" + required: true + content: + application/json: + schema: + type: "object" + required: + - "msatoshi" + - "label" + - "description" + - "expiry" + properties: + msatoshi: + type: "integer" + label: + type: "string" + description: + type: "string" + expiry: + type: "integer" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + payment_hash: + $ref: '#/components/schemas/TypeHashString' + expires_at: + type: "integer" + bolt11: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_pay: + post: + tags: + - "lightning" + - "optional features" + summary: "Pay lightning invoice" + description: "Make a LN payment. expected_msatoshi and expected_description are respectively the amount and description you gave your user for her to create the invoice; they must match the given bolt11 invoice supplied by your user." + operationId: "lightningPayInvoice" + requestBody: + description: "Information to pay invoice" + required: true + content: + application/json: + schema: + type: "object" + required: + - "bolt11" + - "expected_msatoshi" + - "expected_description" + properties: + bolt11: + type: "string" + expected_msatoshi: + type: "integer" + expected_description: + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + id: + type: "integer" + payment_hash: + $ref: '#/components/schemas/TypeHashString' + msatoshi: + type: "integer" + msatoshi_sent: + type: "integer" + created_at: + type: "integer" + status: + type: "string" + payment_preimage: + $ref: '#/components/schemas/TypeHashString' + description: + type: "string" + getroute_tries: + type: "integer" + sendpay_tries: + type: "integer" + route: + type: "array" + items: + type: "object" + properties: + id: + type: "string" + channel: + type: "string" + msatoshi: + type: "integer" + delay: + type: "integer" + failures: + type: "array" + items: + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_newaddr: + get: + tags: + - "lightning" + - "optional features" + summary: "Generates new address from the lightning node" + description: "Returns a Bitcoin bech32 address to fund your LN wallet." + operationId: "lightningGetNewAddress" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + address: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_getconnectionstring: + get: + tags: + - "lightning" + - "optional features" + summary: "Returns the connection string to the lightning node" + description: "Returns the connection string to the lightning node" + operationId: "lightningGetConnectionString" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + connectstring: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_decodebolt11/{bolt11}: + get: + parameters: + - in: "path" + name: "bolt11" + description: "BOLT11 string to decode" + required: true + schema: + type: "string" + tags: + - "lightning" + - "optional features" + summary: "Get the detailed decoded bolt11 string info" + description: "Returns the detailed decoded bolt11 string info" + operationId: "getDecodedBolt11" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + # TODO: describe response + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_getinvoice/{label}: + get: + parameters: + - in: "path" + name: "label" + description: "Xpub label" + required: true + schema: + type: "string" + tags: + - "lightning" + - "optional features" + summary: "Get the LN invoice by its label" + description: "Returns the LN invoice by its label." + operationId: "getLnInvoiceByLabel" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + # TODO: describe response + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ln_connectfund: + post: + tags: + - "lightning" + - "optional features" + summary: "Connect LN to a peer and fund a channel" + description: "Connect LN node to a peer. Fund a channel. When channel is ready, callback will be called." + operationId: "lightningConnectFund" + requestBody: + description: "Information to connect, fund channel and be called back." + required: true + content: + application/json: + schema: + type: "object" + required: + - "peer" + - "msatoshi" + - "callbackUrl" + properties: + peer: + type: "string" + msatoshi: + type: "integer" + callbackUrl: + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + type: "string" + txid: + $ref: '#/components/schemas/TypeHashString' + channel_id: + type: "string" + message: + type: "string" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ots_stamp: + post: + tags: + - "ots" + - "optional features" + summary: "Stamp a hash using OTS" + description: "Stamp supplied hash using OTS. Callback called when stamping is completed." + operationId: "otsStamp" + requestBody: + description: "Information to stamp and callback." + required: true + content: + application/json: + schema: + type: "object" + required: + - "hash" + - "callbackUrl" + properties: + hash: + $ref: '#/components/schemas/TypeHashString' + callbackUrl: + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + # TODO: describe response + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ots_verify: + post: + tags: + - "ots" + - "optional features" + summary: "Verify an OTS file with hash" + description: "If base64 of the OTS file is not supplied, look for local OTS file named after the supplied hash suffixed with .ots." + operationId: "otsVerify" + requestBody: + description: "JSON object containing hash and base64otsfile." + required: true + content: + application/json: + schema: + type: "object" + required: + - "hash" + properties: + hash: + $ref: '#/components/schemas/TypeHashString' + base64otsfile: + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + # TODO: describe response + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ots_info: + post: + tags: + - "ots" + - "optional features" + summary: "Get information about an OTS file" + description: "If base64 of the OTS file is not supplied, look for local OTS file named after the supplied hash suffixed with .ots." + operationId: "otsInfo" + requestBody: + description: "JSON object containing hash and/or base64otsfile." + required: true + content: + application/json: + schema: + type: "object" + properties: + hash: + $ref: '#/components/schemas/TypeHashString' + base64otsfile: + type: "string" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + # TODO: describe response + type: "object" + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ots_getfile/{hash}: + get: + parameters: + - in: "path" + name: "hash" + description: "Stamped hash" + required: true + schema: + type: "string" + tags: + - "ots" + - "optional features" + summary: "Get the OTS file of the stamped hash." + description: "Returns the OTS file of the stamped hash." + operationId: "getOtsFile" + responses: + '200': + description: "successful operation" + content: + application/octet-stream: + schema: + type: string + format: binary + '403': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' +components: + schemas: + WatchedAddress: + type: "object" + required: + - "address" + - "unconfirmedCallbackURL" + - "confirmedCallbackURL" + properties: + id: + type: "string" + event: + type: "string" + imported: + type: "string" + enum: ["0","1"] + inserted: + type: "string" + enum: ["0","1"] + address: + $ref: '#/components/schemas/TypeAddressString' + unconfirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + confirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + estimatesmartfee2blocks: + type: "string" + estimatesmartfee6blocks: + type: "string" + estimatesmartfee36blocks: + type: "string" + estimatesmartfee144blocks: + type: "string" + watching_since: + type: "string" + WatchedByXpubAddress: + type: "object" + required: + - "id" + - "address" + - "imported" + - "unconfirmedCallbackURL" + - "confirmedCallbackURL" + - "watching_since" + - "derivation_path" + - "pub32_index" + properties: + id: + type: "string" + address: + $ref: '#/components/schemas/TypeAddressString' + imported: + type: "string" + enum: ["0","1"] + unconfirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + confirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + watching_since: + type: "string" + derivation_path: + type: "string" + pub32_index: + type: "string" + WatchedXpub: + type: "object" + required: + - "id" + - "pub32" + - "label" + - "derivation_path" + - "last_imported_n" + - "unconfirmedCallbackURL" + - "confirmedCallbackURL" + - "watching_since" + properties: + id: + type: "string" + pub32: + $ref: '#/components/schemas/TypeXpubString' + label: + type: "string" + derivation_path: + type: "string" + last_imported_n: + type: "string" + unconfirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + confirmedCallbackURL: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + watching_since: + type: "string" + Balance: + type: "object" + required: + - "balance" + properties: + balance: + type: "number" + Block: + type: "object" + required: + - "address" + - "callback" + properties: + id: + type: "integer" + hash: + $ref: '#/components/schemas/TypeHashString' + confirmations: + type: "integer" + strippedsize: + type: "integer" + size: + type: "integer" + weight: + type: "integer" + height: + type: "integer" + version: + type: "integer" + versionHex: + $ref: '#/components/schemas/TypeInt32HexString' + merkleroot: + $ref: '#/components/schemas/TypeHashString' + tx: + type: "array" + items: + $ref: '#/components/schemas/TypeHashString' + time: + type: "integer" + mediantime: + type: "integer" + nonce: + type: "integer" + bits: + $ref: '#/components/schemas/TypeInt32HexString' + difficulty: + type: 'integer' + chainwork: + $ref: '#/components/schemas/TypeHashString' + nTx: + type: 'integer' + previousblockhash: + $ref: '#/components/schemas/TypeHashString' + nextblockhash: + $ref: '#/components/schemas/TypeHashString' + Transaction: + type: "object" + properties: + txid: + $ref: '#/components/schemas/TypeHashString' + hash: + $ref: '#/components/schemas/TypeHashString' + version: + type: "integer" + size: + type: "integer" + vsize: + type: "integer" + locktime: + type: "integer" + vin: + type: "array" + items: + $ref: '#/components/schemas/Input' + vout: + type: "array" + items: + $ref: '#/components/schemas/Output' + hex: + $ref: '#/components/schemas/TypeHexString' + blockhash: + $ref: '#/components/schemas/TypeHashString' + confirmations: + type: "integer" + time: + type: "integer" + blocktime: + type: "integer" + BlockchainInfo: + type: "object" + properties: + chain: + type: "string" + enum: ["test", "main"] + blocks: + type: "integer" + headers: + type: "integer" + bestblockhash: + $ref: '#/components/schemas/TypeHashString' + difficulty: + type: "number" + mediantime: + type: "integer" + verificationprogress: + type: "number" + initialblockdownload: + type: "boolean" + chainwork: + $ref: '#/components/schemas/TypeHashString' + size_on_disk: + type: "integer" + pruned: + type: "boolean" + warnings: + type: "string" + softforks: + type: "array" + items: + $ref: '#/components/schemas/TypeSoftFork' + bip9_softforks: + type: "object" + additionalProperties: + $ref: '#/components/schemas/TypeBip9SoftFork' + InstallationInfo: + type: "object" + properties: + api_versions: + type: "array" + items: + type: "string" + setup_version: + type: "string" + bitcoin_version: + type: "string" + features: + type: "array" + items: + type: "object" + properties: + name: + type: "string" + label: + type: "string" + host: + type: "string" + docker: + type: "string" + networks: + type: "array" + items: + type: "string" + extra: + type: "object" + devmode: + type: "boolean" + Input: + type: "object" + properties: + txid: + $ref: '#/components/schemas/TypeHashString' + vout: + type: "integer" + scriptSig: + type: "object" + properties: + asm: + type: "string" + hex: + $ref: '#/components/schemas/TypeHexString' + Output: + type: "object" + properties: + value: + type: "number" + n: + type: "integer" + scriptPubKey: + type: "object" + properties: + asm: + type: "string" + hex: + $ref: '#/components/schemas/TypeHexString' + reqSigs: + type: "integer" + type: + type: "string" + addresses: + type: "array" + items: + $ref: '#/components/schemas/TypeAddressString' + MempoolInfo: + type: "object" + properties: + size: + type: "integer" + bytes: + type: "integer" + usage: + type: "integer" + maxmempool: + type: "integer" + mempoolminfee: + type: "number" + minrelaytxfee: + type: "integer" + TypeHexString: + description: "variable length hex string" + type: "string" + pattern: "^[a-fA-F0-9]+$" + TypeAddressString: + description: "base58 check encoded address" + type: "string" + pattern: "^[a-km-zA-HJ-NP-Z1-9]{26,35}$" + TypeXpubString: + description: "base58 check encoded xpub" + type: "string" + pattern: "^[a-km-zA-HJ-NP-Z1-9]{78,112}$" + TypeHashString: + description: "64 character hex string" + type: "string" + pattern: "^[a-fA-F0-9]{64}$" + TypeInt32HexString: + description: "8 character hex string" + type: "string" + pattern: "^([a-fA-F0-9][a-fA-F0-9]){1,4}$" + TypeSoftFork: + type: "object" + properties: + id: + type: "string" + version: + type: "integer" + reject: + type: "object" + properties: + status: + type: "boolean" + TypeBip9SoftFork: + type: "object" + properties: + status: + type: "string" + startTime: + type: "integer" + timeout: + type: "integer" + since: + type: "integer" + ApiResponseTemporarilyUnavailable: + type: "object" + properties: + reason: + type: "string" + ApiResponseNotAllowed: + description: Access token is missing or invalid + ApiResponseNotFound: + description: Not found + ApiResponseInvalidInput: + description: Invalid Input + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/doc/openapi/v0/cyphernode-callbacks.yaml b/doc/openapi/v0/cyphernode-callbacks.yaml new file mode 100644 index 0000000..79bded3 --- /dev/null +++ b/doc/openapi/v0/cyphernode-callbacks.yaml @@ -0,0 +1,141 @@ +openapi: 3.0.0 +info: + description: "Cyphernode application callbacks v0" + version: "0.1.0" + title: "Cyphernode application callbacks" +externalDocs: + description: "Find out more about Swagger" + url: "http://swagger.io" +servers: + - url: / + description: authoring + - url: http://localhost:8888/ + description: local cyphernode +tags: + - name: "cyphernode callbacks" + description: "Everything bitcoin" + externalDocs: + description: "Find out more" + url: "http://cyphernode.io" + - name: "openapi generator" + externalDocs: + description: "Find out more" + url: "https://github.com/OpenAPITools/openapi-generator" +paths: + /0conf: + post: + summary: "" + description: "" + operationId: "notifyUnconfirmed" + requestBody: + description: "" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConfRequest' + responses: + '201': + description: "successfully created" + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /xconf: + post: + summary: "" + description: "" + operationId: "notifyConfirmed" + requestBody: + description: "" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConfRequest' + responses: + '201': + description: "successfully created" + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /otsconf: + post: + summary: "" + description: "" + operationId: "notifyOtsUpgrade" + requestBody: + description: "" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConfRequest' + responses: + '201': + description: "successfully created" + '405': + $ref: '#/components/schemas/ApiResponseInvalidInput' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' +components: + schemas: + ConfRequest: + type: "object" + properties: + id: + type: "string" + address: + $ref: '#/components/schemas/TypeAddressString' + hash: + $ref: '#/components/schemas/TypeHashString' + vout_n: + type: "integer" + sent_amount: + type: "number" + confirmations: + type: "integer" + received: + type: "string" + size: + type: "integer" + vsize: + type: "integer" + fees: + type: "number" + is_replaceable: + type: "integer" + blockhash: + $ref: '#/components/schemas/TypeHashString' + blocktime: + type: "integer" + blockheight: + type: "integer" + TypeAddressString: + description: "base58 check encoded address" + type: "string" + pattern: "^[a-km-zA-HJ-NP-Z1-9]{26,35}$" + TypeHashString: + description: "64 character hex string" + type: "string" + pattern: "^[a-fA-F0-9]{64}$" + ApiResponseTemporarilyUnavailable: + type: "object" + properties: + reason: + type: "string" + ApiResponseInvalidInput: + description: Invalid Input \ No newline at end of file diff --git a/doc/openapi/v0/cyphernode-internal.yaml b/doc/openapi/v0/cyphernode-internal.yaml new file mode 100644 index 0000000..ed85a1e --- /dev/null +++ b/doc/openapi/v0/cyphernode-internal.yaml @@ -0,0 +1,126 @@ +openapi: 3.0.0 +info: + description: "Cyphernode internal API v0" + version: "0.1.0" + title: "Cyphernode internal API" +externalDocs: + description: "Find out more about Swagger" + url: "http://swagger.io" +servers: + - url: / + description: authoring + - url: http://localhost:8888/ + description: local cyphernode +security: + - BearerAuth: [] +tags: + - name: "cyphernode" + description: "Everything bitcoin" + externalDocs: + description: "Find out more" + url: "http://cyphernode.io" + - name: "openapi generator" + externalDocs: + description: "Find out more" + url: "https://github.com/OpenAPITools/openapi-generator" +paths: + /executecallbacks: + get: + summary: "Try missed callbacks" + description: "Looks in DB for watched addresses, ask the watching Bitcoin node if those addresses got payments, if so it executes the callbacks that would be usually executed when 'conf' is called by the node. This is useful if the watching node went down or there was a network glitch when a transaction on a watched address got confirmed." + operationId: "executeMissedCallbacks" + responses: + '200': + description: "successful operation" + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /conf/{txid}: + get: + parameters: + - in: "path" + name: "txid" + description: "Transaction ID" + required: true + schema: + $ref: '#/components/schemas/TypeHashString' + summary: "Notify confirm" + description: "Confirms a transaction on an imported address. The Watching Bitcoin node will notify Cyphernode (thanks to walletnotify in bitcoin.conf) by calling this endpoint with txid when a tx is new or updated on an address. If address is still being watched (flag in DB), the corresponding callbacks will be called." + operationId: "notifyConf" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + type: "string" + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /ots_backoffice: + get: + summary: "Check for upgraded OTS stamping" + description: "Looks in the DB for non-upgraded OTS stamps, asks the OTS client to check if they are upgraded and executes the callbacks for the newly upgraded ones. Updates DB accordingly." + operationId: "otsBackoffice" + responses: + '200': + description: "successful operation" + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /newblock/{blockHash}: + get: + parameters: + - in: "path" + name: "blockHash" + description: "Blockhash" + required: true + schema: + $ref: '#/components/schemas/TypeHashString' + summary: "Notify confirm" + description: "Notifies cyphernode when a new block is added to the blockchain. Cyphernode will look at the callbacks to be made after x number of confirmations and execute them." + operationId: "newblockConf" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + type: "object" + properties: + result: + type: "string" + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' +components: + schemas: + TypeHashString: + description: "64 character hex string" + type: "string" + pattern: "^[a-fA-F0-9]{64}$" + ApiResponseTemporarilyUnavailable: + type: "object" + properties: + reason: + type: "string" + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/doc/openapi/v1/cyphernode-api.yaml b/doc/openapi/v1/cyphernode-api.yaml new file mode 100644 index 0000000..db4c4bc --- /dev/null +++ b/doc/openapi/v1/cyphernode-api.yaml @@ -0,0 +1,332 @@ +openapi: 3.0.0 +info: + description: "Cyphernode API v1" + version: "1.0.0" + title: "Cyphernode API" +externalDocs: + description: "Find out more about Swagger" + url: "http://swagger.io" +servers: + - url: /v1 + description: authoring + - url: http://localhost:8888/v1 + description: local cyphernode +tags: + - name: "cyphernode" + description: "Everything bitcoin" + externalDocs: + description: "Find out more" + url: "http://cyphernode.io" + - name: "openapi generator" + externalDocs: + description: "Find out more" + url: "https://github.com/OpenAPITools/openapi-generator" + - name: "watchedAddress" + description: "Watch addresses" + - name: "block" + description: "Block information" +paths: + /watchedAddress: + post: + tags: + - "watchedAddress" + summary: "Add a new address to ne watched" + description: "Inserts the address and callbacks in the DB and imports the address to the Watching wallet." + operationId: "addWatchedAddress" + requestBody: + description: "Address that needs to be watched" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + responses: + 201: + description: "successfully created" + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 405: + $ref: '#/components/schemas/ApiResponseInvalidInput' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + get: + tags: + - "watchedAddress" + summary: "Get list of watched addresses" + description: "Returns the list of currently watched addresses and callback information." + operationId: "listWatchedAddress" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + type: "array" + items: + $ref: '#/components/schemas/WatchedAddress' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + /watchedAddress/{address}: + get: + parameters: + - in: "path" + name: "address" + description: "Address" + required: true + schema: + $ref: '#/components/schemas/TypeAddressString' + tags: + - "watchedAddress" + summary: "Show watched address" + description: "" + operationId: "getWatchedAddress" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/WatchedAddress' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 404: + $ref: '#/components/schemas/ApiResponseNotFound' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + delete: + parameters: + - in: "path" + name: "address" + description: "Address" + required: true + schema: + $ref: '#/components/schemas/TypeAddressString' + tags: + - "watchedAddress" + summary: "Delete watched address" + description: "Updates the watched address row in DB so that callbacks won't be called on tx confirmations for that address." + operationId: "deleteWatchedAddress" + responses: + 200: + description: "successful operation" + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 404: + $ref: '#/components/schemas/ApiResponseNotFound' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + /block/{blockHash}: + get: + parameters: + - in: "path" + name: "blockHash" + description: "Blockhash" + required: true + schema: + $ref: '#/components/schemas/TypeHashString' + tags: + - "block" + summary: "Show block info" + description: "" + operationId: "getBlockInfo" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/Block' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 404: + $ref: '#/components/schemas/ApiResponseNotFound' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + /block/best: + get: + tags: + - "block" + summary: "Get the best block info" + description: "" + operationId: "getBestBlockInfo" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/Block' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] + /block/bestHash: + get: + tags: + - "block" + summary: "Get the best block hash." + description: "Returns the best block hash of the watching Bitcoin node." + operationId: "getBestBlockHash" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/TypeHashString' + 401: + $ref: '#/components/schemas/ApiResponseNotAllowed' + 503: + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + security: + - BearerAuth: [] +components: + schemas: + WatchedAddress: + type: "object" + required: + - "address" + - "callback" + properties: + id: + type: "integer" + address: + $ref: '#/components/schemas/TypeAddressString' + callback: + description: "Async callback in case of activity on address" + type: "string" + format: "url" + estimatesmartfee2blocks: + type: "number" + estimatesmartfee6blocks: + type: "number" + estimatesmartfee36blocks: + type: "number" + estimatesmartfee144blocks: + type: "number" + watching_since: + type: "string" + format: "date-time" + Block: + type: "object" + required: + - "address" + - "callback" + properties: + id: + type: "integer" + hash: + $ref: '#/components/schemas/TypeHashString' + confirmations: + type: "integer" + strippedsize: + type: "integer" + size: + type: "integer" + weight: + type: "integer" + height: + type: "integer" + version: + type: "integer" + versionHex: + $ref: '#/components/schemas/TypeInt32HexString' + merkleroot: + $ref: '#/components/schemas/TypeHashString' + transactions: + type: "array" + items: + $ref: '#/components/schemas/TypeHashString' + time: + type: "integer" + mediantime: + type: "integer" + nonce: + type: "integer" + bits: + $ref: '#/components/schemas/TypeInt32HexString' + difficulty: + type: 'integer' + chainwork: + $ref: '#/components/schemas/TypeHashString' + + transactionCount: + type: 'integer' + previousblockhash: + $ref: '#/components/schemas/TypeHashString' + nextblockhash: + $ref: '#/components/schemas/TypeHashString' + TypeAddressString: + description: "base58 check encoded address" + type: "string" + pattern: "^[a-km-zA-HJ-NP-Z1-9]{26,35}$" + TypeHashString: + description: "64 character hex string" + type: "string" + pattern: "^[a-fA-F0-9]{64}$" + TypeInt32HexString: + description: "8 character hex string" + type: "string" + pattern: "^[a-fA-F0-9]{8}$" + ApiResponseTemporarilyUnavailable: + type: "object" + properties: + reason: + type: "string" + ApiResponseNotAllowed: + description: Access token is missing or invalid + ApiResponseNotFound: + description: Not found + ApiResponseInvalidInput: + description: Invalid Input + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..b960a05 --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,97 @@ +#!/bin/sh + +# Must be logged to docker hub: +# docker login -u cyphernode + +# Must enable experimental cli features +# "experimental": "enabled" in ~/.docker/config.json + +image() { + local image=$1 + local dir=$2 + local arch=$3 + + echo "Building and pushing $image from $dir for $arch using $dockerfile tagging as $v1, $v2 and $v3..." + + docker build -t cyphernode/${image}:${arch}-${v3} -t cyphernode/${image}:${arch}-${v2} -t cyphernode/${image}:${arch}-${v1} ${dir}/. \ + && docker push cyphernode/${image}:${arch}-${v3} \ + && docker push cyphernode/${image}:${arch}-${v2} \ + && docker push cyphernode/${image}:${arch}-${v1} + + return $? +} + +manifest() { + local image=$1 + + echo "Creating and pushing manifest for $image for version $v3..." + + docker manifest create cyphernode/${image}:${v3} cyphernode/${image}:${arm_docker}-${v3} cyphernode/${image}:${x86_docker}-${v3} cyphernode/${image}:${aarch64_docker}-${v3} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${arm_docker}-${v3} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${x86_docker}-${v3} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${aarch64_docker}-${v3} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v3} + + [ $? -ne 0 ] && return 1 + + echo "Creating and pushing manifest for $image for version $v2..." + + docker manifest create cyphernode/${image}:${v2} cyphernode/${image}:${arm_docker}-${v2} cyphernode/${image}:${x86_docker}-${v2} cyphernode/${image}:${aarch64_docker}-${v2} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${arm_docker}-${v2} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${x86_docker}-${v2} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${aarch64_docker}-${v2} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v2} + + [ $? -ne 0 ] && return 1 + + echo "Creating and pushing manifest for $image for version $v1..." + + docker manifest create cyphernode/${image}:${v1} cyphernode/${image}:${arm_docker}-${v1} cyphernode/${image}:${x86_docker}-${v1} cyphernode/${image}:${aarch64_docker}-${v1} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${arm_docker}-${v1} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${x86_docker}-${v1} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${aarch64_docker}-${v1} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v1} + + return $? +} + +x86_docker="amd64" +arm_docker="arm" +aarch64_docker="arm64" + +# Build amd64 and arm64 first, building for arm will trigger the manifest creation and push on hub + +#arch_docker=${arm_docker} +#arch_docker=${aarch64_docker} +arch_docker=${x86_docker} + +v1="v0" +v2="v0.2" +v3="v0.2.4" + +echo "\nBuilding Cyphernode Core containers\n" +echo "arch_docker=$arch_docker\n" + +image "gatekeeper" "api_auth_docker/" ${arch_docker} \ +&& image "proxycron" "cron_docker/" ${arch_docker} \ +&& image "otsclient" "otsclient_docker/" ${arch_docker} \ +&& image "proxy" "proxy_docker/" ${arch_docker} \ +&& image "notifier" "notifier_docker/" ${arch_docker} \ +&& image "pycoin" "pycoin_docker/" ${arch_docker} \ +&& image "cyphernodeconf" "cyphernodeconf_docker/" ${arch_docker} + +[ $? -ne 0 ] && echo "Error" && exit 1 + +[ "${arch_docker}" = "${x86_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${aarch64_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${arm_docker}" ] && echo "Built and pushed images, now building and pushing manifest for all archs..." + +manifest "gatekeeper" \ +&& manifest "proxycron" \ +&& manifest "otsclient" \ +&& manifest "proxy" \ +&& manifest "notifier" \ +&& manifest "pycoin" \ +&& manifest "cyphernodeconf" + +[ $? -ne 0 ] && echo "Error" && exit 1 diff --git a/docker-compose-sample.yml b/docker-compose-sample.yml index 8b4adad..f46b4b0 100644 --- a/docker-compose-sample.yml +++ b/docker-compose-sample.yml @@ -2,119 +2,254 @@ version: "3" services: - gatekeeper: - # HTTP authentication API gate - environment: - - "TRACING=1" - image: cyphernode/gatekeeper:latest - ports: - - "443:443" + + ########################## + # BITCOIN # + ########################## + + bitcoin: + image: cyphernode/bitcoin:v0.18.0 + command: $USER bitcoind + volumes: - - "~/cn-files/cn-gatekeeper/certs:/etc/ssl/certs" - - "~/cn-files/cn-gatekeeper/private:/etc/ssl/private" - - "~/cn-files/cn-gatekeeper/keys.properties:/etc/nginx/conf.d/keys.properties" - - "~/cn-files/cn-gatekeeper/api.properties:/etc/nginx/conf.d/api.properties" - command: $USER -# deploy: -# placement: -# constraints: [node.hostname==dev] + - "~/btcdata:/.bitcoin" + - bitcoin_monitor:/bitcoin_monitor + healthcheck: + test: bitcoin-cli echo && touch /bitcoin_monitor/up || rm -f /bitcoin_monitor/up + interval: 10s + timeout: 5s + retries: 1 networks: - cyphernodenet restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + + ########################## + # PROXY # + ########################## proxy: + image: cyphernode/proxy:v0.2.4-local command: $USER ./startproxy.sh - # Bitcoin Mini Proxy environment: - "TRACING=1" - - "WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/watching01.dat" + - "WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet" + - "WATCHER_BTC_NODE_DEFAULT_WALLET=watching01.dat" + - "WATCHER_BTC_NODE_XPUB_WALLET=xpubwatching01.dat" - "WATCHER_BTC_NODE_RPC_USER=bitcoin:CHANGEME" - "WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties" - - "SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/spending01.dat" + - "SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet" + - "SPENDER_BTC_NODE_DEFAULT_WALLET=spending01.dat" - "SPENDER_BTC_NODE_RPC_USER=bitcoin:CHANGEME" - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" - "PROXY_LISTENING_PORT=8888" - "DB_PATH=/proxy/db" - "DB_FILE=/proxy/db/proxydb" - "PYCOIN_CONTAINER=pycoin:7777" + - "WATCHER_BTC_NODE_PRUNED=false" - "OTSCLIENT_CONTAINER=otsclient:6666" - "OTS_FILES=/proxy/otsfiles" - image: cyphernode/proxy:latest + - "XPUB_DERIVATION_GAP=100" volumes: - - "~/cn-files/cn-proxydb:/proxy/db" - - "~/cn-files/cn-lndata:/.lightning" - - "~/cn-files/cn-otsfiles:/proxy/otsfiles" -# deploy: -# placement: -# constraints: [node.hostname==dev] + - "~/cn-dev/dist/cyphernode/proxy:/proxy/db" + + - "~/cn-dev/dist/cyphernode/lightning:/.lightning" + + + - "~/cn-dev/dist/cyphernode/otsclient:/proxy/otsfiles" + networks: - cyphernodenet restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # PROXYCRON # + ########################## proxycron: + image: cyphernode/proxycron:v0.2.4-local environment: - - "PROXY_URL=proxy:8888/executecallbacks" - image: cyphernode/proxycron:latest -# deploy: -# placement: -# constraints: [node.hostname==dev] + - "TX_CONF_URL=proxy:8888/executecallbacks" + - "OTS_URL=proxy:8888/ots_backoffice" networks: - cyphernodenet restart: always + depends_on: + - proxy +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # BROKER # + ########################## + + broker: + image: eclipse-mosquitto:1.6 + networks: + - cyphernodenet + restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # NOTIFIER # + ########################## + + notifier: + image: cyphernode/notifier:v0.2.4-local + command: $USER ./startnotifier.sh + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always + depends_on: + - broker +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # PYCOIN # + ########################## pycoin: - # Pycoin + image: cyphernode/pycoin:v0.2.4-local command: $USER ./startpycoin.sh - image: cyphernode/pycoin:latest environment: - "TRACING=1" - "PYCOIN_LISTENING_PORT=7777" -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always - lightning: - command: $USER lightningd - image: cyphernode/clightning:v0.6.2 - volumes: - - "~/cn-files/cn-lndata:/.lightning" - - "~/cn-files/cn-lndata/bitcoin.conf:/.bitcoin/bitcoin.conf" -# deploy: -# placement: -# constraints: [node.hostname==dev] networks: - cyphernodenet restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + + ########################## + # OTSCLIENT # + ########################## otsclient: + image: cyphernode/otsclient:v0.2.4-local + command: $USER /script/startotsclient.sh environment: - "TRACING=1" - "OTSCLIENT_LISTENING_PORT=6666" - image: cyphernode/otsclient:latest -# deploy: -# placement: -# constraints: [node.hostname==dev] + + - "TESTNET=1" + volumes: - - "~/cn-files/cn-otsfiles:/otsfiles" + - "~/cn-dev/dist/cyphernode/otsclient:/otsfiles" + - "~/btcdata/bitcoin-client.conf:/.bitcoin/bitcoin.conf" command: $USER /script/startotsclient.sh networks: - cyphernodenet restart: always +# deploy: +# placement: +# constraints: [node.hostname==dev] + + + ########################## + # GATEKEEPER # + ########################## + + gatekeeper: + # HTTP authentication API gate + image: cyphernode/gatekeeper:v0.2.4-local + command: $USER + environment: + - "TRACING=1" + + ports: + - "2009:2009" - bitcoin: - command: $USER bitcoind - image: cyphernode/bitcoin:v0.17.0 volumes: - - "~/cn-files/cn-btcdata:/.bitcoin" + - "~/cn-dev/dist/cyphernode/gatekeeper/certs:/etc/ssl/certs" + - "~/cn-dev/dist/cyphernode/gatekeeper/private:/etc/ssl/private" + - "~/cn-dev/dist/cyphernode/gatekeeper/keys.properties:/etc/nginx/conf.d/keys.properties" + - "~/cn-dev/dist/cyphernode/gatekeeper/api.properties:/etc/nginx/conf.d/api.properties" + - "~/cn-dev/dist/cyphernode/gatekeeper/default.conf:/etc/nginx/conf.d/default.conf" + - "~/cn-dev/dist/cyphernode/gatekeeper/htpasswd:/etc/nginx/conf.d/status/htpasswd" + - "~/cn-dev/dist/cyphernode/gatekeeper/installation.json:/etc/nginx/conf.d/s/stats/installation.json" + - "~/cn-dev/dist/cyphernode/gatekeeper/client.7z:/etc/nginx/conf.d/s/stats/client.7z" + - "~/cn-dev/dist/cyphernode/gatekeeper/config.7z:/etc/nginx/conf.d/s/stats/config.7z" + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always + depends_on: + - proxy +# deploy: +# placement: +# constraints: [node.hostname==dev] + + ########################## + # TRAEFIK # + ########################## + + traefik: + image: traefik:v1.7.9-alpine + ports: + - 80:80 + - 443:443 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "~/cn-dev/dist/cyphernode/traefik/traefik.toml:/traefik.toml" + - "~/cn-dev/dist/cyphernode/traefik/acme.json:/acme.json" + - "~/cn-dev/dist/cyphernode/traefik/htpasswd:/htpasswd/htpasswd" + networks: + - cyphernodeappsnet + restart: always + depends_on: + - gatekeeper +# deploy: +# placement: +# constraints: [node.hostname==dev] + + + ########################## + # LIGHTNING # + ########################## + + lightning: + image: cyphernode/clightning:v0.7.1 + command: $USER sh -c 'while [ ! -f "/bitcoin_monitor/up" ]; do echo "bitcoin not ready" ; sleep 10 ; done ; echo "bitcoin ready!" ; lightningd' + + ports: + - "9735:9735" + + volumes: + - "~/cn-dev/dist/cyphernode/lightning:/.lightning" + - "~/btcdata/bitcoin-client.conf:/.bitcoin/bitcoin.conf" + - bitcoin_monitor:/bitcoin_monitor:ro networks: - cyphernodenet restart: always + depends_on: + - bitcoin +# deploy: +# placement: +# constraints: [node.hostname==dev] + + +volumes: + bitcoin_monitor: networks: cyphernodenet: external: true + cyphernodeappsnet: + external: true diff --git a/install/.dockerignore b/install/.dockerignore deleted file mode 100644 index aad322e..0000000 --- a/install/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -SatoshiPortal -data -script -generator-cyphernode/node_modules -generator-cyphernode/package-lock -generator-cyphernode/__tests__ diff --git a/install/.gitignore b/install/.gitignore deleted file mode 100644 index 6320cd2..0000000 --- a/install/.gitignore +++ /dev/null @@ -1 +0,0 @@ -data \ No newline at end of file diff --git a/install/Dockerfile b/install/Dockerfile deleted file mode 100644 index 7d8e6d3..0000000 --- a/install/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM node:11.1-alpine - -RUN apk add --update bash su-exec p7zip openssl nano && rm -rf /var/cache/apk/* -RUN mkdir -p /app -RUN mkdir /.config -RUN chmod a+rwx /.config - -RUN npm install -g yo -COPY generator-cyphernode /app -WORKDIR /app/generator-cyphernode -RUN npm link - -WORKDIR /data - -ENV EDITOR=/usr/bin/nano - -ENTRYPOINT ["/sbin/su-exec"] -RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true diff --git a/install/SatoshiPortal/dockers b/install/SatoshiPortal/dockers deleted file mode 160000 index 93ed4a3..0000000 --- a/install/SatoshiPortal/dockers +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 93ed4a3dc3b24d7fef32b60ea1a820fc82e1580b diff --git a/install/data/.gitkeep b/install/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/install/generator-cyphernode/.gitignore b/install/generator-cyphernode/.gitignore deleted file mode 100644 index ba2a97b..0000000 --- a/install/generator-cyphernode/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -coverage diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js deleted file mode 100644 index 6053f7e..0000000 --- a/install/generator-cyphernode/generators/app/index.js +++ /dev/null @@ -1,530 +0,0 @@ -const Generator = require('yeoman-generator'); -const chalk = require('chalk'); -const wrap = require('wrap-ansi'); -const html2ansi = require('./lib/html2ansi.js'); -const fs = require('fs'); -const validator = require('validator'); -const path = require("path"); -const coinstring = require('coinstring'); -const name = require('./lib/name.js'); -const Archive = require('./lib/archive.js'); -const ApiKey = require('./lib/apikey.js'); -const Cert = require('./lib/cert.js'); - -const featureChoices = require('./features.json'); -const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars -const userRegexp = /^[a-zA-Z0-9\._\-]+$/; -const reset = '\u001B8\u001B[u'; -const clear = '\u001Bc'; - -const configFileVersion='0.1.0'; - -const defaultAPIProperties = ` -# Watcher can: -action_watch=watcher -action_unwatch=watcher -action_getactivewatches=watcher -action_getbestblockhash=watcher -action_getbestblockinfo=watcher -action_getblockinfo=watcher -action_gettransaction=watcher -action_ln_getinfo=watcher -action_ln_create_invoice=watcher - -# Spender can do what the watcher can do, plus: -action_getbalance=spender -action_getnewaddress=spender -action_spend=spender -action_addtobatch=spender -action_batchspend=spender -action_deriveindex=spender -action_derivepubpath=spender -action_ln_pay=spender -action_ln_newaddr=spender -action_ots_stamp=spender -action_ots_getfile=spender - -# Admin can do what the spender can do, plus: - - -# Should be called from inside the Docker network only: -action_conf=internal -action_executecallbacks=internal -action_ots_backoffice=internal -`; - -const prefix = function() { - return chalk.green('Cyphernode')+': '; -}; - -let prompters = []; -fs.readdirSync(path.join(__dirname, "prompters")).forEach(function(file) { - prompters.push(require(path.join(__dirname, "prompters",file))); -}); - -const sleep = function(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -const easeOutCubic = function(t, b, c, d) { - return c*((t=t/d-1)*t*t+1)+b; -} - -const splash = async function() { - let frames = []; - fs.readdirSync(path.join(__dirname,'splash')).forEach(function(file) { - frames.push(fs.readFileSync(path.join(__dirname,'splash',file))); - }); - - const frame0 = frames[0]; - - const frame0lines = frame0.toString().split('\n'); - const frame0lineCount = frame0lines.length; - const steps = 10; - - process.stdout.write(clear); - - await sleep(150); - - for( let i=0; i<=steps; i++ ) { - const pos = easeOutCubic( i, 0, frame0lineCount, steps ) | 0; - process.stdout.write(reset); - for( let l=frame0lineCount-pos; l { - this.props = Object.assign(this.props, props); - }); - } - - - async configuring() { - if( this.props.gatekeeper_recreatekeys || - this.props.gatekeeper_keys.configEntries.length===0 ) { - const apikey = new ApiKey(); - - let configEntries = []; - let clientInformation = []; - - apikey.setId('001'); - apikey.setGroups(['watcher']); - await apikey.randomiseKey(); - configEntries.push(apikey.getConfigEntry()); - clientInformation.push(apikey.getClientInformation()); - - apikey.setId('002'); - apikey.setGroups(['watcher','spender']); - await apikey.randomiseKey(); - configEntries.push(apikey.getConfigEntry()); - clientInformation.push(apikey.getClientInformation()); - - apikey.setId('003'); - apikey.setGroups(['watcher','spender','admin']); - await apikey.randomiseKey(); - configEntries.push(apikey.getConfigEntry()); - clientInformation.push(apikey.getClientInformation()); - - this.props.gatekeeper_keys = { - configEntries: configEntries, - clientInformation: clientInformation - } - } - - if( this.props.gatekeeper_recreatecert || - !this.props.gatekeeper_sslcert || - !this.props.gatekeeper_sslkey ) { - delete this.props.gatekeeper_recreatecert; - const cert = new Cert(); - console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); - try { - const cns = (this.props.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); - const result = await cert.create(cns); - if( result.code === 0 ) { - this.props.gatekeeper_sslkey = result.key.toString(); - this.props.gatekeeper_sslcert = result.cert.toString(); - - // Total array of cns, used to create Cyphernode's URLs - this.props.cns = [] - result.cns.forEach(e => { - this.props.cns.push(e) - }) - } else { - console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); - } - } catch( err ) { - console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); - } - } - - delete this.props.gatekeeper_recreatekeys; - - } - - async writing() { - const configJsonString = JSON.stringify(this.props, null, 4); - const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); - - if( !await archive.writeEntry( 'config.json', configJsonString ) ) { - console.log(chalk.bold.red( 'error! Config archive was not written' )); - } - - const pathProps = [ - 'gatekeeper_datapath', - 'proxy_datapath', - 'bitcoin_datapath', - 'lightning_datapath', - 'otsclient_datapath' - ]; - - for( let pathProp of pathProps ) { - if( this.props[pathProp] === '_custom' ) { - this.props[pathProp] = this.props[pathProp+'_custom'] || ''; - } - } - - for( let m of prompters ) { - const name = m.name(); - for( let t of m.templates(this.props) ) { - const p = path.join(name,t); - this.fs.copyTpl( - this.templatePath(p), - this.destinationPath(p), - this.props - ); - } - } - - if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { - - if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword && - fs.existsSync(this.destinationPath('client.7z')) ) { - fs.unlinkSync( this.destinationPath('client.7z') ); - } - - const archive = new Archive( this.destinationPath('client.7z'), this.props.gatekeeper_clientkeyspassword ); - if( !await archive.writeEntry( 'keys.txt', this.props.gatekeeper_keys.clientInformation.join('\n') ) ) { - console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); - } - if( !await archive.writeEntry( 'cacert.pem', this.props.gatekeeper_sslcert ) ) { - console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); - } - } - - fs.writeFileSync(path.join('/data', 'exitStatus.sh'), 'EXIT_STATUS=0'); - - - } - - install() { - } - - /* some utils */ - - _assignConfigDefaults() { - this.props = Object.assign( { - features: [], - enablehelp: true, - net: 'testnet', - xpub: '', - derivation_path: '0/n', - installer_mode: 'docker', - devmode: false, - devregistry: false, - run_as_different_user: true, - username: 'cyphernode', - docker_mode: 'compose', - bitcoin_rpcuser: 'bitcoin', - bitcoin_rpcpassword: 'CHANGEME', - bitcoin_uacomment: '', - bitcoin_prune: false, - bitcoin_prune_size: 550, - bitcoin_datapath: '', - bitcoin_node_ip: '', - bitcoin_mode: 'internal', - bitcoin_expose: false, - lightning_expose: true, - gatekeeper_apiproperties: defaultAPIProperties, - gatekeeper_ipwhitelist: '', - gatekeeper_keys: { configEntries: [], clientInformation: [] }, - gatekeeper_sslcert: '', - gatekeeper_sslkey: '', - gatekeeper_cns: process.env['DEFAULT_CERT_HOSTNAME'] || '', - proxy_datapath: '', - lightning_implementation: 'c-lightning', - lightning_datapath: '', - lightning_nodename: name.generate(), - lightning_nodecolor: '', - otsclient_datapath: '', - installer_cleanup: false, - default_username: process.env.DEFAULT_USER || '', - gatekeeper_version: process.env.GATEKEEPER_VERSION || 'latest', - proxy_version: process.env.PROXY_VERSION || 'latest', - proxycron_version: process.env.PROXYCRON_VERSION || 'latest', - pycoin_version: process.env.PYCOIN_VERSION || 'latest', - otsclient_version: process.env.OTSCLIENT_VERSION || 'latest', - bitcoin_version: process.env.BITCOIN_VERSION || 'latest', - lightning_version: process.env.LIGHTNING_VERSION || 'latest' - }, this.props ); - } - - _isChecked( name, value ) { - return this.props && this.props[name] && this.props[name].indexOf(value) != -1 ; - } - - _getDefault( name ) { - return this.props && this.props[name]; - } - - _optional(input,validator) { - if( input === undefined || - input === null || - input === '' ) { - return true; - } - return validator(input); - } - - _ipOrFQDNValidator( host ) { - host = (host+"").trim(); - if( !(validator.isIP(host) || - validator.isFQDN(host)) ) { - throw new Error( 'No IP address or fully qualified domain name' ) - } - return true; - } - - _xkeyValidator( xpub ) { - // TOOD: check for version - if( !coinstring.isValid( xpub ) ) { - throw new Error('Not an extended key.'); - } - return true; - } - - _pathValidator( p ) { - return true; - } - - _derivationPathValidator( path ) { - return true; - } - - _colorValidator(color) { - if( !validator.isHexadecimal(color) ) { - throw new Error('Not a hex color.'); - } - return true; - } - - _lightningNodeNameValidator(name) { - if( !name || name.length > 32 ) { - throw new Error('Please enter anything shorter than 32 characters'); - } - return true; - } - - _notEmptyValidator( path ) { - if( !path ) { - throw new Error('Please enter something'); - } - return true; - } - - _usernameValidator( user ) { - if( !userRegexp.test( user ) ) { - throw new Error('Choose a valid username'); - } - return true; - } - - _UACommentValidator( comment ) { - if( !uaCommentRegexp.test( comment ) ) { - throw new Error('Unsafe characters in UA comment. Please use only a-z, A-Z, 0-9, SPACE and .,:_?@'); - } - return true; - } - - _trimFilter( input ) { - return (input+"").trim(); - } - - _featureChoices() { - return this.featureChoices; - } - - _getHelp( topic ) { - if( !this.props.enablehelp || !this.help ) { - return ''; - } - - const helpText = this.help[topic] || this.help['__default__']; - - if( !helpText ||helpText === '' ) { - return ''; - } - - return "\n\n"+wrap( html2ansi(helpText),82 )+"\n\n"; - } - -}; diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js deleted file mode 100644 index 9bf3321..0000000 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ /dev/null @@ -1,102 +0,0 @@ -const chalk = require('chalk'); - -const name = 'gatekeeper'; - -const capitalise = function( txt ) { - return txt.charAt(0).toUpperCase() + txt.substr(1); -}; - -const prefix = function() { - return chalk.green(capitalise(name)+': '); -}; - -const hasAuthKeys = function( props ) { - return props && - props.gatekeeper_keys && - props.gatekeeper_keys.configEntries && - props.gatekeeper_keys.configEntries.length > 0; -} - -const hasCert = function( props ) { - return props && - props.gatekeeper_sslkey && - props.gatekeeper_sslcert -} - -let password = ''; - -module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - // TODO: delete clientKeys archive when password chnages - return [{ - type: 'password', - name: 'gatekeeper_clientkeyspassword', - default: utils._getDefault( 'gatekeeper_clientkeyspassword' ), - message: prefix()+'Enter a password to protect your client keys with'+utils._getHelp('gatekeeper_clientkeyspassword'), - filter: utils._trimFilter, - validate: utils._notEmptyValidator - }, - { - when: function( props ) { - // hacky hack - password = props.gatekeeper_clientkeyspassword; - return true; - }, - type: 'password', - name: 'gatekeeper_clientkeyspassword_c', - default: utils._getDefault( 'gatekeeper_clientkeyspassword_c' ), - message: prefix()+'Confirm your client keys password.'+utils._getHelp('gatekeeper_clientkeyspassword_c'), - filter: utils._trimFilter, - validate: function( input ) { - if(input !== password) { - throw new Error( 'Client keys passwords do not match' ); - } - return true; - } - }, - { - when: function() { return hasAuthKeys( utils.props ); }, - type: 'confirm', - name: 'gatekeeper_recreatekeys', - default: false, - message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') - }, - { - when: function() { return hasCert( utils.props ); }, - type: 'confirm', - name: 'gatekeeper_recreatecert', - default: false, - message: prefix()+'Recreate gatekeeper certificate?'+utils._getHelp('gatekeeper_recreatecert') - }, - { - when: function(props) { return !hasCert( utils.props ) || props.gatekeeper_recreatecert }, - type: 'input', - name: 'gatekeeper_cns', - default: utils._getDefault( 'gatekeeper_cns' ), - message: prefix()+'Gatekeeper cert CNS (ips, domains, wildcard domains seperated by comma)?'+utils._getHelp('gatekeeper_cns') - }, - { - type: 'confirm', - name: 'gatekeeper_edit_apiproperties', - default: false, - message: prefix()+'Edit API properties?'+utils._getHelp('gatekeeper_edit_apiproperties') - }, - { - when: function( props ) { - const r = props.gatekeeper_edit_apiproperties; - delete props.gatekeeper_edit_apiproperties; - return r; - }, - type: 'editor', - name: 'gatekeeper_apiproperties', - message: utils._getHelp('gatekeeper_apiproperties')||' ', - default: utils._getDefault( 'gatekeeper_apiproperties' ) - }]; - }, - templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem', 'htpasswd' ]; - } -}; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js deleted file mode 100644 index a8e5ea7..0000000 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ /dev/null @@ -1,245 +0,0 @@ -const path = require('path'); -const chalk = require('chalk'); - -const name = 'installer'; - -const capitalise = function( txt ) { - return txt.charAt(0).toUpperCase() + txt.substr(1); -}; - -const prefix = function() { - return chalk.green(capitalise(name)+': '); -}; - -const installerDocker = function(props) { - return props.installer_mode === 'docker' -}; - -module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - return [{ - type: 'list', - name: 'installer_mode', - default: utils._getDefault( 'installer_mode' ), - message: prefix()+chalk.red('Where do you want to install cyphernode?')+utils._getHelp('installer_mode'), - choices: [{ - name: "Docker", - value: "docker" - }] - }, - { - when: installerDocker, - type: 'list', - name: 'gatekeeper_datapath', - default: utils._getDefault( 'gatekeeper_datapath' ), - choices: [ - { - name: "/var/run/cyphernode/gatekeeper (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/gatekeeper" - }, - { - name: "~/.cyphernode/gatekeeper", - value: "~/.cyphernode/gatekeeper" - }, - { - name: "~/gatekeeper", - value: "~/gatekeeper" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), - }, - { - when: (props)=>{ return installerDocker(props) && (props.gatekeeper_datapath === '_custom') }, - type: 'input', - name: 'gatekeeper_datapath_custom', - default: utils._getDefault( 'gatekeeper_datapath_custom' ), - filter: utils._trimFilter, - validate: utils._pathValidator, - message: prefix()+'Custom path for gatekeeper data?'+utils._getHelp('gatekeeper_datapath_custom'), - }, - { - when: installerDocker, - type: 'list', - name: 'proxy_datapath', - default: utils._getDefault( 'proxy_datapath' ), - choices: [ - { - name: "/var/run/cyphernode/proxy (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/proxy" - }, - { - name: "~/.cyphernode/proxy", - value: "~/.cyphernode/proxy" - }, - { - name: "~/proxy", - value: "~/proxy" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your proxy data?'+utils._getHelp('proxy_datapath'), - }, - { - when: (props)=>{ return installerDocker(props) && (props.proxy_datapath === '_custom') }, - type: 'input', - name: 'proxy_datapath_custom', - default: utils._getDefault( 'proxy_datapath_custom' ), - filter: utils._trimFilter, - validate: utils._pathValidator, - message: prefix()+'Custom path for your proxy data?'+utils._getHelp('proxy_datapath_custom'), - }, - { - when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, - type: 'list', - name: 'bitcoin_datapath', - default: utils._getDefault( 'bitcoin_datapath' ), - choices: [ - { - name: "/var/run/cyphernode/bitcoin (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/bitcoin" - }, - { - name: "~/.cyphernode/bitcoin", - value: "~/.cyphernode/bitcoin" - }, - { - name: "~/bitcoin", - value: "~/bitcoin" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your bitcoin full node data?'+utils._getHelp('bitcoin_datapath'), - }, - { - when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' && props.bitcoin_datapath === '_custom' }, - type: 'input', - name: 'bitcoin_datapath_custom', - default: utils._getDefault( 'bitcoin_datapath_custom' ), - filter: utils._trimFilter, - validate: utils._pathValidator, - message: prefix()+'Custom path for your bitcoin full node data?'+utils._getHelp('bitcoin_datapath_custom'), - }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, - type: 'list', - name: 'lightning_datapath', - default: utils._getDefault( 'lightning_datapath' ), - choices: [ - { - name: "/var/run/cyphernode/lightning (needs sudo - "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/lightning" - }, - { - name: "~/.cyphernode/lightning", - value: "~/.cyphernode/lightning" - }, - { - name: "~/lightning", - value: "~/lightning" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your lightning node data?'+utils._getHelp('lightning_datapath'), - }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 && props.lightning_datapath === '_custom'}, - type: 'input', - name: 'lightning_datapath_custom', - default: utils._getDefault( 'lightning_datapath_custom' ), - filter: utils._trimFilter, - validate: utils._pathValidator, - message: prefix()+'Custom path for your lightning node data?'+utils._getHelp('lightning_datapath_custom'), - }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 }, - type: 'list', - name: 'otsclient_datapath', - default: utils._getDefault( 'otsclient_datapath' ), - choices: [ - { - name: "/var/run/cyphernode/otsclient (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/otsclient" - }, - { - name: "~/.cyphernode/otsclient", - value: "~/.cyphernode/otsclient" - }, - { - name: "~/otsclient", - value: "~/otsclient" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your OTS data?'+utils._getHelp('otsclient_datapath'), - }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 && props.otsclient_datapath === '_custom' }, - type: 'input', - name: 'otsclient_datapath_custom', - default: utils._getDefault( 'otsclient_datapath_custom' ), - filter: utils._trimFilter, - validate: utils._pathValidator, - message: prefix()+'Where is your otsclient data?'+utils._getHelp('otsclient_datapath_custom'), - }, - { - when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, - type: 'confirm', - name: 'bitcoin_expose', - default: utils._getDefault( 'bitcoin_expose' ), - message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils._getHelp('bitcoin_expose'), - }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, - type: 'confirm', - name: 'lightning_expose', - default: utils._getDefault( 'lightning_expose' ), - message: prefix()+'Expose lightning node outside of the docker network?'+utils._getHelp('lightning_expose'), - }, - { - when: installerDocker, - type: 'list', - name: 'docker_mode', - default: utils._getDefault( 'docker_mode' ), - message: prefix()+'What docker mode: docker swarm or docker-compose?'+utils._getHelp('docker_mode'), - choices: [{ - name: "docker swarm", - value: "swarm" - }, - { - name: "docker-compose", - value: "compose" - }] - }, - { - type: 'confirm', - name: 'installer_cleanup', - default: utils._getDefault( 'installer_cleanup' ), - message: prefix()+'Cleanup installer after installation?'+utils._getHelp('installer_cleanup'), - }]; - }, - templates: function( props ) { - if( props.installer_mode === 'docker' ) { - return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh', path.join('docker', 'docker-compose.yaml')]; - } - return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh']; - } -}; diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties b/install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties deleted file mode 100644 index aa952a1..0000000 --- a/install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties +++ /dev/null @@ -1,6 +0,0 @@ - -# Watcher can do stuff -# Spender can do what the watcher can do plus more stuff -# Admin can do what the spender can do plus even more stuff - -<%- gatekeeper_apiproperties %> diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd deleted file mode 100644 index 7cf9383..0000000 --- a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd +++ /dev/null @@ -1 +0,0 @@ -admin:<%- gatekeeper_statuspw %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml deleted file mode 100644 index eba5633..0000000 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ /dev/null @@ -1,150 +0,0 @@ -version: "3" - -services: - gatekeeper: - # HTTP authentication API gate - environment: - - "TRACING=1" - image: cyphernode/gatekeeper:<%= gatekeeper_version %> - ports: - - "443:443" - volumes: - - "<%= gatekeeper_datapath %>/certs:/etc/ssl/certs" - - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" - - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties" - - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" - - "<%= gatekeeper_datapath %>/htpasswd:/etc/nginx/conf.d/status/htpasswd" - - "<%= gatekeeper_datapath %>/installation.json:/etc/nginx/conf.d/status/installation.json" - - "<%= gatekeeper_datapath %>/client.7z:/etc/nginx/conf.d/status/client.7z" - - "<%= gatekeeper_datapath %>/config.7z:/etc/nginx/conf.d/status/config.7z" - command: $USER - -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always - proxy: - command: $USER ./startproxy.sh - # Bitcoin Mini Proxy - environment: - - "TRACING=1" - - "WATCHER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat" - - "WATCHER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - - "WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties" - - "SPENDER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat" - - "SPENDER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" - - "PROXY_LISTENING_PORT=8888" - - "DB_PATH=/proxy/db" - - "DB_FILE=/proxy/db/proxydb" - - "PYCOIN_CONTAINER=pycoin:7777" -<% if ( use_xpub && xpub ) { %> - - "DERIVATION_PUB32=<%= xpub %>" - - "DERIVATION_PATH=<%= derivation_path %>" -<% } %> - - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - - "OTSCLIENT_CONTAINER=otsclient:6666" - - "OTS_FILES=/proxy/otsfiles" - image: cyphernode/proxy:<%= proxy_version %> -<% if ( devmode ) { %> - ports: - - "8888:8888" -<% } %> - volumes: - - "<%= proxy_datapath %>:/proxy/db" - <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> - - "<%= lightning_datapath %>:/.lightning" - <% } %> - <% if ( features.indexOf('otsclient') !== -1 ) { %> - - "<%= otsclient_datapath %>:/proxy/otsfiles" - <% } %> - -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always - proxycron: - environment: - - "TX_CONF_URL=proxy:8888/executecallbacks" - - "OTS_URL=proxy:8888/ots_backoffice" - image: cyphernode/proxycron:<%= proxycron_version %> -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always - pycoin: - # Pycoin - command: $USER ./startpycoin.sh - image: cyphernode/pycoin:<%= pycoin_version %> - environment: - - "TRACING=1" - - "PYCOIN_LISTENING_PORT=7777" -<% if ( devmode ) { %> - ports: - - "7777:7777" -<% } %> -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always -<% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> - lightning: - command: $USER lightningd - image: cyphernode/clightning:<%= lightning_version %> - - <% if( lightning_expose ) { %> - ports: - - "9735:9735" - <% } %> - volumes: - - "<%= lightning_datapath%>:/.lightning" - - "<%= lightning_datapath%>/bitcoin.conf:/.bitcoin/bitcoin.conf" -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - restart: always -<% } %> -<% if ( features.indexOf('otsclient') !== -1 ) { %> - otsclient: - environment: - - "TRACING=1" - - "OTSCLIENT_LISTENING_PORT=6666" - image: cyphernode/otsclient:<%= otsclient_version %> -# deploy: -# placement: -# constraints: [node.hostname==dev] - volumes: - - "<%= otsclient_datapath%>:/otsfiles" - command: $USER /script/startotsclient.sh - networks: - - cyphernodenet - restart: always -<% } %> - -<% if( bitcoin_mode === 'internal' ) { %> - bitcoin: - command: $USER bitcoind - image: cyphernode/bitcoin:<%= bitcoin_version %> -<% if( bitcoin_expose ) { %> - ports: - - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" -<% } %> - volumes: - - "<%= bitcoin_datapath%>:/.bitcoin" - networks: - - cyphernodenet - restart: always -<% } %> -networks: - cyphernodenet: - external: true diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh deleted file mode 100644 index 7fc1279..0000000 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" - -<% if (docker_mode == 'swarm') { %> -export USER=$(id -u):$(id -g) -export ARCH=$(uname -m) -docker stack rm cyphernode -<% } else if(docker_mode == 'compose') { %> -export USER=$(id -u):$(id -g) -export ARCH=$(uname -m) -docker-compose -f $current_path/docker-compose.yaml down -<% } %> diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json deleted file mode 100644 index 668451c..0000000 --- a/install/generator-cyphernode/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "generator-cyphernode", - "version": "0.0.0", - "description": "", - "homepage": "", - "author": { - "name": "jash", - "email": "jash@schulterklopfer-productions.de", - "url": "" - }, - "files": [ - "generators" - ], - "main": "generators/index.js", - "keywords": [ - "cyphernode", - "yeoman-generator" - ], - "engines": { - "npm": ">= 4.0.0" - }, - "dependencies": { - "@rauschma/stringio": "^1.4.0", - "chalk": "^2.1.0", - "coinstring": "^2.3.0", - "parse5": "^5.1.0", - "validator": "^10.8.0", - "wrap-ansi": "^4.0.0", - "yeoman-environment": "2.3.3", - "yeoman-generator": "2.0.5" - }, - "repository": "git@github.com:schulterklopfer/cyphernode.git", - "license": "MIT" -} diff --git a/notifier_docker/Dockerfile b/notifier_docker/Dockerfile new file mode 100644 index 0000000..3da0f2b --- /dev/null +++ b/notifier_docker/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-mosquitto:1.6 + +ENV HOME /notifier + +RUN apk --no-cache --update add jq curl su-exec + +WORKDIR ${HOME} + +COPY script/* ./ + +RUN chmod +x startnotifier.sh requesthandler.sh \ + && chmod o+w . + +ENTRYPOINT ["su-exec"] + +# docker run --rm -d -p 1883:1883 -p 9001:9001 --network cyphernodenet --name broker eclipse-mosquitto +# docker run --rm -it --network cyphernodenet --name mq1 mqtt-client diff --git a/notifier_docker/script/requesthandler.sh b/notifier_docker/script/requesthandler.sh new file mode 100644 index 0000000..f5888c8 --- /dev/null +++ b/notifier_docker/script/requesthandler.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +. ./trace.sh +. ./web.sh +. ./response.sh + +main() { + trace "Entering main()..." + + local msg + local cmd + local response + local response_topic + + # Messages should have this form: + # {"response-topic":"response/5541","cmd":"web","url":"2557df870b9a:1111/callback1conf","body":"eyJpZCI6IjUxIiwiYWRkc...dCI6MTUxNzYwMH0K"} + while read msg; do + trace "[main] New msg just arrived!" + trace "[main] msg=${msg}" + + cmd=$(echo ${msg} | jq -r ".cmd") + trace "[main] cmd=${cmd}" + + response_topic=$(echo ${msg} | jq -r '."response-topic"') + trace "[main] response_topic=${response_topic}" + + case "${cmd}" in + web) + response=$(web "${msg}") + publish_response "${response}" "${response_topic}" ${?} + ;; + esac + trace "[main] msg processed" + done +} + +export TRACING=1 + +main +trace "[requesthandler] exiting" +exit $? diff --git a/notifier_docker/script/response.sh b/notifier_docker/script/response.sh new file mode 100644 index 0000000..288687d --- /dev/null +++ b/notifier_docker/script/response.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +. ./trace.sh + +publish_response() { + trace "Entering publish_response()..." + + local response=${1} + local response_topic=${2} + local returncode=${3} + + trace "[publish_response] response=${response}" + trace "[publish_response] response_topic=${response_topic}" + trace "[publish_response] returncode=${returncode}" + + trace "[publish_response] mosquitto_pub -h broker -t \"${response_topic}\" -m \"${response}\"" + mosquitto_pub -h broker -t "${response_topic}" -m "${response}" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} diff --git a/notifier_docker/script/startnotifier.sh b/notifier_docker/script/startnotifier.sh new file mode 100644 index 0000000..64bc336 --- /dev/null +++ b/notifier_docker/script/startnotifier.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./trace.sh + +mosquitto_sub -h broker -t notifier | ./requesthandler.sh diff --git a/notifier_docker/script/trace.sh b/notifier_docker/script/trace.sh new file mode 100644 index 0000000..4c0a1c2 --- /dev/null +++ b/notifier_docker/script/trace.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "$(date -Is) $$ ${1}" 1>&2 + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 + fi +} diff --git a/notifier_docker/script/web.sh b/notifier_docker/script/web.sh new file mode 100644 index 0000000..10b0ccd --- /dev/null +++ b/notifier_docker/script/web.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +. ./trace.sh + +web() { + trace "Entering web()..." + + local msg=${1} + local url + local body + local returncode + local response + local result + + trace "[web] msg=${msg}" + url=$(echo ${msg} | jq ".url") + trace "[web] url=${url}" + + body=$(echo ${msg} | jq -e ".body") + # jq -e will have a return code of 1 if the supplied tag is null. + if [ "$?" -eq "0" ]; then + # body tag not null, so it's a POST + # The body field has been based-64 to avoid dealing with escaping special chars + body=$(echo "${body}" | base64 -d) + trace "[web] body=${body}" + else + body= + trace "[web] no body, GET request" + fi + + response=$(curl_it "${url}" "${body}") + returncode=$? + trace_rc ${returncode} + + echo "${response}" + + return ${returncode} +} + +curl_it() { + trace "Entering curl_it()..." + + local url=$(echo "${1}" | tr -d '"') + local data=${2} + local returncode + local response + local rnd=$(dd if=/dev/urandom bs=5 count=1 | xxd -pc 5) + + if [ -n "${data}" ]; then + trace "[curl_it] curl -o webresponse-${rnd} -m 20 -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" -k ${url}" + rc=$(curl -o webresponse-${rnd} -m 20 -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" -k ${url}) + returncode=$? + else + trace "[curl_it] curl -o webresponse-$$ -m 20 -w \"%{http_code}\" -k ${url}" + rc=$(curl -o webresponse-${rnd} -m 20 -w "%{http_code}" -k ${url}) + returncode=$? + fi + trace "[curl_it] HTTP return code=${rc}" + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + response=$(cat webresponse-${rnd} | base64 | tr -d '\n' ; rm webresponse-${rnd}) + else + response= + fi + # When curl is unable to connect, http_code is "000" which is not a valid JSON number + [ "${rc}" -eq "0" ] && rc=0 + response="{\"curl_code\":${returncode},\"http_code\":${rc},\"body\":\"${response}\"}" + + echo "${response}" + + if [ "${returncode}" -eq "0" ]; then + if [ "${rc}" -lt "400" ]; then + return 0 + else + return ${rc} + fi + else + return ${returncode} + fi +} diff --git a/otsclient_docker/Dockerfile b/otsclient_docker/Dockerfile index 1ae1779..417b397 100644 --- a/otsclient_docker/Dockerfile +++ b/otsclient_docker/Dockerfile @@ -4,7 +4,12 @@ RUN apk add --update --no-cache \ git \ jq \ su-exec \ - && yarn global add javascript-opentimestamps + && git clone https://github.com/opentimestamps/javascript-opentimestamps.git \ + && cd javascript-opentimestamps \ +# Handle 'Error: could not get uid/gid' +# See: https://github.com/npm/uid-number/issues/3 + && npm config set unsafe-perm true \ + && npm install -g WORKDIR /script diff --git a/otsclient_docker/script/otsclient.sh b/otsclient_docker/script/otsclient.sh index 1861b6e..e572bff 100644 --- a/otsclient_docker/script/otsclient.sh +++ b/otsclient_docker/script/otsclient.sh @@ -1,9 +1,10 @@ #!/bin/sh +# There an OTS server on testnet: ots.test4mind.com + . ./trace.sh -stamp() -{ +stamp() { trace "Entering stamp()..." local hash=${1} @@ -13,9 +14,15 @@ stamp() local returncode local data - trace "[stamp] ots-cli.js stamp -d ${hash}" - result=$(cd /otsfiles && ots-cli.js stamp -d ${hash} 2>&1) - returncode=$? + if [ "${TESTNET}" -eq "1" ]; then + trace "[stamp] ots-cli.js stamp -c \"https://ots.testnet.kexkey.com\" -d ${hash}" + result=$(cd /otsfiles && ots-cli.js stamp -c "https://ots.testnet.kexkey.com" -d ${hash} 2>&1) + returncode=$? + else + trace "[stamp] ots-cli.js stamp -d ${hash}" + result=$(cd /otsfiles && ots-cli.js stamp -d ${hash} 2>&1) + returncode=$? + fi trace_rc ${returncode} trace "[stamp] result=${result}" @@ -32,7 +39,7 @@ stamp() # String found data="${data}success\"}" else - # String nor found + # String not found data="${data}error\",\"error\":\"${result}\"}" fi @@ -43,8 +50,7 @@ stamp() return ${returncode} } -upgrade() -{ +upgrade() { trace "Entering upgrade()..." local hash=${1} @@ -53,9 +59,15 @@ upgrade() local result local returncode - trace "[upgrade] ots-cli.js upgrade ${hash}.ots" - result=$(cd /otsfiles && ots-cli.js upgrade ${hash}.ots 2>&1) - returncode=$? + if [ "${TESTNET}" -eq "1" ]; then + trace "[upgrade] ots-cli.js -l \"https://testnet.calendar.kexkey.com/\" --no-default-whitelist upgrade -c \"https://testnet.calendar.kexkey.com/\" ${hash}.ots" + result=$(cd /otsfiles && ots-cli.js -l "https://testnet.calendar.kexkey.com/" --no-default-whitelist upgrade -c "https://testnet.calendar.kexkey.com/" ${hash}.ots 2>&1) + returncode=$? + else + trace "[upgrade] ots-cli.js upgrade ${hash}.ots" + result=$(cd /otsfiles && ots-cli.js upgrade ${hash}.ots 2>&1) + returncode=$? + fi trace_rc ${returncode} trace "[upgrade] result=${result}" @@ -81,3 +93,166 @@ upgrade() return ${returncode} } + +verify() { + trace "Entering verify()..." + + local request=${1} + local hash=$(echo "${request}" | jq ".hash" | tr -d '"') + trace "[verify] hash=${hash}" + local base64otsfile=$(echo "${request}" | jq ".base64otsfile" | tr -d '"') + trace "[verify] base64otsfile=${base64otsfile}" + + local result + local returncode + local message + local data + + # Let's create the OTS file locally from the base64 + trace "[verify] Creating /otsfiles/otsfile-$$.ots" + echo "${base64otsfile}" | base64 -d > /otsfiles/otsfile-$$.ots + + if [ "${TESTNET}" -eq "1" ]; then + trace "[verify] ots-cli.js -l \"https://testnet.calendar.kexkey.com/\" --no-default-whitelist verify -d ${hash} /otsfiles/otsfile-$$.ots" + result=$(ots-cli.js -l "https://testnet.calendar.kexkey.com/" --no-default-whitelist verify -d ${hash} /otsfiles/otsfile-$$.ots 2>&1) + returncode=$? + else + trace "[verify] ots-cli.js verify -d ${hash} /otsfiles/otsfile-$$.ots" + result=$(ots-cli.js verify -d ${hash} /otsfiles/otsfile-$$.ots 2>&1) + returncode=$? + fi + trace_rc ${returncode} + trace "[verify] result=${result}" + + trace "[verify] Removing temporary file /otsfiles/otsfile-$$.ots..." + rm /otsfiles/otsfile-$$.ots + + # /script $ ots-cli.js -v v -d 7d694f669d6da235a5fb9ef8c89da55e30b59eb662a7131f85344d798fc3280c /otsfiles/Order_10019_1543447088465.ots + # Assuming target hash is '7d694f669d6da235a5fb9ef8c89da55e30b59eb662a7131f85344d798fc3280c' + # Success! Bitcoin block 551876 attests existence as of 2018-11-28 GMT + + # /script $ ots-cli.js -v v -d 7d694f669d6da235a5fb9ef8c89da55e30b59eb662a7131f85344d798fc3280c /otsfiles/Order_10019_1543447088465_incomplete.ots + # Assuming target hash is '7d694f669d6da235a5fb9ef8c89da55e30b59eb662a7131f85344d798fc3280c' + # Got 1 attestation(s) from https://bob.btc.calendar.opentimestamps.org + # Got 1 attestation(s) from https://finney.calendar.eternitywall.com + # Got 1 attestation(s) from https://alice.btc.calendar.opentimestamps.org + # Got 1 attestation(s) from https://btc.calendar.catallaxy.com + # Success! Bitcoin block 551876 attests existence as of 2018-11-28 GMT + + # /script # ots-cli.js -v v -d 3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d531 aaa + # Assuming target hash is '3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d531' + # Calendar https://btc.calendar.catallaxy.com: Pending confirmation in Bitcoin blockchain + # Calendar https://alice.btc.calendar.opentimestamps.org: Pending confirmation in Bitcoin blockchain + # Calendar https://bob.btc.calendar.opentimestamps.org: Pending confirmation in Bitcoin blockchain + # Calendar https://finney.calendar.eternitywall.com: Pending confirmation in Bitcoin blockchain + + # /script # ots-cli.js -v v -d 3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d531 allo + # Assuming target hash is '3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d531' + # Error! allo is not a timestamp file. + + # /script # ots-cli.js -v v -d 3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d53 aaa + # Assuming target hash is '3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d53' + # Expected digest 3eb3df18d9f8ee502c77f3b231818988c2c1cd44baa39e663d14708a7053d531 + # File does not match original! + # File does not match original! + + # Let's send one of those possible outcomes: + # - If last line begins with "Success!": Success + block height + # - If "Pending confirmation in Bitcoin blockchain" found: Pending + # - Otherwise: Error + + # - Error! allo is not a timestamp file. + # - File does not match original! + # - Whatever the last line output is + + data="{\"method\":\"verify\",\"hash\":\"${hash}\",\"result\":\"" + + trace "[verify] grepping..." + echo "${result}" | grep "Success!" > /dev/null + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + # "Success!" found + data="${data}success" + else + # "Success!" not found + echo "${result}" | grep "Pending" > /dev/null + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + # "Pending" found + data="${data}pending" + else + # "Pending" not found + data="${data}error" + fi + fi + + data="${data}\",\"message\":\"${result}\"}" + + trace "[verify] data=${data}" + + echo "${data}" + + return ${returncode} +} + +info() { + trace "Entering info()..." + + local request=${1} + local base64otsfile=$(echo "${request}" | jq ".base64otsfile" | tr -d '"') + trace "[info] base64otsfile=${base64otsfile}" + + local result + local returncode + local message + local data + + # Let's create the OTS file locally from the base64 + trace "[info] Creating /otsfiles/otsfile-$$.ots" + echo "${base64otsfile}" | base64 -d > /otsfiles/otsfile-$$.ots + + trace "[info] ots-cli.js info /otsfiles/otsfile-$$.ots" + result=$(ots-cli.js info /otsfiles/otsfile-$$.ots 2>&1 | base64 | tr -d '\n') + returncode=$? + trace_rc ${returncode} + trace "[info] result=${result}" + + trace "[info] Removing temporary file /otsfiles/otsfile-$$.ots..." + rm /otsfiles/otsfile-$$.ots + + # /otsfiles # ots-cli.js info a2d4ff9c70b7b884e04e04c184a7bf8a07dca029a68efa4d0477cea0c6f8ac2b.ots + # File sha256 hash: a2d4ff9c70b7b884e04e04c184a7bf8a07dca029a68efa4d0477cea0c6f8ac2b + # Timestamp: + # append 0736f76dfd242f5156321c561d11ef47 + # sha256 + # -> append 4820230d20f302a17a45f0de0e3e23a6 + # sha256 + # prepend 5d5da8e6 + # append 8b6d6af19f6ac839 + # verify PendingAttestation('https://alice.btc.calendar.opentimestamps.org') + # -> append 9c5e80c7251b313b180acc6e2341d9de + # sha256 + # prepend 5d5da8e6 + # append 59d56c4ad5d8d6e4 + # verify PendingAttestation('https://bob.btc.calendar.opentimestamps.org') + # -> append a437fa964b029950dc8f507de448cd08 + # sha256 + # prepend 5d5da8e6 + # append d25542b20883d479 + # verify PendingAttestation('https://finney.calendar.eternitywall.com') + # -> append a34c1ae4a38e776450a643d298abf428 + # sha256 + # prepend 5d5da8e7 + # append 60ed070138239971 + # verify PendingAttestation('https://btc.calendar.catallaxy.com') + + data="{\"method\":\"info\",\"result\":\"${result}\"}" + trace "[info] data=${data}" + + echo "${data}" + + return ${returncode} +} diff --git a/otsclient_docker/script/requesthandler.sh b/otsclient_docker/script/requesthandler.sh index 7ad8d08..6e9cb5c 100644 --- a/otsclient_docker/script/requesthandler.sh +++ b/otsclient_docker/script/requesthandler.sh @@ -10,76 +10,92 @@ main() { - trace "Entering main()..." + trace "Entering main()..." - local step=0 - local cmd - local http_method - local line - local content_length - local response - local returncode + local step=0 + local cmd + local http_method + local line + local content_length + local response + local returncode - while read line; do - line=$(echo "${line}" | tr -d '\r\n') - trace "[main] line=${line}" + while read line; do + line=$(echo "${line}" | tr -d '\r\n') + trace "[main] line=${line}" - if [ "${cmd}" = "" ]; then - # First line! - # Looking for something like: - # GET /cmd/params HTTP/1.1 - # POST / HTTP/1.1 - cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1) - trace "[main] cmd=${cmd}" - http_method=$(echo "${line}" | cut -d ' ' -f1) - trace "[main] http_method=${http_method}" - if [ "${http_method}" = "GET" ]; then - step=1 - fi - fi - if [ "${line}" = "" ]; then - trace "[main] empty line" - if [ ${step} -eq 1 ]; then - trace "[main] body part finished, disconnecting" - break - else - trace "[main] headers part finished, body incoming" - step=1 - fi - fi - # line=content-length: 406 - case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) - content_length=$(echo ${line} | cut -d ':' -f2) - trace "[main] content_length=${content_length}"; - ;; - esac - if [ ${step} -eq 1 ]; then - trace "[main] step=${step}" - if [ "${http_method}" = "POST" ]; then - read -n ${content_length} line - trace "[main] line=${line}" - fi - case "${cmd}" in - stamp) - # GET http://192.168.111.152:8080/stamp/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + if [ "${cmd}" = "" ]; then + # First line! + # Looking for something like: + # GET /cmd/params HTTP/1.1 + # POST / HTTP/1.1 + cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1) + trace "[main] cmd=${cmd}" + http_method=$(echo "${line}" | cut -d ' ' -f1) + trace "[main] http_method=${http_method}" + if [ "${http_method}" = "GET" ]; then + step=1 + fi + fi + if [ "${line}" = "" ]; then + trace "[main] empty line" + if [ ${step} -eq 1 ]; then + trace "[main] body part finished, disconnecting" + break + else + trace "[main] headers part finished, body incoming" + step=1 + fi + fi + # line=content-length: 406 + case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) + content_length=$(echo "${line}" | cut -d ':' -f2) + trace "[main] content_length=${content_length}"; + ;; + esac + if [ ${step} -eq 1 ]; then + trace "[main] step=${step}" + if [ "${http_method}" = "POST" ]; then + read -n ${content_length} line + trace "[main] line=${line}" + fi + case "${cmd}" in + stamp) + # GET http://192.168.111.152:8080/stamp/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 - response=$(stamp $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) - response_to_client "${response}" ${?} - break - ;; - upgrade) - # GET http://192.168.111.152:8080/upgrade/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + response=$(stamp $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + upgrade) + # GET http://192.168.111.152:8080/upgrade/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 - response=$(upgrade $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) - response_to_client "${response}" ${?} - break - ;; - esac - break - fi - done - trace "[main] exiting" - return 0 + response=$(upgrade $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + verify) + # POST http://192.168.111.152:8080/verify + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + + response=$(verify "${line}") + response_to_client "${response}" ${?} + break + ;; + info) + # POST http://192.168.111.152:8080/info + # BODY {"base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + + response=$(info "${line}") + response_to_client "${response}" ${?} + break + ;; + esac + break + fi + done + trace "[main] exiting" + return 0 } export TRACING diff --git a/otsclient_docker/script/responsetoclient.sh b/otsclient_docker/script/responsetoclient.sh index c907e76..ee2ab28 100644 --- a/otsclient_docker/script/responsetoclient.sh +++ b/otsclient_docker/script/responsetoclient.sh @@ -2,20 +2,23 @@ . ./trace.sh -response_to_client() -{ - trace "Entering response_to_client()..." +response_to_client() { + trace "Entering response_to_client()..." - local response=${1} - local returncode=${2} + local response=${1} + local returncode=${2} + local contenttype=${3} + local length=$(echo -en "${response}" | wc -c) - ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" - [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" + [ -z "${contenttype}" ] && contenttype="application/json" - echo -e "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" + ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" + [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" - # Small delay needed for the data to be processed correctly by peer - sleep 0.2s + echo -en "Content-Type: ${contenttype}\r\nContent-Length: ${length}\r\n\r\n${response}" + + # Small delay needed for the data to be processed correctly by peer + sleep 1 } case "${0}" in *responsetoclient.sh) response_to_client $@;; esac diff --git a/otsclient_docker/script/trace.sh b/otsclient_docker/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/otsclient_docker/script/trace.sh +++ b/otsclient_docker/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi } diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile new file mode 100644 index 0000000..5fd3dbf --- /dev/null +++ b/proxy_docker/Dockerfile @@ -0,0 +1,25 @@ +FROM cyphernode/alpine-glibc-base:3.8 + +ENV HOME /proxy + +RUN apk add --update --no-cache \ + sqlite \ + jq \ + curl \ + su-exec + +WORKDIR ${HOME} + +COPY app/data/* ./ +COPY app/script/* ./ +COPY --from=cyphernode/clightning:v0.7.1 /usr/local/bin/lightning-cli ./ +COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_rr /usr/bin/mosquitto_sub /usr/bin/mosquitto_pub /usr/bin/ +COPY --from=eclipse-mosquitto:1.6 /usr/lib/libmosquitto* /usr/lib/ + +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh tests* \ + && chmod o+w . \ + && mkdir db + +VOLUME ["${HOME}/db", "/.lightning"] + +ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 deleted file mode 100644 index ff124f1..0000000 --- a/proxy_docker/Dockerfile.amd64 +++ /dev/null @@ -1,39 +0,0 @@ -FROM alpine:3.8 - -# Taking care of glibc shit (glibc not natively supported by Alpine but lightning-cli uses it) - -ENV GLIBC_VERSION 2.27-r0 -ENV GLIBC_SHA256 938bceae3b83c53e7fa9cc4135ce45e04aae99256c5e74cf186c794b97473bc7 -ENV GLIBCBIN_SHA256 3a87874e57b9d92e223f3e90356aaea994af67fb76b71bb72abfb809e948d0d6 -# Download and install glibc (https://github.com/jeanblanchard/docker-alpine-glibc/blob/master/Dockerfile) -RUN wget -O /etc/apk/keys/sgerrand.rsa.pub https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$GLIBC_VERSION/sgerrand.rsa.pub \ - && wget -O glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" \ - && echo "$GLIBC_SHA256 glibc.apk" | sha256sum -c - \ - && wget -O glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" \ - && echo "$GLIBCBIN_SHA256 glibc-bin.apk" | sha256sum -c - \ - && apk add --update --no-cache glibc-bin.apk glibc.apk \ - && /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib \ - && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \ - && rm -rf glibc.apk glibc-bin.apk - -ENV HOME /proxy - -RUN apk add --update --no-cache \ - sqlite \ - jq \ - curl \ - su-exec - -WORKDIR ${HOME} - -COPY app/data/* ./ -COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ./ - -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ - && chmod o+w . \ - && mkdir db - -VOLUME ["${HOME}/db", "/.lightning"] - -ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 deleted file mode 100644 index ed8d44e..0000000 --- a/proxy_docker/Dockerfile.arm32v6 +++ /dev/null @@ -1,35 +0,0 @@ -FROM alpine:3.8 - -# Taking care of glibc shit (glibc not natively supported by Alpine but lightning-cli uses it) - -ENV GLIBC_VERSION 2.27-r0 -# Download and install glibc (https://github.com/jeanblanchard/docker-alpine-glibc/blob/master/Dockerfile) -RUN apk add --update --no-cache wget \ - && wget -O glibc.apk "https://github.com/yangxuan8282/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" \ - && wget -O glibc-bin.apk "https://github.com/yangxuan8282/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" \ - && apk add --allow-untrusted --update --no-cache glibc-bin.apk glibc.apk \ - && /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib \ - && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \ - && rm -rf glibc.apk glibc-bin.apk - -ENV HOME /proxy - -RUN apk add --update --no-cache \ - sqlite \ - jq \ - curl \ - su-exec - -WORKDIR ${HOME} - -COPY app/data/* ./ -COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ./ - -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ - && chmod o+w . \ - && mkdir db - -VOLUME ["${HOME}/db", "/.lightning"] - -ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/README.md b/proxy_docker/README.md index f833882..60e41c2 100644 --- a/proxy_docker/README.md +++ b/proxy_docker/README.md @@ -24,40 +24,28 @@ docker run --rm -d -p 8888:8888 --network cyphernodenet --env-file env.propertie ```properties TRACING=1 -WATCHER_BTC_NODE_RPC_URL=btcnode:18332/wallet/watching01.dat -WATCHER_BTC_NODE_RPC_USER=rpc_username:rpc_password -WATCHER_BTC_NODE_RPC_CFG=/proxy/watcher_btcnode_curlcfg.properties -SPENDER_BTC_NODE_RPC_URL=btcnode:18332/wallet/spending01.dat -SPENDER_BTC_NODE_RPC_USER=rpc_username:rpc_password -SPENDER_BTC_NODE_RPC_CFG=/proxy/spender_btcnode_curlcfg.properties +WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet +WATCHER_BTC_NODE_DEFAULT_WALLET=watching01.dat +WATCHER_BTC_NODE_XPUB_WALLET=xpubwatching01.dat +WATCHER_BTC_NODE_RPC_USER=bitcoin:CHANGEME +WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties +SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet +SPENDER_BTC_NODE_DEFAULT_WALLET=spending01.dat +SPENDER_BTC_NODE_RPC_USER=bitcoin:CHANGEME +SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties PROXY_LISTENING_PORT=8888 # Variable substitutions don't work DB_PATH=/proxy/db DB_FILE=/proxy/db/proxydb # Pycoin container -PYCOIN_CONTAINER=pycoinnode:7777 -# OTS container -OTS_CONTAINER=otsnode:6666 - +PYCOIN_CONTAINER=pycoin:7777 DERIVATION_PUB32=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb DERIVATION_PATH=0/n WATCHER_BTC_NODE_PRUNED=false -``` - -## Choose the right architecture - -...by modifying the following line in Dockerfile: - -```shell -COPY app/bin/lightning-cli_x86 ${HOME}/lightning-cli -``` - -...to lightning-cli_arm if running on a RPi. - -## Building docker image - -```shell -docker build -t btcproxyimg . +# OTS container +OTSCLIENT_CONTAINER=otsclient:6666 +OTS_FILES=/proxy/otsfiles +XPUB_DERIVATION_GAP=100 ``` ## Create sqlite3 database path and give rights diff --git a/proxy_docker/app/data/cyphernode.sql b/proxy_docker/app/data/cyphernode.sql index 48bb1ad..97aeef0 100644 --- a/proxy_docker/app/data/cyphernode.sql +++ b/proxy_docker/app/data/cyphernode.sql @@ -1,17 +1,30 @@ PRAGMA foreign_keys = ON; +CREATE TABLE watching_by_pub32 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pub32 TEXT UNIQUE, + label TEXT UNIQUE, + derivation_path TEXT, + callback0conf TEXT, + callback1conf TEXT, + last_imported_n INTEGER, + watching INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); + CREATE TABLE watching ( id INTEGER PRIMARY KEY AUTOINCREMENT, - address TEXT, + address TEXT UNIQUE, watching INTEGER DEFAULT FALSE, callback0conf TEXT, calledback0conf INTEGER DEFAULT FALSE, callback1conf TEXT, calledback1conf INTEGER DEFAULT FALSE, imported INTEGER DEFAULT FALSE, + watching_by_pub32_id INTEGER REFERENCES watching_by_pub32, + pub32_index INTEGER, inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_watching_address ON watching (address); CREATE TABLE watching_tx ( watching_id INTEGER REFERENCES watching, @@ -54,6 +67,19 @@ CREATE TABLE recipient ( ); CREATE INDEX idx_recipient_address ON recipient (address); +CREATE TABLE watching_by_txid ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + txid TEXT UNIQUE, + watching INTEGER DEFAULT FALSE, + callback1conf TEXT, + calledback1conf INTEGER DEFAULT FALSE, + callbackxconf TEXT, + calledbackxconf INTEGER DEFAULT FALSE, + nbxconf INTEGER, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_watching_by_txid_txid ON watching_by_txid (txid); + CREATE TABLE stamp ( id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT UNIQUE, @@ -63,7 +89,6 @@ CREATE TABLE stamp ( calledback INTEGER DEFAULT FALSE, inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_stamp_hash ON stamp (hash); CREATE INDEX idx_stamp_calledback ON stamp (calledback); CREATE TABLE cyphernode_props ( @@ -75,3 +100,24 @@ CREATE TABLE cyphernode_props ( CREATE INDEX idx_cp_property ON cyphernode_props (property); INSERT INTO cyphernode_props (property, value) VALUES ("version", "0.1"); +INSERT INTO cyphernode_props (property, value) VALUES ("pay_index", "0"); + +CREATE TABLE ln_invoice ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE, + bolt11 TEXT UNIQUE, + payment_hash TEXT, + msatoshi INTEGER, + status TEXT, + pay_index INTEGER, + msatoshi_received INTEGER, + paid_at INTEGER, + description TEXT, + expires_at INTEGER, + callback_url TEXT, + calledback INTEGER DEFAULT FALSE, + callback_failed INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_lninvoice_label ON ln_invoice (label); +CREATE INDEX idx_lninvoice_bolt11 ON ln_invoice (bolt11); diff --git a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh index 0b69b16..5de49f3 100644 --- a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh +++ b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh @@ -1,10 +1,14 @@ #!/bin/sh -sqlite3 db/proxydb ".tables" | grep "stamp" > /dev/null +echo "Checking for OTS support in DB..." +sqlite3 $DB_FILE ".tables" | grep "stamp" > /dev/null if [ "$?" -eq "1" ]; then # stamp not there, we have to migrate - echo "Migrating database from v0 to v0.1..." + echo "Migrating database for OTS support..." + echo "Backing up current DB..." + cp $DB_FILE $DB_FILE-sqlmigrate20181213_0-0.1 + echo "Altering DB..." cat sqlmigrate20181213_0-0.1.sql | sqlite3 $DB_FILE else - echo "Database v0 to v0.1 migration already done, skipping!" + echo "Database OTS support migration already done, skipping!" fi diff --git a/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh b/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh new file mode 100644 index 0000000..0fea10b --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +echo "Checking for full LN support in DB..." +exists=$(sqlite3 $DB_FILE "SELECT value FROM cyphernode_props WHERE property='pay_index'") +if [ -z "${exists}" ]; then + # pay_index not found, let's migrate + echo "Migrating database for full LN support..." + echo "Backing up current DB..." + cp $DB_FILE $DB_FILE-sqlmigrate20190104_0.1-0.2 + echo "Altering DB..." + cat sqlmigrate20190104_0.1-0.2.sql | sqlite3 $DB_FILE +else + echo "Database full LN support migration already done, skipping!" +fi diff --git a/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sql b/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sql new file mode 100644 index 0000000..20b2528 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sql @@ -0,0 +1,36 @@ +PRAGMA foreign_keys = ON; + +INSERT INTO cyphernode_props (property, value) VALUES ("pay_index", "0"); + +CREATE TABLE ln_invoice ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE, + bolt11 TEXT UNIQUE, + payment_hash TEXT, + msatoshi INTEGER, + status TEXT, + pay_index INTEGER, + msatoshi_received INTEGER, + paid_at INTEGER, + description TEXT, + expires_at INTEGER, + callback_url TEXT, + calledback INTEGER DEFAULT FALSE, + callback_failed INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_lninvoice_label ON ln_invoice (label); +CREATE INDEX idx_lninvoice_bolt11 ON ln_invoice (bolt11); + +CREATE TABLE watching_by_txid ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + txid TEXT UNIQUE, + watching INTEGER DEFAULT FALSE, + callback1conf TEXT, + calledback1conf INTEGER DEFAULT FALSE, + callbackxconf TEXT, + calledbackxconf INTEGER DEFAULT FALSE, + nbxconf INTEGER, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_watching_by_txid_txid ON watching_by_txid (txid); diff --git a/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh new file mode 100644 index 0000000..0c68e39 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +echo "Checking for watch by xpub support in DB..." +sqlite3 $DB_FILE ".tables" | grep "watching_by_pub32" > /dev/null +if [ "$?" -eq "1" ]; then + # watching_by_pub32 not there, we have to migrate + echo "Migrating database for watch by xpub support..." + echo "Backing up current DB..." + cp $DB_FILE $DB_FILE-sqlmigrate20190130_0.1-0.2 + echo "Altering DB..." + cat sqlmigrate20190130_0.1-0.2.sql | sqlite3 $DB_FILE +else + echo "Database watch by xpub support migration already done, skipping!" +fi diff --git a/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql new file mode 100644 index 0000000..f30771b --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql @@ -0,0 +1,21 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE watching_by_pub32 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pub32 TEXT UNIQUE, + label TEXT UNIQUE, + derivation_path TEXT, + callback0conf TEXT, + callback1conf TEXT, + last_imported_n INTEGER, + watching INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); + +-- for all duplicate addresses, we only keep the last one inserted +DELETE FROM watching WHERE id NOT IN (SELECT MAX(id) FROM watching GROUP BY address); +DROP INDEX idx_watching_address; +CREATE UNIQUE INDEX idx_watching_address ON watching(address); + +ALTER TABLE watching ADD COLUMN watching_by_pub32_id INTEGER REFERENCES watching_by_pub32; +ALTER TABLE watching ADD COLUMN pub32_index INTEGER; diff --git a/proxy_docker/app/script/bitcoin.sh b/proxy_docker/app/script/bitcoin.sh index 5fc0632..4c7e596 100644 --- a/proxy_docker/app/script/bitcoin.sh +++ b/proxy_docker/app/script/bitcoin.sh @@ -10,7 +10,7 @@ deriveindex() trace "[deriveindex] index=${index}" local pub32=$DERIVATION_PUB32 - local path=$(echo -e $DERIVATION_PATH | sed -En "s/n/${index}/p") + local path=$(echo -e "$DERIVATION_PATH" | sed -En "s/n/${index}/p") # pub32=$(grep "derivation.pub32" config.properties | cut -d'=' -f2) # path=$(grep "derivation.path" config.properties | cut -d'=' -f2 | sed -En "s/n/${index}/p") @@ -21,6 +21,15 @@ deriveindex() return $? } +derivepubpath() { + trace "Entering derivepubpath()..." + + # {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"} + + send_to_pycoin $1 + return $? +} + send_to_pycoin() { trace "Entering send_to_pycoin()..." diff --git a/proxy_docker/app/script/blockchainrpc.sh b/proxy_docker/app/script/blockchainrpc.sh index bc24ffc..661d91f 100644 --- a/proxy_docker/app/script/blockchainrpc.sh +++ b/proxy_docker/app/script/blockchainrpc.sh @@ -3,8 +3,7 @@ . ./trace.sh . ./sendtobitcoinnode.sh -get_best_block_hash() -{ +get_best_block_hash() { trace "Entering get_best_block_hash()..." local data='{"method":"getbestblockhash"}' @@ -12,8 +11,7 @@ get_best_block_hash() return $? } -getestimatesmartfee() -{ +getestimatesmartfee() { trace "Entering getestimatesmartfee()..." local nb_blocks=${1} @@ -22,8 +20,7 @@ getestimatesmartfee() return $? } -get_block_info() -{ +get_block_info() { trace "Entering get_block_info()..." local block_hash=${1} @@ -34,18 +31,16 @@ get_block_info() return $? } -get_best_block_info() -{ +get_best_block_info() { trace "Entering get_best_block_info()..." - local block_hash=$(echo "$(get_best_block_hash)" | jq ".result" | tr -d '"') + local block_hash=$(echo "$(get_best_block_hash)" | jq -r ".result") trace "[get_best_block_info] block_hash=${block_hash}" get_block_info ${block_hash} return $? } -get_rawtransaction() -{ +get_rawtransaction() { trace "Entering get_rawtransaction()..." local txid=${1} @@ -56,14 +51,43 @@ get_rawtransaction() return $? } -get_transaction() -{ +get_transaction() { trace "Entering get_transaction()..." local txid=${1} trace "[get_transaction] txid=${txid}" + local to_spender_node=${2} + trace "[get_transaction] to_spender_node=${to_spender_node}" + local data="{\"method\":\"gettransaction\",\"params\":[\"${txid}\",true]}" trace "[get_transaction] data=${data}" - send_to_watcher_node "${data}" + if [ -z "${to_spender_node}" ]; then + send_to_watcher_node "${data}" + else + send_to_spender_node "${data}" + fi + return $? +} + +get_blockchain_info() { + trace "Entering get_blockchain_info()..." + + local data='{"method":"getblockchaininfo"}' + send_to_watcher_node "${data}" | jq ".result" + return $? +} + +get_mempool_info() { + trace "Entering get_mempool_info()..." + + local data='{"method":"getmempoolinfo"}' + send_to_watcher_node "${data}" | jq ".result" + return $? +} +get_blockhash() { + trace "Entering get_blockhash()..." + local blockheight=${1} + local data="{\"method\":\"getblockhash\",\"params\":[${blockheight}]}" + send_to_watcher_node "${data}" | jq ".result" return $? } diff --git a/proxy_docker/app/script/call_lightningd.sh b/proxy_docker/app/script/call_lightningd.sh index 25c6fc0..8811d23 100644 --- a/proxy_docker/app/script/call_lightningd.sh +++ b/proxy_docker/app/script/call_lightningd.sh @@ -2,101 +2,435 @@ . ./trace.sh -ln_create_invoice() -{ - trace "Entering ln_create_invoice()..." +ln_create_invoice() { + trace "Entering ln_create_invoice()..." - local result + local result + local data + local id - local request=${1} - local msatoshi=$(echo "${request}" | jq ".msatoshi" | tr -d '"') - trace "[ln_create_invoice] msatoshi=${msatoshi}" - local label=$(echo "${request}" | jq ".label") - trace "[ln_create_invoice] label=${label}" - local description=$(echo "${request}" | jq ".description") - trace "[ln_create_invoice] description=${description}" - local expiry=$(echo "${request}" | jq ".expiry" | tr -d '"') - trace "[ln_create_invoice] expiry=${expiry}" + local request=${1} + local msatoshi=$(echo "${request}" | jq -r ".msatoshi") + trace "[ln_create_invoice] msatoshi=${msatoshi}" + local label=$(echo "${request}" | jq -r ".label") + trace "[ln_create_invoice] label=${label}" + local description=$(echo "${request}" | jq -r ".description") + trace "[ln_create_invoice] description=${description}" + local expiry=$(echo "${request}" | jq -r ".expiry") + trace "[ln_create_invoice] expiry=${expiry}" + local callback_url=$(echo "${request}" | jq -r ".callbackUrl") + trace "[ln_create_invoice] callback_url=${callback_url}" - result=$(./lightning-cli invoice ${msatoshi} "${label}" "${description}" ${expiry}) - returncode=$? - trace_rc ${returncode} - trace "[ln_create_invoice] result=${result}" + #/proxy $ ./lightning-cli invoice 10000 "t1" "t1d" 60 + #{ + # "payment_hash": "a74e6cccb06e26bcddc32c43674f9c3cf6b018a4cb9e9ff7f835cc59b091ae06", + # "expires_at": 1546648644, + # "bolt11": "lnbc100n1pwzllqgpp55a8xen9sdcntehwr93pkwnuu8nmtqx9yew0flalcxhx9nvy34crqdq9wsckgxqzpucqp2rzjqt04ll5ft3mcuy8hws4xcku2pnhma9r9mavtjtadawyrw5kgzp7g7zr745qq3mcqqyqqqqlgqqqqqzsqpcr85k33shzaxscpj29fadmjmfej6y2p380x9w4kxydqpxq87l6lshy69fry9q2yrtu037nt44x77uhzkdyn8043n5yj8tqgluvmcl69cquaxr68" + #} - echo "${result}" + trace "[ln_create_invoice] ./lightning-cli invoice ${msatoshi} \"${label}\" \"${description}\" ${expiry}" + result=$(./lightning-cli invoice ${msatoshi} "${label}" "${description}" ${expiry}) + returncode=$? + trace_rc ${returncode} + trace "[ln_create_invoice] result=${result}" - return ${returncode} + if [ "${returncode}" -ne "0" ]; then + data=${result} + else + local bolt11=$(echo "${result}" | jq -r ".bolt11") + trace "[ln_create_invoice] bolt11=${bolt11}" + local payment_hash=$(echo "${result}" | jq -r ".payment_hash") + trace "[ln_create_invoice] payment_hash=${payment_hash}" + local expires_at=$(echo "${result}" | jq -r ".expires_at") + trace "[ln_create_invoice] expires_at=${expires_at}" + + # Let's get the connect string if provided in configuration + local connectstring=$(get_connection_string) + + sql "INSERT OR IGNORE INTO ln_invoice (label, bolt11, callback_url, payment_hash, expires_at, msatoshi, description, status) VALUES (\"${label}\", \"${bolt11}\", \"${callback_url}\", \"${payment_hash}\", ${expires_at}, ${msatoshi}, \"${description}\", \"unpaid\")" + trace_rc $? + id=$(sql "SELECT id FROM ln_invoice WHERE bolt11=\"${bolt11}\"") + trace_rc $? + + data="{\"id\":\"${id}\"," + data="${data}\"label\":\"${label}\"," + data="${data}\"bolt11\":\"${bolt11}\"," + if [ -n "${connectstring}" ]; then + data="${data}\"connectstring\":\"${connectstring}\"," + fi + data="${data}\"callback_url\":\"${callback_url}\"," + data="${data}\"payment_hash\":\"${payment_hash}\"," + data="${data}\"msatoshi\":${msatoshi}," + data="${data}\"status\":\"unpaid\"," + data="${data}\"description\":\"${description}\"," + data="${data}\"expires_at\":${expires_at}}" + trace "[ln_create_invoice] data=${data}" + fi + + echo "${data}" + + return ${returncode} } -ln_getinfo() -{ - trace "Entering ln_get_info()..." +ln_get_connection_string() { + trace "Entering ln_get_connection_string()..." - local result + echo "{\"connectstring\":\"$(get_connection_string)\"}" +} - result=$(./lightning-cli getinfo) - returncode=$? - trace_rc ${returncode} - trace "[ln_getinfo] result=${result}" +get_connection_string() { + trace "Entering get_connection_string()..." - echo "${result}" + # Let's get the connect string if provided in configuration + local connectstring + local getinfo=$(ln_getinfo) + echo ${getinfo} | jq -e '.address[0]' > /dev/null + if [ "$?" -eq 0 ]; then + # If there's an address + connectstring="$(echo ${getinfo} | jq -r '((.id + "@") + (.address[0] | ((.address + ":") + (.port | tostring))))')" + trace "[get_connection_string] connectstring=${connectstring}" + fi - return ${returncode} + echo "${connectstring}" +} + +ln_getinfo() { + trace "Entering ln_get_info()..." + + local result + + result=$(./lightning-cli getinfo) + returncode=$? + trace_rc ${returncode} + trace "[ln_getinfo] result=${result}" + + echo "${result}" + + return ${returncode} +} + +ln_getinvoice() { + trace "Entering ln_getinvoice()..." + + local label=${1} + local result + + result=$(./lightning-cli listinvoices ${label}) + returncode=$? + trace_rc ${returncode} + trace "[ln_getinvoice] result=${result}" + + echo "${result}" + + return ${returncode} +} + +ln_delinvoice() { + trace "Entering ln_delinvoice()..." + + local label=${1} + local result + local returncode + local rc + + trace "[ln_delinvoice] ./lightning-cli delinvoice ${label} \"unpaid\"" + result=$(./lightning-cli delinvoice ${label} "unpaid") + returncode=$? + trace_rc ${returncode} + trace "[ln_delinvoice] result=${result}" + + if [ "${returncode}" -ne "0" ]; then + # Special case of error: if status is expired, we're ok + echo "${result}" | grep "not unpaid" > /dev/null + rc=$? + trace_rc ${rc} + + if [ "${rc}" -eq "0" ]; then + trace "Invoice is paid or expired, it's ok" + # String found + returncode=0 + fi + fi + + echo "${result}" + + return ${returncode} +} + +ln_decodebolt11() { + trace "Entering ln_decodebolt11()..." + + local bolt11=${1} + local result + + result=$(./lightning-cli decodepay ${bolt11}) + returncode=$? + trace_rc ${returncode} + trace "[ln_decodebolt11] result=${result}" + + echo "${result}" + + return ${returncode} +} + +ln_connectfund() { + trace "Entering ln_connectfund()..." + + # {"peer":"nodeId@ip:port","msatoshi":"100000","callbackUrl":"https://callbackUrl/?channelReady=f3y2c3cvm4uzg2gq"} + + local result + local returncode + local tx + local txid + local nodeId + local data + local channel_id + local msg + + local request=${1} + local peer=$(echo "${request}" | jq -r ".peer") + trace "[ln_connectfund] peer=${peer}" + local msatoshi=$(echo "${request}" | jq ".msatoshi") + trace "[ln_connectfund] msatoshi=${msatoshi}" + local callback_url=$(echo "${request}" | jq -r ".callbackUrl") + trace "[ln_connectfund] callback_url=${callback_url}" + + # Let's first try to connect to peer + trace "[ln_connectfund] ./lightning-cli connect ${peer}" + result=$(./lightning-cli connect ${peer}) + returncode=$? + trace_rc ${returncode} + trace "[ln_connectfund] result=${result}" + + if [ "${returncode}" -eq "0" ]; then + # Connected + +# ./lightning-cli connect 038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9@180.181.208.42:9735 +# { +# "id": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9" +# } + +# ./lightning-cli connect 021a1b197aa79242532b23cb9a8d9cb78631f95f811457675fa1b362fe6d1c24b8@172.81.180.244:9735 +# { "code" : -1, "message" : "172.1.180.244:9735: Connection establishment: Operation timed out. " } + + nodeId=$(echo "${result}" | jq -r ".id") + trace "[ln_connectfund] nodeId=${nodeId}" + + # Now let's fund a channel with peer + trace "[ln_connectfund] ./lightning-cli fundchannel ${nodeId} ${msatoshi}" + result=$(./lightning-cli fundchannel ${nodeId} ${msatoshi}) + returncode=$? + trace_rc ${returncode} + trace "[ln_connectfund] result=${result}" + + if [ "${returncode}" -eq "0" ]; then + # funding succeeded + +# ./lightning-cli fundchannel 038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9 1000000 +# { +# "tx": "020000000001011594f707cf2ec076278072bc64f893bbd70188db42ea49e9ba531ee3c7bc8ed00100000000ffffffff0240420f00000000002200206149ff97921356191dc1f2e9ab997c459a71e8050d272721abf4b4d8a92d2419a6538900000000001600142cab0184d0f8098f75ebe05172b5864395e033f402483045022100b25cd5a9d49b5cc946f72a58ccc0afe652d99c25fba98d68be035a286f55849802203de5b504c44f775a0101b6025f116b73bf571e776e4efcac0475721bfde4d08a0121038360308a394158b0799196c5179a6480a75db73207fb93d4a673d934c9f786f400000000", +# "txid": "747bf7d1c40bebed578b3f02a3d8da9a56885851a3c4bdb6e1b8de19223559a4", +# "channel_id": "a459352219deb8e1b6bdc4a3515888569adad8a3023f8b57edeb0bc4d1f77b74" +# } + +# ./lightning-cli fundchannel 038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9 100000 +# { "code" : 301, "message" : "Cannot afford transaction" } + + # Let's find what to watch + txid=$(echo "${result}" | jq ".txid") + tx=$(echo "${result}" | jq ".tx") + channel_id=$(echo "${result}" | jq ".channel_id") + + data="{\"txid\":${txid},\"xconfCallbackURL\":\"${callback_url}\",\"nbxconf\":6}" + + result=$(watchtxidrequest "${data}") + returncode=$? + trace_rc ${returncode} + trace "[ln_connectfund] result=${result}" + + if [ "${returncode}" -eq "0" ]; then + result="{\"result\":\"success\",\"txid\":${txid},\"channel_id\":${channel_id}}" + else + trace "[ln_connectfund] Error watching txid, result=${result}" + result="{\"result\":\"failed\",\"message\":\"Failed at watching txid\"}" + fi + else + # Error funding + trace "[ln_connectfund] Error funding, result=${result}" + msg=$(echo "${result}" | jq ".message") + result="{\"result\":\"failed\",\"message\":${msg}}" + fi + else + # Error connecting + trace "[ln_connectfund] Error connecting, result=${result}" + msg=$(echo "${result}" | jq ".message") + result="{\"result\":\"failed\",\"message\":${msg}}" + fi + + echo "${result}" + + return ${returncode} } ln_pay() { - trace "Entering ln_pay()..." + trace "Entering ln_pay()..." - local result + # We'll use pay that will manage the routing and waitsendpay to make sure a payment succeeded or failed. + # 1. pay + # 2. waitsendpay IF pay returned a status of "pending" (code 200) - local request=${1} - local bolt11=$(echo "${request}" | jq ".bolt11" | tr -d '"') - trace "[ln_pay] bolt11=${bolt11}" - local expected_msatoshi=$(echo "${request}" | jq ".expected_msatoshi") - trace "[ln_pay] expected_msatoshi=${expected_msatoshi}" - local expected_description=$(echo "${request}" | jq ".expected_description") - trace "[ln_pay] expected_description=${expected_description}" + local result + local returncode + local code + local status + local payment_hash - result=$(./lightning-cli decodepay ${bolt11}) + local request=${1} + local bolt11=$(echo "${request}" | jq -r ".bolt11") + trace "[ln_pay] bolt11=${bolt11}" + local expected_msatoshi=$(echo "${request}" | jq ".expected_msatoshi") + trace "[ln_pay] expected_msatoshi=${expected_msatoshi}" + local expected_description=$(echo "${request}" | jq ".expected_description") + trace "[ln_pay] expected_description=${expected_description}" - local invoice_msatoshi=$(echo "${result}" | jq ".msatoshi") - trace "[ln_pay] invoice_msatoshi=${invoice_msatoshi}" - local invoice_description=$(echo "${result}" | jq ".description") - trace "[ln_pay] invoice_description=${invoice_description}" + # Let's first decode the bolt11 string to make sure we are paying the good invoice + trace "[ln_pay] ./lightning-cli decodepay ${bolt11}" + result=$(./lightning-cli decodepay ${bolt11}) + returncode=$? + trace_rc ${returncode} + trace "[ln_pay] result=${result}" - if [ "${expected_msatoshi}" != "${invoice_msatoshi}" ]; then - result="{\"result\":\"error\",\"expected_msatoshi\":${expected_msatoshi},\"invoice_msatoshi\":${invoice_msatoshi}}" - returncode=1 - elif [ "${expected_description}" != "${invoice_description}" ]; then - result="{\"result\":\"error\",\"expected_description\":${expected_description},\"invoice_description\":${invoice_description}}" - returncode=1 - else - result=$(./lightning-cli pay ${bolt11}) - returncode=$? - trace_rc ${returncode} - fi - trace "[ln_pay] result=${result}" + if [ "${returncode}" -eq "0" ]; then + local invoice_msatoshi=$(echo "${result}" | jq ".msatoshi") + trace "[ln_pay] invoice_msatoshi=${invoice_msatoshi}" + local invoice_description=$(echo "${result}" | jq ".description") + trace "[ln_pay] invoice_description=${invoice_description}" - echo "${result}" + # The amount must match + if [ "${expected_msatoshi}" != "${invoice_msatoshi}" ]; then + result="{\"result\":\"error\",\"expected_msatoshi\":${expected_msatoshi},\"invoice_msatoshi\":${invoice_msatoshi}}" + returncode=1 + elif [ "${expected_description}" != '""' ] && [ "${expected_description}" != "${invoice_description}" ]; then + # If expected description is empty, we accept any description on the invoice. Amount is the important thing. - return ${returncode} + result="{\"result\":\"error\",\"expected_description\":${expected_description},\"invoice_description\":${invoice_description}}" + returncode=1 + else + # Amount and description is as expected, let's pay! + trace "[ln_pay] Amount and description are as expected, let's try to pay!" + + trace "[ln_pay] ./lightning-cli pay -k bolt11=${bolt11} retry_for=15" + result=$(./lightning-cli pay -k bolt11=${bolt11} retry_for=15) + returncode=$? + trace_rc ${returncode} + trace "[ln_pay] result=${result}" + + # The result should contain a status field with value pending, complete or failed. + # If complete, we can return with success. + # If failed, we can return with failed. + # If pending, we should keep trying until complete or failed status, before responding to client. + # We'll use waitsendpay for that. + + if [ "${returncode}" -ne "0" ]; then + trace "[ln_pay] payment not complete, let's see what's going on." + + code=$(echo "${result}" | jq -e ".code") + # jq -e will have a return code of 1 if the supplied tag is null. + if [ "$?" -eq "0" ]; then + # code tag not null, so there's an error + trace "[ln_pay] Error code found, code=${code}" + + if [ "${code}" -eq "200" ]; then + trace "[ln_pay] Code 200, let's fetch status in data, should be pending..." + status=$(echo "${result}" | jq -r ".data.status") + trace "[ln_pay] status=${status}" + else + trace "[ln_pay] Failure code, response will be the cli result." + fi + else + # code tag not found + trace "[ln_pay] No error code, getting the status..." + status=$(echo "${result}" | jq -r ".status") + trace "[ln_pay] status=${status}" + fi + + if [ "${status}" = "pending" ]; then + trace "[ln_pay] Ok let's deal with pending status with waitsendpay." + + payment_hash=$(echo "${result}" | jq -r ".data.payment_hash") + trace "[ln_pay] ./lightning-cli waitsendpay ${payment_hash} 15" + result=$(./lightning-cli waitsendpay ${payment_hash} 15) + returncode=$? + trace_rc ${returncode} + trace "[ln_pay] result=${result}" + + if [ "${returncode}" -ne "0" ]; then + trace "[ln_pay] Failed!" + else + trace "[ln_pay] Successfully paid!" + fi + fi + else + trace "[ln_pay] Successfully paid!" + fi + fi + fi + +# Example of error result: +# +# { "code" : 204, "message" : "failed: WIRE_TEMPORARY_CHANNEL_FAILURE (Outgoing subdaemon died)", "data" : +# { +# "erring_index": 0, +# "failcode": 4103, +# "erring_node": "031b867d9d6631a1352cc0f37bcea94bd5587a8d4f40416c4ce1a12511b1e68f56", +# "erring_channel": "1452982:62:0" +# } } +# +# +# Example of successful result: +# +# { +# "id": 44, +# "payment_hash": "de648062da7117903291dab2075881e49ddd78efbf82438e4a2f486a7ebe0f3a", +# "destination": "02be93d1dad1ccae7beea7b42f8dbcfbdafb4d342335c603125ef518200290b450", +# "msatoshi": 207000, +# "msatoshi_sent": 207747, +# "created_at": 1548380406, +# "status": "complete", +# "payment_preimage": "a7ef27e9a94d63e4028f35ca4213fd9008227ad86815cd40d3413287d819b145", +# "description": "Order 43012 - Satoshi Larrivee", +# "getroute_tries": 1, +# "sendpay_tries": 1, +# "route": [ +# { +# "id": "02be93d1dad1ccae7beea7b42f8dbcfbdafb4d342335c603125ef518200290b450", +# "channel": "1452749:174:0", +# "msatoshi": 207747, +# "delay": 10 +# } +# ], +# "failures": [ +# ] +# } + + echo "${result}" + + return ${returncode} } -ln_newaddr() -{ - trace "Entering ln_newaddr()..." +ln_newaddr() { + trace "Entering ln_newaddr()..." - local result + local result - call_lightningd newaddr - result=$(./lightning-cli newaddr) - returncode=$? - trace_rc ${returncode} - trace "[ln_newaddr] result=${result}" + result=$(./lightning-cli newaddr) + returncode=$? + trace_rc ${returncode} + trace "[ln_newaddr] result=${result}" - echo "${result}" + echo "${result}" - return ${returncode} + return ${returncode} } -case "${0}" in *call_lightningd.sh) call_lightningd $@;; esac +case "${0}" in *call_lightningd.sh) ./lightning-cli $@;; esac diff --git a/proxy_docker/app/script/callbacks_job.sh b/proxy_docker/app/script/callbacks_job.sh index 3edd87b..294880a 100644 --- a/proxy_docker/app/script/callbacks_job.sh +++ b/proxy_docker/app/script/callbacks_job.sh @@ -2,20 +2,21 @@ . ./trace.sh . ./sql.sh +. ./notify.sh -do_callbacks() -{ +do_callbacks() { ( flock -x 200 || return 0 trace "Entering do_callbacks()..." # Let's fetch all the watching addresses still being watched but not called back - local callbacks=$(sql 'SELECT DISTINCT callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id WHERE NOT calledback0conf and watching_id NOT NULL and callback0conf NOT NULL and watching') + local callbacks=$(sql 'SELECT DISTINCT w.callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE NOT calledback0conf AND watching_id NOT NULL AND w.callback0conf NOT NULL AND w.watching') trace "[do_callbacks] callbacks0conf=${callbacks}" local returncode local address + local url local IFS=$'\n' for row in ${callbacks} do @@ -29,7 +30,7 @@ do_callbacks() fi done - callbacks=$(sql 'SELECT DISTINCT callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w, watching_tx wt, tx t WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf and confirmations>0 and callback1conf NOT NULL and watching') + callbacks=$(sql 'SELECT DISTINCT w.callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path FROM watching w, watching_tx wt, tx t LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf AND confirmations>0 AND w.callback1conf NOT NULL AND w.watching') trace "[do_callbacks] callbacks1conf=${callbacks}" for row in ${callbacks} @@ -42,11 +43,88 @@ do_callbacks() trace_rc $? fi done + + callbacks=$(sql "SELECT id, label, bolt11, callback_url, payment_hash, msatoshi, status, pay_index, msatoshi_received, paid_at, description, expires_at FROM ln_invoice WHERE NOT calledback AND callback_failed") + trace "[do_callbacks LN] ln_callbacks=${callbacks}" + + for row in ${callbacks} + do + ln_manage_callback ${row} + trace_rc $? + done + ) 200>./.callbacks.lock } -build_callback() -{ +ln_manage_callback() { + trace "Entering ln_manage_callback()..." + + local row=$@ + trace "[ln_manage_callback] row=${row}" + + local id=$(echo "${row}" | cut -d '|' -f1) + trace "[ln_manage_callback] id=${id}" + local label=$(echo "${row}" | cut -d '|' -f2) + trace "[ln_manage_callback] label=${label}" + local bolt11=$(echo "${row}" | cut -d '|' -f3) + trace "[ln_manage_callback] bolt11=${bolt11}" + local callback_url=$(echo "${row}" | cut -d '|' -f4) + trace "[ln_manage_callback] callback_url=${callback_url}" + local payment_hash=$(echo "${row}" | cut -d '|' -f5) + trace "[ln_manage_callback] payment_hash=${payment_hash}" + local msatoshi=$(echo "${row}" | cut -d '|' -f6) + trace "[ln_manage_callback] msatoshi=${msatoshi}" + local status=$(echo "${row}" | cut -d '|' -f7) + trace "[ln_manage_callback] status=${status}" + local pay_index=$(echo "${row}" | cut -d '|' -f8) + trace "[ln_manage_callback] pay_index=${pay_index}" + local msatoshi_received=$(echo "${row}" | cut -d '|' -f9) + trace "[ln_manage_callback] msatoshi_received=${msatoshi_received}" + local paid_at=$(echo "${row}" | cut -d '|' -f10) + trace "[ln_manage_callback] paid_at=${paid_at}" + local description=$(echo "${row}" | cut -d '|' -f11) + trace "[ln_manage_callback] description=${description}" + local expires_at=$(echo "${row}" | cut -d '|' -f12) + trace "[ln_manage_callback] expires_at=${expires_at}" + local returncode + + if [ -z "${callback_url}" ]; then + # No callback url provided for that invoice + sql "UPDATE ln_invoice SET calledback=1 WHERE id=\"${id}\"" + trace_rc $? + return + fi + + data="{\"id\":\"${id}\"," + data="${data}\"label\":\"${label}\"," + data="${data}\"bolt11\":\"${bolt11}\"," + data="${data}\"callback_url\":\"${callback_url}\"," + data="${data}\"payment_hash\":\"${payment_hash}\"," + data="${data}\"msatoshi\":${msatoshi}," + data="${data}\"status\":\"${status}\"," + data="${data}\"pay_index\":${pay_index}," + data="${data}\"msatoshi_received\":${msatoshi_received}," + data="${data}\"paid_at\":${paid_at}," + data="${data}\"description\":\"${description}\"," + data="${data}\"expires_at\":${expires_at}}" + trace "[ln_manage_callback] data=${data}" + + curl_callback "${callback_url}" "${data}" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + sql "UPDATE ln_invoice SET calledback=1 WHERE id=\"${id}\"" + trace_rc $? + else + trace "[ln_manage_callback] callback failed: ${callback_url}" + sql "UPDATE ln_invoice SET callback_failed=1 WHERE id=\"${id}\"" + trace_rc $? + fi + + return ${returncode} +} + +build_callback() { trace "Entering build_callback()..." local row=$@ @@ -66,6 +144,11 @@ build_callback() local blocktime local blockheight + local pub32_index + local pub32 + local label + local derivation_path + # callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id trace "[build_callback] row=${row}" @@ -79,7 +162,7 @@ build_callback() trace "[build_callback] txid=${txid}" vout_n=$(echo "${row}" | cut -d '|' -f4) trace "[build_callback] vout_n=${vout_n}" - sent_amount=$(echo "${row}" | cut -d '|' -f5) + sent_amount=$(echo "${row}" | cut -d '|' -f5 | awk '{ printf "%.8f", $0 }') trace "[build_callback] sent_amount=${sent_amount}" confirmations=$(echo "${row}" | cut -d '|' -f6) trace "[build_callback] confirmations=${confirmations}" @@ -106,6 +189,17 @@ build_callback() blocktime=$(echo "${row}" | cut -d '|' -f13) trace "[build_callback] blocktime=${blocktime}" + pub32_index=$(echo "${row}" | cut -d '|' -f16) + trace "[build_callback] pub32_index=${pub32_index}" + if [ -n "${pub32_index}" ]; then + pub32=$(echo "${row}" | cut -d '|' -f17) + trace "[build_callback] pub32=${pub32}" + label=$(echo "${row}" | cut -d '|' -f18) + trace "[build_callback] label=${label}" + derivation_path=$(echo "${row}" | cut -d '|' -f19) + trace "[build_callback] derivation_path=${derivation_path}" + fi + data="{\"id\":\"${id}\"," data="${data}\"address\":\"${address}\"," data="${data}\"hash\":\"${txid}\"," @@ -124,6 +218,12 @@ build_callback() data="${data}\"blocktime\":\"$(date -Is -d @${blocktime})\"," data="${data}\"blockheight\":${blockheight}" fi + if [ -n "${pub32_index}" ]; then + data="${data}\"pub32\":\"${pub32}\"," + data="${data}\"pub32_label\":\"${label}\"," + derivation_path=$(echo -e $derivation_path | sed -En "s/n/${pub32_index}/p") + data="${data}\"pub32_derivation_path\":\"${derivation_path}\"" + fi data="${data}}" trace "[build_callback] data=${data}" @@ -131,16 +231,13 @@ build_callback() return $? } -curl_callback() -{ +curl_callback() { trace "Entering curl_callback()..." - local url=${1} - local data=${2} + local returncode - trace "[curl_callback] curl -H \"Content-Type: application/json\" -d \"${data}\" ${url}" - curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url} - local returncode=$? + notify_web "${1}" "${2}" + returncode=$? trace_rc ${returncode} return ${returncode} diff --git a/proxy_docker/app/script/callbacks_txid.sh b/proxy_docker/app/script/callbacks_txid.sh new file mode 100644 index 0000000..9e6c1e1 --- /dev/null +++ b/proxy_docker/app/script/callbacks_txid.sh @@ -0,0 +1,123 @@ +#!/bin/sh + +. ./trace.sh +. ./sql.sh + +do_callbacks_txid() { + ( + flock -x 200 || return 0 + + trace "Entering do_callbacks_txid()..." + + # Let's fetch all the watching txid still being watched but not called back + local callbacks=$(sql 'SELECT id, txid, callback1conf, 1 FROM watching_by_txid WHERE watching AND callback1conf NOT NULL AND NOT calledback1conf') + trace "[do_callbacks_txid] callbacks1conf=${callbacks}" + + local returncode + local address + local url + local id + local IFS=$'\n' + for row in ${callbacks} + do + build_callback_txid ${row} + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + id=$(echo "${row}" | cut -d '|' -f1) + sql "UPDATE watching_by_txid SET calledback1conf=1 WHERE id=\"${id}\"" + trace_rc $? + else + trace "[do_callbacks_txid] callback returncode has error, we don't flag as calledback yet." + fi + done + + local callbacks=$(sql 'SELECT id, txid, callbackxconf, nbxconf FROM watching_by_txid WHERE watching AND calledback1conf AND callbackxconf NOT NULL AND NOT calledbackxconf') + trace "[do_callbacks_txid] callbacksxconf=${callbacks}" + + for row in ${callbacks} + do + build_callback_txid ${row} + returncode=$? + if [ "${returncode}" -eq 0 ]; then + id=$(echo "${row}" | cut -d '|' -f1) + sql "UPDATE watching_by_txid SET calledbackxconf=1, watching=0 WHERE id=\"${id}\"" + trace_rc $? + else + trace "[do_callbacks_txid] callback returncode has error, we don't flag as calledback yet." + fi + done + + ) 200>./.callbacks.lock +} + +build_callback_txid() { + trace "Entering build_callback_txid()..." + + local row=$@ + local id + local txid + local url + local nbxconf + local blockhash + local blockheight + local confirmations + local data + local tx_raw_details + + # id, txid, url, nbconf + + trace "[build_callback_txid] row=${row}" + id=$(echo "${row}" | cut -d '|' -f1) + trace "[build_callback_txid] id=${id}" + txid=$(echo "${row}" | cut -d '|' -f2) + trace "[build_callback_txid] txid=${txid}" + url=$(echo "${row}" | cut -d '|' -f3) + trace "[build_callback_txid] url=${url}" + nbxconf=$(echo "${row}" | cut -d '|' -f4) + trace "[build_callback_txid] nbxconf=${nbxconf}" + + tx_raw_details=$(get_rawtransaction ${txid}) + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + confirmations=$(echo "${tx_raw_details}" | jq '.result.confirmations') + trace "[build_callback_txid] confirmations=${confirmations}" + + if [ "${confirmations}" -ge "${nbxconf}" ]; then + trace "[build_callback_txid] Number of confirmations for tx is at least what we're looking for, callback time!" + # Number of confirmations for transaction is at least what we want + # Let's prepare the callback! + + # blockhash=$(echo "${tx_raw_details}" | jq '.result.blockhash') + # trace "[build_callback_txid] blockhash=${blockhash}" + # blockheight=$(get_block_info $(echo "${blockhash}" | tr -d '"') | jq '.result.height') + # trace "[build_callback_txid] blockheight=${blockheight}" + + data="{\"id\":\"${id}\"," + data="${data}\"txid\":\"${txid}\"," + data="${data}\"confirmations\":${confirmations}" + data="${data}}" + trace "[build_callback_txid] data=${data}" + + curl_callback_txid "${url}" "${data}" + return $? + else + trace "[build_callback_txid] Number of confirmations for tx is not enough to call back." + return 1 + fi + fi +} + +curl_callback_txid() { + trace "Entering curl_callback_txid()..." + + local returncode + + notify_web "${1}" "${2}" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} diff --git a/proxy_docker/app/script/computefees.sh b/proxy_docker/app/script/computefees.sh index 640e24d..c8dfa2d 100644 --- a/proxy_docker/app/script/computefees.sh +++ b/proxy_docker/app/script/computefees.sh @@ -35,7 +35,7 @@ compute_fees() local fees=$(awk "BEGIN { printf(\"%.8f\", ${vin_total_amount}-${vout_total_amount}); exit }") trace "[compute_fees] fees=${fees}" - echo ${fees} + echo "${fees}" } compute_vin_total_amount() @@ -43,8 +43,7 @@ compute_vin_total_amount() trace "Entering compute_vin_total_amount()..." local main_tx=${1} -# local vin_txids=$(echo ${main_tx} | jq '.result.vin[].txid') - local vin_txids_vout=$(echo ${main_tx} | jq '.result.vin[] | ((.txid + "-") + (.vout | tostring))') + local vin_txids_vout=$(echo "${main_tx}" | jq '.result.vin[] | ((.txid + "-") + (.vout | tostring))') trace "[compute_vin_total_amount] vin_txids_vout=${vin_txids_vout}" local returncode local vin_txid_vout @@ -64,8 +63,7 @@ compute_vin_total_amount() for vin_txid_vout in ${vin_txids_vout} do -# vin_txid=$(echo ${vin_txid} | tr -d '"') - vin_txid=$(echo ${vin_txid_vout} | tr -d '"' | cut -d '-' -f1) + vin_txid=$(echo "${vin_txid_vout}" | tr -d '"' | cut -d '-' -f1) # Check if we already have the tx in our DB vin_raw_tx=$(sql "SELECT raw_tx FROM tx WHERE txid=\"${vin_txid}\"") if [ -z "${vin_raw_tx}" ]; then @@ -76,21 +74,20 @@ compute_vin_total_amount() return ${returncode} fi fi -# vout=$(echo ${main_tx} | jq ".result.vin[] | select(.txid == \"${vin_txid}\") | .vout") - vout=$(echo ${vin_txid_vout} | tr -d '"' | cut -d '-' -f2) + vout=$(echo "${vin_txid_vout}" | tr -d '"' | cut -d '-' -f2) trace "[compute_vin_total_amount] vout=${vout}" - vin_vout_amount=$(echo ${vin_raw_tx} | jq ".result.vout[] | select(.n == ${vout}) | .value" | awk '{ printf "%.8f", $0 }') + vin_vout_amount=$(echo "${vin_raw_tx}" | jq ".result.vout[] | select(.n == ${vout}) | .value" | awk '{ printf "%.8f", $0 }') trace "[compute_vin_total_amount] vin_vout_amount=${vin_vout_amount}" vin_total_amount=$(awk "BEGIN { printf(\"%.8f\", ${vin_total_amount}+${vin_vout_amount}); exit}") trace "[compute_vin_total_amount] vin_total_amount=${vin_total_amount}" - vin_hash=$(echo ${vin_raw_tx} | jq ".result.hash") - vin_confirmations=$(echo ${vin_raw_tx} | jq ".result.confirmations") - vin_timereceived=$(echo ${vin_raw_tx} | jq ".result.time") - vin_size=$(echo ${vin_raw_tx} | jq ".result.size") - vin_vsize=$(echo ${vin_raw_tx} | jq ".result.vsize") - vin_blockhash=$(echo ${vin_raw_tx} | jq ".result.blockhash") - vin_blockheight=$(echo ${vin_raw_tx} | jq ".result.blockheight") - vin_blocktime=$(echo ${vin_raw_tx} | jq ".result.blocktime") + vin_hash=$(echo "${vin_raw_tx}" | jq ".result.hash") + vin_confirmations=$(echo "${vin_raw_tx}" | jq ".result.confirmations") + vin_timereceived=$(echo "${vin_raw_tx}" | jq ".result.time") + vin_size=$(echo "${vin_raw_tx}" | jq ".result.size") + vin_vsize=$(echo "${vin_raw_tx}" | jq ".result.vsize") + vin_blockhash=$(echo "${vin_raw_tx}" | jq ".result.blockhash") + vin_blockheight=$(echo "${vin_raw_tx}" | jq ".result.blockheight") + vin_blocktime=$(echo "${vin_raw_tx}" | jq ".result.blocktime") # Let's insert the vin tx in the DB just in case it would be useful if ! ${txid_already_inserted}; then @@ -104,7 +101,7 @@ compute_vin_total_amount() fi done - echo ${vin_total_amount} + echo "${vin_total_amount}" return 0 } diff --git a/proxy_docker/app/script/confirmation.sh b/proxy_docker/app/script/confirmation.sh index ab37340..48bbfa1 100644 --- a/proxy_docker/app/script/confirmation.sh +++ b/proxy_docker/app/script/confirmation.sh @@ -21,8 +21,10 @@ confirmation_request() return $? } -confirmation() -{ +confirmation() { + ( + flock -x 201 + trace "Entering confirmation()..." local returncode @@ -41,7 +43,7 @@ confirmation() # First of all, let's make sure we're working on watched addresses... local address local addresseswhere - local addresses=$(echo ${tx_details} | jq ".result.details[].address") + local addresses=$(echo "${tx_details}" | jq ".result.details[].address") local notfirst=false local IFS=$'\n' @@ -56,7 +58,7 @@ confirmation() notfirst=true fi done - local rows=$(sql "SELECT id, address FROM watching WHERE address IN (${addresseswhere}) AND watching") + local rows=$(sql "SELECT id, address, watching_by_pub32_id, pub32_index FROM watching WHERE address IN (${addresseswhere}) AND watching") if [ ${#rows} -eq 0 ]; then trace "[confirmation] No watched address in this tx!" return 0 @@ -66,7 +68,7 @@ confirmation() local tx=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") local id_inserted local tx_raw_details=$(get_rawtransaction ${txid}) - local tx_nb_conf=$(echo ${tx_details} | jq '.result.confirmations') + local tx_nb_conf=$(echo "${tx_details}" | jq '.result.confirmations') # Sometimes raw tx are too long to be passed as paramater, so let's write # it to a temp file for it to be read by sqlite3 and then delete the file @@ -79,13 +81,13 @@ confirmation() # Let's first insert the tx in our DB - local tx_hash=$(echo ${tx_raw_details} | jq '.result.hash') - local tx_ts_firstseen=$(echo ${tx_details} | jq '.result.timereceived') - local tx_amount=$(echo ${tx_details} | jq '.result.amount') + local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') + local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') + local tx_amount=$(echo "${tx_details}" | jq '.result.amount') - local tx_size=$(echo ${tx_raw_details} | jq '.result.size') - local tx_vsize=$(echo ${tx_raw_details} | jq '.result.vsize') - local tx_replaceable=$(echo ${tx_details} | jq '.result."bip125-replaceable"') + local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') + local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') + local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0) local fees=$(compute_fees "${txid}") @@ -97,9 +99,9 @@ confirmation() local tx_blocktime=null if [ "${tx_nb_conf}" -gt "0" ]; then trace "[confirmation] tx_nb_conf=${tx_nb_conf}" - tx_blockhash=$(echo ${tx_details} | jq '.result.blockhash') + tx_blockhash=$(echo "${tx_details}" | jq '.result.blockhash') tx_blockheight=$(get_block_info $(echo ${tx_blockhash} | tr -d '"') | jq '.result.height') - tx_blocktime=$(echo ${tx_details} | jq '.result.blocktime') + tx_blocktime=$(echo "${tx_details}" | jq '.result.blocktime') fi sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, blockhash, blockheight, blocktime, raw_tx) VALUES (\"${txid}\", ${tx_hash}, ${tx_nb_conf}, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${tx_blockhash}, ${tx_blockheight}, ${tx_blocktime}, readfile('rawtx-${txid}.blob'))" @@ -110,15 +112,15 @@ confirmation() else # TX found in our DB. - # 1-conf or executecallbacks on an unconfirmed tx or spending watched address (in this case, we probably missed conf) + # 1-conf or executecallbacks on an unconfirmed tx or spending watched address (in this case, we probably missed conf) or spending to a watched address (in this case, spend inserted the tx in the DB) - local tx_blockhash=$(echo ${tx_details} | jq '.result.blockhash') + local tx_blockhash=$(echo "${tx_details}" | jq '.result.blockhash') trace "[confirmation] tx_blockhash=${tx_blockhash}" if [ "${tx_blockhash}" = "null" ]; then trace "[confirmation] probably being called by executecallbacks without any confirmations since the last time we checked" else - local tx_blockheight=$(get_block_info $(echo ${tx_blockhash} | tr -d '"') | jq '.result.height') - local tx_blocktime=$(echo ${tx_details} | jq '.result.blocktime') + local tx_blockheight=$(get_block_info $(echo "${tx_blockhash}" | tr -d '"') | jq '.result.height') + local tx_blocktime=$(echo "${tx_details}" | jq '.result.blocktime') sql "UPDATE tx SET confirmations=${tx_nb_conf}, @@ -128,9 +130,8 @@ confirmation() raw_tx=readfile('rawtx-${txid}.blob') WHERE txid=\"${txid}\"" trace_rc $? - - id_inserted=${tx} fi + id_inserted=${tx} fi # Delete the temp file containing the raw tx (see above) rm rawtx-${txid}.blob @@ -140,7 +141,7 @@ confirmation() tx=$(sql "SELECT tx_id FROM watching_tx WHERE tx_id=\"${tx}\"") if [ -z "${tx}" ]; then - trace "[confirmation] For this tx, there's no watching_tx row, let's create" + trace "[confirmation] For this tx, there's no watching_tx row, let's create it" local watching_id # If the tx is batched and pays multiple watched addresses, we have to insert @@ -149,8 +150,10 @@ confirmation() do watching_id=$(echo "${row}" | cut -d '|' -f1) address=$(echo "${row}" | cut -d '|' -f2) - tx_vout_n=$(echo ${tx_details} | jq ".result.details[] | select(.address==\"${address}\") | .vout") - tx_vout_amount=$(echo ${tx_details} | jq ".result.details[] | select(.address==\"${address}\") | .amount") + # In the case of us spending to a watched address, the address appears twice in the details, + # once on the spend side (negative amount) and once on the receiving side (positive amount) + tx_vout_n=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .vout") + tx_vout_amount=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .amount | fabs" | awk '{ printf "%.8f", $0 }') sql "INSERT OR IGNORE INTO watching_tx (watching_id, tx_id, vout, amount) VALUES (${watching_id}, ${id_inserted}, ${tx_vout_n}, ${tx_vout_amount})" trace_rc $? done @@ -159,8 +162,25 @@ confirmation() fi ######################################################################################################## - do_callbacks + ######################################################################################################## + # Let's now grow the watch window in the case of a xpub watcher... + trace "[confirmation] Let's now grow the watch window in the case of a xpub watcher" + for row in ${rows} + do + watching_by_pub32_id=$(echo "${row}" | cut -d '|' -f3) + pub32_index=$(echo "${row}" | cut -d '|' -f4) + if [ -n "${watching_by_pub32_id}" ]; then + extend_watchers ${watching_by_pub32_id} ${pub32_index} + fi + done + + ######################################################################################################## + + ) 201>./.confirmation.lock + + # There's a lock in callbacks, let's get out of the confirmation lock before entering another one + do_callbacks echo '{"result":"confirmed"}' return 0 diff --git a/proxy_docker/app/script/getactivewatches.sh b/proxy_docker/app/script/getactivewatches.sh index f2187b0..e639945 100644 --- a/proxy_docker/app/script/getactivewatches.sh +++ b/proxy_docker/app/script/getactivewatches.sh @@ -3,62 +3,129 @@ . ./trace.sh . ./sql.sh -getactivewatches() -{ - trace "Entering getactivewatches()..." +getactivewatches() { + trace "Entering getactivewatches()..." - local watches - watches=$(sql "SELECT id, address, imported, callback0conf, callback1conf, inserted_ts FROM watching WHERE watching AND NOT calledback1conf") - returncode=$? - trace_rc ${returncode} + local watches + # Let's build the string directly with sqlite instead of manipulating multiple strings afterwards, it's faster. + # {"id":"${id}","address":"${address}","imported":"${imported}","unconfirmedCallbackURL":"${cb0conf_url}","confirmedCallbackURL":"${cb1conf_url}","watching_since":"${timestamp}"} + watches=$(sql "SELECT '{\"id\":\"' || id || '\",\"address\":\"' || address || '\",\"imported\":\"' || imported || '\",\"unconfirmedCallbackURL\":\"' || callback0conf || '\",\"confirmedCallbackURL\":\"' || callback1conf || '\",\"watching_since\":\"' || inserted_ts || '\"}' FROM watching WHERE watching AND NOT calledback1conf") + returncode=$? + trace_rc ${returncode} - local id - local address - local imported - local inserted - local cb0conf_url - local cb1conf_url - local timestamp - local notfirst=false + local notfirst=false - echo -n "{\"watches\":[" + echo -n "{\"watches\":[" - local IFS=$'\n' - for row in ${watches} - do - if ${notfirst}; then - echo "," - else - notfirst=true - fi - trace "[getactivewatches] row=${row}" - id=$(echo "${row}" | cut -d '|' -f1) - trace "[getactivewatches] id=${id}" - address=$(echo "${row}" | cut -d '|' -f2) - trace "[getactivewatches] address=${address}" - imported=$(echo "${row}" | cut -d '|' -f3) - trace "[getactivewatches] imported=${imported}" - cb0conf_url=$(echo "${row}" | cut -d '|' -f4) - trace "[getactivewatches] cb0conf_url=${cb0conf_url}" - cb1conf_url=$(echo "${row}" | cut -d '|' -f5) - trace "[getactivewatches] cb1conf_url=${cb1conf_url}" - timestamp=$(echo "${row}" | cut -d '|' -f6) - trace "[getactivewatches] timestamp=${timestamp}" + local IFS=$'\n' + for row in ${watches} + do + if ${notfirst}; then + echo "," + else + notfirst=true + fi + trace "[getactivewatches] row=${row}" - data="{\"id\":\"${id}\"," - data="${data}\"address\":\"${address}\"," - data="${data}\"imported\":\"${imported}\"," - data="${data}\"unconfirmedCallbackURL\":\"${cb0conf_url}\"," - data="${data}\"confirmedCallbackURL\":\"${cb1conf_url}\"," - data="${data}\"watching_since\":\"${timestamp}\"}" - trace "[getactivewatches] data=${data}" + echo -n "${row}" + done - echo -n "${data}" - done + echo "]}" - echo "]}" - - return ${returncode} + return ${returncode} } -case "${0}" in *getactivewatches.sh) getactivewatches;; esac +getactivewatchesbyxpub() { + trace "Entering getactivewatchesbyxpub()..." + + local xpub=${1} + local returncode + + getactivewatchesxpub "pub32" ${xpub} + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} + +getactivewatchesbylabel() { + trace "Entering getactivewatchesbylabel()..." + + local label=${1} + local returncode + + getactivewatchesxpub "label" ${label} + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} + +getactivewatchesxpub() { + trace "Entering getactivewatchesxpub()..." + + local where=${1} + trace "[getactivewatchesxpub] where=${where}" + local value=${2} + trace "[getactivewatchesxpub] value=${value}" + local watches + + # Let's build the string directly with sqlite instead of manipulating multiple strings afterwards, it's faster. + # {"id":"${id}","address":"${address}","imported":"${imported}","unconfirmedCallbackURL":"${cb0conf_url}","confirmedCallbackURL":"${cb1conf_url}","watching_since":"${timestamp}","derivation_path":"${derivation_path}","pub32_index":"${pub32_index}"} + watches=$(sql "SELECT '{\"id\":\"' || w.id || '\",\"address\":\"' || address || '\",\"imported\":\"' || imported || '\",\"unconfirmedCallbackURL\":\"' || w.callback0conf || '\",\"confirmedCallbackURL\":\"' || w.callback1conf || '\",\"watching_since\":\"' || w.inserted_ts || '\",\"derivation_path\":\"' || derivation_path || '\",\"pub32_index\":\"' || pub32_index || '\"}' FROM watching w, watching_by_pub32 w32 WHERE watching_by_pub32_id = w32.id AND ${where} = \"${value}\" AND w.watching AND NOT calledback1conf") + returncode=$? + trace_rc ${returncode} + + local notfirst=false + + echo -n "{\"watches\":[" + + local IFS=$'\n' + for row in ${watches} + do + if ${notfirst}; then + echo "," + else + notfirst=true + fi + trace "[getactivewatchesxpub] row=${row}" + + echo -n "${row}" + done + + echo "]}" + + return ${returncode} +} + +getactivexpubwatches() { + trace "Entering getactivexpubwatches()..." + + local watches + # Let's build the string directly with sqlite instead of manipulating multiple strings afterwards, it's faster. + # {"id":"${id}","pub32":"${pub32}","label":"${label}","derivation_path":"${derivation_path}","last_imported_n":${last_imported_n},"unconfirmedCallbackURL":"${cb0conf_url}","confirmedCallbackURL":"${cb1conf_url}","watching_since":"${timestamp}"} + watches=$(sql "SELECT '{\"id\":\"' || id || '\",\"pub32\":\"' || pub32 || '\",\"label\":\"' || label || '\",\"derivation_path\":\"' || derivation_path || '\",\"last_imported_n\":' || last_imported_n || ',\"unconfirmedCallbackURL\":\"' || callback0conf || '\",\"confirmedCallbackURL\":\"' || callback1conf || '\",\"watching_since\":\"' || inserted_ts || '\"}' FROM watching_by_pub32 WHERE watching") + returncode=$? + trace_rc ${returncode} + + local notfirst=false + + echo -n "{\"watches\":[" + + local IFS=$'\n' + for row in ${watches} + do + if ${notfirst}; then + echo "," + else + notfirst=true + fi + trace "[getactivexpubwatches] row=${row}" + + echo -n "${row}" + done + + echo "]}" + + return ${returncode} +} diff --git a/proxy_docker/app/script/importaddress.sh b/proxy_docker/app/script/importaddress.sh index fce18ee..ea3b619 100644 --- a/proxy_docker/app/script/importaddress.sh +++ b/proxy_docker/app/script/importaddress.sh @@ -3,19 +3,50 @@ . ./trace.sh . ./sendtobitcoinnode.sh -importaddress_rpc() -{ - trace "[Entering importaddress_rpc()]" +importaddress_rpc() { + trace "[Entering importaddress_rpc()]" - local address=${1} - local data="{\"method\":\"importaddress\",\"params\":[\"${address}\",\"\",false]}" - local result - result=$(send_to_watcher_node ${data}) - local returncode=$? + local address=${1} + local data="{\"method\":\"importaddress\",\"params\":[\"${address}\",\"\",false]}" + local result + result=$(send_to_watcher_node ${data}) + local returncode=$? - echo "${result}" + echo "${result}" - return ${returncode} + return ${returncode} } -case "${0}" in *importaddress.sh) importaddress_rpc $@;; esac +importmulti_rpc() { + trace "[Entering importmulti_rpc()]" + + local walletname=${1} + local label=${2} + local addresses=$(echo "${3}" | jq ".addresses" | tr -d '\n ') +# trace "[importmulti_rpc] addresses=${addresses}" + + # Will look like: + # [{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"}] + + # We want: + # [{"scriptPubKey":{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},"timestamp":"now","watchonly":true,"label":"xpub"},{"scriptPubKey":{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},"timestamp":"now","watchonly":true,"label":"xpub"},{"scriptPubKey":{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},"timestamp":"now","watchonly":true,"label":"xpub"}] + + # {"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"}, + # {"scriptPubKey":{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},"timestamp":"now","watchonly":true,"label":"xpub"}, + + addresses=$(echo "${addresses}" | sed "s/\"address\"/\"scriptPubKey\":\{\"address\"/g" | sed "s/}/},\"timestamp\":\"now\",\"watchonly\":true,\"label\":\"${label}\"}/g") +# trace "[importmulti_rpc] addresses=${addresses}" + + # Now we use that in the RPC string + + local rpcstring="{\"method\":\"importmulti\",\"params\":[${addresses},{\"rescan\":false}]}" +# trace "[importmulti_rpc] rpcstring=${rpcstring}" + + local result + result=$(send_to_watcher_node_wallet ${walletname} ${rpcstring}) + local returncode=$? + + echo "${result}" + + return ${returncode} +} diff --git a/proxy_docker/app/script/manage_missed_conf.sh b/proxy_docker/app/script/manage_missed_conf.sh index 610e776..d58f4f9 100644 --- a/proxy_docker/app/script/manage_missed_conf.sh +++ b/proxy_docker/app/script/manage_missed_conf.sh @@ -5,81 +5,76 @@ . ./importaddress.sh . ./confirmation.sh -manage_not_imported() -{ - # When we tried to import watched addresses in the watching node, - # if it didn't succeed, we try again here. +manage_not_imported() { + # When we tried to import watched addresses in the watching node, + # if it didn't succeed, we try again here. - trace "[Entering manage_not_imported()]" + trace "[Entering manage_not_imported()]" - local watches=$(sql 'SELECT address FROM watching WHERE watching AND NOT imported') - trace "[manage_not_imported] watches=${watches}" + local watches=$(sql 'SELECT address FROM watching WHERE watching AND NOT imported') + trace "[manage_not_imported] watches=${watches}" - local result - local returncode - local IFS=$'\n' - for address in ${watches} - do - result=$(importaddress_rpc "${address}") - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -eq 0 ]; then - sql "UPDATE watching SET imported=1 WHERE address=\"${address}\"" - fi - done + local result + local returncode + local IFS=$'\n' + for address in ${watches} + do + result=$(importaddress_rpc "${address}") + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + sql "UPDATE watching SET imported=1 WHERE address=\"${address}\"" + fi + done - return 0 + return 0 } -manage_missed_conf() -{ - # Maybe we missed confirmations, because we were down or no network or - # whatever, so we look at what might be missed and do confirmations. +manage_missed_conf() { + # Maybe we missed confirmations, because we were down or no network or + # whatever, so we look at what might be missed and do confirmations. - trace "[Entering manage_missed_conf()]" + trace "[Entering manage_missed_conf()]" -# local watches=$(sql 'SELECT address FROM watching WHERE txid IS NULL AND watching AND imported') - #local watches=$(sql 'SELECT address FROM watching LEFT JOIN watching_tx ON id = watching_id WHERE watching AND imported AND tx_id IS NULL') - local watches=$(sql 'SELECT address FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx t ON t.id = tx_id WHERE watching AND imported AND (tx_id IS NULL OR t.confirmations=0)') - trace "[manage_missed_conf] watches=${watches}" - if [ ${#watches} -eq 0 ]; then - trace "[manage_missed_conf] Nothing missed!" - return 0 - fi + local watches=$(sql 'SELECT address FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx t ON t.id = tx_id WHERE watching AND imported AND (tx_id IS NULL OR t.confirmations=0)') + trace "[manage_missed_conf] watches=${watches}" + if [ ${#watches} -eq 0 ]; then + trace "[manage_missed_conf] Nothing missed!" + return 0 + fi - local addresses - local data - local result - local returncode - local IFS=$'\n' - for address in ${watches} - do - if [ -z ${addresses} ]; then - addresses="[\"${address}\"" - else - addresses="${addresses},\"${address}\"" - fi - done - addresses="${addresses}]" + local addresses + local data + local result + local returncode + local IFS=$'\n' + for address in ${watches} + do + if [ -z ${addresses} ]; then + addresses="[\"${address}\"" + else + addresses="${addresses},\"${address}\"" + fi + done + addresses="${addresses}]" - # Watching addresses with UTXO are transactions being watched that went through without us knowing it, we missed the conf - data="{\"method\":\"listunspent\",\"params\":[0, 9999999, ${addresses}]}" - local unspents - unspents=$(send_to_watcher_node ${data}) - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -ne 0 ]; then - return ${returncode} - fi + # Watching addresses with UTXO are transactions being watched that went through without us knowing it, we missed the conf + data="{\"method\":\"listunspent\",\"params\":[0, 9999999, ${addresses}]}" + local unspents + unspents=$(send_to_watcher_node ${data}) + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -ne 0 ]; then + return ${returncode} + fi -# | tr -d '"' - local txids=$(echo ${unspents} | jq ".result[].txid" | tr -d '"') - for txid in ${txids} - do - confirmation "${txid}" - done + local txids=$(echo "${unspents}" | jq -r ".result[].txid") + for txid in ${txids} + do + confirmation "${txid}" + done - return 0 + return 0 } diff --git a/proxy_docker/app/script/newblock.sh b/proxy_docker/app/script/newblock.sh new file mode 100644 index 0000000..c266eeb --- /dev/null +++ b/proxy_docker/app/script/newblock.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +. ./trace.sh +. ./callbacks_txid.sh + +newblock() { + trace "Entering newblock()..." + + local request=${1} + local blockhash=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3) + + do_callbacks_txid +} diff --git a/proxy_docker/app/script/notify.sh b/proxy_docker/app/script/notify.sh new file mode 100644 index 0000000..f190ed1 --- /dev/null +++ b/proxy_docker/app/script/notify.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +. ./trace.sh + +notify_web() { + trace "Entering notify_web()..." + + local url=${1} + + # Let's encode the body to base64 so we won't have to escape the special chars... + local body=$(echo "${2}" | base64 | tr -d '\n') + + local returncode + local response + local http_code + local curl_code + + # We use the pid as the response-topic, so there's no conflict in responses. + trace "[notify_web] mosquitto_rr -h broker -W 21 -t notifier -e \"response/$$\" -m \"{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}\"" + response=$(mosquitto_rr -h broker -W 21 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}") + returncode=$? + trace_rc ${returncode} + + # The response looks like this: {"curl_code":0,"http_code":200,"body":"..."} where the body + # is the base64(response body) but we don't need the response content here. + trace "[notify_web] response=${response}" + curl_code=$(echo "${response}" | jq -r ".curl_code") + trace "[notify_web] curl_code=${curl_code}" + http_code=$(echo "${response}" | jq -r ".http_code") + trace "[notify_web] http_code=${http_code}" + + if [ "${curl_code}" -eq "0" ] && [ "${returncode}" -eq "0" ]; then + if [ "${http_code}" -lt "400" ]; then + return 0 + else + return ${http_code} + fi + else + return ${curl_code} || ${returncode} + fi + +} \ No newline at end of file diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index 70548cb..ef6d243 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -2,15 +2,21 @@ . ./trace.sh -serve_ots_stamp() -{ +serve_ots_stamp() { trace "Entering serve_ots_stamp()..." local request=${1} - local hash=$(echo "${request}" | jq ".hash" | tr -d '"') + local hash=$(echo "${request}" | jq -r ".hash") trace "[serve_ots_stamp] hash=${hash}" - local callbackUrl=$(echo "${request}" | jq ".callbackUrl" | tr -d '"') + local callbackUrl + callbackUrl=$(echo "${request}" | jq -er ".callbackUrl") + if [ "$?" -ne "0" ]; then + # callbackUrl tag null, so there's no callbackUrl provided + callbackUrl= + else + callbackUrl=$(echo "${callbackUrl}" | tr -d '"') + fi trace "[serve_ots_stamp] callbackUrl=${callbackUrl}" local result @@ -39,7 +45,7 @@ serve_ots_stamp() errorstring="Duplicate stamping request, hash already exists in DB and been OTS requested" returncode=1 else - errorstring=$(request_ots_stamp "${hash}") + errorstring=$(request_ots_stamp "${hash}" ${id_inserted}) returncode=$? fi else @@ -49,7 +55,7 @@ serve_ots_stamp() if [ "${returncode}" -eq "0" ]; then id_inserted=$(sql "SELECT id FROM stamp WHERE hash='${hash}'") trace_rc $? - errorstring=$(request_ots_stamp "${hash}") + errorstring=$(request_ots_stamp "${hash}" ${id_inserted}) returncode=$? trace_rc ${returncode} else @@ -75,16 +81,17 @@ serve_ots_stamp() return ${returncode} } -request_ots_stamp() -{ +request_ots_stamp() { # Request the OTS server to stamp local hash=${1} + local id=${2} local returncode local result local errorstring trace "[request_ots_stamp] Stamping..." + trace "[request_ots_stamp] curl -s ${OTSCLIENT_CONTAINER}/stamp/${hash}" result=$(curl -s ${OTSCLIENT_CONTAINER}/stamp/${hash}) returncode=$? trace_rc ${returncode} @@ -107,18 +114,18 @@ request_ots_stamp() if [ "${returncode}" -eq "0" ]; then # "already exists" found, let's try updating DB again trace "[request_ots_stamp] was already requested to the OTS server... let's update the DB, looks like it didn't work on first try" - sql "UPDATE stamp SET requested=1 WHERE hash='${hash}'" + sql "UPDATE stamp SET requested=1 WHERE id=${id}" errorstring="Duplicate stamping request, hash already exists in DB and been OTS requested" returncode=1 else # If OTS CLIENT responded with an error, it is not down, it just can't stamp it. ABORT. trace "[request_ots_stamp] Stamping error: ${errorstring}" - sql "DELETE FROM stamp WHERE hash='${hash}'" + sql "DELETE FROM stamp WHERE id=${id}" returncode=1 fi else trace "[request_ots_stamp] Stamping request sent successfully!" - sql "UPDATE stamp SET requested=1 WHERE hash='${hash}'" + sql "UPDATE stamp SET requested=1 WHERE id=${id}" errorstring="" returncode=0 fi @@ -132,8 +139,7 @@ request_ots_stamp() return ${returncode} } -serve_ots_backoffice() -{ +serve_ots_backoffice() { # What we want to do here: # ======================== # Re-request the unrequested calls to ots_stamp @@ -146,13 +152,14 @@ serve_ots_backoffice() local returncode # Let's fetch all the incomplete stamping request - local callbacks=$(sql 'SELECT hash, callbackUrl, requested, upgraded FROM stamp WHERE NOT calledback') + local callbacks=$(sql 'SELECT hash, callbackUrl, requested, upgraded, id FROM stamp WHERE NOT calledback') trace "[serve_ots_backoffice] callbacks=${callbacks}" local url local hash local requested local upgraded + local id local IFS=$'\n' for row in ${callbacks} do @@ -163,10 +170,12 @@ serve_ots_backoffice() trace "[serve_ots_backoffice] requested=${requested}" upgraded=$(echo "${row}" | cut -d '|' -f4) trace "[serve_ots_backoffice] upgraded=${upgraded}" + id=$(echo "${row}" | cut -d '|' -f5) + trace "[serve_ots_backoffice] id=${id}" if [ "${requested}" -ne "1" ]; then # Re-request the unrequested calls to ots_stamp - request_ots_stamp "${hash}" + request_ots_stamp "${hash}" ${id} returncode=$? else if [ "${upgraded}" -ne "1" ]; then @@ -188,7 +197,7 @@ serve_ots_backoffice() else # No failure, upgraded trace "[serve_ots_backoffice] just upgraded!" - sql "UPDATE stamp SET upgraded=1 WHERE hash=\"${hash}\"" + sql "UPDATE stamp SET upgraded=1 WHERE id=${id}" trace_rc $? upgraded=1 @@ -200,13 +209,24 @@ serve_ots_backoffice() url=$(echo "${row}" | cut -d '|' -f2) trace "[serve_ots_backoffice] url=${url}" - # Call back newly upgraded stamps - curl -H "X-Forwarded-Proto: https" ${url} - returncode=$? - trace_rc ${returncode} + # Call back newly upgraded stamps if url provided + if [ -n ${url} ]; then + trace "[serve_ots_backoffice] url is not empty, now trying to call it!" - if [ "${returncode}" -eq "0" ]; then - sql "UPDATE stamp SET calledback=1 WHERE hash=\"${hash}\"" + notify_web "${url}" + returncode=$? + trace_rc ${returncode} + + # Even if curl executed ok, we need to make sure the http return code is also ok + + if [ "${returncode}" -eq "0" ]; then + sql "UPDATE stamp SET calledback=1 WHERE id=${id}" + trace_rc $? + fi + else + trace "[serve_ots_backoffice] url is empty, obviously won't try to call it!" + + sql "UPDATE stamp SET calledback=1 WHERE id=${id}" trace_rc $? fi fi @@ -214,8 +234,7 @@ serve_ots_backoffice() done } -serve_ots_getfile() -{ +serve_ots_getfile() { trace "Entering serve_ots_getfile()..." local hash=${1} @@ -227,3 +246,182 @@ serve_ots_getfile() return ${returncode} } + +serve_ots_verify() { + + trace "Entering serve_ots_verify()..." + + local request=${1} + local hash + hash=$(echo "${request}" | jq -e ".hash") + if [ "$?" -ne "0" ]; then + # Hash tag null, so there's no hash provided + trace "[serve_ots_verify] hash field missing!" + echo "{\"method\":\"ots_verify\",\"result\":\"error\",\"message\":\"hash must be provided\"}" + return 1 + else + hash=$(echo "${hash}" | tr -d '"') + fi + trace "[serve_ots_verify] hash=${hash}" + local base64otsfile + base64otsfile=$(echo "${request}" | jq -e ".base64otsfile") + if [ "$?" -ne "0" ]; then + # base64otsfile tag null, so there's no base64otsfile provided + base64otsfile= + else + base64otsfile=$(echo "${base64otsfile}" | tr -d '"') + fi + trace "[serve_ots_verify] base64otsfile=${base64otsfile}" + + # If file is provided, we will execute info on it + # If file not provided, we will check for hash.ots in our folder and execute info on it + if [ -z "${base64otsfile}" ]; then + if [ -f otsfiles/${hash}.ots ]; then + trace "[serve_ots_verify] Constructing base64otsfile from provided hash, file otsfiles/${hash}.ots" + base64otsfile=$(cat otsfiles/${hash}.ots | base64 | tr -d '\n') + else + trace "[serve_ots_verify] File otsfiles/${hash}.ots does not exists!" + echo "{\"method\":\"ots_verify\",\"hash\":\"${hash}\",\"result\":\"error\",\"message\":\"OTS File not found\"}" + return 1 + fi + fi + + local result + local message + local returncode + + trace "[serve_ots_verify] request_ots_verify \"${hash}\" \"${base64otsfile}\"" + result=$(request_ots_verify "${hash}" "${base64otsfile}") + returncode=$? + trace_rc ${returncode} + + message=$(echo ${result} | jq ".message") + result=$(echo ${result} | jq ".result") + result="{\"method\":\"ots_verify\",\"hash\":\"${hash}\",\"result\":${result},\"message\":${message}}" + + trace "[serve_ots_verify] result=${result}" + + # Output response to stdout before exiting with return code + echo "${result}" + + return ${returncode} +} + +request_ots_verify() { + # Request the OTS server to verify + + local hash=${1} + trace "[request_ots_verify] hash=${hash}" + local base64otsfile=${2} + trace "[request_ots_verify] base64otsfile=${base64otsfile}" + local returncode + local result + local data + + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + data="{\"hash\":\"${hash}\",\"base64otsfile\":\"${base64otsfile}\"}" + trace "[request_ots_verify] data=${data}" + + trace "[request_ots_verify] Verifying..." + trace "[request_ots_stamp] curl -s -d \"${data}\" ${OTSCLIENT_CONTAINER}/verify" + result=$(curl -s -d "${data}" ${OTSCLIENT_CONTAINER}/verify) + returncode=$? + trace_rc ${returncode} + trace "[request_ots_verify] Verifying result=${result}" + + if [ "${returncode}" -ne 0 ]; then + trace "[request_ots_verify] Verifying error" + fi + + echo "${result}" + return ${returncode} +} + +serve_ots_info() { + + trace "Entering serve_ots_info()..." + + local request=${1} + local hash + hash=$(echo "${request}" | jq -e ".hash") + if [ "$?" -ne "0" ]; then + # Hash tag null, so there's no hash provided + hash= + else + hash=$(echo "${hash}" | tr -d '"') + fi + trace "[serve_ots_info] hash=${hash}" + local base64otsfile + base64otsfile=$(echo "${request}" | jq -e ".base64otsfile") + if [ "$?" -ne "0" ]; then + # base64otsfile tag null, so there's no base64otsfile provided + base64otsfile= + else + base64otsfile=$(echo "${base64otsfile}" | tr -d '"') + fi + trace "[serve_ots_info] base64otsfile=${base64otsfile}" + + # If file is provided, we will execute info on it + # If file not provided, we will check for hash.ots in our folder and execute info on it + if [ -z "${base64otsfile}" ]; then + if [ -f otsfiles/${hash}.ots ]; then + trace "[serve_ots_info] Constructing base64otsfile from provided hash, file otsfiles/${hash}.ots" + base64otsfile=$(cat otsfiles/${hash}.ots | base64 | tr -d '\n') + else + trace "[serve_ots_info] File otsfiles/${hash}.ots does not exists!" + echo "{\"method\":\"ots_info\",\"result\":\"error\",\"message\":\"OTS File not found\"}" + return 1 + fi + fi + + local result + local message + local returncode + + trace "[serve_ots_info] request_ots_info \"${base64otsfile}\"" + result=$(request_ots_info "${base64otsfile}") + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + result=$(echo ${result} | jq ".result") + result="{\"method\":\"ots_info\",\"result\":\"success\",\"message\":${result}}" + else + result="{\"method\":\"ots_info\",\"result\":\"error\",\"message\":${result}}" + fi + + trace "[serve_ots_info] result=${result}" + + # Output response to stdout before exiting with return code + echo "${result}" + + return ${returncode} +} + +request_ots_info() { + # Request the OTS server to verify + + local base64otsfile=${1} + trace "[request_ots_info] base64otsfile=${base64otsfile}" + local returncode + local result + local data + + # BODY {"base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + data="{\"base64otsfile\":\"${base64otsfile}\"}" + trace "[request_ots_info] data=${data}" + + trace "[request_ots_info] Parsing..." + trace "[request_ots_info] curl -s -d \"${data}\" ${OTSCLIENT_CONTAINER}/info" + result=$(curl -s -d "${data}" ${OTSCLIENT_CONTAINER}/info) + returncode=$? + trace_rc ${returncode} + trace "[request_ots_info] OTS info result=${result}" + + if [ "${returncode}" -ne 0 ]; then + trace "[request_ots_info] OTS info error" + fi + + echo "${result}" + return ${returncode} +} diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 433ffba..dad4ca2 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -18,9 +18,9 @@ . ./bitcoin.sh . ./call_lightningd.sh . ./ots.sh +. ./newblock.sh -main() -{ +main() { trace "Entering main()..." local step=0 @@ -60,7 +60,7 @@ main() fi # line=content-length: 406 case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) - content_length=$(echo ${line} | cut -d ':' -f2) + content_length=$(echo "${line}" | cut -d ':' -f2) trace "[main] content_length=${content_length}"; ;; esac @@ -71,6 +71,21 @@ main() trace "[main] line=${line}" fi case "${cmd}" in + helloworld) + # GET http://192.168.111.152:8080/helloworld + response_to_client "Hello, world!" 0 + break + ;; + installation_info) + # GET http://192.168.111.152:8080/info + if [ -f "$DB_PATH/info.json" ]; then + response=$( cat "$DB_PATH/info.json" ) + else + response='{ "error": "missing installation data" }' + fi + response_to_client "${response}" ${?} + break + ;; watch) # POST http://192.168.111.152:8080/watch # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"} @@ -86,6 +101,59 @@ main() response_to_client "${response}" ${?} break ;; + watchxpub) + # POST http://192.168.111.152:8080/watchxpub + # BODY {"label":"4421","pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/n","nstart":0,"unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"} + # curl -H "Content-Type: application/json" -d '{"label":"2219","pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/1/n","nstart":55,"unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}' proxy:8888/watchxpub + + response=$(watchpub32request "${line}") + response_to_client "${response}" ${?} + break + ;; + unwatchxpubbyxpub) + # GET http://192.168.111.152:8080/unwatchxpubbyxpub/tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk + + response=$(unwatchpub32request "${line}") + response_to_client "${response}" ${?} + break + ;; + unwatchxpubbylabel) + # GET http://192.168.111.152:8080/unwatchxpubbylabel/4421 + + response=$(unwatchpub32labelrequest "${line}") + response_to_client "${response}" ${?} + break + ;; + getactivewatchesbyxpub) + # GET http://192.168.111.152:8080/getactivewatchesbyxpub/tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk + + response=$(getactivewatchesbyxpub $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + getactivewatchesbylabel) + # GET http://192.168.111.152:8080/getactivewatchesbylabel/4421 + + response=$(getactivewatchesbylabel $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + getactivexpubwatches) + # GET http://192.168.111.152:8080/getactivexpubwatches + + response=$(getactivexpubwatches) + response_to_client "${response}" ${?} + break + ;; + watchtxid) + # POST http://192.168.111.152:8080/watchtxid + # BODY {"txid":"b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387","confirmedCallbackURL":"192.168.111.233:1111/callback1conf","xconfCallbackURL":"192.168.111.233:1111/callbackXconf","nbxconf":6} + # curl -H "Content-Type: application/json" -d '{"txid":"b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387","confirmedCallbackURL":"192.168.111.233:1111/callback1conf","xconfCallbackURL":"192.168.111.233:1111/callbackXconf","nbxconf":6}' proxy:8888/watchtxid + + response=$(watchtxidrequest "${line}") + response_to_client "${response}" ${?} + break + ;; getactivewatches) # curl (GET) 192.168.111.152:8080/getactivewatches @@ -100,6 +168,13 @@ main() response_to_client "${response}" ${?} break ;; + newblock) + # curl (GET) 192.168.111.152:8080/newblock/000000000000005c987120f3b6f995c95749977ef1a109c89aa74ce4bba97c1f + + response=$(newblock "${line}") + response_to_client "${response}" ${?} + break + ;; getbestblockhash) # curl (GET) http://192.168.111.152:8080/getbestblockhash @@ -107,6 +182,13 @@ main() response_to_client "${response}" ${?} break ;; + getblockhash) + # curl (GET) http://192.168.111.152:8080/getblockhash/522322 + + response=$(get_blockhash $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; getblockinfo) # curl (GET) http://192.168.111.152:8080/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea @@ -114,6 +196,12 @@ main() response_to_client "${response}" ${?} break ;; + getblockchaininfo) + # http://192.168.111.152:8080/getblockchaininfo + + response=$(get_blockchain_info) + response_to_client "${response}" ${?} + ;; gettransaction) # curl (GET) http://192.168.111.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 @@ -144,10 +232,25 @@ main() response_to_client "${response}" ${?} break ;; + getbalancebyxpub) + # curl (GET) http://192.168.111.152:8080/getbalancebyxpub/upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb + + response=$(getbalancebyxpub $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + getbalancebyxpublabel) + # curl (GET) http://192.168.111.152:8080/getbalancebyxpublabel/2219 + + response=$(getbalancebyxpublabel $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; getnewaddress) # curl (GET) http://192.168.111.152:8080/getnewaddress + # curl (GET) http://192.168.111.152:8080/getnewaddress/bech32 - response=$(getnewaddress) + response=$(getnewaddress $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) response_to_client "${response}" ${?} break ;; @@ -159,11 +262,20 @@ main() response_to_client "${response}" ${?} break ;; + bumpfee) + # POST http://192.168.111.152:8080/bumpfee + # BODY {"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648","confTarget":4} + # BODY {"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"} + + response=$(bumpfee "${line}") + response_to_client "${response}" ${?} + break + ;; addtobatch) # POST http://192.168.111.152:8080/addtobatch # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} - response=$(addtobatching $(echo "${line}" | jq ".address" | tr -d '"') $(echo "${line}" | jq ".amount")) + response=$(addtobatching $(echo "${line}" | jq -r ".address") $(echo "${line}" | jq ".amount")) response_to_client "${response}" ${?} break ;; @@ -188,7 +300,14 @@ main() # BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"} # BODY {"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"} - response=$(send_to_pycoin "${line}") + response=$(derivepubpath "${line}") + response_to_client "${response}" ${?} + break + ;; + getmempoolinfo) + # curl GET http://192.168.111.152:8080/getmempoolinfo + + response=$(get_mempool_info) response_to_client "${response}" ${?} break ;; @@ -199,9 +318,16 @@ main() response_to_client "${response}" ${?} break ;; + ln_getconnectionstring) + # GET http://192.168.111.152:8080/ln_getconnectionstring + + response=$(ln_get_connection_string) + response_to_client "${response}" ${?} + break + ;; ln_create_invoice) # POST http://192.168.111.152:8080/ln_create_invoice - # BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"900"} + # BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"900","callback_url":"http://192.168.122.159"} response=$(ln_create_invoice "${line}") response_to_client "${response}" ${?} @@ -222,10 +348,45 @@ main() response_to_client "${response}" ${?} break ;; + ln_connectfund) + # POST http://192.168.111.152:8080/ln_connectfund + # BODY {"peer":"nodeId@ip:port","msatoshi":"100000","callbackUrl":"https://callbackUrl/?channelReady=f3y2c3cvm4uzg2gq"} + # curl -H "Content-Type: application/json" -d '{"peer":"nodeId@ip:port","msatoshi":"100000","callbackUrl":"https://callbackUrl/?channelReady=f3y2c3cvm4uzg2gq"}' proxy:8888/ln_connectfund + + response=$(ln_connectfund "${line}") + response_to_client "${response}" ${?} + break + ;; + ln_getinvoice) + # GET http://192.168.111.152:8080/ln_getinvoice/label + # GET http://192.168.111.152:8080/ln_getinvoice/koNCcrSvhX3dmyFhW + + response=$(ln_getinvoice $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + ln_delinvoice) + # GET http://192.168.111.152:8080/ln_delinvoice/label + # GET http://192.168.111.152:8080/ln_delinvoice/koNCcrSvhX3dmyFhW + + response=$(ln_delinvoice $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + ln_decodebolt11) + # GET http://192.168.111.152:8080/ln_decodebolt11/bolt11 + # GET http://192.168.111.152:8080/ln_decodebolt11/lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3 + + response=$(ln_decodebolt11 $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; ots_stamp) # POST http://192.168.111.152:8080/ots_stamp # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","callbackUrl":"192.168.111.233:1111/callbackUrl"} + # curl -v -d "{\"hash\":\"a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8\"}" localhost:8888/ots_stamp + response=$(serve_ots_stamp "${line}") response_to_client "${response}" ${?} break @@ -243,6 +404,32 @@ main() serve_ots_getfile $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3) break ;; + ots_verify) + # POST http://192.168.111.152:8080/ots_verify + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7"} + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + + # curl -v -d "{\"hash\":\"a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8\"}" localhost:8888/ots_verify + # curl -v -d "{\"hash\":\"a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8\",\"base64otsfile\":\"$(cat a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8.ots | base64 | tr -d '\n')\"}" localhost:8888/ots_verify + + response=$(serve_ots_verify "${line}") + response_to_client "${response}" ${?} + break + ;; + ots_info) + # POST http://192.168.111.152:8080/ots_info + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7"} + # BODY {"base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","base64otsfile":"AE9wZW5UaW1lc3RhbXBzAABQcm9vZ...gABYiWDXPXGQEDxNch"} + + # curl -v -d "{\"hash\":\"a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8\"}" localhost:8888/ots_info + # curl -v -d "{\"base64otsfile\":\"$(cat a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8.ots | base64 | tr -d '\n')\"}" localhost:8888/ots_info + # curl -v -d "{\"hash\":\"a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8\",\"base64otsfile\":\"$(cat a6ea81a46fec3d02d40815b8667b388351edecedc1cc9f97aab55b566db7aac8.ots | base64 | tr -d '\n')\"}" localhost:8888/ots_info + + response=$(serve_ots_info "${line}") + response_to_client "${response}" ${?} + break + ;; esac break fi @@ -257,4 +444,5 @@ export DB_PATH export DB_FILE main +trace "[requesthandler] exiting" exit $? diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh index ad4467d..40f2277 100644 --- a/proxy_docker/app/script/responsetoclient.sh +++ b/proxy_docker/app/script/responsetoclient.sh @@ -9,16 +9,17 @@ response_to_client() local response=${1} local returncode=${2} local contenttype=${3} + local length=$(echo -en "${response}" | wc -c) [ -z "${contenttype}" ] && contenttype="application/json" ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" - echo -en "Content-Type: ${contenttype}\r\nContent-Length: ${#response}\r\n\r\n${response}" + echo -en "Content-Type: ${contenttype}\r\nContent-Length: ${length}\r\n\r\n${response}" # Small delay needed for the data to be processed correctly by peer - sleep 0.5s + sleep 1 } htmlfile_response_to_client() @@ -43,7 +44,7 @@ htmlfile_response_to_client() [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" # Small delay needed for the data to be processed correctly by peer - sleep 0.5s + sleep 1 } binfile_response_to_client() @@ -68,7 +69,7 @@ binfile_response_to_client() [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" # Small delay needed for the data to be processed correctly by peer - sleep 0.5s + sleep 1 } case "${0}" in *responsetoclient.sh) response_to_client $@;; esac diff --git a/proxy_docker/app/script/sendtobitcoinnode.sh b/proxy_docker/app/script/sendtobitcoinnode.sh index 3a9814a..b82a641 100644 --- a/proxy_docker/app/script/sendtobitcoinnode.sh +++ b/proxy_docker/app/script/sendtobitcoinnode.sh @@ -2,58 +2,86 @@ . ./trace.sh -send_to_watcher_node() -{ - trace "Entering send_to_watcher_node()..." - send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_NODE_RPC_CFG} $@ - local returncode=$? - trace_rc ${returncode} - return ${returncode} +send_to_watcher_node() { + trace "Entering send_to_watcher_node()..." + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL}/${WATCHER_BTC_NODE_DEFAULT_WALLET} ${WATCHER_NODE_RPC_CFG} $@ + local returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -ne 0 ]; then + # Ok, since we now have multiple watching wallets, we need to try them all if it fails + # We have 2 right now: watching and watching-for-xpubs + send_to_watcher_node_wallet ${WATCHER_BTC_NODE_XPUB_WALLET} $@ + returncode=$? + trace_rc ${returncode} + fi + + return ${returncode} +} + +send_to_xpub_watcher_wallet() { + trace "Entering send_to_xpub_watcher_wallet()..." + + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL}/${WATCHER_BTC_NODE_XPUB_WALLET} ${WATCHER_NODE_RPC_CFG} $@ + local returncode=$? + trace_rc ${returncode} + return ${returncode} +} + +send_to_watcher_node_wallet() { + trace "Entering send_to_watcher_node_wallet()..." + local walletname=$1 + shift + trace "[send_to_watcher_node_wallet] walletname=${walletname}" + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL}/${walletname} ${WATCHER_NODE_RPC_CFG} $@ + local returncode=$? + trace_rc ${returncode} + return ${returncode} } send_to_spender_node() { - trace "Entering send_to_spender_node()..." - send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_NODE_RPC_CFG} $@ - local returncode=$? - trace_rc ${returncode} - return ${returncode} + trace "Entering send_to_spender_node()..." + send_to_bitcoin_node ${SPENDER_NODE_RPC_URL}/${SPENDER_BTC_NODE_DEFAULT_WALLET} ${SPENDER_NODE_RPC_CFG} $@ + local returncode=$? + trace_rc ${returncode} + return ${returncode} } send_to_bitcoin_node() { - trace "Entering send_to_bitcoin_node()..." - local returncode - local result - local errorstring - local node_url=${1} - local config=${2} - local data=${3} + trace "Entering send_to_bitcoin_node()..." + local returncode + local result + local errorstring + local node_url=${1} + local config=${2} + local data=${3} - trace "[send_to_bitcoin_node] curl -s --user ${user} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" - result=$(curl -s --config ${config} -H "Content-Type: application/json" -d "${data}" ${node_url}) - returncode=$? - trace_rc ${returncode} - trace "[send_to_bitcoin_node] result=${result}" + trace "[send_to_bitcoin_node] curl -m 20 -s --config ${config} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" + result=$(curl -m 20 -s --config ${config} -H "Content-Type: application/json" -d "${data}" ${node_url}) + returncode=$? + trace_rc ${returncode} + trace "[send_to_bitcoin_node] result=${result}" - if [ "${returncode}" -eq 0 ]; then - # Node responded, let's see if we got an error message from the node - # jq -e will have a return code of 1 if the supplied tag is null. - errorstring=$(echo "${result}" | jq -e ".error") - if [ "$?" -eq "0" ]; then - # Error tag not null, so there's an error - trace "[send_to_bitcoin_node] Node responded, error found in response: ${errorstring}" - returncode=1 - else - trace "[send_to_bitcoin_node] Node responded, no error found in response, yayy!" - fi - fi + if [ "${returncode}" -eq 0 ]; then + # Node responded, let's see if we got an error message from the node + # jq -e will have a return code of 1 if the supplied tag is null. + errorstring=$(echo "${result}" | jq -e ".error") + if [ "$?" -eq "0" ]; then + # Error tag not null, so there's an error + trace "[send_to_bitcoin_node] Node responded, error found in response: ${errorstring}" + returncode=1 + else + trace "[send_to_bitcoin_node] Node responded, no error found in response, yayy!" + fi + fi - # Output response to stdout before exiting with return code - echo "${result}" + # Output response to stdout before exiting with return code + echo "${result}" - trace_rc ${returncode} - return ${returncode} + trace_rc ${returncode} + return ${returncode} } case "${0}" in *sendtobitcoinnode.sh) send_to_bitcoin_node $@;; esac diff --git a/proxy_docker/app/script/sql.sh b/proxy_docker/app/script/sql.sh index 75214e0..e410c7f 100644 --- a/proxy_docker/app/script/sql.sh +++ b/proxy_docker/app/script/sql.sh @@ -2,11 +2,10 @@ . ./trace.sh -sql() -{ - trace "sqlite3 ${DB_FILE} '${1}'" - sqlite3 -cmd ".timeout 20000" ${DB_FILE} "${1}" +sql() { + trace "sqlite3 -cmd \".timeout 20000\" ${DB_FILE} \"${1}\"" + sqlite3 -cmd ".timeout 20000" ${DB_FILE} "${1}" +# sqlite3 ${DB_FILE} "PRAGMA busy_timeout=20000; ${1}" + return $? } - -case "${0}" in *sql.sh) sql $@;; esac diff --git a/proxy_docker/app/script/startproxy.sh b/proxy_docker/app/script/startproxy.sh index eaec2e3..a865012 100644 --- a/proxy_docker/app/script/startproxy.sh +++ b/proxy_docker/app/script/startproxy.sh @@ -10,7 +10,7 @@ export DB_PATH export DB_FILE trim() { - echo -e $1 | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]]*$//' + echo -e "$1" | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]]*$//' } createCurlConfig() { @@ -45,4 +45,9 @@ chmod 0600 $DB_FILE createCurlConfig ${WATCHER_BTC_NODE_RPC_CFG} ${WATCHER_BTC_NODE_RPC_USER} createCurlConfig ${SPENDER_BTC_NODE_RPC_CFG} ${SPENDER_BTC_NODE_RPC_USER} +. ${DB_PATH}/config.sh +if [ "${FEATURE_LIGHTNING}" = "true" ]; then + ./waitanyinvoice.sh & +fi + nc -vlkp${PROXY_LISTENING_PORT} -e ./requesthandler.sh diff --git a/proxy_docker/app/script/tests-cb.sh b/proxy_docker/app/script/tests-cb.sh index bccbed8..ed659f3 100644 --- a/proxy_docker/app/script/tests-cb.sh +++ b/proxy_docker/app/script/tests-cb.sh @@ -1,7 +1,7 @@ #!/bin/sh read line -echo ${line} 1>&2 +echo "${line}" 1>&2 echo -ne "HTTP/1.1 200 OK\r\n" echo -e "Content-Type: application/json\r\nContent-Length: 0\r\n\r\n" diff --git a/proxy_docker/app/script/trace.sh b/proxy_docker/app/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/proxy_docker/app/script/trace.sh +++ b/proxy_docker/app/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi } diff --git a/proxy_docker/app/script/unwatchrequest.sh b/proxy_docker/app/script/unwatchrequest.sh index 0302259..8c0a334 100644 --- a/proxy_docker/app/script/unwatchrequest.sh +++ b/proxy_docker/app/script/unwatchrequest.sh @@ -3,25 +3,80 @@ . ./trace.sh . ./sql.sh -unwatchrequest() -{ - trace "Entering unwatchrequest()..." +unwatchrequest() { + trace "Entering unwatchrequest()..." - local request=${1} - local address=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3) - local returncode - trace "[unwatchrequest] Unwatch request on address ${address})" + local request=${1} + local address=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3) + local returncode + trace "[unwatchrequest] Unwatch request on address ${address}" - sql "UPDATE watching SET watching=0 WHERE address=\"${address}\"" - returncode=$? - trace_rc ${returncode} + sql "UPDATE watching SET watching=0 WHERE address=\"${address}\"" + returncode=$? + trace_rc ${returncode} - data="{\"event\":\"unwatch\",\"address\":\"${address}\"}" - trace "[unwatchrequest] responding=${data}" + data="{\"event\":\"unwatch\",\"address\":\"${address}\"}" + trace "[unwatchrequest] responding=${data}" - echo "${data}" + echo "${data}" - return ${returncode} + return ${returncode} } -case "${0}" in *unwatchrequest.sh) unwatchrequest $@;; esac +unwatchpub32request() { + trace "Entering unwatchpub32request()..." + + local request=${1} + local pub32=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3) + local id + local returncode + trace "[unwatchpub32request] Unwatch pub32 ${pub32}" + + id=$(sql "SELECT id FROM watching_by_pub32 WHERE pub32='${pub32}'") + trace "[unwatchpub32request] id: ${id}" + + sql "UPDATE watching_by_pub32 SET watching=0 WHERE id=${id}" + returncode=$? + trace_rc ${returncode} + + sql "UPDATE watching SET watching=0 WHERE watching_by_pub32_id=\"${id}\"" + returncode=$? + trace_rc ${returncode} + + data="{\"event\":\"unwatchxpubbyxpub\",\"pub32\":\"${pub32}\"}" + trace "[unwatchpub32request] responding=${data}" + + echo "${data}" + + return ${returncode} +} + +unwatchpub32labelrequest() { + trace "Entering unwatchpub32labelrequest()..." + + local request=${1} + local label=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3) + local id + local returncode + trace "[unwatchpub32labelrequest] Unwatch xpub label ${label}" + + id=$(sql "SELECT id FROM watching_by_pub32 WHERE label='${label}'") + returncode=$? + trace_rc ${returncode} + trace "[unwatchpub32labelrequest] id: ${id}" + + sql "UPDATE watching_by_pub32 SET watching=0 WHERE id=${id}" + returncode=$? + trace_rc ${returncode} + + sql "UPDATE watching SET watching=0 WHERE watching_by_pub32_id=\"${id}\"" + returncode=$? + trace_rc ${returncode} + + data="{\"event\":\"unwatchxpubbylabel\",\"label\":\"${label}\"}" + trace "[unwatchpub32labelrequest] responding=${data}" + + echo "${data}" + + return ${returncode} +} diff --git a/proxy_docker/app/script/waitanyinvoice.sh b/proxy_docker/app/script/waitanyinvoice.sh new file mode 100644 index 0000000..9930f55 --- /dev/null +++ b/proxy_docker/app/script/waitanyinvoice.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +. ./trace.sh +. ./sql.sh +. ./callbacks_job.sh + +ln_waitanyinvoice() { + trace "Entering ln_waitanyinvoice()..." + + local lastpay_index=${1} + trace "[ln_waitanyinvoice] lastpay_index=${lastpay_index}" + local returncode + local result + local bolt11 + local pay_index + local row + local id + local callback_url + + #{ + # "label": "Duvapk2OWvlmL3kf5GyDF", + # "bolt11": "lnbc1546230n1pwzanfspp5t5jr3rsjkh37986nzkta8kk0yyzam833l5e4an5nu6cyhjcjluwqdq9u2d2zxqr3jscqp2rzjqt04ll5ft3mcuy8hws4xcku2pnhma9r9mavtjtadawyrw5kgzp7g7zr745qq3mcqqyqqqqlgqqqqqzsqpclrjg6vtg4cmqj46przdgwp6rk0xmzfa7ga3wnm4h4evzxy3z32aqw77av65cz2mgcf02dak3vl25epq90dw289zz9x87dyjy6nqvchsq6p8nmj", + # "payment_hash": "5d24388e12b5e3e29f531597d3dacf2105dd9e31fd335ece93e6b04bcb12ff1c", + # "msatoshi": 154623000, + # "status": "paid", + # "pay_index": 12, + # "msatoshi_received": 154623000, + # "paid_at": 1546571113, + # "description": "...", + # "expires_at": 1546589056 + #}, + + result=$(./lightning-cli waitanyinvoice ${lastpay_index}) + returncode=$? + trace_rc ${returncode} + trace "[ln_waitanyinvoice] result=${result}" + + if [ "${returncode}" -eq "0" ]; then + bolt11=$(echo "${result}" | jq -r ".bolt11") + pay_index=$(echo "${result}" | jq -r ".pay_index") + msatoshi_received=$(echo "${result}" | jq -r ".msatoshi_received") + status=$(echo "${result}" | jq -r ".status") + paid_at=$(echo "${result}" | jq -r ".paid_at") + + sql "UPDATE ln_invoice SET status=\"${status}\", pay_index=${pay_index}, msatoshi_received=${msatoshi_received}, paid_at=${paid_at} WHERE bolt11=\"${bolt11}\"" + row=$(sql "SELECT id, label, bolt11, callback_url, payment_hash, msatoshi, status, pay_index, msatoshi_received, paid_at, description, expires_at FROM ln_invoice WHERE NOT calledback AND bolt11=\"${bolt11}\"") + + if [ -n "${row}" ]; then + ln_manage_callback ${row} + fi + + sql "UPDATE cyphernode_props SET value="${pay_index}" WHERE property=\"pay_index\"" + fi +} + +while : +do + pay_index=$(sql "SELECT value FROM cyphernode_props WHERE property='pay_index'") + trace "[waitanyinvoice] pay_index=${pay_index}" + ln_waitanyinvoice ${pay_index} + sleep 5 +done diff --git a/proxy_docker/app/script/walletoperations.sh b/proxy_docker/app/script/walletoperations.sh index 6ae21f7..e5051d6 100644 --- a/proxy_docker/app/script/walletoperations.sh +++ b/proxy_docker/app/script/walletoperations.sh @@ -3,191 +3,332 @@ . ./trace.sh . ./sendtobitcoinnode.sh -spend() -{ - trace "Entering spend()..." +spend() { + trace "Entering spend()..." - local data - local request=${1} - local address=$(echo "${request}" | jq ".address" | tr -d '"') - trace "[spend] address=${address}" - local amount=$(echo "${request}" | jq ".amount" | awk '{ printf "%.8f", $0 }') - trace "[spend] amount=${amount}" - local response - local id_inserted + local data + local request=${1} + local address=$(echo "${request}" | jq -r ".address") + trace "[spend] address=${address}" + local amount=$(echo "${request}" | jq ".amount" | awk '{ printf "%.8f", $0 }') + trace "[spend] amount=${amount}" + local response + local id_inserted + local tx_details + local tx_raw_details - response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}") - local returncode=$? - trace_rc ${returncode} - trace "[spend] response=${response}" + response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}") + local returncode=$? + trace_rc ${returncode} + trace "[spend] response=${response}" - if [ "${returncode}" -eq 0 ]; then - local txid=$(echo ${response} | jq ".result" | tr -d '"') - trace "[spend] txid=${txid}" + if [ "${returncode}" -eq 0 ]; then + local txid=$(echo "${response}" | jq -r ".result") + trace "[spend] txid=${txid}" - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address -# sql "INSERT OR IGNORE INTO watching (watching, txid) VALUES (0, ${txid})" - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" - trace_rc $? - id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") - trace_rc $? - sql "INSERT OR IGNORE INTO recipient (address, amount, tx_id) VALUES (\"${address}\", ${amount}, ${id_inserted})" - trace_rc $? + # Let's get transaction details on the spending wallet so that we have fee information + tx_details=$(get_transaction ${txid} "spender") + tx_raw_details=$(get_rawtransaction ${txid}) - data="{\"status\":\"accepted\"" - data="${data},\"hash\":\"${txid}\"}" - else - local message=$(echo "${response}" | jq -e ".error.message") - data="{\"message\":${message}}" - fi + # Amounts and fees are negative when spending so we absolute those fields + local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') + local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') + local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') + local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') + local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') + local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') + tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0) + local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') + local rawtx=$(echo "${tx_details}" | jq '.result.hex') - trace "[spend] responding=${data}" - echo "${data}" + # Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation + sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" + trace_rc $? + id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") + trace_rc $? + sql "INSERT OR IGNORE INTO recipient (address, amount, tx_id) VALUES (\"${address}\", ${amount}, ${id_inserted})" + trace_rc $? - return ${returncode} + data="{\"status\":\"accepted\"" + data="${data},\"hash\":\"${txid}\"}" + else + local message=$(echo "${response}" | jq -e ".error.message") + data="{\"message\":${message}}" + fi + + trace "[spend] responding=${data}" + echo "${data}" + + return ${returncode} } -getbalance() -{ - trace "Entering getbalance()..." +bumpfee() { + trace "Entering bumpfee()..." - local response - local data='{"method":"getbalance"}' - response=$(send_to_spender_node "${data}") - local returncode=$? - trace_rc ${returncode} - trace "[getbalance] response=${response}" + local request=${1} + local txid=$(echo "${request}" | jq -r ".txid") + trace "[bumpfee] txid=${txid}" - if [ "${returncode}" -eq 0 ]; then - local balance=$(echo ${response} | jq ".result") - trace "[getbalance] balance=${balance}" + local confTarget + local response + local returncode - data="{\"balance\":${balance}}" - else - trace "[getbalance] Coudn't get balance!" - data="" - fi + # jq -e will have a return code of 1 if the supplied tag is null. + confTarget=$(echo "${request}" | jq -e ".confTarget") + if [ "$?" -ne "0" ]; then + # confTarget tag null, so there's no confTarget + trace "[bumpfee] confTarget=" + response=$(send_to_spender_node "{\"method\":\"bumpfee\",\"params\":[\"${txid}\"]}") + returncode=$? + else + trace "[bumpfee] confTarget=${confTarget}" + response=$(send_to_spender_node "{\"method\":\"bumpfee\",\"params\":[\"${txid}\",{\"confTarget\":${confTarget}}]}") + returncode=$? + fi - trace "[getbalance] responding=${data}" - echo "${data}" + trace_rc ${returncode} + trace "[bumpfee] response=${response}" - return ${returncode} + if [ "${returncode}" -eq 0 ]; then + trace "[bumpfee] error!" + else + trace "[bumpfee] success!" + fi + + echo "${response}" + + return ${returncode} } -getnewaddress() -{ - trace "Entering getnewaddress()..." +getbalance() { + trace "Entering getbalance()..." - local response - local data='{"method":"getnewaddress"}' - response=$(send_to_spender_node "${data}") - local returncode=$? - trace_rc ${returncode} - trace "[getnewaddress] response=${response}" + local response + local data='{"method":"getbalance"}' + response=$(send_to_spender_node "${data}") + local returncode=$? + trace_rc ${returncode} + trace "[getbalance] response=${response}" - if [ "${returncode}" -eq 0 ]; then - local address=$(echo ${response} | jq ".result") - trace "[getnewaddress] address=${address}" + if [ "${returncode}" -eq 0 ]; then + local balance=$(echo ${response} | jq ".result") + trace "[getbalance] balance=${balance}" - data="{\"address\":${address}}" - else - trace "[getnewaddress] Coudn't get a new address!" - data="" - fi + data="{\"balance\":${balance}}" + else + trace "[getbalance] Coudn't get balance!" + data="" + fi - trace "[getnewaddress] responding=${data}" - echo "${data}" + trace "[getbalance] responding=${data}" + echo "${data}" - return ${returncode} + return ${returncode} } -addtobatching() -{ - trace "Entering addtobatching()..." +getbalancebyxpublabel() { + trace "Entering getbalancebyxpublabel()..." - local address=${1} - trace "[addtobatching] address=${address}" - local amount=${2} - trace "[addtobatching] amount=${amount}" + local label=${1} + trace "[getbalancebyxpublabel] label=${label}" + local xpub - sql "INSERT OR IGNORE INTO recipient (address, amount) VALUES (\"${address}\", ${amount})" - returncode=$? - trace_rc ${returncode} + xpub=$(sql "SELECT pub32 FROM watching_by_pub32 WHERE label=\"${label}\"") + trace "[getbalancebyxpublabel] xpub=${xpub}" - return ${returncode} + getbalancebyxpub ${xpub} "getbalancebyxpublabel" + returncode=$? + + return ${returncode} } -batchspend() -{ - trace "Entering batchspend()..." +getbalancebyxpub() { + trace "Entering getbalancebyxpub()..." - local data - local response - local recipientswhere - local recipientsjson - local id_inserted + # ./bitcoin-cli -rpcwallet=xpubwatching01.dat listunspent 0 9999999 "$(./bitcoin-cli -rpcwallet=xpubwatching01.dat getaddressesbylabel upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb | jq "keys" | tr -d '\n ')" | jq "[.[].amount] | add" - # We will batch all the addresses in DB without a TXID - local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL') - trace "[batchspend] batching=${batching}" + local xpub=${1} + trace "[getbalancebyxpub] xpub=${xpub}" - local returncode - local address - local amount - local notfirst=false - local IFS=$'\n' - for row in ${batching} - do - trace "[batchspend] row=${row}" - address=$(echo "${row}" | cut -d '|' -f1) - trace "[batchspend] address=${address}" - amount=$(echo "${row}" | cut -d '|' -f2) - trace "[batchspend] amount=${amount}" + # If called from getbalancebyxpublabel, set the correct event for response + local event=${2:-"getbalancebyxpub"} + trace "[getbalancebyxpub] event=${event}" + local addresses + local balance + local data + local returncode - if ${notfirst}; then - recipientswhere="${recipientswhere}," - recipientsjson="${recipientsjson}," - else - notfirst=true - fi + # addresses=$(./bitcoin-cli -rpcwallet=xpubwatching01.dat getaddressesbylabel upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb | jq "keys" | tr -d '\n ') + data="{\"method\":\"getaddressesbylabel\",\"params\":[${xpub}]}" + trace "[getbalancebyxpub] data=${data}" + addresses=$(send_to_xpub_watcher_wallet ${data} | jq "keys" | tr -d '\n ') - recipientswhere="${recipientswhere}\"${address}\"" - recipientsjson="${recipientsjson}\"${address}\":${amount}" - done + # ./bitcoin-cli -rpcwallet=xpubwatching01.dat listunspent 0 9999999 "$addresses" | jq "[.[].amount] | add" - response=$(send_to_spender_node "{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}]}") - returncode=$? - trace_rc ${returncode} - trace "[batchspend] response=${response}" + data="{\"method\":\"listunspent\",\"params\":[0, 9999999, \"${addresses}\"]}" + trace "[getbalancebyxpub] data=${data}" + balance=$(send_to_xpub_watcher_wallet ${data} | jq "[.[].amount] | add | . * 100000000 | trunc | . / 100000000") + returncode=$? + trace_rc ${returncode} + trace "[getbalancebyxpub] balance=${balance}" - if [ "${returncode}" -eq 0 ]; then - local txid=$(echo ${response} | jq ".result" | tr -d '"') - trace "[batchspend] txid=${txid}" + data="{\"event\":\"${event}\",\"xpub\":\"${xpub}\",\"balance\":${balance}}" - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address -# sql "INSERT OR IGNORE INTO watching (watching, txid) VALUES (0, ${txid})" -# trace_rc $? - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -eq 0 ]; then - id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") - trace "[batchspend] id_inserted: ${id_inserted}" - sql "UPDATE recipient SET tx_id=${id_inserted} WHERE address IN (${recipientswhere})" - trace_rc $? - fi + echo "${data}" - data="{\"status\":\"accepted\"" - data="${data},\"hash\":\"${txid}\"}" - else - local message=$(echo "${response}" | jq -e ".error.message") - data="{\"message\":${message}}" - fi - - trace "[batchspend] responding=${data}" - echo "${data}" - - return ${returncode} + return ${returncode} +} + +getnewaddress() { + trace "Entering getnewaddress()..." + + local address_type=${1} + trace "[getnewaddress] address_type=${address_type}" + + local response + local data + if [ -z "${address_type}" ]; then + data='{"method":"getnewaddress"}' + else + data="{\"method\":\"getnewaddress\",\"params\":[\"\",\"${address_type}\"]}" + fi + response=$(send_to_spender_node "${data}") + local returncode=$? + trace_rc ${returncode} + trace "[getnewaddress] response=${response}" + + if [ "${returncode}" -eq 0 ]; then + local address=$(echo ${response} | jq ".result") + trace "[getnewaddress] address=${address}" + + data="{\"address\":${address}}" + else + trace "[getnewaddress] Coudn't get a new address!" + data="" + fi + + trace "[getnewaddress] responding=${data}" + echo "${data}" + + return ${returncode} +} + +addtobatching() { + trace "Entering addtobatching()..." + + local address=${1} + trace "[addtobatching] address=${address}" + local amount=${2} + trace "[addtobatching] amount=${amount}" + + sql "INSERT OR IGNORE INTO recipient (address, amount) VALUES (\"${address}\", ${amount})" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} + +batchspend() { + trace "Entering batchspend()..." + + local data + local response + local recipientswhere + local recipientsjson + local id_inserted + local tx_details + local tx_raw_details + + # We will batch all the addresses in DB without a TXID + local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL') + trace "[batchspend] batching=${batching}" + + local returncode + local address + local amount + local notfirst=false + local IFS=$'\n' + for row in ${batching} + do + trace "[batchspend] row=${row}" + address=$(echo "${row}" | cut -d '|' -f1) + trace "[batchspend] address=${address}" + amount=$(echo "${row}" | cut -d '|' -f2) + trace "[batchspend] amount=${amount}" + + if ${notfirst}; then + recipientswhere="${recipientswhere}," + recipientsjson="${recipientsjson}," + else + notfirst=true + fi + + recipientswhere="${recipientswhere}\"${address}\"" + recipientsjson="${recipientsjson}\"${address}\":${amount}" + done + + response=$(send_to_spender_node "{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}]}") + returncode=$? + trace_rc ${returncode} + trace "[batchspend] response=${response}" + + if [ "${returncode}" -eq 0 ]; then + local txid=$(echo "${response}" | jq -r ".result") + trace "[batchspend] txid=${txid}" + + # Let's get transaction details on the spending wallet so that we have fee information + tx_details=$(get_transaction ${txid} "spender") + tx_raw_details=$(get_rawtransaction ${txid}) + + # Amounts and fees are negative when spending so we absolute those fields + local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') + local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') + local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') + local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') + local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') + local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') + tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0) + local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') + local rawtx=$(echo "${tx_details}" | jq '.result.hex') + + # Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation + sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") + trace "[batchspend] id_inserted: ${id_inserted}" + sql "UPDATE recipient SET tx_id=${id_inserted} WHERE address IN (${recipientswhere})" + trace_rc $? + fi + + data="{\"status\":\"accepted\"" + data="${data},\"hash\":\"${txid}\"}" + else + local message=$(echo "${response}" | jq -e ".error.message") + data="{\"message\":${message}}" + fi + + trace "[batchspend] responding=${data}" + echo "${data}" + + return ${returncode} +} + +create_wallet() { + trace "[Entering create_wallet()]" + + local walletname=${1} + + local rpcstring="{\"method\":\"createwallet\",\"params\":[\"${walletname}\",true]}" + trace "[create_wallet] rpcstring=${rpcstring}" + + local result + result=$(send_to_watcher_node ${rpcstring}) + local returncode=$? + + echo "${result}" + + return ${returncode} } -#case "${0}" in *walletoperations.sh) getbalance $@;; esac diff --git a/proxy_docker/app/script/watchrequest.sh b/proxy_docker/app/script/watchrequest.sh index 9846d80..a893e82 100644 --- a/proxy_docker/app/script/watchrequest.sh +++ b/proxy_docker/app/script/watchrequest.sh @@ -4,16 +4,16 @@ . ./importaddress.sh . ./sql.sh . ./sendtobitcoinnode.sh +. ./bitcoin.sh -watchrequest() -{ +watchrequest() { trace "Entering watchrequest()..." local returncode local request=${1} - local address=$(echo "${request}" | jq ".address" | tr -d '"') - local cb0conf_url=$(echo "${request}" | jq ".unconfirmedCallbackURL" | tr -d '"') - local cb1conf_url=$(echo "${request}" | jq ".confirmedCallbackURL" | tr -d '"') + local address=$(echo "${request}" | jq -r ".address") + local cb0conf_url=$(echo "${request}" | jq -r ".unconfirmedCallbackURL") + local cb1conf_url=$(echo "${request}" | jq -r ".confirmedCallbackURL") local imported local inserted local id_inserted @@ -29,7 +29,7 @@ watchrequest() imported=0 fi - sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})" + sql "INSERT OR REPLACE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})" returncode=$? trace_rc ${returncode} if [ "${returncode}" -eq 0 ]; then @@ -71,4 +71,266 @@ watchrequest() return ${returncode} } -case "${0}" in *watchrequest.sh) watchrequest $@;; esac +watchpub32request() { + trace "Entering watchpub32request()..." + + local returncode + local request=${1} + local label=$(echo "${request}" | jq -r ".label") + trace "[watchpub32request] label=${label}" + local pub32=$(echo "${request}" | jq -r ".pub32") + trace "[watchpub32request] pub32=${pub32}" + local path=$(echo "${request}" | jq -r ".path") + trace "[watchpub32request] path=${path}" + local nstart=$(echo "${request}" | jq ".nstart") + trace "[watchpub32request] nstart=${nstart}" + local cb0conf_url=$(echo "${request}" | jq -r ".unconfirmedCallbackURL") + trace "[watchpub32request] cb0conf_url=${cb0conf_url}" + local cb1conf_url=$(echo "${request}" | jq -r ".confirmedCallbackURL") + trace "[watchpub32request] cb1conf_url=${cb1conf_url}" + + watchpub32 ${label} ${pub32} ${path} ${nstart} ${cb0conf_url} ${cb1conf_url} + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} + +watchpub32() { + trace "Entering watchpub32()..." + + local returncode + local label=${1} + trace "[watchpub32] label=${label}" + local pub32=${2} + trace "[watchpub32] pub32=${pub32}" + local path=${3} + trace "[watchpub32] path=${path}" + local nstart=${4} + trace "[watchpub32] nstart=${nstart}" + local last_n=$((${nstart}+${XPUB_DERIVATION_GAP})) + trace "[watchpub32] last_n=${last_n}" + local cb0conf_url=${5} + trace "[watchpub32] cb0conf_url=${cb0conf_url}" + local cb1conf_url=${6} + trace "[watchpub32] cb1conf_url=${cb1conf_url}" + + # upto_n is used when extending the watching window + local upto_n=${7} + trace "[watchpub32] upto_n=${upto_n}" + + local id_inserted + local result + local error_msg + local data + + # Derive with pycoin... + # {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"} + if [ -n "${upto_n}" ]; then + # If upto_n provided, then we create from nstart to upto_n (instead of + GAP) + last_n=${upto_n} + fi + local subspath=$(echo -e $path | sed -En "s/n/${nstart}-${last_n}/p") + trace "[watchpub32] subspath=${subspath}" + local addresses + addresses=$(derivepubpath "{\"pub32\":\"${pub32}\",\"path\":\"${subspath}\"}") + returncode=$? + trace_rc ${returncode} +# trace "[watchpub32] addresses=${addresses}" + + if [ "${returncode}" -eq 0 ]; then +# result=$(create_wallet "${pub32}") +# returncode=$? +# trace_rc ${returncode} +# trace "[watchpub32request] result=${result}" + trace "[watchpub32] Skipping create_wallet" + + if [ "${returncode}" -eq 0 ]; then + # Importmulti in Bitcoin Core... + result=$(importmulti_rpc "${WATCHER_BTC_NODE_XPUB_WALLET}" "${pub32}" "${addresses}") + returncode=$? + trace_rc ${returncode} + trace "[watchpub32] result=${result}" + + if [ "${returncode}" -eq 0 ]; then + if [ -n "${upto_n}" ]; then + # Update existing row, we are extending the watching window + sql "UPDATE watching_by_pub32 set last_imported_n=${upto_n} WHERE pub32=\"${pub32}\"" + else + # Insert in our DB... + sql "INSERT OR REPLACE INTO watching_by_pub32 (pub32, label, derivation_path, watching, callback0conf, callback1conf, last_imported_n) VALUES (\"${pub32}\", \"${label}\", \"${path}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${last_n})" + fi + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq 0 ]; then + id_inserted=$(sql "SELECT id FROM watching_by_pub32 WHERE label='${label}'") + trace "[watchpub32] id_inserted: ${id_inserted}" + + addresses=$(echo ${addresses} | jq ".addresses[].address") + insert_watches "${addresses}" "${cb0conf_url}" "${cb1conf_url}" ${id_inserted} ${nstart} + else + error_msg="Can't insert xpub watcher in DB" + fi + else + error_msg="Can't import addresses" + fi + else + error_msg="Can't create wallet" + fi + else + error_msg="Can't derive addresses" + fi + + if [ -z "${error_msg}" ]; then + data="{\"id\":\"${id_inserted}\", + \"event\":\"watchxpub\", + \"pub32\":\"${pub32}\", + \"label\":\"${label}\", + \"path\":\"${path}\", + \"nstart\":\"${nstart}\", + \"unconfirmedCallbackURL\":\"${cb0conf_url}\", + \"confirmedCallbackURL\":\"${cb1conf_url}\"}" + + returncode=0 + else + data="{\"error\":\"${error_msg}\", + \"event\":\"watchxpub\", + \"pub32\":\"${pub32}\", + \"label\":\"${label}\", + \"path\":\"${path}\", + \"nstart\":\"${nstart}\", + \"unconfirmedCallbackURL\":\"${cb0conf_url}\", + \"confirmedCallbackURL\":\"${cb1conf_url}\"}" + + returncode=1 + fi + trace "[watchpub32] responding=${data}" + + echo "${data}" + + return ${returncode} +} + +insert_watches() { + trace "Entering insert_watches()..." + + local addresses=${1} + local callback0conf=${2} + local callback1conf=${3} + local xpub_id=${4} + local nstart=${5} + local inserted_values="" + + local IFS=$'\n' + for address in ${addresses} + do + # (address, watching, callback0conf, callback1conf, imported, watching_by_pub32_id) + if [ -n "${inserted_values}" ]; then + inserted_values="${inserted_values}," + fi + inserted_values="${inserted_values}(${address}, 1, \"${callback0conf}\", \"${callback1conf}\", 1" + if [ -n "${xpub_id}" ]; then + inserted_values="${inserted_values}, ${xpub_id}, ${nstart}" + nstart=$((${nstart} + 1)) + fi + inserted_values="${inserted_values})" + done +# trace "[insert_watches] inserted_values=${inserted_values}" + + sql "INSERT OR REPLACE INTO watching (address, watching, callback0conf, callback1conf, imported, watching_by_pub32_id, pub32_index) VALUES ${inserted_values}" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} + +extend_watchers() { + trace "Entering extend_watchers()..." + + local watching_by_pub32_id=${1} + trace "[extend_watchers] watching_by_pub32_id=${watching_by_pub32_id}" + local pub32_index=${2} + trace "[extend_watchers] pub32_index=${pub32_index}" + local upgrade_to_n=$((${pub32_index} + ${XPUB_DERIVATION_GAP})) + trace "[extend_watchers] upgrade_to_n=${upgrade_to_n}" + + local last_imported_n + local row + row=$(sql "SELECT pub32, label, derivation_path, callback0conf, callback1conf, last_imported_n FROM watching_by_pub32 WHERE id=${watching_by_pub32_id} AND watching") + returncode=$? + trace_rc ${returncode} + + trace "[extend_watchers] row=${row}" + local pub32=$(echo "${row}" | cut -d '|' -f1) + trace "[extend_watchers] pub32=${pub32}" + local label=$(echo "${row}" | cut -d '|' -f2) + trace "[extend_watchers] label=${label}" + local derivation_path=$(echo "${row}" | cut -d '|' -f3) + trace "[extend_watchers] derivation_path=${derivation_path}" + local callback0conf=$(echo "${row}" | cut -d '|' -f4) + trace "[extend_watchers] callback0conf=${callback0conf}" + local callback1conf=$(echo "${row}" | cut -d '|' -f5) + trace "[extend_watchers] callback1conf=${callback1conf}" + local last_imported_n=$(echo "${row}" | cut -d '|' -f6) + trace "[extend_watchers] last_imported_n=${last_imported_n}" + + if [ "${last_imported_n}" -lt "${upgrade_to_n}" ]; then + # We want to keep our gap between last tx and last n watched... + # For example, if the last imported n is 155 and we just got a tx with pub32 index of 66, + # we want to extend the watched addresses to 166 if our gap is 100 (default). + trace "[extend_watchers] We have addresses to add to watchers!" + + watchpub32 ${label} ${pub32} ${derivation_path} $((${last_imported_n} + 1)) ${callback0conf} ${callback1conf} ${upgrade_to_n} > /dev/null + returncode=$? + trace_rc ${returncode} + else + trace "[extend_watchers] Nothing to add!" + fi + + return ${returncode} +} + +watchtxidrequest() { + trace "Entering watchtxidrequest()..." + + local returncode + local request=${1} + trace "[watchtxidrequest] request=${request}" + local txid=$(echo "${request}" | jq -r ".txid") + trace "[watchtxidrequest] txid=${txid}" + local cb1conf_url=$(echo "${request}" | jq -r ".confirmedCallbackURL") + trace "[watchtxidrequest] cb1conf_url=${cb1conf_url}" + local cbxconf_url=$(echo "${request}" | jq -r ".xconfCallbackURL") + trace "[watchtxidrequest] cbxconf_url=${cbxconf_url}" + local nbxconf=$(echo "${request}" | jq ".nbxconf") + trace "[watchtxidrequest] nbxconf=${nbxconf}" + local inserted + local id_inserted + local result + trace "[watchtxidrequest] Watch request on txid (${txid}), cb 1-conf (${cb1conf_url}) and cb x-conf (${cbxconf_url}) on ${nbxconf} confirmations." + + sql "INSERT OR IGNORE INTO watching_by_txid (txid, watching, callback1conf, callbackxconf, nbxconf) VALUES (\"${txid}\", 1, \"${cb1conf_url}\", \"${cbxconf_url}\", ${nbxconf})" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + inserted=1 + id_inserted=$(sql "SELECT id FROM watching_by_txid WHERE txid='${txid}'") + trace "[watchtxidrequest] id_inserted: ${id_inserted}" + else + inserted=0 + fi + + local data="{\"id\":\"${id_inserted}\", + \"event\":\"watchtxid\", + \"inserted\":\"${inserted}\", + \"txid\":\"${txid}\", + \"confirmedCallbackURL\":\"${cb1conf_url}\", + \"xconfCallbackURL\":\"${cbxconf_url}\", + \"nbxconf\":${nbxconf}}" + trace "[watchtxidrequest] responding=${data}" + + echo "${data}" + + return ${returncode} +} diff --git a/proxy_docker/env.properties b/proxy_docker/env.properties index 4801108..364305d 100644 --- a/proxy_docker/env.properties +++ b/proxy_docker/env.properties @@ -1,8 +1,10 @@ TRACING=1 -WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/watching01.dat +WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet +WATCHER_BTC_NODE_DEFAULT_WALLET=watching01.dat WATCHER_BTC_NODE_RPC_USER=rpc_username:rpc_password WATCHER_BTC_NODE_RPC_CFG=/proxy/watcher_btcnode_curlcfg.properties -SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/spending01.dat +SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet +SPENDER_BTC_NODE_DEFAULT_WALLET=spending01.dat SPENDER_BTC_NODE_RPC_USER=rpc_username:rpc_password SPENDER_BTC_NODE_RPC_CFG=/proxy/spender_btcnode_curlcfg.properties PROXY_LISTENING_PORT=8888 @@ -18,3 +20,4 @@ OTS_FILES=/otsfiles DERIVATION_PUB32=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb DERIVATION_PATH=0/n WATCHER_BTC_NODE_PRUNED=false +XPUB_DERIVATION_GAP=100 diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 95f8038..0000000 --- a/publish.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -ARCH=$(uname -m) - -docker tag cyphernodeconf registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker tag cyphernode/bitcoin registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker tag cyphernode/clightning registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker tag cyphernode/otsclient registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker tag cyphernode/proxy registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker tag cyphernode/proxycron registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker tag cyphernode/pycoin registry.skp.rocks:5000/$ARCH/cyphernode/pycoin - -docker push registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker push registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker push registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker push registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker push registry.skp.rocks:5000/$ARCH/cyphernode/pycoin diff --git a/pycoin_docker/Dockerfile b/pycoin_docker/Dockerfile index 7b45efa..32b820b 100644 --- a/pycoin_docker/Dockerfile +++ b/pycoin_docker/Dockerfile @@ -3,9 +3,9 @@ FROM python:3.6-alpine3.8 ENV HOME /pycoin RUN apk add --update --no-cache git jq su-exec \ - && pip install --no-cache-dir pycoin \ && cd / \ && git clone https://github.com/Kexkey/pycoin.git \ + && mkdir /usr/local/lib/python3.6/site-packages/pycoin \ && cp -rf pycoin/pycoin/* /usr/local/lib/python3.6/site-packages/pycoin \ && rm -rf pycoin/* @@ -14,9 +14,10 @@ COPY script/requesthandler.sh ${HOME}/requesthandler.sh COPY script/responsetoclient.sh ${HOME}/responsetoclient.sh COPY script/startpycoin.sh ${HOME}/startpycoin.sh COPY script/trace.sh ${HOME}/trace.sh +COPY script/ku /usr/local/bin/ku WORKDIR ${HOME} -RUN chmod +x startpycoin.sh requesthandler.sh +RUN chmod +x startpycoin.sh requesthandler.sh /usr/local/bin/ku ENTRYPOINT ["su-exec"] diff --git a/pycoin_docker/script/ku b/pycoin_docker/script/ku new file mode 100644 index 0000000..b8b661d --- /dev/null +++ b/pycoin_docker/script/ku @@ -0,0 +1,10 @@ +#!/usr/local/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from pycoin.cmds.ku import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/pycoin_docker/script/pycoin.sh b/pycoin_docker/script/pycoin.sh index d9d8e4e..4e3e387 100644 --- a/pycoin_docker/script/pycoin.sh +++ b/pycoin_docker/script/pycoin.sh @@ -2,46 +2,45 @@ . ./trace.sh -derive() -{ - trace "Entering derive()..." +derive() { + trace "Entering derive()..." - local request=${1} - local pub32=$(echo "${request}" | jq ".pub32" | tr -d '"') - local path=$(echo "${request}" | jq ".path" | tr -d '"') + local request=${1} + local pub32=$(echo "${request}" | jq -r ".pub32") + local path=$(echo "${request}" | jq -r ".path") - local result - local returncode - trace "[derive] path=${path}" - trace "[derive] pub32=${pub32}" + local result + local returncode + trace "[derive] path=${path}" + trace "[derive] pub32=${pub32}" - result=$(ku -n BTC -s ${path} -a E:${pub32}) + result=$(ku -n BTC -s ${path} -a E:${pub32}) - returncode=$? - trace_rc ${returncode} - trace "[derive] result=${result}" + returncode=$? + trace_rc ${returncode} + trace "[derive] result=${result}" - local notfirst=false + local notfirst=false - echo -n "{\"addresses\":[" + echo -n "{\"addresses\":[" - local IFS=$'\n' - for address in ${result} - do - if ${notfirst}; then - echo "," - else - notfirst=true - fi - trace "[derive] address=${address}" + local IFS=$'\n' + for address in ${result} + do + if ${notfirst}; then + echo "," + else + notfirst=true + fi + trace "[derive] address=${address}" - data="{\"address\":\"${address}\"}" - trace "[derive] data=${data}" + data="{\"address\":\"${address}\"}" + trace "[derive] data=${data}" - echo -n "${data}" - done + echo -n "${data}" + done - echo "]}" + echo "]}" - return ${returncode} + return ${returncode} } diff --git a/pycoin_docker/script/requesthandler.sh b/pycoin_docker/script/requesthandler.sh index b5e12dc..5be6d0c 100644 --- a/pycoin_docker/script/requesthandler.sh +++ b/pycoin_docker/script/requesthandler.sh @@ -49,7 +49,7 @@ main() fi # line=content-length: 406 case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) - content_length=$(echo ${line} | cut -d ':' -f2) + content_length=$(echo "${line}" | cut -d ':' -f2) trace "[main] content_length=${content_length}"; ;; esac diff --git a/pycoin_docker/script/responsetoclient.sh b/pycoin_docker/script/responsetoclient.sh index 8204395..6d8e9f2 100644 --- a/pycoin_docker/script/responsetoclient.sh +++ b/pycoin_docker/script/responsetoclient.sh @@ -8,11 +8,12 @@ response_to_client() local response=${1} local returncode=${2} + local length=$(echo -n "${response}" | wc -c) ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" - echo -en "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" + echo -en "Content-Type: application/json\r\nContent-Length: ${length}\r\n\r\n${response}" # Small delay needed for the data to be processed correctly by peer sleep 0.2s diff --git a/pycoin_docker/script/trace.sh b/pycoin_docker/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/pycoin_docker/script/trace.sh +++ b/pycoin_docker/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi }