Browse Source

Merge pull request #98 from Samourai-Wallet/develop

Merge develop into master for v1.3.0
umbrel v1.3.0
kenshin samourai 5 years ago
committed by GitHub
parent
commit
3c829a9237
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 20
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 23
      .github/ISSUE_TEMPLATE/support-request.md
  4. 1
      .gitignore
  5. 4
      README.md
  6. 107
      RELEASES.md
  7. 8
      accounts/index.js
  8. 45
      doc/DOCKER_advanced_setups.md
  9. 61
      doc/DOCKER_setup.md
  10. 9
      doc/POST_auth_login.md
  11. 9
      doc/POST_auth_refresh.md
  12. 1
      doc/POST_pushtx.md
  13. 3
      doc/POST_pushtx_schedule.md
  14. 11
      doc/POST_xpub.md
  15. 9
      doc/POST_xpub_lock.md
  16. 12
      docker/my-dojo/.env
  17. 8
      docker/my-dojo/bitcoin/Dockerfile
  18. 7
      docker/my-dojo/conf/docker-node.conf.tpl
  19. 34
      docker/my-dojo/conf/docker-tor.conf.tpl
  20. 6
      docker/my-dojo/docker-compose.yaml
  21. 3
      docker/my-dojo/install/install-scripts.sh
  22. 41
      docker/my-dojo/install/upgrade-scripts.sh
  23. 5
      docker/my-dojo/nginx/mainnet.conf
  24. 5
      docker/my-dojo/nginx/testnet.conf
  25. 58
      docker/my-dojo/node/keys.index.js
  26. 2
      docker/my-dojo/overrides/bitcoind.install.yaml
  27. 56
      docker/my-dojo/tor/Dockerfile
  28. 29
      docker/my-dojo/tor/restart.sh
  29. 44
      docker/my-dojo/tor/torrc
  30. 97
      keys/index-example.js
  31. 27
      lib/bitcoin/addresses-helper.js
  32. 34
      lib/bitcoin/hd-accounts-helper.js
  33. 12
      lib/bitcoin/parallel-address-derivation.js
  34. 8
      lib/bitcoind-rpc/transactions.js
  35. 26
      lib/db/mysql-db-wrapper.js
  36. 35
      lib/http-server/http-server.js
  37. 224
      lib/indexer-rpc/rpc-client.js
  38. 179
      lib/remote-importer/btccom-wrapper.js
  39. 131
      lib/remote-importer/esplora-wrapper.js
  40. 90
      lib/remote-importer/insight-wrapper.js
  41. 143
      lib/remote-importer/local-indexer-wrapper.js
  42. 4
      lib/remote-importer/oxt-wrapper.js
  43. 8
      lib/remote-importer/remote-importer.js
  44. 10
      lib/remote-importer/sources-mainnet.js
  45. 16
      lib/remote-importer/sources-testnet.js
  46. 21
      lib/util.js
  47. 9
      lib/wallet/address-info.js
  48. 19
      lib/wallet/wallet-service.js
  49. 997
      package-lock.json
  50. 6
      package.json
  51. 5
      pushtx/index-orchestrator.js
  52. 8
      pushtx/index.js
  53. 16
      static/admin/lib/api-wrapper.js
  54. 5
      static/admin/tool/index.html
  55. 11
      static/admin/tool/index.js
  56. 33
      tracker/blockchain-processor.js
  57. 21
      tracker/index.js
  58. 79
      tracker/tracker-rest-api.js

38
.github/ISSUE_TEMPLATE/bug_report.md

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
Do not submit screenshots containing PII (logins, passwords, keys, IP addresses, etc)
**Desktop (please complete the following information):**
- OS: [e.g. Ubuntu vX.X]
- Dojo Version [e.g. 22]
- Dojo Advanced Setups used (external bitcoind, bitcoind exposed to external apps, etc)
**Smartphone (please complete the following information):**
- Device: [e.g. Samsung Galaxy X]
- OS: [e.g. AndroidX.X]
- Wallet Version [e.g. X.X.X]
**Additional context**
Add any other context about the problem here.

20
.github/ISSUE_TEMPLATE/feature_request.md

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

23
.github/ISSUE_TEMPLATE/support-request.md

@ -0,0 +1,23 @@
---
name: Support Request
about: Describe your issue
title: ''
labels: ''
assignees: ''
---
**Describe the issue**
A clear and concise description of what the issue is.
**Screenshots**
If applicable, add screenshots to help explain your problem.
Do not submit screenshots containing PII (logins, passwords, keys, IP addresses, etc)
**Desktop (please complete the following information):**
- OS: [e.g. Ubuntu vX.X]
- Dojo Version [e.g. 22]
- Dojo Advanced Setups used (external bitcoind, bitcoind exposed to external apps, etc)
**Additional context**
Add any other context about the problem here.

1
.gitignore

@ -8,4 +8,5 @@ keys/index.js
keys/sslcert/
node_modules/
private-tests/
static/admin/conf/index.js
*.log

4
README.md

@ -65,10 +65,10 @@ Authentication is enforced by an API key and Json Web Tokens.
* Default option relies on the local bitcoind and makes you 100% independent of Samourai Wallet's infrastructure. This option is recommended for better privacy.
* Activation of bitcoind as the data source:
* Edit /keys/index.js and set "explorers.bitcoind" to "active". OXT API will be ignored.
* Edit /keys/index.js and set "indexer.active" to "local_bitcoind". OXT API will be ignored.
* Activation of OXT as the data source (through socks5):
* Edit /keys/index.js and set "explorers.bitcoind" to "inactive".
* Edit /keys/index.js and set "indexer.active" to "third_party_explorer".
* Main drawbacks of using your local bitcoind for these imports:
* This option is considered as experimental.

107
RELEASES.md

@ -2,10 +2,117 @@
## Releases ##
- [v1.3.0](#1_3_0)
- [v1.2.0](#1_2_0)
- [v1.1.0](#1_1_0)
<a name="1_3_0"/>
## Samourai Dojo v1.3.0 ##
### Notable changes ###
#### Update of configuration parameters ####
Configuration parameter ```NODE_IMPORT_FROM_BITCOIND``` is replaced by ```NODE_ACTIVE_INDEXER```.
The supported values for the new parameter are:
- ```local_bitcoind``` (equivalent to former ```NODE_IMPORT_FROM_BITCOIND=active```)
- ```third_party_explorer``` (equivalent to former ```NODE_IMPORT_FROM_BITCOIND=inactive```)
**Upgrade of Dojo to v1.3.0 automatically sets the parameter to the default value** ```local_bitcoind```.
#### Installation of Tor from source code archives ####
Previous versions of Dojo used the git repository operated by the Tor Project during the build of the Tor container. Starting with this version, Dojo will download an archive of the source code.
Users living in countries blocking the access to resources provided by the Tor Project can easily switch to a mirror site by editing this [line](https://github.com/Samourai-Wallet/samourai-dojo/blob/develop/docker/my-dojo/tor/Dockerfile#L4) before installing or upgrading their Dojo.
The default source used by Dojo is the archive provided by the [Tor Project](https://archive.torproject.org/tor-package-archive).
#### Add support of Tor bridges ####
The Tor container now supports the configuration of Tor bridges. For some users, it may be appropriate to configure Tor bridges in order to circumvent a local censorship of the Tor network. See [this section](https://github.com/Samourai-Wallet/samourai-dojo/blob/develop/doc/DOCKER_advanced_setups.md#tor_bridges) of the documentation for the activation of Tor bridges on your Dojo.
#### Add Blocks rescan feature to the maintenance tool ####
This version introduces a new "Blocks Rescan" feature accessible from the Maintenance Tool.
"Blocks Rescan" allows to rescan a range of blocks for all the addresses currently tracked by your Dojo (loose addresses or addresses derived for your xpubs). This feature comes in handy when the block confirming a missing transaction is known by the user.
#### Add Esplora as the new external data source for testnet ####
The testnet version of Dojo now relies on the Esplora API as its external data source for imports and rescans.
Previously used API (BTC.COM and Insight) have been removed.
Default URL used for the Esplora API is https://blockstream.info/testnet. A local Esplora instance can be used by editing this [line](https://github.com/Samourai-Wallet/samourai-dojo/blob/develop/docker/my-dojo/.env#L44).
#### Remove support of HTTPS by NodeJS ####
Support of HTTPS by the NodeJS server has been removed.
#### Upgrade of bitcoind to v0.19.0.1 ####
Upgrade to Bitcoin Core v0.19.0.1.
#### Update bitcoinjs to v5.1.4 ####
The bitcoinjs library has been updated to v5.1.4.
### Change log ###
#### MyDojo ####
- [#71](https://github.com/Samourai-Wallet/samourai-dojo/pull/71) update to use latest bitcoinjs
- [#74](https://github.com/Samourai-Wallet/samourai-dojo/pull/74) adding bridge support to tor-container
- [#80](https://github.com/Samourai-Wallet/samourai-dojo/pull/80) add support of blocks rescans in the maintenance tool
- [#83](https://github.com/Samourai-Wallet/samourai-dojo/pull/83) removed unused support of https by nodejs apps
- [#84](https://github.com/Samourai-Wallet/samourai-dojo/pull/84) install tor from source code archive
- [#85](https://github.com/Samourai-Wallet/samourai-dojo/pull/85) add esplora as a data source for testnet imports and rescans
- [#90](https://github.com/Samourai-Wallet/samourai-dojo/pull/90) update the remote importer
- [#91](https://github.com/Samourai-Wallet/samourai-dojo/pull/91) improve the tracking of loose addresses
- [#93](https://github.com/Samourai-Wallet/samourai-dojo/pull/93) increase timeouts defined in docker-compose files (for raspi hardwares)
- [#93](https://github.com/Samourai-Wallet/samourai-dojo/pull/93) upgrade bitcoind to bitcoin core 0.19.0.1
#### Bug fixes ####
- [#73](https://github.com/Samourai-Wallet/samourai-dojo/pull/73) remove unhandled promise error
- [#79](https://github.com/Samourai-Wallet/samourai-dojo/pull/79) retry to send sql requests on detection of a lock
- [#94](https://github.com/Samourai-Wallet/samourai-dojo/pull/94) improve the transaction cache implemented for bitcoind rpc client
#### Documentation ####
- [b5dd967](https://github.com/Samourai-Wallet/samourai-dojo/commit/b5dd9673c159b469fb19f43c33a0c0dd21b2fe5a) update api doc (see #75)
- [16926a8](https://github.com/Samourai-Wallet/samourai-dojo/commit/16926a86fb637fb06510d1418474f62d3570cfd3) update docker doc
#### Misc ####
- [#76](https://github.com/Samourai-Wallet/samourai-dojo/pull/76) pin versions in package-lock.json
### Credits ###
- junderw
- kenshin-samourai
- LaurentMT
- nickodev
<a name="1_2_0"/>
## Samourai Dojo v1.2.0 ##

8
accounts/index.js

@ -53,8 +53,7 @@
// Initialize the http server
const port = keys.ports.account
const httpsOptions = keys.https.account
const httpServer = new HttpServer(port, httpsOptions)
const httpServer = new HttpServer(port)
// Initialize the rest api endpoints
const authRestApi = new AuthRestApi(httpServer)
@ -73,4 +72,7 @@
// Attach the web sockets server to the web server
notifServer.attach(httpServer)
})()
})().catch(err => {
console.error(err)
process.exit(1)
})

45
doc/DOCKER_advanced_setups.md

@ -6,6 +6,14 @@ The configuration files of Dojo provide a few advanced options allowing to tune
A word of caution, though, the default values of these options try to maximize your privacy at a network level. Most of the advanced setups described in this document may damage your privacy. Use at your own risk!
## Table of Content ##
- [External Bitcoin full node](#external_bitcoind)
- [bitcoind RPC API ans ZMQ notifications exposed to external apps](#exposed_rpc_zmq)
- [Static onion address for bitcoind hidden service](#static_onion)
- [Configure Tor Bridges](#tor_bridges)
- [Support of testnet](#testnet)
<a name="external_bitcoind"/>
## External Bitcoin full node ##
@ -161,6 +169,43 @@ nano ./conf/docker-bitcoind.conf
Note: this option has no effect if your setup relies on a external full node (i.e. if BITCOIND_INSTALL is set to "off").
<a name="tor_bridges"/>
## Configure Tor Bridges ##
By default, Dojo doesn't try to hide that Tor is being used. For the majority of Dojo users, connecting to Tor with the default configuration is appropriate and will work successfully. For some users, it may be appropriate to configure Tor Bridges in order to circumvent censorship enforced by ISP, censorship enforcement bodies and other interested parties.
The following steps allow to activate the use of Tor bridges by Dojo.
```
# Stop your Dojo
./dojo.sh stop
# Head over to https://bridges.torproject.org
# Click on "Get bridges", then you will see a form with "Advanced Options" header
# Leave the Pluggable Transport as "obfs4" and click on "Get Bridges" button
# Solve the captcha, you will get the bridge addresses, usually three lines:
# obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0
# obfs4 ...
# obfs4 ...
# Edit the tor config file
nano ./conf/docker-tor.conf
#
# Set the value of TOR_USE_BRIDGES to "on"
#
# Set the values of TOR_BRIDGE_n properties with info returned by the website
# For instance, if the first line generated by the website is:
# obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0
# You will have to set:
# TOR_BRIDGE_1=obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0
#
# Save and exit nano
#
```
<a name="testnet"/>
## Support of testnet ##

61
doc/DOCKER_setup.md

@ -10,9 +10,9 @@ MyDojo is a set of Docker containers providing a full Samourai backend composed
## Table of Content ##
- [Architecture](#architecture)
- [Requirements](#requirements)
- [Configuration files](#config_files)
- [First-time install procedure](#install)
- [Upgrade procedure](#upgrade)
- [Configuration files](#config_files)
- [Dojo shell script](#shell_script)
- [Dojo maintenance tool](#maintenance_tool)
- [Pairing your wallet to your Dojo](#pairing)
@ -69,6 +69,29 @@ MyDojo is a set of Docker containers providing a full Samourai backend composed
* Tor Browser installed on the host machine (or on another machine if your host is a headless server)
<a name="config_files"/>
## Configuration files ##
Each new release of Dojo is packaged with 4 template files stored in the `<dojo_dir>/docker/my-dojo/conf` directory:
- docker-common.conf.tpl
- docker-bitcoin.conf.tpl
- docker-mysql.conf.tpl
- docker-node.conf.tpl
These template files define default values for configuration options of your Dojo.
During the first-time installation (dojo.sh install) these templates are used to initialize the configuration files (files with .conf extension) that will be used by your Dojo.
During an upgrade (dojo.sh upgrade), the content of the template files is merged with the content of the configuration files, preserving the values that you may have modified in the configuration files. A backup of the configuration files is saved in the same directory (files with .save extension).
Most options provided in the configuration files can be later modified. New values will become active after a call to
```
./dojo.sh restart
```
<a name="install"/>
## First-time Setup ##
@ -97,11 +120,6 @@ This procedure allows to install a new Dojo from scratch.
* `BITCOIND_RPC_USER` = login protecting the access to the RPC API of your full node,
* `BITCOIND_RPC_PASSWORD` = password protecting the access to the RPC API of your full node.
* If your machine has a lot of RAM, it's recommended that you increase the value of `BITCOIND_DB_CACHE` for a faster Initial Block Download.
* This file also provides a few additional settings for advanced setups:
* static onion address for your full node,
* bitcoind RPC API exposed to external apps,
* use of an external full node.
See this [doc](./DOCKER_advanced_setups.md) for more details.
* Edit docker-mysql.conf.tpl and provide a new value for the following parameters:
* `MYSQL_ROOT_PASSWORD` = password protecting the root account of MySQL,
@ -114,6 +132,14 @@ This procedure allows to install a new Dojo from scratch.
* `NODE_JWT_SECRET` = secret used by your Dojo for the initialization of a cryptographic key signing Json Web Tokens.
These parameters will protect the access to your Dojo. Be sure to provide alphanumeric values with enough entropy.
* Dojo provides a few additional settings for advanced setups:
* static onion address for your full node,
* bitcoind RPC API exposed to external apps,
* use of an external full node,
* use of Tor Bridges,
* support of testnet.
See this [doc](./DOCKER_advanced_setups.md) for more details.
* Open the docker quickstart terminal or a terminal console and go to the `<dojo_dir>/docker/my-dojo` directory. This directory contains a script named dojo.sh which will be your entrypoint for all operations related to the management of your Dojo.
@ -173,29 +199,6 @@ Docker and Docker Compose are going to build new images and containers for your
Note: The upgrade process will override all manual modifications of the files stored under the `<dojo_dir>` directory with an exception for the three configuration files stored in the `<dojo_dir>/docker/my-dojo/conf` directory.
<a name="config_files"/>
## Configuration files ##
Each new release of Dojo is packaged with 4 template files stored in the `<dojo_dir>/docker/my-dojo/conf` directory:
- docker-common.conf.tpl
- docker-bitcoin.conf.tpl
- docker-mysql.conf.tpl
- docker-node.conf.tpl
These template files define default values for configuration options of your Dojo.
During the first-time installation (dojo.sh install) these templates are used to initialize the configuration files (files with .conf extension) that will be used by your Dojo.
During an upgrade (dojo.sh upgrade), the content of the template files is merged with the content of the configuration files, preserving the values that you may have modified in the configuration files. A backup of the configuration files is saved in the same directory (files with .save extension).
Most options provided in the configuration files can be later modified. New values will become active after a call to
```
./dojo.sh restart
```
<a name="shell_script"/>
## Dojo shell script ##

9
doc/POST_auth_login.md

@ -2,7 +2,7 @@
Authenticate to the backend by providing the API key expected by the server. If authentication succeeds, the endpoint returns a json embedding an access token and a refresh token (JSON Web Tokens). The access token must be passed as an argument or in the `Authorization` HTTP header for all later calls to the backend (account & pushtx REST API + websockets). The refresh token must be passed as an argument or in the `Authorization` HTTP header for later calls to /auth/refresh allowing to generate a new access token.
Authentication is activated in /keys/inndex.js configuration file
Authentication is activated in /keys/index.js configuration file
```
auth: {
@ -44,6 +44,9 @@ auth: {
POST /auth/login
```
The API Key must be passed in the body of the request as an url encoded argument.
## Parameters
* **apikey** - `string` - The API key securing access to the backend
@ -51,7 +54,9 @@ POST /auth/login
### Example
```
POST /auth/login?apikey=myAPIKey
POST /auth/login
apikey=myAPIKey
```
#### Success

9
doc/POST_auth_refresh.md

@ -1,12 +1,15 @@
# Refresh the access token
Request a new access token from the backend. A valid refresh token must be passed as an argument or through the `Authorization` HTTP header (with the `Bearer` scheme).
Request a new access token from the backend.
```
POST /auth/refresh
```
The Refresh Token must be passed in the body of the request as an url encoded argument or through the `Authorization` HTTP header (with the `Bearer` scheme).
## Parameters
* **rt** - `string` - A valid refresh token
@ -14,7 +17,9 @@ POST /auth/refresh
### Example
```
POST /auth/refresh?rt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJTYW1vdXJhaSBXYWxsZXQgYmFja2VuZCIsInR5cGUiOiJyZWZyZXNoLXRva2VuIiwiaWF0IjoxNTQ0MTAzOTI5LCJleHAiOjE1NDQxMTExMjl9.6gykKq31WL4Jq7hfmoTwi1fpmBTtAeFb4KjfmSO6l00
POST /auth/refresh
rt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJTYW1vdXJhaSBXYWxsZXQgYmFja2VuZCIsInR5cGUiOiJyZWZyZXNoLXRva2VuIiwiaWF0IjoxNTQ0MTAzOTI5LCJleHAiOjE1NDQxMTExMjl9.6gykKq31WL4Jq7hfmoTwi1fpmBTtAeFb4KjfmSO6l00
```
#### Success

1
doc/POST_pushtx.md

@ -10,6 +10,7 @@ POST /pushtx/
* **tx** - `hex string` - The raw transaction hex
* **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme).
### Example
```

3
doc/POST_pushtx_schedule.md

@ -7,6 +7,9 @@ Schedule the delayed push of an ordered list of transactions (used for programma
POST /pushtx/schedule
```
Parameters must be passed in the body of the request as json encoded arguments.
## Parameters
* **script** - `ScriptStep[]` - An array of ScriptStep objects defining the script.

11
doc/POST_xpub.md

@ -4,10 +4,14 @@ Notify the server of the new HD account for tracking. When new accounts are sent
Response time for restored accounts might be long if there is much previous activity.
```
POST /xpub
```
Parameters must be passed in the body of the request as url encoded arguments.
## Parameters
* **xpub** - `string` - The extended public key for the HD Account
* **type** - `string` - Whether this is a newly-created account or one being restored. Recognized values are `'new'` and `'restore'`.
@ -15,12 +19,13 @@ POST /xpub
* **force** - `boolean` (optional) - Force an override of derivation scheme even if xpub is locked. Used for `'restore'` operation.
* **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme).
### Example
```
POST /xpub?xpub=xpub0123456789&type=restore
POST /xpub?xpub=xpub0123456789&type=new&segwit=bip49
POST /xpub?xpub=xpub0123456789&type=restore&segwit=bip84
POST /xpub
xpub=xpub0123456789&type=restore&segwit=bip84
```
#### Success

9
doc/POST_xpub_lock.md

@ -2,20 +2,27 @@
To avoid errors related to `POST xpub` and SegWit derivation type, this endpoint allows locking of the type of an xpub in the database.
```
POST /xpub/:xpub/lock
```
Parameters must be passed in the body of the request as url encoded arguments.
## Parameters
* **address** - `string` - The first address of the internal chain for this `xpub`, derivation path `M/1/0`. Use compressed P2PHK address regardless of HD derivation scheme.
* **message** - `string` - Either `"lock"` or `"unlock"`
* **signature** - `string` - The base64-encoded signature of the double SHA256 hash of `[varuint length of message string, message string]`. Signature scheme follows [bitcoinjs-message](https://github.com/bitcoinjs/bitcoinjs-message/blob/master/index.js) with a message prefix matching the [coin type](https://github.com/bitcoinjs/bitcoinjs-lib/blob/v3.1.1/src/networks.js). Use the ECPair associated with the `M/1/0` address to sign.
* **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme).
### Example
```
POST /xpub/xpub0123456789/lock?address=1address&message=lock&signature=Base64X==
POST /xpub/xpub0123456789/lock
address=1address&message=lock&signature=Base64X==
```
#### Success

12
docker/my-dojo/.env

@ -10,12 +10,12 @@
COMPOSE_CONVERT_WINDOWS_PATHS=1
DOJO_VERSION_TAG=1.2.0
DOJO_VERSION_TAG=1.3.0
DOJO_DB_VERSION_TAG=1.1.0
DOJO_BITCOIND_VERSION_TAG=1.2.0
DOJO_NODEJS_VERSION_TAG=1.2.0
DOJO_NGINX_VERSION_TAG=1.2.0
DOJO_TOR_VERSION_TAG=1.1.0
DOJO_BITCOIND_VERSION_TAG=1.3.0
DOJO_NODEJS_VERSION_TAG=1.3.0
DOJO_NGINX_VERSION_TAG=1.3.0
DOJO_TOR_VERSION_TAG=1.2.0
#########################################
@ -41,7 +41,7 @@ NODE_GAP_EXTERNAL=100
NODE_GAP_INTERNAL=100
NODE_ADDR_FILTER_THRESHOLD=1000
NODE_URL_OXT_API=https://api.oxt.me
NODE_URL_BTCCOM_API=https://tchain.api.btc.com/v3
NODE_URL_ESPLORA_API=https://blockstream.info/testnet
NODE_ADDR_DERIVATION_MIN_CHILD=2
NODE_ADDR_DERIVATION_MAX_CHILD=2
NODE_ADDR_DERIVATION_THRESHOLD=10

8
docker/my-dojo/bitcoin/Dockerfile

@ -5,10 +5,10 @@ FROM debian:stretch
# INSTALL BITCOIN
#################################################################
ENV BITCOIN_HOME /home/bitcoin
ENV BITCOIN_VERSION 0.18.1
ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-0.18.1/bitcoin-0.18.1-x86_64-linux-gnu.tar.gz
ENV BITCOIN_SHA256 600d1db5e751fa85903e935a01a74f5cc57e1e7473c15fd3e17ed21e202cfe5a
ENV BITCOIN_ASC_URL https://bitcoincore.org/bin/bitcoin-core-0.18.1/SHA256SUMS.asc
ENV BITCOIN_VERSION 0.19.0.1
ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-0.19.0.1/bitcoin-0.19.0.1-x86_64-linux-gnu.tar.gz
ENV BITCOIN_SHA256 732cc96ae2e5e25603edf76b8c8af976fe518dd925f7e674710c6c8ee5189204
ENV BITCOIN_ASC_URL https://bitcoincore.org/bin/bitcoin-core-0.19.0.1/SHA256SUMS.asc
ENV BITCOIN_PGP_KEY 01EA5486DE18A882D4C2684590C8019E36C2E964
RUN set -ex && \

7
docker/my-dojo/conf/docker-node.conf.tpl

@ -20,10 +20,9 @@ NODE_ADMIN_KEY=myAdminKey
# Type: alphanumeric
NODE_JWT_SECRET=myJwtSecret
# Data source used for imports and rescans (bitcoind or OXT)
# Note: support of local bitcoind is an experimental feature
# Values: active | inactive
NODE_IMPORT_FROM_BITCOIND=active
# Indexer or third-party service used for imports and rescans of addresses
# Values: local_bitcoind | third_party_explorer
NODE_ACTIVE_INDEXER=local_bitcoind
# FEE TYPE USED FOR FEES ESTIMATIONS BY BITCOIND
# Allowed values are ECONOMICAL or CONSERVATIVE

34
docker/my-dojo/conf/docker-tor.conf.tpl

@ -0,0 +1,34 @@
#########################################
# CONFIGURATION OF TOR CONTAINER
#########################################
#
# USE TOR BRIDGES
#
# To get Tor bridges head over to https://bridges.torproject.org and click on
# Get bridges, then you will see a form with "Advanced Options" header
# leave the Pluggable Transport as obfs4 and click on Get Bridges button
# solve the captcah, you will get the bridge addresses (usually 3)
#
# Then, set TOR_USE_BRIDGES to "on" and initialize the TOR_BRIDGE_n options
# with the 3 lines generated by the online tool.
#
# For instance, if the first line generated by the tool is:
# obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0
# You will have to set:
# TOR_BRIDGE_1=obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0
#
# Activate the use of Tor bridges
# Value: on | off
TOR_USE_BRIDGES=off
# Bridge 1
TOR_BRIDGE_1=ToBeDefined
# Bridge 2
TOR_BRIDGE_2=ToBeDefined
# Bridge 3
TOR_BRIDGE_3=ToBeDefined

6
docker/my-dojo/docker-compose.yaml

@ -32,10 +32,11 @@ services:
- ./conf/docker-bitcoind.conf
- ./conf/docker-node.conf
restart: on-failure
command: "/home/node/app/wait-for-it.sh db:3306 --timeout=360 --strict -- /home/node/app/restart.sh"
command: "/home/node/app/wait-for-it.sh db:3306 --timeout=720 --strict -- /home/node/app/restart.sh"
expose:
- "8080"
- "8081"
- "8082"
volumes:
- data-nodejs:/data
depends_on:
@ -53,7 +54,7 @@ services:
- ./.env
- ./conf/docker-common.conf
restart: on-failure
command: "/wait-for node:8080 --timeout=360 -- nginx"
command: "/wait-for node:8080 --timeout=720 -- nginx"
expose:
- "80"
volumes:
@ -73,6 +74,7 @@ services:
context: ./tor
env_file:
- ./.env
- ./conf/docker-tor.conf
restart: on-failure
command: /restart.sh
volumes:

3
docker/my-dojo/install/install-scripts.sh

@ -49,6 +49,9 @@ init_config_files() {
cp ./conf/docker-node.conf.tpl ./conf/docker-node.conf
echo "Initialized docker-node.conf"
cp ./conf/docker-tor.conf.tpl ./conf/docker-tor.conf
echo "Initialized docker-tor.conf"
# Initialize config files for nginx and the maintenance tool
if [ "$COMMON_BTC_NETWORK" == "testnet" ]; then
cp ./nginx/testnet.conf ./nginx/dojo.conf

41
docker/my-dojo/install/upgrade-scripts.sh

@ -33,11 +33,7 @@ update_config_files() {
echo "Initialized 2_update.sql"
# Initialize config files for MyDojo
if [ -f ./conf/docker-common.conf ]; then
update_config_file ./conf/docker-common.conf ./conf/docker-common.conf.tpl
else
cp ./conf/docker-common.conf.tpl ./conf/docker-common.conf
fi
update_config_file ./conf/docker-common.conf ./conf/docker-common.conf.tpl
echo "Initialized docker-common.conf"
update_config_file ./conf/docker-bitcoind.conf ./conf/docker-bitcoind.conf.tpl
@ -49,6 +45,9 @@ update_config_files() {
update_config_file ./conf/docker-node.conf ./conf/docker-node.conf.tpl
echo "Initialized docker-node.conf"
update_config_file ./conf/docker-tor.conf ./conf/docker-tor.conf.tpl
echo "Initialized docker-tor.conf"
# Initialize config files for nginx and the maintenance tool
if [ "$COMMON_BTC_NETWORK" == "testnet" ]; then
cp ./nginx/testnet.conf ./nginx/dojo.conf
@ -65,18 +64,22 @@ update_config_files() {
# Update a configuration file from template
update_config_file() {
sed "s/^#.*//g;s/=.*//g;/^$/d" $1 > ./original.keys.raw
grep -f ./original.keys.raw $1 > ./original.lines.raw
if [ -f $1 ]; then
sed "s/^#.*//g;s/=.*//g;/^$/d" $1 > ./original.keys.raw
grep -f ./original.keys.raw $1 > ./original.lines.raw
cp -p $1 "$1.save"
cp -p $2 $1
cp -p $1 "$1.save"
cp -p $2 $1
while IFS='=' read -r key val ; do
sed -i "s/$key=.*/$key=$val/g" "$1"
done < ./original.lines.raw
while IFS='=' read -r key val ; do
sed -i "s~$key=.*~$key=$val~g" "$1"
done < ./original.lines.raw
rm ./original.keys.raw
rm ./original.lines.raw
rm ./original.keys.raw
rm ./original.lines.raw
else
cp $2 $1
fi
}
# Update dojo database
@ -86,6 +89,15 @@ update_dojo_db() {
# Clean-up
cleanup() {
#################
# Clean-up v1.3.0
#################
# Remove deprecated torrc file
if [ -f ./tor/torrc ]; then
rm ./tor/torrc
fi
#################
# Clean-up v1.1.0
#################
@ -94,4 +106,5 @@ cleanup() {
if [ -f ./bitcoin/bitcoin.conf ]; then
rm ./bitcoin/bitcoin.conf
fi
}

5
docker/my-dojo/nginx/mainnet.conf

@ -34,6 +34,11 @@ server {
proxy_pass http://node:8081/;
}
# Tracker server is separate, so proxy first
location /v2/tracker/ {
proxy_pass http://node:8082/;
}
# Proxy requests to maintenance tool
location /admin/ {
proxy_pass http://node:8080/static/admin/;

5
docker/my-dojo/nginx/testnet.conf

@ -34,6 +34,11 @@ server {
proxy_pass http://node:8081/;
}
# Tracker server is separate, so proxy first
location /test/v2/tracker/ {
proxy_pass http://node:8082/;
}
# Proxy requests to maintenance tool
location /admin/ {
proxy_pass http://node:8080/static/admin/;

58
docker/my-dojo/node/keys.index.js

@ -70,8 +70,10 @@ module.exports = {
ports: {
// Port used by the API
account: 8080,
// Port used by pushtx
// Port used by the pushtx API
pushtx: 8081,
// Port used by the tracker API
trackerApi: 8082,
// Port used by the tracker for its notifications
tracker: 5555,
// Port used by pushtx for its notifications
@ -79,45 +81,6 @@ module.exports = {
// Port used by the pushtx orchestrator for its notifications
orchestrator: 5557
},
/*
* HTTPS
* Activate only if node js is used as frontend web server
* (no nginx proxy server)
*/
https: {
// HTTPS for the API
account: {
// Activate https
active: false,
// Filepath of server private key
// (shoud be stored in keys/sslcert)
keypath: '',
// Passphrase of the private key
passphrase: '',
// Filepath of server certificate
// (shoud be stored in keys/sslcert)
certpath: '',
// Filepath of CA certificate
// (shoud be stored in keys/sslcert)
capath: ''
},
// HTTPS for pushtx
pushtx: {
// Activate https
active: false,
// Filepath of server private key
// (shoud be stored in keys/sslcert)
keypath: '',
// Passphrase of the private key
passphrase: '',
// Filepath of server certificate
// (shoud be stored in keys/sslcert)
certpath: '',
// Filepath of CA certificate
// (shoud be stored in keys/sslcert)
capath: ''
}
},
/*
* Authenticated access to the APIs (account & pushtx)
*/
@ -188,21 +151,20 @@ module.exports = {
transactions: 50
},
/*
* Third party explorers
* Indexer or third party service
* used for fast scan of addresses
*/
explorers: {
// Use local bitcoind for imports and rescans
// or use OXT as a fallback
// Values: active | inactive
bitcoind: process.env.NODE_IMPORT_FROM_BITCOIND,
indexer: {
// Active indexer
// Values: local_bitcoind | third_party_explorer
active: process.env.NODE_ACTIVE_INDEXER,
// Use a SOCKS5 proxy for all communications with external services
// Values: null if no socks5 proxy used, otherwise the url of the socks5 proxy
socks5Proxy: 'socks5h://172.28.1.4:9050',
// OXT (mainnet)
oxt: process.env.NODE_URL_OXT_API,
// BTC.COM (testnet)
btccom: process.env.NODE_URL_BTCCOM_API
// Esplora (testnet)
esplora: process.env.NODE_URL_ESPLORA_API,
},
/*
* Max number of transactions per address

2
docker/my-dojo/overrides/bitcoind.install.yaml

@ -11,7 +11,7 @@ services:
- ./conf/docker-common.conf
- ./conf/docker-bitcoind.conf
restart: on-failure
command: "/wait-for-it.sh tor:9050 --timeout=360 --strict -- /restart.sh"
command: "/wait-for-it.sh tor:9050 --timeout=720 --strict -- /restart.sh"
expose:
- "8333"
- "28256"

56
docker/my-dojo/tor/Dockerfile

@ -1,23 +1,61 @@
FROM debian:stretch
ENV TOR_HOME /var/lib/tor
ENV TOR_HOME /var/lib/tor
ENV TOR_URL https://archive.torproject.org/tor-package-archive
ENV TOR_VERSION 0.3.5.8
ENV TOR_GPG_KEY1 0xEB5A896A28988BF5
ENV TOR_GPG_KEY2 0xC218525819F78451
ENV TOR_GPG_KEY3 0x21194EBB165733EA
ENV TOR_GPG_KEY4 0x6AFEE6D49E92B601
ENV GOLANG_DL_URL https://dl.google.com/go
ENV GOLANG_ARCHIVE go1.11.13.linux-amd64.tar.gz
ENV GOLANG_SHA256 50fe8e13592f8cf22304b9c4adfc11849a2c3d281b1d7e09c924ae24874c6daa
ENV OBFS4_URL https://github.com/Yawning/obfs4.git
ENV OBFS4_VERSION 0.0.11
# Install Tor
RUN set -ex && \
apt-get update && \
apt-get install -y git libevent-dev zlib1g-dev libssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils && \
apt-get install -y git libevent-dev zlib1g-dev libssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils gpg wget && \
mkdir -p /usr/local/src/ && \
git clone https://git.torproject.org/tor.git /usr/local/src/tor && \
cd /usr/local/src/tor && \
git checkout tor-0.3.5.8 && \
./autogen.sh && \
cd /usr/local/src && \
wget -qO "tor-$TOR_VERSION.tar.gz" "$TOR_URL/tor-$TOR_VERSION.tar.gz" && \
wget -qO "tor-$TOR_VERSION.tar.gz.asc" "$TOR_URL/tor-$TOR_VERSION.tar.gz.asc" && \
gpg --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$TOR_GPG_KEY1" && \
gpg --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$TOR_GPG_KEY2" && \
gpg --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$TOR_GPG_KEY3" && \
gpg --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$TOR_GPG_KEY4" && \
gpg --verify "tor-$TOR_VERSION.tar.gz.asc" && \
tar -xzvf "tor-$TOR_VERSION.tar.gz" -C /usr/local/src && \
cd "/usr/local/src/tor-$TOR_VERSION" && \
./configure \
--disable-asciidoc \
--sysconfdir=/etc \
--disable-unittests && \
make && make install && \
cd .. && \
rm -rf tor
rm -rf "tor-$TOR_VERSION" && \
rm "tor-$TOR_VERSION.tar.gz" && \
rm "tor-$TOR_VERSION.tar.gz.asc"
# Install Golang & OBFS4 proxy
RUN cd /usr/local/src && \
echo "$GOLANG_SHA256 *$GOLANG_ARCHIVE" > GO_CHECKSUMS && \
wget "$GOLANG_DL_URL/$GOLANG_ARCHIVE" && \
sha256sum -c GO_CHECKSUMS 2>&1 | grep OK && \
tar -C /usr/local/lib -xzf "$GOLANG_ARCHIVE" && \
ln -s /usr/local/lib/go/bin/go /usr/local/bin/ && \
git clone "$OBFS4_URL" /usr/local/src/obfs4proxy && \
cd obfs4proxy && \
git checkout "tags/obfs4proxy-$OBFS4_VERSION" && \
go build -o obfs4proxy/obfs4proxy ./obfs4proxy && \
cp ./obfs4proxy/obfs4proxy /usr/local/bin && \
cd .. && \
rm "$GOLANG_ARCHIVE" && \
rm -rf obfs4proxy
# Create group & user tor
RUN addgroup --system -gid 1107 tor && \
@ -32,10 +70,6 @@ RUN mkdir -p "$TOR_HOME/.tor" && \
chown -Rv tor:tor "$TOR_HOME" && \
chmod -R 750 "$TOR_HOME"
# Copy Tor configuration file
COPY ./torrc /etc/tor/torrc
RUN chown tor:tor /etc/tor/torrc
# Copy restart script
COPY ./restart.sh /restart.sh

29
docker/my-dojo/tor/restart.sh

@ -6,4 +6,31 @@ echo "## Set permissions on /var/lib/tor dir ###"
chmod 750 /var/lib/tor
echo "## Start tor #############################"
tor
tor_options=(
--SocksPort 172.28.1.4:9050
--SocksPolicy "accept 172.28.0.0/16"
--SocksPolicy "reject *"
--DataDirectory /var/lib/tor/.tor
--DataDirectoryGroupReadable 1
--HiddenServiceDir /var/lib/tor/hsv2dojo
--HiddenServiceVersion 2
--HiddenServicePort "80 172.29.1.3:80"
--HiddenServiceDir /var/lib/tor/hsv3dojo
--HiddenServiceVersion 3
--HiddenServicePort "80 172.29.1.3:80"
--HiddenServiceDir /var/lib/tor/hsv2bitcoind
--HiddenServiceVersion 2
--HiddenServicePort "8333 172.28.1.5:8333"
--HiddenServiceDirGroupReadable 1
)
if [ "$TOR_USE_BRIDGES" == "on" ]; then
tor_options+=(--ClientTransportPlugin "obfs4 exec /usr/local/bin/obfs4proxy")
tor_options+=(--UseBridges 1)
tor_options+=(--Bridge "$TOR_BRIDGE_1")
tor_options+=(--Bridge "$TOR_BRIDGE_2")
tor_options+=(--Bridge "$TOR_BRIDGE_3")
fi
tor "${tor_options[@]}"

44
docker/my-dojo/tor/torrc

@ -1,44 +0,0 @@
## Tor opens a socks proxy on port 9050 by default -- even if you don't
## configure one below. Set "SocksPort 0" if you plan to run Tor only
## as a relay, and not make any local application connections yourself.
# Socks is only available from dojonet
SocksPort 172.28.1.4:9050
## Entry policies to allow/deny SOCKS requests based on IP address.
## First entry that matches wins. If no SocksPolicy is set, we accept
## all (and only) requests that reach a SocksPort. Untrusted users who
## can access your SocksPort may be able to learn about the connections
## you make.
# Socks is only available from dojonet
SocksPolicy accept 172.28.0.0/16
SocksPolicy reject *
## The directory for keeping all the keys/etc. By default, we store
## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
DataDirectory /var/lib/tor/.tor
DataDirectoryGroupReadable 1
############### This section is just for location-hidden services ###
## Once you have configured a hidden service, you can look at the
## contents of the file ".../hidden_service/hostname" for the address
## to tell people.
## HiddenServicePort x y:z says to redirect requests on port x to the
## address y:z.
HiddenServiceDir /var/lib/tor/hsv2dojo
HiddenServiceVersion 2
HiddenServicePort 80 172.29.1.3:80
HiddenServiceDir /var/lib/tor/hsv3dojo
HiddenServiceVersion 3
HiddenServicePort 80 172.29.1.3:80
HiddenServiceDir /var/lib/tor/hsv2bitcoind
HiddenServiceVersion 2
HiddenServicePort 8333 172.28.1.5:8333
HiddenServiceDirGroupReadable 1

97
keys/index-example.js

@ -15,7 +15,7 @@ module.exports = {
/*
* Dojo version
*/
dojoVersion: '1.2.0',
dojoVersion: '1.3.0',
/*
* Bitcoind
*/
@ -67,8 +67,10 @@ module.exports = {
ports: {
// Port used by the API
account: 8080,
// Port used by pushtx
// Port used by pushtx API
pushtx: 8081,
// Port used by the tracker API
trackerApi: 8082,
// Port used by the tracker for its notifications
tracker: 5555,
// Port used by pushtx for its notifications
@ -76,45 +78,6 @@ module.exports = {
// Port used by the pushtx orchestrator for its notifications
orchestrator: 5557
},
/*
* HTTPS
* Activate only if node js is used as frontend web server
* (no nginx proxy server)
*/
https: {
// HTTPS for the API
account: {
// Activate https
active: false,
// Filepath of server private key
// (shoud be stored in keys/sslcert)
keypath: '',
// Passphrase of the private key
passphrase: '',
// Filepath of server certificate
// (shoud be stored in keys/sslcert)
certpath: '',
// Filepath of CA certificate
// (shoud be stored in keys/sslcert)
capath: ''
},
// HTTPS for pushtx
pushtx: {
// Activate https
active: false,
// Filepath of server private key
// (shoud be stored in keys/sslcert)
keypath: '',
// Passphrase of the private key
passphrase: '',
// Filepath of server certificate
// (shoud be stored in keys/sslcert)
certpath: '',
// Filepath of CA certificate
// (shoud be stored in keys/sslcert)
capath: ''
}
},
/*
* Authenticated access to the APIs (account & pushtx)
*/
@ -184,14 +147,23 @@ module.exports = {
transactions: 50
},
/*
* Third party explorers
* Indexer or third party service
* used for fast scan of addresses
*/
explorers: {
// Use local bitcoind for imports and rescans
// or use OXT as a fallback
// Values: active | inactive
bitcoind: 'active',
indexer: {
// Active indexer
// Values: local_bitcoind | local_indexer | third_party_explorer
active: 'local_bitcoind',
// Local indexer
localIndexer: {
// IP address or hostname
host: '127.0.0.1',
// Port
port: 50001,
// Support of batch requests
// Values: active | inactive
batchRequests: 'inactive'
},
// Use a SOCKS5 proxy for all communications with external services
// Values: null if no socks5 proxy used, otherwise the url of the socks5 proxy
socks5Proxy: null,
@ -270,26 +242,11 @@ module.exports = {
ports: {
account: 18080,
pushtx: 18081,
trackerApi: 18082,
tracker: 15555,
notifpushtx: 15556,
orchestrator: 15557
},
https: {
account: {
active: false,
keypath: '',
passphrase: '',
certpath: '',
capath: ''
},
pushtx: {
active: false,
keypath: '',
passphrase: '',
certpath: '',
capath: ''
}
},
auth: {
activeStrategy: null,
mandatory: false,
@ -322,13 +279,15 @@ module.exports = {
multiaddr: {
transactions: 50
},
explorers: {
bitcoind: 'inactive',
indexer: {
active: 'third_party_explorer',
localIndexer: {
host: '127.0.0.1',
port: 50001,
batchRequests: 'inactive'
},
socks5Proxy: null,
insight: [
'https://testnet-api.example.com'
],
btccom: 'https://tchain.api.btc.com/v3'
esplora: 'https://blockstream.info/testnet'
},
addrFilterThreshold: 1000,
addrDerivationPool: {

27
lib/bitcoin/addresses-helper.js

@ -7,7 +7,7 @@
const bitcoin = require('bitcoinjs-lib')
const btcMessage = require('bitcoinjs-message')
const activeNet = require('./network').network
const { p2pkh, p2sh, p2wpkh } = bitcoin.payments
/**
* A singleton providing Addresses helper functions
@ -20,8 +20,10 @@ class AddressesHelper {
* @returns {string} return the derived address
*/
p2pkhAddress(pubKeyBuffer) {
const pubKeyHash = bitcoin.crypto.hash160(pubKeyBuffer)
return bitcoin.address.toBase58Check(pubKeyHash, activeNet.pubKeyHash)
return p2pkh({
pubkey: pubKeyBuffer,
network: activeNet,
}).address
}
/**
@ -30,11 +32,13 @@ class AddressesHelper {
* @returns {string} return the derived address
*/
p2wpkhP2shAddress(pubKeyBuffer) {
const pubKeyHash = bitcoin.crypto.hash160(pubKeyBuffer)
const witnessProgram = bitcoin.script.witnessPubKeyHash.output.encode(pubKeyHash)
const scriptPubKey = bitcoin.crypto.hash160(witnessProgram)
const outputScript = bitcoin.script.scriptHash.output.encode(scriptPubKey)
return bitcoin.address.fromOutputScript(outputScript, activeNet)
return p2sh({
redeem: p2wpkh({
pubkey: pubKeyBuffer,
network: activeNet,
}),
network: activeNet,
}).address
}
/**
@ -43,9 +47,10 @@ class AddressesHelper {
* @returns {string} return the derived address
*/
p2wpkhAddress(pubKeyBuffer) {
const pubKeyHash = bitcoin.crypto.hash160(pubKeyBuffer)
const outputScript = bitcoin.script.witnessPubKeyHash.output.encode(pubKeyHash)
return bitcoin.address.fromOutputScript(outputScript, activeNet).toLowerCase()
return p2wpkh({
pubkey: pubKeyBuffer,
network: activeNet,
}).address.toLowerCase()
}
/**

34
lib/bitcoin/hd-accounts-helper.js

@ -105,7 +105,7 @@ class HDAccountsHelper {
}
/**
* Translates
* Translates
* - a xpub/ypub/zpub into a xpub
* - a tpub/upub/vpub into a tpub
* @param {string} xpub - extended public key to be translated
@ -120,7 +120,7 @@ class HDAccountsHelper {
&& ver != this.MAGIC_TPUB
&& ver != this.MAGIC_YPUB
&& ver != this.MAGIC_UPUB
&& ver != this.MAGIC_ZPUB
&& ver != this.MAGIC_ZPUB
&& ver != this.MAGIC_VPUB
) {
//Logger.error(null, 'HdAccountsHelper.xlatXPUB() : Incorrect format')
@ -177,12 +177,12 @@ class HDAccountsHelper {
}
let p = v
if (p >= this.LOCKED) {
ret.locked = true
p -= this.LOCKED
}
switch (p) {
case this.BIP44:
case this.BIP49:
@ -190,7 +190,7 @@ class HDAccountsHelper {
ret.type = p
break
}
return ret
}
@ -201,16 +201,16 @@ class HDAccountsHelper {
* @returns {integer}
*/
makeType(type, locked) {
let p =
let p =
(type >= this.LOCKED)
? type - this.LOCKED
: type
locked = !!locked
if (locked)
p += this.LOCKED
return p
}
@ -245,7 +245,7 @@ class HDAccountsHelper {
}
/**
* Checks if a hd account is a valid hdnode
* Checks if a hd account is a valid bip32
* @param {string} xpub - hd account
* @returns {boolean} returns true if hd account is valid, false otherwise
*/
@ -258,7 +258,7 @@ class HDAccountsHelper {
const xlatedXpub = this.xlatXPUB(xpub)
// Parse input as an HD Node. Throws if invalid
const node = bitcoin.HDNode.fromBase58(xlatedXpub, activeNet)
const node = bitcoin.bip32.fromBase58(xlatedXpub, activeNet)
// Check and see if this is a private key
if (!node.isNeutered())
@ -278,7 +278,7 @@ class HDAccountsHelper {
/**
* Get the hd node associated to an hd account
* @param {string} xpub - hd account
* @returns {HDNode}
* @returns {bip32}
*/
getNode(xpub) {
if (this.isValid(xpub))
@ -291,7 +291,7 @@ class HDAccountsHelper {
* Derives an address for an hd account
* @param {int} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {HDNode} chainNode - Parent HDNode used for derivation
* @param {bip32} chainNode - Parent bip32 used for derivation
* @param {int} index - index to be derived
* @param {int} type - type of derivation
* @returns {Promise - object} returns an object {address: '...', chain: <int>, index: <int>}
@ -307,13 +307,13 @@ class HDAccountsHelper {
switch (type) {
case this.BIP44:
addr.address = indexNode.getAddress()
addr.address = addrHelper.p2pkhAddress(indexNode.publicKey)
break
case this.BIP49:
addr.address = addrHelper.p2wpkhP2shAddress(indexNode.getPublicKeyBuffer())
addr.address = addrHelper.p2wpkhP2shAddress(indexNode.publicKey)
break
case this.BIP84:
addr.address = addrHelper.p2wpkhAddress(indexNode.getPublicKeyBuffer())
addr.address = addrHelper.p2wpkhAddress(indexNode.publicKey)
break
}
@ -382,7 +382,7 @@ class HDAccountsHelper {
Logger.error(null, 'A problem was met during parallel addresses derivation')
reject()
}
} catch(e) {
Logger.error(e, 'A problem was met during parallel addresses derivation')
reject(e)

12
lib/bitcoin/parallel-address-derivation.js

@ -21,7 +21,7 @@ const BIP84 = 2
* Derives an address for an hd account
* @param {int} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {HDNode} chainNode - Parent HDNode used for derivation
* @param {bip32} chainNode - Parent bip32 used for derivation
* @param {int} index - index to be derived
* @param {int} type - type of derivation
* @returns {Promise - object} returns an object {address: '...', chain: <int>, index: <int>}
@ -37,13 +37,13 @@ const deriveAddress = async function(chain, chainNode, index, type) {
switch (type) {
case BIP44:
addr.address = indexNode.getAddress()
addr.address = addrHelper.p2pkhAddress(indexNode.publicKey)
break
case BIP49:
addr.address = addrHelper.p2wpkhP2shAddress(indexNode.getPublicKeyBuffer())
addr.address = addrHelper.p2wpkhP2shAddress(indexNode.publicKey)
break
case BIP84:
addr.address = addrHelper.p2wpkhAddress(indexNode.getPublicKeyBuffer())
addr.address = addrHelper.p2wpkhAddress(indexNode.publicKey)
break
}
@ -61,7 +61,7 @@ process.on('message', async (msg) => {
const type = msg.type
// Parse input as an HD Node. Throws if invalid
const node = bitcoin.HDNode.fromBase58(xpub, activeNet)
const node = bitcoin.bip32.fromBase58(xpub, activeNet)
// Check and see if this is a private key
if (!node.isNeutered())
@ -88,5 +88,5 @@ process.on('message', async (msg) => {
error: e
})
}
})

8
lib/bitcoind-rpc/transactions.js

@ -53,9 +53,11 @@ class Transactions {
* @returns {Promise}
*/
async getTransaction(txid, fees) {
const keyCache = `${txid}-${fees ? '1' : '0'}`
// Return transaction from cache when possible
if (this.txCache.has(txid))
return this.txCache.get(txid)
if (this.txCache.has(keyCache))
return this.txCache.get(keyCache)
try {
const tx = await this.rpcClient.getrawtransaction(txid, true)
@ -107,7 +109,7 @@ class Transactions {
// Store in cache
if (ret.block && ret.block.hash)
this.txCache.set(txid, ret)
this.txCache.set(keyCache, ret)
return ret

26
lib/db/mysql-db-wrapper.js

@ -378,15 +378,33 @@ class MySqlDbWrapper {
/**
* Send a query
*/
async _query(query) {
async _query(query, retries) {
queryDebug && Logger.info(query)
if (retries == null)
retries = 5
return new Promise((resolve, reject) => {
try {
this.pool.query(query, null, (err, result, fields) => {
this.pool.query(query, null, async (err, result, fields) => {
if (err) {
this.queryError(err, query)
reject(err)
// Retry the request on lock errors
if ((err.code == 'ER_LOCK_DEADLOCK' ||
err.code == 'ER_LOCK_TIMEOUT' ||
err.code == 'ER_LOCK_WAIT_TIMEOUT') && (retries > 0)
) {
try {
this.queryError('Lock detected. Retry request in a few ms', query)
const sleepMillis = Math.floor((Math.random() * 100) + 1)
await new Promise(resolve2 => setTimeout(resolve2, sleepMillis))
const res = await this._query(query, retries - 1)
resolve(res)
} catch(err) {
reject(err)
}
} else {
reject(err)
}
} else {
queryDebug && Logger.info(result)
resolve(result)

35
lib/http-server/http-server.js

@ -5,7 +5,6 @@
'use strict'
const fs = require('fs')
const https = require('https')
const express = require('express')
const helmet = require('helmet')
const Logger = require('../logger')
@ -19,15 +18,11 @@ class HttpServer {
/**
* Constructor
* @param {int} port - port used by the http server
* @param {object} httpsOptions - https options
*/
constructor(port, httpsOptions) {
constructor(port) {
// Initialize server port
this.port = port
// Store https options
this.httpsOptions = httpsOptions
// Listening server instance
this.server = null
@ -58,30 +53,10 @@ class HttpServer {
HttpServer.sendError(res, ret, 500)
})
if (this.httpsOptions == null || !this.httpsOptions.active) {
// Start a http server
this.server = this.app.listen(this.port, () => {
Logger.info('HTTP server listening on port ' + this.port)
})
} else {
// Start a https server
const options = {
key: fs.readFileSync(this.httpsOptions.keypath),
cert: fs.readFileSync(this.httpsOptions.certpath),
requestCert: false,
rejectUnauthorized: false
}
if (this.httpsOptions.capath)
options.ca = fs.readFileSync(this.httpsOptions.capath)
if (this.httpsOptions.passphrase)
options.passphrase = this.httpsOptions.passphrase
this.server = https.createServer(options, this.app).listen(this.port, () => {
Logger.info('HTTPS server listening on port ' + this.port)
})
}
// Start a http server
this.server = this.app.listen(this.port, () => {
Logger.info('HTTP server listening on port ' + this.port)
})
this.server.timeout = 600 * 1000
// @see https://github.com/nodejs/node/issues/13391

224
lib/indexer-rpc/rpc-client.js

@ -0,0 +1,224 @@
/*!
* lib/indexer_rpc/rpc-client.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const net = require('net')
const makeConcurrent = require('make-concurrent')
const network = require('../bitcoin/network')
const keys = require('../../keys')[network.key]
/**
* RPC client for an indexer
* following the Electrum protocol
*/
class RpcClient {
/**
* Constructor
*/
constructor(opts) {
this._opts = {
host: keys.indexer.localIndexer.host,
port: keys.indexer.localIndexer.port,
concurrency: Infinity,
timeout: 10000
}
if (this._opts.concurrency !== Infinity) {
this._call = makeConcurrent(
this._call.bind(this),
{concurrency: this._opts.concurrency}
)
}
}
/**
* Set an option
* @param {string} key
* @param {*} value
* @return {RpcClient}
*/
set(key, value) {
this._opts[key] = value
return this
}
/**
* Get an option
* @param {string} key
* @return {*}
*/
get(key) {
return this._opts[key]
}
/**
* Check if an error returned by the wrapper
* is a connection error.
* @param {string} err - error message
* @returns {boolean} returns true if message related to a connection error
*/
static isConnectionError(err) {
if (typeof err != 'string')
return false
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') != -1)
const isConnRejected = (err.indexOf('Connection Rejected') != -1)
return (isTimeoutError || isConnRejected)
}
/**
* Check if the rpc api is ready to process requests
* @returns {Promise}
*/
static async waitForIndexerRpcApi(opts) {
let client = new RpcClient(opts)
try {
await client.sendRequest('server.version', 'dojo', ['1.0', '1.4'])
} catch(e) {
client = null
Logger.info('Indexer RPC API is still unreachable. New attempt in 20s.')
return util.delay(20000).then(() => {
return RpcClient.waitForIndexerRpcApi()
})
}
}
/**
* Send multiple requests (batch mode)
* @param {Object[]} batch - array of objects {method: ..., params: ...}
* @return {Promise}
*/
async sendBatch(batch) {
return this._call(batch, true)
}
/**
* Send multiple requests (flood mode)
* @param {Object[]} batch - array of objects {method: ..., params: ...}
* @return {Promise}
*/
async sendRequests(batch) {
return this._call(batch, false)
}
/**
* Send a request
* @param {string} method - called method
* @return {Promise}
*/
async sendRequest(method, ...params) {
const batch = [{method: method, params: params}]
const ret = await this._call(batch, false)
return ret[0]
}
/**
* Send requests (internal method)
* @param {Object[]} data - array of objects {method: ..., params: ...}
* @returns {Promise}
*/
async _call(data, batched) {
return new Promise((resolve, reject) => {
let methodId = 0
let requests = []
let responses = []
let response = ''
const requestErrorMsg = `Indexer JSON-RPC: host=${this._opts.host} port=${this._opts.port}:`
// Prepare an array of requests
requests = data.map(req => {
return JSON.stringify({
jsonrpc: '2.0',
method: req.method,
params: req.params || [],
id: methodId++
})
})
// If batch mode
// send a single batched request
if (batched)
requests = [`[${requests.join(',')}]`]
// Initialize the connection
const conn = net.Socket()
conn.setTimeout(this._opts.timeout)
conn.setEncoding('utf8')
conn.setKeepAlive(true, 0)
conn.setNoDelay(true)
conn.on('connect', () => {
// Send the requests
for (let request of requests)
conn.write(request + '\n')
})
conn.on('timeout', () => {
const e = new Error('ETIMEDOUT')
e.errorno = 'ETIMEDOUT'
e.code = 'ETIMEDOUT'
e.connect = false
conn.emit('error', e)
})
conn.on('data', chunk => {
// Process the received chunk char by char
for (let c of chunk) {
response += c
// Detect the end of a response
if (c == '\n') {
try {
// Parse the response
let parsed = JSON.parse(response)
if (parsed.error)
throw new Error(JSON.stringify(parsed.error))
// Add the parsed reponse to the array of responses
if (batched) {
responses = parsed.map(p => { return {idxAddr: p.id, txs: p.result} })
} else {
responses.push({idxAddr: parsed.id, txs: parsed.result})
}
// Reset the response
response = ''
// If all responses have been received
// close the connection
if (responses.length == data.length)
conn.end()
} catch (err) {
reject(
new Error(`${requestErrorMsg} Error Parsing JSON: ${err.message}, data: ${response}`)
)
}
}
}
})
conn.on('end', e => {
// Connection closed
// we can return the responses
resolve(responses)
})
conn.on('error', e => {
reject(new Error(`${requestErrorMsg} Request error: ${e}`))
})
// Connect to the RPC API
conn.connect({
host: this._opts.host,
port: this._opts.port
})
})
}
}
module.exports = RpcClient

179
lib/remote-importer/btccom-wrapper.js

@ -1,179 +0,0 @@
/*!
* lib/remote-importer\btccom-wrapper.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const rp = require('request-promise-native')
const addrHelper = require('../bitcoin/addresses-helper')
const util = require('../util')
const Logger = require('../logger')
const network = require('../bitcoin/network')
const keys = require('../../keys')[network.key]
const Wrapper = require('./wrapper')
/**
* Wrapper for the btc.com block explorer APIs
*/
class BtcComWrapper extends Wrapper {
/**
* Constructor
*/
constructor(url) {
super(url, keys.explorers.socks5Proxy)
}
/**
* Send a GET request to the API
* @param {string} route
* @returns {Promise}
*/
async _get(route) {
const params = {
url: `${this.base}${route}`,
method: 'GET',
json: true,
timeout: 15000
}
// Sets socks proxy agent if required
if (keys.explorers.socks5Proxy != null)
params['agent'] = this.socksProxyAgent
return rp(params)
}
/**
* Get a page of transactions related to a given address
* @param {string} address - bitcoin address
* @param {integer} page - page index
* @returns {Promise}
*/
async _getTxsForAddress(address, page) {
const uri = `/address/${address}/tx?page=${page}&verbose=1`
const results = await this._get(uri)
return results.data.list.map(tx => tx.hash)
}
/**
* Retrieve information for a given address
* @param {string} address - bitcoin address
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an object
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddress(address, filterAddr) {
const reqAddr = addrHelper.isBech32(address)
? addrHelper.getScriptHashFromBech32(address)
: address
const uri = `/address/${reqAddr}`
const result = await this._get(uri)
const ret = {
address: address,
ntx: result.data.tx_count,
txids: []
}
// Check if we should filter this address
if (filterAddr && ret.ntx > keys.addrFilterThreshold) {
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`)
return ret
}
const nbPagesApi = Math.ceil(ret.ntx / BtcComWrapper.NB_TXS_PER_PAGE)
const nbPages = Math.min(20, nbPagesApi)
const aPages = new Array(nbPages)
const listPages = Array.from(aPages, (val, idx) => idx + 1)
const results = await util.seriesCall(listPages, idx => {
return this._getTxsForAddress(reqAddr, idx)
})
for (let txids of results)
ret.txids = ret.txids.concat(txids)
return ret
}
/**
* Retrieve information for a given list of addresses
* @param {string[]} addresses - array of bitcoin addresses
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an array of objects
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddresses(addresses, filterAddr) {
const ret = []
const reqAddresses = []
const xlatedBech32Addr = {}
for (let a of addresses) {
if (addrHelper.isBech32(a)) {
const scriptHash = addrHelper.getScriptHashFromBech32(a)
reqAddresses.push(scriptHash)
xlatedBech32Addr[scriptHash] = a
} else {
reqAddresses.push(a)
}
}
// Send a batch request for all the addresses
const strReqAddresses = reqAddresses.join(',')
const uri = `/address/${strReqAddresses}`
const results = await this._get(uri)
const foundAddresses = Array.isArray(results.data)
? results.data
: [results.data]
for (let a of foundAddresses) {
if (a && a.tx_count > 0) {
// Translate bech32 address
const address = xlatedBech32Addr.hasOwnProperty(a.address)
? xlatedBech32Addr[a.address]
: a.address
if (a.tx_count <= 2) {
// Less than 3 transactions for this address
// all good
const retAddr = {
address: address,
ntx: a.tx_count,
txids: []
}
retAddr.txids = (a.tx_count == 1)
? [a.first_tx]
: [a.first_tx, a.last_tx]
ret.push(retAddr)
} else {
// More than 2 transactions for this address
// We need more requests to the API
if (filterAddr && a.tx_count > keys.addrFilterThreshold) {
Logger.info(` import of ${address} rejected (too many transactions - ${a.tx_count})`)
} else {
const retAddr = await this.getAddress(address)
ret.push(retAddr)
}
}
}
}
return ret
}
}
// BTC.COM acepts a max of 50txs per page
BtcComWrapper.NB_TXS_PER_PAGE = 50
module.exports = BtcComWrapper

131
lib/remote-importer/esplora-wrapper.js

@ -0,0 +1,131 @@
/*!
* lib/remote-importer\esplora-wrapper.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const rp = require('request-promise-native')
const addrHelper = require('../bitcoin/addresses-helper')
const util = require('../util')
const Logger = require('../logger')
const network = require('../bitcoin/network')
const keys = require('../../keys')[network.key]
const Wrapper = require('./wrapper')
/**
* Wrapper for the esplora block explorer APIs
*/
class EsploraWrapper extends Wrapper {
/**
* Constructor
*/
constructor(url) {
super(url, keys.indexer.socks5Proxy)
}
/**
* Send a GET request to the API
* @param {string} route
* @returns {Promise}
*/
async _get(route) {
const params = {
url: `${this.base}${route}`,
method: 'GET',
json: true,
timeout: 15000
}
// Sets socks proxy agent if required
if (keys.indexer.socks5Proxy != null)
params['agent'] = this.socksProxyAgent
return rp(params)
}
/**
* Get a page of transactions related to a given address
* @param {string} address - bitcoin address
* @param {string} lastSeenTxid - last seen txid
* (see https://github.com/Blockstream/esplora/blob/master/API.md)
* @returns {Promise}
*/
async _getTxsForAddress(address, lastSeenTxid) {
let uri = `/api/address/${address}/txs`
if (lastSeenTxid)
uri = uri + `/chain/${lastSeenTxid}`
const results = await this._get(uri)
return results.map(tx => tx.txid)
}
/**
* Retrieve information for a given address
* @param {string} address - bitcoin address
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an object
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddress(address, filterAddr) {
const ret = {
address: address,
ntx: 0,
txids: []
}
let lastSeenTxid = null
while (true) {
const txids = await this._getTxsForAddress(address, lastSeenTxid)
if (txids.length == 0)
// we have all the transactions
return ret
ret.txids = ret.txids.concat(txids)
ret.ntx += ret.txids.length
if (txids.length < EsploraWrapper.NB_TXS_PER_PAGE) {
// we have all the transactions
return ret
} else if (filterAddr && ret.ntx > keys.addrFilterThreshold) {
// we have too many transactions
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`)
ret.txids = []
ret.ntx = 0
return ret
} else {
// we need a new iteration
lastSeenTxid = txids[txids.length-1]
}
}
}
/**
* Retrieve information for a given list of addresses
* @param {string[]} addresses - array of bitcoin addresses
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an array of objects
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddresses(addresses, filterAddr) {
const ret = []
for (let a of addresses) {
const retAddr = await this.getAddress(a, filterAddr)
ret.push(retAddr)
}
return ret
}
}
// Esplora returns a max of 25 txs per page
EsploraWrapper.NB_TXS_PER_PAGE = 25
module.exports = EsploraWrapper

90
lib/remote-importer/insight-wrapper.js

@ -1,90 +0,0 @@
/*!
* lib/remote-importer/insight-wrapper.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const rp = require('request-promise-native')
const Logger = require('../logger')
const network = require('../bitcoin/network')
const keys = require('../../keys')[network.key]
const Wrapper = require('./wrapper')
/**
* Wrapper for the Insight block explorer APIs
*/
class InsightWrapper extends Wrapper {
/**
* Constructor
*/
constructor(url) {
super(url, keys.explorers.socks5Proxy)
}
/**
* Send a GET request to the API
* @param {string} route
* @returns {Promise}
*/
async _get(route) {
const params = {
url: `${this.base}${route}`,
method: 'GET',
json: true,
timeout: 15000
}
// Sets socks proxy agent if required
if (keys.explorers.socks5Proxy != null)
params['agent'] = this.socksProxyAgent
return rp(params)
}
/**
* Retrieve information for a given address
* @param {string} address - bitcoin address
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an object
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddress(address, filterAddr) {
const uri = `/addr/${address}`
// Param filterAddr isn't used for insight
const result = await this._get(uri)
const ret = {
address: result.addrStr,
txids: [],
ntx: result.txApperances
}
// Check if we should filter this address
if (filterAddr && ret.ntx > keys.addrFilterThreshold) {
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`)
return ret
}
ret.txids = result.transactions
return ret
}
/**
* Retrieve information for a given list of addresses
* @param {string} addresses - array of bitcoin addresses
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an array of objects
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddresses(addresses, filterAddr) {
// Not implemented for this api
throw "Not implemented"
}
}
module.exports = InsightWrapper

143
lib/remote-importer/local-indexer-wrapper.js

@ -0,0 +1,143 @@
/*!
* lib/remote-importer/local-indexer-wrapper.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const bitcoin = require('bitcoinjs-lib')
const Logger = require('../logger')
const util = require('../util')
const network = require('../bitcoin/network')
const activeNet = network.network
const keys = require('../../keys')[network.key]
const RpcClient = require('../indexer-rpc/rpc-client')
const Wrapper = require('./wrapper')
/**
* Wrapper for a local indexer
* Currently supports indexers
* providing a RPC API compliant
* with a subset of the electrum protocol
*/
class LocalIndexerWrapper extends Wrapper {
/**
* Constructor
*/
constructor() {
super(null, null)
// RPC client
this.client = new RpcClient()
}
/**
* Translate a bitcoin address into a script hash
* (@see https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes)
* @param {string} address - bitcoin address
* @returns {string} returns the script hash associated to the address
*/
_getScriptHash(address) {
const bScriptPubKey = bitcoin.address.toOutputScript(address, activeNet)
const bScriptHash = bitcoin.crypto.sha256(bScriptPubKey)
return util.reverseBuffer(bScriptHash).toString('hex')
}
/**
* Retrieve information for a given address
* @param {string} address - bitcoin address
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an object
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddress(address, filterAddr) {
const ret = {
address: address,
ntx: 0,
txids: []
}
const scriptHash = this._getScriptHash(address)
const results = await this.client.sendRequest(
LocalIndexerWrapper.GET_HISTORY_RPC_CMD,
scriptHash
)
for (let r of results.txs) {
ret.txids.push(r.tx_hash)
ret.ntx++
}
if (filterAddr && ret.ntx > keys.addrFilterThreshold) {
Logger.info(` import of ${address} rejected (too many transactions - ${ret.ntx})`)
return {
address: address,
ntx: 0,
txids: []
}
}
return ret
}
/**
* Retrieve information for a given list of addresses
* @param {string} addresses - array of bitcoin addresses
* @param {boolean} filterAddr - True if an upper bound should be used
* for #transactions associated to the address, False otherwise
* @returns {Promise} returns an array of objects
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>}
*/
async getAddresses(addresses, filterAddr) {
const ret = {}
// Build an array of script hashes
const scriptHashes = addresses.map(a => this._getScriptHash(a))
// Build an array of commands
const commands = scriptHashes.map(s => {
return {
method: LocalIndexerWrapper.GET_HISTORY_RPC_CMD,
params: [s]
}
})
// Send the requests
const results = (keys.indexer.localIndexer.batchRequests == 'active')
? await this.client.sendBatch(commands)
: await this.client.sendRequests(commands)
for (let r of results) {
const addr = addresses[r.idxAddr]
const txids = r.txs.map(t => t.tx_hash)
ret[addr] = {
address: addr,
ntx: txids.length,
txids: txids
}
}
const aRet = Object.values(ret)
for (let i in aRet) {
if (filterAddr && aRet[i].ntx > keys.addrFilterThreshold) {
Logger.info(` import of ${aRet[i].address} rejected (too many transactions - ${aRet[i].ntx})`)
aRet.splice(i, 1)
}
}
return aRet
}
}
/**
* Get history RPC command (Electrum protocol)
*/
LocalIndexerWrapper.GET_HISTORY_RPC_CMD = 'blockchain.scripthash.get_history'
module.exports = LocalIndexerWrapper

4
lib/remote-importer/oxt-wrapper.js

@ -20,7 +20,7 @@ class OxtWrapper extends Wrapper {
* Constructor
*/
constructor(url) {
super(url, keys.explorers.socks5Proxy)
super(url, keys.indexer.socks5Proxy)
}
/**
@ -37,7 +37,7 @@ class OxtWrapper extends Wrapper {
}
// Sets socks proxy agent if required
if (keys.explorers.socks5Proxy != null)
if (keys.indexer.socks5Proxy != null)
params['agent'] = this.socksProxyAgent
return rp(params)

8
lib/remote-importer/remote-importer.js

@ -144,9 +144,13 @@ class RemoteImporter {
let gaps = [gap.external, gap.internal]
// Allow custom higher gap limits
// for local scans relying on bitcoind
if (gapLimit && keys.explorers.bitcoind)
// for local scans relying on bitcoind or on a local indexer
if (gapLimit
&& ((keys.indexer.active == 'local_bitcoind')
|| (keys.indexer.active == 'local_indexer'))
) {
gaps = [gapLimit, gapLimit]
}
startIndex = (startIndex == null) ? -1 : startIndex - 1

10
lib/remote-importer/sources-mainnet.js

@ -9,6 +9,7 @@ const Logger = require('../logger')
const keys = require('../../keys')[network.key]
const Sources = require('./sources')
const BitcoindWrapper = require('./bitcoind-wrapper')
const LocalIndexerWrapper = require('./local-indexer-wrapper')
const OxtWrapper = require('./oxt-wrapper')
@ -29,14 +30,19 @@ class SourcesMainnet extends Sources {
* Initialize the external data source
*/
_initSource() {
if (keys.explorers.bitcoind == 'active') {
if (keys.indexer.active == 'local_bitcoind') {
// If local bitcoind option is activated
// we'll use the local node as our unique source
this.source = new BitcoindWrapper()
Logger.info('Activated Bitcoind as the data source for imports')
} else if (keys.indexer.active == 'local_indexer') {
// If local indexer option is activated
// we'll use the local indexer as our unique source
this.source = new LocalIndexerWrapper()
Logger.info('Activated local indexer as the data source for imports')
} else {
// Otherwise, we'll use the rest api provided by OXT
this.source = new OxtWrapper(keys.explorers.oxt)
this.source = new OxtWrapper(keys.indexer.oxt)
Logger.info('Activated OXT API as the data source for imports')
}
}

16
lib/remote-importer/sources-testnet.js

@ -10,7 +10,8 @@ const Logger = require('../logger')
const keys = require('../../keys')[network.key]
const Sources = require('./sources')
const BitcoindWrapper = require('./bitcoind-wrapper')
const BtcComWrapper = require('./btccom-wrapper')
const LocalIndexerWrapper = require('./local-indexer-wrapper')
const EsploraWrapper = require('./esplora-wrapper')
/**
@ -30,15 +31,20 @@ class SourcesTestnet extends Sources {
* Initialize the external data source
*/
_initSource() {
if (keys.explorers.bitcoind == 'active') {
if (keys.indexer.active == 'local_bitcoind') {
// If local bitcoind option is activated
// we'll use the local node as our unique source
this.source = new BitcoindWrapper()
Logger.info('Activated Bitcoind as the data source for imports')
} else if (keys.indexer.active == 'local_indexer') {
// If local indexer option is activated
// we'll use the local indexer as our unique source
this.source = new LocalIndexerWrapper()
Logger.info('Activated local indexer as the data source for imports')
} else {
// Otherwise, we'll use the rest api provided by OXT
this.source = new BtcComWrapper(keys.explorers.btccom)
Logger.info('Activated BTC.COM API as the data source for imports')
// Otherwise, we'll use the rest api provided by Esplora
this.source = new EsploraWrapper(keys.indexer.esplora)
Logger.info('Activated Esplora API as the data source for imports')
}
}

21
lib/util.js

@ -132,6 +132,27 @@ class Util {
return Util.isHashStr(hash) && hash.length == 64
}
/**
* Reverse buffer content (swap endiannes)
*/
static reverseBuffer(buffer) {
if (buffer.length < 1)
return buffer
let j = buffer.length - 1
let tmp = 0
for (let i = 0; i < buffer.length / 2; i++) {
tmp = buffer[i]
buffer[i] = buffer[j]
buffer[j] = tmp
j--
}
return buffer
}
/**
* Sum an array of values
*/

9
lib/wallet/address-info.js

@ -65,9 +65,12 @@ class AddressInfo {
this.path = ['M', info.hdAddrChain, info.hdAddrIndex].join('/')
}
if (res.loose.indexOf(this.address) > -1) {
this.tracked = true
this.type = 'loose'
for (let a of res.loose) {
if (a.addrAddress == this.address) {
this.tracked = true
this.type = 'loose'
break
}
}
return this.loadInfo()

19
lib/wallet/wallet-service.js

@ -9,6 +9,9 @@ const Logger = require('../logger')
const db = require('../db/mysql-db-wrapper')
const hdaService = require('../bitcoin/hd-accounts-service')
const hdaHelper = require('../bitcoin/hd-accounts-helper')
const network = require('../bitcoin/network')
const activeNet = network.network
const keys = require('../../keys')[network.key]
const WalletInfo = require('./wallet-info')
@ -60,9 +63,13 @@ class WalletService {
await db.addAddresses(bip49.addrs)
await db.addAddresses(bip84.addrs)
await db.addAddresses(pubkeys.addrs)
// Ensure addresses exist and filter them
// Ensure addresses exist
await walletInfo.ensureAddresses()
//await this._forceEnsureAddressesForActivePubkeys(active)
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the address and load them
await walletInfo.filterAddresses()
await walletInfo.loadAddressesInfo()
// Load the most recent transactions
@ -136,9 +143,13 @@ class WalletService {
await db.addAddresses(bip49.addrs)
await db.addAddresses(bip84.addrs)
await db.addAddresses(pubkeys.addrs)
// Ensure addresses exist and filter them
// Ensure addresses exist
await walletInfo.ensureAddresses()
//await this._forceEnsureAddressesForActivePubkeys(active)
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses
await walletInfo.filterAddresses()
// Load the utxos
await walletInfo.loadUtxos()

997
package-lock.json

File diff suppressed because it is too large

6
package.json

@ -1,6 +1,6 @@
{
"name": "samourai-dojo",
"version": "1.2.0",
"version": "1.3.0",
"description": "Backend server for Samourai Wallet",
"main": "accounts/index.js",
"scripts": {
@ -17,7 +17,7 @@
"async-sema": "2.1.2",
"bip39": "2.4.0",
"bitcoind-rpc-client": "0.3.1",
"bitcoinjs-lib": "3.2.0",
"bitcoinjs-lib": "5.1.4",
"bitcoinjs-message": "1.0.1",
"body-parser": "1.18.3",
"express": "4.16.3",
@ -37,6 +37,6 @@
"zeromq": "4.2.0"
},
"devDependencies": {
"mocha": "^3.5.0"
"mocha": "^6.2.0"
}
}

5
pushtx/index-orchestrator.js

@ -46,4 +46,7 @@
const orchestrator = new Orchestrator()
orchestrator.start()
})()
})().catch(err => {
console.error(err)
process.exit(1)
})

8
pushtx/index.js

@ -45,8 +45,7 @@
// Initialize the http server
const port = keys.ports.pushtx
const httpsOptions = keys.https.pushtx
const httpServer = new HttpServer(port, httpsOptions)
const httpServer = new HttpServer(port)
// Initialize the PushTx rest api
const pushtxRestApi = new PushTxRestApi(httpServer)
@ -54,4 +53,7 @@
// Start the http server
httpServer.start()
})()
})().catch(err => {
console.error(err)
process.exit(1)
})

16
static/admin/lib/api-wrapper.js

@ -133,7 +133,23 @@ var lib_api = {
return this.sendGetUriEncoded(uri, {});
},
/**
* Rescans a range of blocks
*/
getBlocksRescan: function(fromHeight, toHeight) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/tracker/' + prefix + '/rescan';
//let uri = 'http://127.0.0.1:8082/' + prefix + '/rescan';
return this.sendGetUriEncoded(
uri,
{
'fromHeight': fromHeight,
'toHeight': toHeight
}
);
},
/**
* HTTP requests methods
*/

5
static/admin/tool/index.html

@ -71,7 +71,10 @@
<a href="#">UNSPENT</a>
</li>
<li id="link-tx">
<a href="#">TRANSACTION</a>
<a href="#">TX</a>
</li>
<li id="link-rescan-blocks">
<a href="#">BLOCKS RESCAN</a>
</li>
</ul>
</div>

11
static/admin/tool/index.js

@ -48,6 +48,7 @@ function initTabs() {
'#link-xpub',
'#link-info-address',
'#link-rescan-address',
'#link-rescan-blocks',
'#link-multiaddr',
'#link-unspent',
'#link-tx'
@ -110,6 +111,12 @@ function preparePage() {
placeholder = 'ENTER A BITCOIN ADDRESS';
} else if (activeTab == '#link-rescan-address') {
placeholder = 'ENTER A BITCOIN ADDRESS';
} else if (activeTab == '#link-rescan-blocks') {
$("#cell-args").removeClass('fullwidth');
$("#cell-args").addClass('halfwidth');
$("#cell-args2").show();
placeholder = 'RESCAN BLOCKS FROM HEIGHT...';
placeholder2 = '...TO HEIGHT (OPTIONAL)';
} else if (activeTab == '#link-multiaddr') {
placeholder = 'ENTER /MULTIADDR URL ARGUMENTS (e.g.: active=xpub0123456789&new=address2|address3&pubkey=pubkey4)';
} else if (activeTab == '#link-unspent') {
@ -164,6 +171,10 @@ function processAction(activeTab, args, args2, args3) {
return lib_api.getAddressInfo(args);
} else if (activeTab == '#link-rescan-address') {
return lib_api.getAddressRescan(args);
} else if (activeTab == '#link-rescan-blocks') {
const fromHeight = parseInt(args);
const toHeight = (args2) ? parseInt(args2) : fromHeight;
return lib_api.getBlocksRescan(fromHeight, toHeight);
} else if (activeTab == '#link-tx') {
return lib_api.getTransaction(args);
}

33
tracker/blockchain-processor.js

@ -320,6 +320,39 @@ class BlockchainProcessor extends AbstractProcessor {
await db.deleteBlocksAfterHeight(height)
}
/**
* Rescan a range of blocks
* @param {integer} fromHeight - height of first block
* @param {integer} toHeight - height of last block
* @returns {Promise}
*/
async rescanBlocks(fromHeight, toHeight) {
// Get highest block processed by the tracker
const highest = await db.getHighestBlock()
const dbMaxHeight = highest.blockHeight
if (toHeight == null)
toHeight = fromHeight
toHeight = Math.min(toHeight, dbMaxHeight)
const blockRange = _.range(fromHeight, toHeight + 1)
Logger.info(`Blocks Rescan : starting a rescan for ${blockRange.length} blocks`)
// Process the blocks
return util.seriesCall(blockRange, async height => {
try {
Logger.info(`Rescanning block ${height}`)
const hash = await this.client.getblockhash(height)
const header = await this.client.getblockheader(hash)
return this.processBlock(header)
} catch(e) {
Logger.error(e, 'BlockchainProcessor.rescan()')
throw e
}
}, 'Tracker rescan', true)
}
/**
* Process a block
* @param {object} header - block header

21
tracker/index.js

@ -11,7 +11,9 @@
const keys = require('../keys')[network.key]
const db = require('../lib/db/mysql-db-wrapper')
const Logger = require('../lib/logger')
const HttpServer = require('../lib/http-server/http-server')
const Tracker = require('./tracker')
const TrackerRestApi = require('./tracker-rest-api')
Logger.info('Process ID: ' + process.pid)
@ -33,8 +35,23 @@
db.connect(dbConfig)
// Start the tracker
// Initialize the tracker
const tracker = new Tracker()
// Initialize the http server
const port = keys.ports.trackerApi
const httpServer = new HttpServer(port)
// Initialize the rest api endpoints
const trackerRestApi = new TrackerRestApi(httpServer, tracker)
// Start the http server
httpServer.start()
// Start the tracker
tracker.start()
})()
})().catch(err => {
console.error(err)
process.exit(1)
})

79
tracker/tracker-rest-api.js

@ -0,0 +1,79 @@
/*!
* tracker/tracker-rest-api.js
* Copyright (c) 2016-2019, Samourai Wallet (CC BY-NC-ND 4.0 License).
*/
'use strict'
const qs = require('querystring')
const validator = require('validator')
const bodyParser = require('body-parser')
const Logger = require('../lib/logger')
const errors = require('../lib/errors')
const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const network = require('../lib/bitcoin/network')
const keys = require('../keys')[network.key]
/**
* Tracker API endpoints
*/
class TrackerRestApi {
/**
* Constructor
* @param {pushtx.HttpServer} httpServer - HTTP server
* @param {tracker.Tracker} tracker - tracker
*/
constructor(httpServer, tracker) {
this.httpServer = httpServer
this.tracker = tracker
const urlencodedParser = bodyParser.urlencoded({ extended: true })
// Establish routes. Proxy server strips /pushtx
this.httpServer.app.get(
`/${keys.prefixes.support}/rescan`,
authMgr.checkHasAdminProfile.bind(authMgr),
this.getBlocksRescan.bind(this),
HttpServer.sendAuthError
)
}
/**
* Rescan a range of blocks
*/
async getBlocksRescan(req, res) {
// Check request arguments
if (!req.query)
return HttpServer.sendError(res, errors.body.INVDATA)
if (!req.query.fromHeight || !validator.isInt(req.query.fromHeight))
return HttpServer.sendError(res, errors.body.INVDATA)
if (req.query.toHeight && !validator.isInt(req.query.toHeight))
return HttpServer.sendError(res, errors.body.INVDATA)
// Retrieve the request arguments
const fromHeight = parseInt(req.query.fromHeight)
const toHeight = req.query.toHeight ? parseInt(req.query.toHeight) : fromHeight
if (req.query.toHeight && (toHeight < fromHeight))
return HttpServer.sendError(res, errors.body.INVDATA)
try {
await this.tracker.blockchainProcessor.rescanBlocks(fromHeight, toHeight)
const ret = {
status: 'Rescan complete',
fromHeight: fromHeight,
toHeight: toHeight
}
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} catch(e) {
return HttpServer.sendError(res, e)
}
}
}
module.exports = TrackerRestApi
Loading…
Cancel
Save