Browse Source

Merge v1.11.0 into umbrel

umbrel
Lounès Ksouri 3 years ago
parent
commit
7f0b4a0986
No known key found for this signature in database GPG Key ID: F8DC83D24F68572D
  1. 23
      RELEASES.md
  2. 4
      accounts/api-helper.js
  3. 7
      accounts/fees-rest-api.js
  4. 2
      accounts/headers-rest-api.js
  5. 51
      accounts/multiaddr-rest-api.js
  6. 13
      accounts/notifications-server.js
  7. 16
      accounts/notifications-service.js
  8. 2
      accounts/status-rest-api.js
  9. 6
      accounts/status.js
  10. 18
      accounts/support-rest-api.js
  11. 4
      accounts/transactions-rest-api.js
  12. 2
      accounts/unspent-rest-api.js
  13. 12
      accounts/wallet-rest-api.js
  14. 16
      accounts/xpub-rest-api.js
  15. 6
      docker/my-dojo/.env
  16. 9
      docker/my-dojo/bitcoin/Dockerfile
  17. 7
      docker/my-dojo/bitcoin/restart.sh
  18. 50
      docker/my-dojo/bitcoin/rpcauth.py
  19. 4
      docker/my-dojo/conf/docker-bitcoind.conf.tpl
  20. 85
      docker/my-dojo/dojo.sh
  21. 2
      docker/my-dojo/explorer/Dockerfile
  22. 2
      docker/my-dojo/node/Dockerfile
  23. 2
      docker/my-dojo/node/keys.index.js
  24. 6
      docker/my-dojo/tor/Dockerfile
  25. 18
      docker/my-dojo/tor/restart.sh
  26. 2
      docker/my-dojo/whirlpool/Dockerfile
  27. 4
      keys/index-example.js
  28. 6
      lib/auth/authorizations-manager.js
  29. 3
      lib/auth/localapikey-strategy-configurator.js
  30. 38
      lib/bitcoin/addresses-helper.js
  31. 12
      lib/bitcoin/addresses-service.js
  32. 101
      lib/bitcoin/hd-accounts-helper.js
  33. 69
      lib/bitcoin/hd-accounts-service.js
  34. 21
      lib/bitcoin/parallel-address-derivation.js
  35. 4
      lib/bitcoind-rpc/fees.js
  36. 2
      lib/bitcoind-rpc/headers.js
  37. 2
      lib/bitcoind-rpc/latest-block.js
  38. 7
      lib/bitcoind-rpc/rpc-client.js
  39. 14
      lib/bitcoind-rpc/transactions.js
  40. 159
      lib/db/mysql-db-wrapper.js
  41. 33
      lib/http-server/http-server.js
  42. 12
      lib/indexer-rpc/rpc-client.js
  43. 4
      lib/remote-importer/bitcoind-wrapper.js
  44. 4
      lib/remote-importer/esplora-wrapper.js
  45. 2
      lib/remote-importer/local-indexer-wrapper.js
  46. 2
      lib/remote-importer/local-rest-indexer-wrapper.js
  47. 4
      lib/remote-importer/oxt-wrapper.js
  48. 29
      lib/remote-importer/remote-importer.js
  49. 6
      lib/remote-importer/sources-mainnet.js
  50. 6
      lib/remote-importer/sources-testnet.js
  51. 4
      lib/remote-importer/sources.js
  52. 3
      lib/remote-importer/wrapper.js
  53. 131
      lib/util.js
  54. 34
      lib/wallet/address-info.js
  55. 37
      lib/wallet/hd-account-info.js
  56. 21
      lib/wallet/wallet-entities.js
  57. 48
      lib/wallet/wallet-info.js
  58. 28
      lib/wallet/wallet-service.js
  59. 3341
      package-lock.json
  60. 30
      package.json
  61. 12
      pushtx/orchestrator.js
  62. 8
      pushtx/pushtx-processor.js
  63. 4
      pushtx/pushtx-rest-api.js
  64. 8
      pushtx/transactions-scheduler.js
  65. 14
      scripts/patches/revert-hd-accounts.js
  66. 20
      scripts/patches/translate-hd-accounts.js
  67. 2
      static/admin/dmt/index.js
  68. 10
      static/admin/dmt/status/status.js
  69. 2
      static/admin/dmt/txs-tools/txs-tools.js
  70. 8
      static/admin/dmt/xpubs-tools/xpubs-tools.js
  71. 4
      static/admin/lib/auth-utils.js
  72. 8
      static/admin/lib/common-script.js
  73. 2
      static/admin/lib/errors-utils.js
  74. 2
      static/admin/lib/format-utils.js
  75. 2
      test/lib/bitcoin/addresses-helper-test.js
  76. 38
      test/lib/bitcoin/hd-accounts-helper-test.js
  77. 10
      tracker/block-worker.js
  78. 16
      tracker/block.js
  79. 32
      tracker/blockchain-processor.js
  80. 10
      tracker/blocks-processor.js
  81. 64
      tracker/mempool-processor.js
  82. 30
      tracker/tracker.js
  83. 19
      tracker/transaction.js
  84. 19
      tracker/transactions-bundle.js

23
RELEASES.md

@ -3,6 +3,7 @@
## Releases ##
- [v1.11.0](#1_11_0)
- [v1.10.1](#1_10_1)
- [v1.10.0](#1_10_0)
- [v1.9.0](#1_9_0)
@ -17,6 +18,28 @@
- [v1.2.0](#1_2_0)
- [v1.1.0](#1_1_0)
<a name="1_11_0"/>
## Samourai Dojo v1.11.0 ##
## Breaking ##
- Dojo now requires Node.js v14
#### Features ####
- [#mr242](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/242) postmix decoy change addresses
- [#mr241](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/241) update ZeroMQ and Node.js
- [#mr240](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/240) update Node.js dependencies
- [#mr239](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/239) update Tor and remove v2 onion addresses
- [#mr238](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/238) use RPC auth instead of basic auth
- other minor improvements
#### Bug fixes ####
- [#mr237](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/237) fix tracker initialization
- [commit 3ee4ecc6](https://code.samourai.io/dojo/samourai-dojo/-/commit/3ee4ecc645dc88632f4e7bfd00fafe602bcaef13) fix importing from local_bitcoind
- other minor fixes
<a name="1_10_1"/>
## Samourai Dojo v1.10.1 ##

4
accounts/api-helper.js

@ -70,7 +70,9 @@ class ApiHelper {
item = item.toLowerCase()
ret.addAddress(item, false)
}
} catch(e) {}
} catch(e) {
Logger.error(e, 'API : ApiHelper.parseEntities() : Invalid arguments')
}
}
return ret

7
accounts/fees-rest-api.js

@ -9,7 +9,7 @@ const rpcFees = require('../lib/bitcoind-rpc/fees')
const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**
@ -29,6 +29,11 @@ class FeesRestApi {
authMgr.checkAuthentication.bind(authMgr),
this.getFees.bind(this),
)
this.httpServer.app.post(
'/fees',
authMgr.checkAuthentication.bind(authMgr),
this.getFees.bind(this),
)
// Refresh the network fees
rpcFees.refresh()
}

2
accounts/headers-rest-api.js

@ -12,7 +12,7 @@ const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**

51
accounts/multiaddr-rest-api.js

@ -12,7 +12,7 @@ const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**
@ -32,18 +32,20 @@ class MultiaddrRestApi {
const urlencodedParser = bodyParser.urlencoded({ extended: true })
this.httpServer.app.get(
'/multiaddr',
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.getMultiaddr.bind(this),
'/multiaddr',
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.getMultiaddr.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.post(
'/multiaddr',
urlencodedParser,
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.postMultiaddr.bind(this),
'/multiaddr',
urlencodedParser,
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.postMultiaddr.bind(this),
HttpServer.sendAuthError
)
}
@ -57,16 +59,19 @@ class MultiaddrRestApi {
// Check request params
if (!apiHelper.checkEntitiesParams(req.query))
return HttpServer.sendError(res, errors.multiaddr.NOACT)
//return HttpServer.sendError(res, '')
// Parse params
const entities = apiHelper.parseEntitiesParams(req.query)
if (entities.active.addrs.length === 1 || entities.active.addrs.length === 2 || entities.active.xpubs.length === 1)
return HttpServer.sendError(res, '')
const result = await walletService.getWalletInfo(
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
)
const ret = JSON.stringify(result, null, 2)
@ -78,7 +83,7 @@ class MultiaddrRestApi {
} finally {
if (debugApi) {
const strParams =
`${req.query.active ? req.query.active : ''} \
`${req.query.active ? req.query.active : ''} \
${req.query.new ? req.query.new : ''} \
${req.query.pubkey ? req.query.pubkey : ''} \
${req.query.bip49 ? req.query.bip49 : ''} \
@ -104,11 +109,11 @@ class MultiaddrRestApi {
const entities = apiHelper.parseEntitiesParams(req.body)
const result = await walletService.getWalletInfo(
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
)
HttpServer.sendOkDataOnly(res, result)
@ -119,7 +124,7 @@ class MultiaddrRestApi {
} finally {
if (debugApi) {
const strParams =
`${req.body.active ? req.body.active : ''} \
`${req.body.active ? req.body.active : ''} \
${req.body.new ? req.body.new : ''} \
${req.body.pubkey ? req.body.pubkey : ''} \
${req.body.bip49 ? req.body.bip49 : ''} \

13
accounts/notifications-server.js

@ -5,7 +5,7 @@
'use strict'
const _ = require('lodash')
const zmq = require('zeromq')
const zmq = require('zeromq/v5-compat')
const WebSocket = require('websocket')
const Logger = require('../lib/logger')
const network = require('../lib/bitcoin/network')
@ -27,9 +27,6 @@ class NotificationsServer {
this.httpServer = null
// Notifications service
this.notifService = null
// Initialize the zmq socket for communications
// with the tracker
this._initTrackerSocket()
}
/**
@ -40,8 +37,12 @@ class NotificationsServer {
this.httpServer = httpServer
if (this.notifService !== null) return
this.notifService = new NotificationsService(httpServer.server)
// Initialize the zmq socket for communications
// with the tracker
this._initTrackerSocket()
}
@ -54,7 +55,7 @@ class NotificationsServer {
this.sock.subscribe('block')
this.sock.subscribe('transaction')
this.sock.on('message', (topic, message, sequence) => {
this.sock.on('message', (topic, message) => {
switch(topic.toString()) {
case 'block':
try {

16
accounts/notifications-service.js

@ -14,7 +14,7 @@ const apiHelper = require('./api-helper')
const status = require('./status')
const authMgr = require('../lib/auth/authorizations-manager')
const debug = !!(process.argv.indexOf('ws-debug') > -1)
const debug = process.argv.indexOf('ws-debug') > -1
/**
@ -38,7 +38,7 @@ class NotificationsService {
// Cache registering the most recent subscriptions received
// Used to filter multiple subscriptions sent by external apps.
this.cacheSubs = LRU({
this.cacheSubs = new LRU({
// Maximum number of subscriptions to store in cache
// Estimate: 1000 clients with an average of 5 subscriptions
max: 5000,
@ -80,7 +80,7 @@ class NotificationsService {
})
conn.on('message', msg => {
if (msg.type == 'utf8')
if (msg.type === 'utf8')
this._handleWSMessage(msg.utf8Data, conn)
else
this._closeWSConnection(conn, true)
@ -229,7 +229,7 @@ class NotificationsService {
/**
* Unsubscribe from a topic
* @param {string} topic - topic
* @param {int} cid - client id
* @param {number} cid - client id
*/
_unsub(topic, cid) {
if (!this.subs[topic])
@ -241,7 +241,7 @@ class NotificationsService {
this.subs[topic].splice(index, 1)
if (this.subs[topic].length == 0) {
if (this.subs[topic].length === 0) {
delete this.subs[topic]
if (this.cachePubKeys.hasOwnProperty(topic))
delete this.cachePubKeys[topic]
@ -391,7 +391,7 @@ class NotificationsService {
for (let cid of this.subs[topic]) {
if (!clients[cid])
clients[cid] = []
if (clients[cid].indexOf(topic) == -1)
if (clients[cid].indexOf(topic) === -1)
clients[cid].push(topic)
}
}
@ -429,7 +429,7 @@ class NotificationsService {
}
// Move on if the custom transaction has no inputs or outputs
if (ctx.inputs.length == 0 && ctx.out.length == 0)
if (ctx.inputs.length === 0 && ctx.out.length === 0)
continue
// Send custom transaction to client
@ -454,7 +454,7 @@ class NotificationsService {
/**
* Dispatch notification for an authentication error
* @param {string} err - error
* @param {integer} cid - connection id
* @param {number} cid - connection id
*/
notifyAuthError(err, cid) {
const data = {

2
accounts/status-rest-api.js

@ -11,7 +11,7 @@ const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const status = require('./status')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**

6
accounts/status.js

@ -29,7 +29,7 @@ class Status {
/**
* Get current status
* @returns {Promise - object} status object
* @returns {Promise<object>} status object
*/
async getCurrent() {
const uptime = util.timePeriod((Date.now() - this.t0) / 1000, false)
@ -49,8 +49,8 @@ class Status {
let indexerMaxHeight = null
let indexerUrl = null
if (indexerType == 'third_party_explorer') {
indexerUrl = (network.key == 'bitcoin')
if (indexerType === 'third_party_explorer') {
indexerUrl = (network.key === 'bitcoin')
? keys.indexer.oxt
: keys.indexer.esplora
}

18
accounts/support-rest-api.js

@ -19,7 +19,7 @@ const AddressInfo = require('../lib/wallet/address-info')
const apiHelper = require('./api-helper')
const keys = require('../keys')[network.key]
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**
@ -94,7 +94,7 @@ class SupportRestApi {
try {
// Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.addr).addrs
if (entities.length == 0)
if (entities.length === 0)
return HttpServer.sendError(res, errors.address.INVALID)
const address = entities[0]
@ -133,7 +133,7 @@ class SupportRestApi {
try {
// Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.addr).addrs
if (entities.length == 0)
if (entities.length === 0)
return HttpServer.sendError(res, errors.address.INVALID)
const address = entities[0]
@ -162,7 +162,7 @@ class SupportRestApi {
try {
// Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0)
if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0]
@ -174,7 +174,7 @@ class SupportRestApi {
const ret = this._formatXpubInfoResult(info)
HttpServer.sendRawData(res, ret)
} catch(e) {
if(e == errors.db.ERROR_NO_HD_ACCOUNT) {
if(e === errors.db.ERROR_NO_HD_ACCOUNT) {
const ret = this._formatXpubInfoResult(info)
HttpServer.sendRawData(res, ret)
} else {
@ -210,7 +210,7 @@ class SupportRestApi {
try {
// Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0)
if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0]
@ -226,10 +226,10 @@ class SupportRestApi {
await hdaService.rescan(xpub, gapLimit, startIndex)
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} catch(e) {
if (e == errors.db.ERROR_NO_HD_ACCOUNT) {
if (e === errors.db.ERROR_NO_HD_ACCOUNT) {
ret.status = 'Error: Not tracking xpub'
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} else if (e == errors.xpub.OVERLAP) {
} else if (e === errors.xpub.OVERLAP) {
ret.status = 'Error: Rescan in progress'
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} else {
@ -256,7 +256,7 @@ class SupportRestApi {
try {
// Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0)
if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0]

4
accounts/transactions-rest-api.js

@ -15,7 +15,7 @@ const network = require('../lib/bitcoin/network')
const apiHelper = require('./api-helper')
const keys = require('../keys')[network.key]
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**
@ -86,7 +86,7 @@ class TransactionsRestApi {
const result = await walletService.getWalletTransactions(active, page, count)
if (excludeNullXfer) {
result.txs = result.txs.filter(tx => {
return tx['result'] != 0
return tx['result'] !== 0
})
}

2
accounts/unspent-rest-api.js

@ -12,7 +12,7 @@ const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**

12
accounts/wallet-rest-api.js

@ -11,9 +11,9 @@ const walletService = require('../lib/wallet/wallet-service')
const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const hdaService = require('../lib/bitcoin/hd-accounts-service')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
const debugApi = process.argv.indexOf('api-debug') > -1
/**
* Wallet API endpoints
@ -60,6 +60,10 @@ class WalletRestApi {
// Parse params
const entities = apiHelper.parseEntitiesParams(req.query)
if (req.query.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,
@ -102,6 +106,10 @@ class WalletRestApi {
// Parse params
const entities = apiHelper.parseEntitiesParams(req.body)
if (req.body.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,

16
accounts/xpub-rest-api.js

@ -113,9 +113,9 @@ class XPubRestApi {
if (argSegwit) {
const segwit = argSegwit.toLowerCase()
if (segwit == 'bip49')
if (segwit === 'bip49')
scheme = hdaHelper.BIP49
else if (segwit == 'bip84')
else if (segwit === 'bip84')
scheme = hdaHelper.BIP84
else
return HttpServer.sendError(res, errors.xpub.SEGWIT)
@ -125,7 +125,7 @@ class XPubRestApi {
const forceOverride = argForceOverride ? argForceOverride : false
// Process action
if (argAction == 'new') {
if (argAction === 'new') {
// New hd account
try {
await hdaService.createHdAccount(xpub, scheme)
@ -133,7 +133,7 @@ class XPubRestApi {
} catch(e) {
HttpServer.sendError(res, e)
}
} else if (argAction == 'restore') {
} else if (argAction === 'restore') {
// Restore hd account
try {
await hdaService.restoreHdAccount(xpub, scheme, forceOverride)
@ -229,7 +229,7 @@ class XPubRestApi {
if (status != null) {
ret['import_in_progress'] = true
ret['status'] = status['status']
if (ret['status'] == remoteImporter.STATUS_RESCAN)
if (ret['status'] === remoteImporter.STATUS_RESCAN)
ret['hits'] = status['txs_int'] + status['txs_ext']
else
ret['hits'] = status['txs']
@ -268,7 +268,7 @@ class XPubRestApi {
if (!req.body.message)
return HttpServer.sendError(res, errors.body.NOMSG)
if (!(req.body.message == 'lock' || req.body.message == 'unlock'))
if (!(req.body.message === 'lock' || req.body.message === 'unlock'))
return HttpServer.sendError(res, errors.sig.INVMSG)
// Extract arguments
@ -289,7 +289,7 @@ class XPubRestApi {
try {
// Check the signature and process the request
await hdaService.verifyXpubSignature(xpub, argAddr, argSig, argMsg, scheme)
const lock = (argMsg == 'unlock') ? false : true
const lock = argMsg !== 'unlock'
const ret = await hdaService.lockHdAccount(xpub, lock)
HttpServer.sendOkData(res, {derivation: ret})
} catch(e) {
@ -385,7 +385,7 @@ class XPubRestApi {
}
} catch(e) {
const err = (e == errors.xpub.PRIVKEY) ? e : errors.xpub.INVALID
const err = (e === errors.xpub.PRIVKEY) ? e : errors.xpub.INVALID
throw err
}
}

6
docker/my-dojo/.env

@ -10,12 +10,12 @@
COMPOSE_CONVERT_WINDOWS_PATHS=1
DOJO_VERSION_TAG=1.10.1
DOJO_VERSION_TAG=1.11.0
DOJO_DB_VERSION_TAG=1.3.0
DOJO_BITCOIND_VERSION_TAG=1.12.0
DOJO_NODEJS_VERSION_TAG=1.10.1
DOJO_NODEJS_VERSION_TAG=1.11.0
DOJO_NGINX_VERSION_TAG=1.6.0
DOJO_TOR_VERSION_TAG=1.9.0
DOJO_TOR_VERSION_TAG=1.10.0
DOJO_EXPLORER_VERSION_TAG=1.7.0
DOJO_INDEXER_VERSION_TAG=1.3.0
DOJO_WHIRLPOOL_VERSION_TAG=1.4.0

9
docker/my-dojo/bitcoin/Dockerfile

@ -19,7 +19,7 @@ ARG TOR_LINUX_GID
RUN set -ex && \
apt-get update && \
apt-get install -qq --no-install-recommends ca-certificates dirmngr gosu gpg gpg-agent wget && \
apt-get install -qq --no-install-recommends ca-certificates dirmngr gosu gpg gpg-agent wget python3 && \
rm -rf /var/lib/apt/lists/*
# Build and install bitcoin binaries
@ -56,6 +56,13 @@ RUN chown bitcoin:bitcoin /wait-for-it.sh && \
chmod u+x /wait-for-it.sh && \
chmod g+x /wait-for-it.sh
# Copy rpcauth.py script
COPY ./rpcauth.py /rpcauth.py
RUN chown bitcoin:bitcoin /rpcauth.py && \
chmod u+x /rpcauth.py && \
chmod g+x /rpcauth.py
EXPOSE 8333 9501 9502 28256
USER bitcoin

7
docker/my-dojo/bitcoin/restart.sh

@ -1,6 +1,9 @@
#!/bin/bash
set -e
# Generate RPC auth payload
BITCOIND_RPC_AUTH=$(./rpcauth.py $BITCOIND_RPC_USER $BITCOIND_RPC_PASSWORD)
echo "## Start bitcoind #############################"
bitcoind_options=(
@ -18,11 +21,10 @@ bitcoind_options=(
-proxy=$NET_DOJO_TOR_IPV4:9050
-rpcallowip=0.0.0.0/0
-rpcbind=$NET_DOJO_BITCOIND_IPV4
-rpcpassword=$BITCOIND_RPC_PASSWORD
-rpcport=28256
-rpcthreads=$BITCOIND_RPC_THREADS
-rpcworkqueue=$BITCOIND_RPC_WORK_QUEUE
-rpcuser=$BITCOIND_RPC_USER
-rpcauth=$BITCOIND_RPC_AUTH
-server=1
-txindex=1
-zmqpubhashblock=tcp://0.0.0.0:9502
@ -32,7 +34,6 @@ bitcoind_options=(
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
bitcoind_options+=(-listen=1)
bitcoind_options+=(-bind="$NET_DOJO_BITCOIND_IPV4")
bitcoind_options+=(-externalip=$(cat /var/lib/tor/hsv2bitcoind/hostname))
bitcoind_options+=(-externalip=$(cat /var/lib/tor/hsv3bitcoind/hostname))
fi

50
docker/my-dojo/bitcoin/rpcauth.py

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from argparse import ArgumentParser
from base64 import urlsafe_b64encode
from binascii import hexlify
from getpass import getpass
from os import urandom
import hmac
def generate_salt(size):
"""Create size byte hex salt"""
return hexlify(urandom(size)).decode()
def generate_password():
"""Create 32 byte b64 password"""
return urlsafe_b64encode(urandom(32)).decode('utf-8')
def password_to_hmac(salt, password):
m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), 'SHA256')
return m.hexdigest()
def main():
parser = ArgumentParser(description='Create login credentials for a JSON-RPC user')
parser.add_argument('username', help='the username for authentication')
parser.add_argument('password', help='leave empty to generate a random password or specify "-" to prompt for password', nargs='?')
args = parser.parse_args()
if not args.password:
args.password = generate_password()
elif args.password == '-':
args.password = getpass()
# Create 16 byte hex salt
salt = generate_salt(16)
password_hmac = password_to_hmac(salt, args.password)
## Comment out original script output
# print('String to be appended to bitcoin.conf:')
# print('rpcauth={0}:{1}${2}'.format(args.username, salt, password_hmac))
# print('Your password:\n{0}'.format(args.password))
## Added custom script output to use in restart.sh
print('{0}:{1}${2}'.format(args.username, salt, password_hmac))
if __name__ == '__main__':
main()

4
docker/my-dojo/conf/docker-bitcoind.conf.tpl

@ -28,7 +28,7 @@ BITCOIND_RPC_THREADS=6
# RPC Work queue size
# Type: integer
BITCOIND_RPC_WORK_QUEUE=16
BITCOIND_RPC_WORK_QUEUE=64
# Mempool expiry in hours
# Defines how long transactions stay in your local mempool before expiring
@ -120,4 +120,4 @@ BITCOIND_ZMQ_BLK_HASH=9502
# Defines how long Dojo waits for a clean shutdown of bitcoind before shutting down the bitcoind container
# This parameter is inactive if BITCOIND_INSTALL is set to 'off'
# Type: integer
BITCOIND_SHUTDOWN_DELAY=180
BITCOIND_SHUTDOWN_DELAY=180

85
docker/my-dojo/dojo.sh

@ -85,7 +85,6 @@ stop() {
# Renewal of bitcoind onion address
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
if [ "$BITCOIND_EPHEMERAL_HS" = "on" ]; then
$( docker exec -it tor rm -rf /var/lib/tor/hsv2bitcoind ) &> /dev/null
$( docker exec -it tor rm -rf /var/lib/tor/hsv3bitcoind ) &> /dev/null
fi
fi
@ -355,77 +354,35 @@ upgrade() {
# Display the onion addresses
onion() {
version=3
# Extract version arguments
if [ $# -gt 0 ]; then
for option in $@
do
case "$option" in
v2 ) version=2 ;;
v3 ) version=3 ;;
* ) break ;;
esac
done
fi
echo " "
echo "WARNING: Do not share these onion addresses with anyone!"
echo " To allow another person to use this Dojo with their Samourai Wallet,"
echo " you should share the QRCodes provided by the Maintenance Tool."
echo " "
if [ $version -eq 3 ]; then
# V3 onion addresses
V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname )
echo "Dojo API and Maintenance Tool = $V3_ADDR"
echo " "
if [ "$EXPLORER_INSTALL" == "on" ]; then
V3_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv3explorer/hostname )
echo "Block Explorer = $V3_ADDR_EXPLORER"
echo " "
fi
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
V3_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv3whirlpool/hostname )
echo "Your private Whirlpool client (do not share) = $V3_ADDR_WHIRLPOOL"
echo " "
fi
if [ "$BITCOIND_INSTALL" == "on" ]; then
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
V3_ADDR_BTCD=$( docker exec -it tor cat /var/lib/tor/hsv3bitcoind/hostname )
echo "Your local bitcoind (do not share) = $V3_ADDR_BTCD"
echo " "
fi
fi
# V3 onion addresses
V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname )
echo "Dojo API and Maintenance Tool = $V3_ADDR"
echo " "
else
# v2 onion addresses
V2_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv2dojo/hostname )
echo "Dojo API and Maintenance Tool = $V2_ADDR"
if [ "$EXPLORER_INSTALL" == "on" ]; then
V3_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv3explorer/hostname )
echo "Block Explorer = $V3_ADDR_EXPLORER"
echo " "
fi
if [ "$EXPLORER_INSTALL" == "on" ]; then
V2_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv2explorer/hostname )
echo "Block Explorer = $V2_ADDR_EXPLORER"
echo " "
fi
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
V3_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv3whirlpool/hostname )
echo "Your private Whirlpool client (do not share) = $V3_ADDR_WHIRLPOOL"
echo " "
fi
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
V2_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv2whirlpool/hostname )
echo "Your private Whirlpool client (do not share) = $V2_ADDR_WHIRLPOOL"
if [ "$BITCOIND_INSTALL" == "on" ]; then
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
V3_ADDR_BTCD=$( docker exec -it tor cat /var/lib/tor/hsv3bitcoind/hostname )
echo "Your local bitcoind (do not share) = $V3_ADDR_BTCD"
echo " "
fi
if [ "$BITCOIND_INSTALL" == "on" ]; then
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
V2_ADDR_BTCD=$( docker exec -it tor cat /var/lib/tor/hsv2bitcoind/hostname )
echo "Your local bitcoind (do not share) = $V2_ADDR_BTCD"
echo " "
fi
fi
fi
}
@ -561,11 +518,7 @@ help() {
echo " Available options:"
echo " -n [VALUE] : display the last VALUE lines"
echo " "
echo " onion [version] Display the Tor onion addresses allowing your wallet to access your dojo."
echo " "
echo " Available versions:"
echo " v2: display Tor v2 onion addresses"
echo " v3 (default): display Tor v3 onion addresses"
echo " onion Display the Tor onion addresses allowing your wallet to access your dojo."
echo " "
echo " restart Restart your dojo."
echo " "
@ -659,7 +612,7 @@ case "$subcommand" in
logs "$module" $numlines
;;
onion )
onion "$@"
onion
;;
restart )
restart

2
docker/my-dojo/explorer/Dockerfile

@ -1,4 +1,4 @@
FROM node:12-alpine
FROM node:14-alpine
ENV NODE_ENV production

2
docker/my-dojo/node/Dockerfile

@ -1,4 +1,4 @@
FROM node:12-alpine
FROM node:14-alpine
ENV NODE_ENV production

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

@ -14,7 +14,7 @@ const bitcoinNetwork = (process.env.COMMON_BTC_NETWORK == 'testnet')
let explorerActive = 'oxt'
let explorerUrl = 'https://oxt.me'
let explorerPassword = ''
if (process.env.EXPLORER_INSTALL == 'on') {
if (process.env.EXPLORER_INSTALL === 'on') {
try {
explorerUrl = fs.readFileSync('/var/lib/tor/hsv3explorer/hostname', 'utf8').replace('\n', '')
explorerPassword = process.env.EXPLORER_KEY

6
docker/my-dojo/tor/Dockerfile

@ -3,7 +3,7 @@ FROM debian:buster-slim
ENV TOR_HOME /var/lib/tor
ENV TOR_URL https://dist.torproject.org
ENV TOR_MIRROR_URL https://tor.eff.org/dist
ENV TOR_VERSION 0.4.5.8
ENV TOR_VERSION 0.4.6.6
ENV TOR_GPG_KS_URI hkp://keyserver.ubuntu.com:80
ENV TOR_GPG_KEY1 0xEB5A896A28988BF5
ENV TOR_GPG_KEY2 0xC218525819F78451
@ -11,8 +11,8 @@ ENV TOR_GPG_KEY3 0x21194EBB165733EA
ENV TOR_GPG_KEY4 0x6AFEE6D49E92B601
ENV GOLANG_DL_URL https://dl.google.com/go
ENV GOLANG_ARCHIVE go1.16.4.linux-amd64.tar.gz
ENV GOLANG_SHA256 7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59
ENV GOLANG_ARCHIVE go1.16.6.linux-amd64.tar.gz
ENV GOLANG_SHA256 be333ef18b3016e9d7cb7b1ff1fdb0cac800ca0be4cf2290fe613b3d069dfe0d
ENV OBFS4_URL https://github.com/Yawning/obfs4.git
ENV OBFS4_VERSION 0.0.11

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

@ -13,9 +13,6 @@ tor_options=(
--SocksPolicy "reject *"
--DataDirectory /var/lib/tor/.tor
--DataDirectoryGroupReadable 1
--HiddenServiceDir /var/lib/tor/hsv2dojo
--HiddenServiceVersion 2
--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:80"
--HiddenServiceDir /var/lib/tor/hsv3dojo
--HiddenServiceVersion 3
--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:80"
@ -23,11 +20,6 @@ tor_options=(
if [ "$BITCOIND_INSTALL" == "on" ]; then
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv2bitcoind)
tor_options+=(--HiddenServiceVersion 2)
tor_options+=(--HiddenServicePort "8333 $NET_DOJO_BITCOIND_IPV4:8333")
tor_options+=(--HiddenServiceDirGroupReadable 1)
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv3bitcoind)
tor_options+=(--HiddenServiceVersion 3)
tor_options+=(--HiddenServicePort "8333 $NET_DOJO_BITCOIND_IPV4:8333")
@ -36,11 +28,6 @@ if [ "$BITCOIND_INSTALL" == "on" ]; then
fi
if [ "$EXPLORER_INSTALL" == "on" ]; then
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv2explorer)
tor_options+=(--HiddenServiceVersion 2)
tor_options+=(--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:9080")
tor_options+=(--HiddenServiceDirGroupReadable 1)
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv3explorer)
tor_options+=(--HiddenServiceVersion 3)
tor_options+=(--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:9080")
@ -48,11 +35,6 @@ if [ "$EXPLORER_INSTALL" == "on" ]; then
fi
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv2whirlpool)
tor_options+=(--HiddenServiceVersion 2)
tor_options+=(--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:8898")
tor_options+=(--HiddenServiceDirGroupReadable 1)
tor_options+=(--HiddenServiceDir /var/lib/tor/hsv3whirlpool)
tor_options+=(--HiddenServiceVersion 3)
tor_options+=(--HiddenServicePort "80 $NET_DMZ_NGINX_IPV4:8898")

2
docker/my-dojo/whirlpool/Dockerfile

@ -27,7 +27,7 @@ RUN set -ex && \
# Install Tor
ENV WHIRLPOOL_TOR_URL https://dist.torproject.org
ENV WHIRLPOOL_TOR_MIRROR_URL https://tor.eff.org/dist
ENV WHIRLPOOL_TOR_VERSION 0.4.4.8
ENV WHIRLPOOL_TOR_VERSION 0.4.6.6
ENV WHIRLPOOL_TOR_GPG_KS_URI hkp://keyserver.ubuntu.com:80
ENV WHIRLPOOL_TOR_GPG_KEY1 0xEB5A896A28988BF5
ENV WHIRLPOOL_TOR_GPG_KEY2 0xC218525819F78451

4
keys/index-example.js

@ -16,7 +16,7 @@ module.exports = {
/*
* Dojo version
*/
dojoVersion: '1.10.1',
dojoVersion: '1.11.0',
/*
* Bitcoind
*/
@ -233,7 +233,7 @@ module.exports = {
* Testnet parameters
*/
testnet: {
dojoVersion: '1.9.0',
dojoVersion: '1.11.0',
bitcoind: {
rpc: {
user: 'user',

6
lib/auth/authorizations-manager.js

@ -162,7 +162,7 @@ class AuthorizationsManager {
try {
const decodedToken = this.isAuthenticated(token)
if (decodedToken['prf'] == this.TOKEN_PROFILE_ADMIN) {
if (decodedToken['prf'] === this.TOKEN_PROFILE_ADMIN) {
req.authorizations = {decoded_access_token: decodedToken}
next()
} else {
@ -249,7 +249,7 @@ class AuthorizationsManager {
{algorithms: [this.JWT_ALGO]}
)
if (payload['type'] != this.TOKEN_TYPE_ACCESS)
if (payload['type'] !== this.TOKEN_TYPE_ACCESS)
throw errors.auth.INVALID_JWT
return payload
@ -309,7 +309,7 @@ class AuthorizationsManager {
{algorithms: [this.JWT_ALGO]}
)
if (payload['type'] != this.TOKEN_TYPE_REFRESH)
if (payload['type'] !== this.TOKEN_TYPE_REFRESH)
throw errors.auth.INVALID_JWT
return payload

3
lib/auth/localapikey-strategy-configurator.js

@ -33,7 +33,6 @@ class LocalApiKeyStrategyConfigurator {
/**
* Authentication
* @param {object} req - http request object
* @param {string} apiKey - api key received
* @param {function} done - callback
*/
@ -41,7 +40,7 @@ class LocalApiKeyStrategyConfigurator {
const _adminKey = keys.auth.strategies[LocalApiKeyStrategyConfigurator.NAME].adminKey
const _apiKeys = keys.auth.strategies[LocalApiKeyStrategyConfigurator.NAME].apiKeys
if (apiKey == _adminKey) {
if (apiKey === _adminKey) {
// Check if received key is a valid api key
Logger.info('Auth : Successful authentication with an admin key')
return done(null, {'profile': authorzMgr.TOKEN_PROFILE_ADMIN})

38
lib/bitcoin/addresses-helper.js

@ -65,7 +65,7 @@ class AddressesHelper {
verifySignature(msg, address, sig) {
try {
const prefix = activeNet.messagePrefix
return btcMessage.verify(msg, prefix, address, sig)
return btcMessage.verify(msg, address, sig, prefix)
} catch(e) {
return false
}
@ -77,7 +77,7 @@ class AddressesHelper {
* @returns {boolean} return true if str is a supported pubkey format, false otherwise
*/
isSupportedPubKey(str) {
return (str.length == 66 && (str.startsWith('02') || str.startsWith('03')))
return (str.length === 66 && (str.startsWith('02') || str.startsWith('03')))
}
/**
@ -97,7 +97,7 @@ class AddressesHelper {
/**
* Get the script hash associated to a Bech32 address
* @param {string} str - bech32 address
* @returns {string} script hash in hex format
* @returns {string | null} script hash in hex format
*/
getScriptHashFromBech32(str) {
try {
@ -114,12 +114,12 @@ class AddressesHelper {
* @returns {boolean} return true if output is a P2PKH script, otherwise return false
*/
isP2pkhScript(scriptpubkey) {
return scriptpubkey.length == 25
&& scriptpubkey[0] == OPS.OP_DUP
&& scriptpubkey[1] == OPS.OP_HASH160
&& scriptpubkey[2] == 0x14
&& scriptpubkey[23] == OPS.OP_EQUALVERIFY
&& scriptpubkey[24] == OPS.OP_CHECKSIG
return scriptpubkey.length === 25
&& scriptpubkey[0] === OPS.OP_DUP
&& scriptpubkey[1] === OPS.OP_HASH160
&& scriptpubkey[2] === 0x14
&& scriptpubkey[23] === OPS.OP_EQUALVERIFY
&& scriptpubkey[24] === OPS.OP_CHECKSIG
}
/**
@ -128,10 +128,10 @@ class AddressesHelper {
* @returns {boolean} return true if output is a P2SH script, otherwise return false
*/
isP2shScript(scriptpubkey) {
return scriptpubkey.length == 23
&& scriptpubkey[0] == OPS.OP_HASH160
&& scriptpubkey[1] == 0x14
&& scriptpubkey[22] == OPS.OP_EQUAL
return scriptpubkey.length === 23
&& scriptpubkey[0] === OPS.OP_HASH160
&& scriptpubkey[1] === 0x14
&& scriptpubkey[22] === OPS.OP_EQUAL
}
/**
@ -140,9 +140,9 @@ class AddressesHelper {
* @returns {boolean} return true if output is a P2WPKH script, otherwise return false
*/
isP2wpkhScript(scriptpubkey) {
return scriptpubkey.length == 22
&& scriptpubkey[0] == OPS.OP_0
&& scriptpubkey[1] == 0x14
return scriptpubkey.length === 22
&& scriptpubkey[0] === OPS.OP_0
&& scriptpubkey[1] === 0x14
}
/**
@ -151,9 +151,9 @@ class AddressesHelper {
* @returns {boolean} return true if output is a P2WSH script, otherwise return false
*/
isP2wshScript(scriptpubkey) {
return scriptpubkey.length == 34
&& scriptpubkey[0] == OPS.OP_0
&& scriptpubkey[1] == 0x20
return scriptpubkey.length === 34
&& scriptpubkey[0] === OPS.OP_0
&& scriptpubkey[1] === 0x20
}
/**

12
lib/bitcoin/addresses-service.js

@ -21,24 +21,24 @@ class AddressesService {
/**
* Rescan the blockchain for an address
* @param {string} address - bitcoin address
* @returns {Promise}
* @returns {Promise<boolean | undefined>}
*/
async rescan(address) {
const hdaccount = await db.getUngroupedHDAccountsByAddresses([address])
// Don't filter addresses associated to an HDAccount
const filterAddr = !(hdaccount.length > 0 && hdaccount[0]['hdID'])
return remote.importAddresses([address], filterAddr)
return await remote.importAddresses([address], filterAddr)
}
/**
* Restore an address in db
* @param {string[]} addresses - array of bitcoin addresses
* @param {boolean} filterAddr - true if addresses should be filter, false otherwise
* @returns {Promise}
* @returns {Promise<boolean | undefined>}
*/
async restoreAddresses(address, filterAddr) {
return remote.importAddresses(address, filterAddr)
async restoreAddresses(addresses, filterAddr) {
return await remote.importAddresses(addresses, filterAddr)
}
}
module.exports = new AddressesService()
module.exports = new AddressesService()

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

@ -16,6 +16,7 @@ const activeNet = network.network
const keys = require('../../keys/')[network.key]
const addrHelper = require('./addresses-helper')
const MAX_SAFE_INT_32 = Math.pow(2, 31) - 1;
/**
* A singleton providing HD Accounts helper functions
@ -32,6 +33,12 @@ class HDAccountsHelper {
this.BIP84 = 2
this.LOCKED = 1<<7
// known HD accounts
this.RICOCHET_ACCT = MAX_SAFE_INT_32;
this.POSTMIX_ACCT = MAX_SAFE_INT_32 - 1;
this.PREMIX_ACCT = MAX_SAFE_INT_32 - 2;
this.BADBANK_ACCT = MAX_SAFE_INT_32 - 3;
// Magic numbers
this.MAGIC_XPUB = 0x0488b21e
this.MAGIC_TPUB = 0x043587cf
@ -41,7 +48,7 @@ class HDAccountsHelper {
this.MAGIC_VPUB = 0x045f1cf6
// HD accounts cache
this.nodes = LRU({
this.nodes = new LRU({
// Maximum number of nodes to store in cache
max: 1000,
// Function used to compute length of item
@ -81,7 +88,7 @@ class HDAccountsHelper {
* @returns {boolean} returns true if xpub encodes a xpub/tpub, false otherwise
*/
isXpub(xpub) {
return (xpub.indexOf('xpub') == 0) || (xpub.indexOf('tpub') == 0)
return (xpub.indexOf('xpub') === 0) || (xpub.indexOf('tpub') === 0)
}
/**
@ -90,7 +97,7 @@ class HDAccountsHelper {
* @returns {boolean} returns true if xpub encodes a ypub/upub, false otherwise
*/
isYpub(xpub) {
return (xpub.indexOf('ypub') == 0) || (xpub.indexOf('upub') == 0)
return (xpub.indexOf('ypub') === 0) || (xpub.indexOf('upub') === 0)
}
/**
@ -99,7 +106,7 @@ class HDAccountsHelper {
* @returns {boolean} returns true if xpub encodes a zpub/vpub, false otherwise
*/
isZpub(xpub) {
return (xpub.indexOf('zpub') == 0) || (xpub.indexOf('vpub') == 0)
return (xpub.indexOf('zpub') === 0) || (xpub.indexOf('vpub') === 0)
}
/**
@ -114,15 +121,15 @@ class HDAccountsHelper {
const ver = decoded.readInt32BE()
if (
ver != this.MAGIC_XPUB
&& ver != this.MAGIC_TPUB
&& ver != this.MAGIC_YPUB
&& ver != this.MAGIC_UPUB
&& ver != this.MAGIC_ZPUB
&& ver != this.MAGIC_VPUB
ver !== this.MAGIC_XPUB
&& ver !== this.MAGIC_TPUB
&& ver !== this.MAGIC_YPUB
&& ver !== this.MAGIC_UPUB
&& ver !== this.MAGIC_ZPUB
&& ver !== this.MAGIC_VPUB
) {
//Logger.error(null, 'HdAccountsHelper : xlatXPUB() : Incorrect format')
return ''
Logger.error(null, 'HdAccountsHelper : xlatXPUB() : Incorrect format')
throw errors.xpub.INVALID
}
let xlatVer = 0
@ -165,7 +172,7 @@ class HDAccountsHelper {
/**
* Classify the hd account type retrieved from db
* @param {integer} v - HD Account type (db encoding)
* @param {number} v - HD Account type (db encoding)
* @returns {object} object storing the type and lock status of the hd account
*/
classify(v) {
@ -194,9 +201,9 @@ class HDAccountsHelper {
/**
* Encode hd account type and lock status in db format
* @param {integer} type - HD Account type (db encoding)
* @param {number} type - HD Account type (db encoding)
* @param {boolean} locked - lock status of the hd account
* @returns {integer}
* @returns {number}
*/
makeType(type, locked) {
let p =
@ -214,7 +221,7 @@ class HDAccountsHelper {
/**
* Return a string representation of the hd account type
* @param {integer} v - HD Account type (db encoding)
* @param {number} v - HD Account type (db encoding)
* @returns {string}
*/
typeString(v) {
@ -252,6 +259,10 @@ class HDAccountsHelper {
return true
try {
if (!(this.isXpub(xpub) || this.isYpub(xpub) || this.isZpub(xpub))) {
throw errors.xpub.INVALID
}
// Translate the xpub
const xlatedXpub = this.xlatXPUB(xpub)
@ -268,7 +279,7 @@ class HDAccountsHelper {
return true
} catch(e) {
if (e == errors.xpub.PRIVKEY) throw e
if (e === errors.xpub.PRIVKEY) throw e
return false
}
}
@ -276,7 +287,7 @@ class HDAccountsHelper {
/**
* Get the hd node associated to an hd account
* @param {string} xpub - hd account
* @returns {bip32}
* @returns {[bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface]}
*/
getNode(xpub) {
if (this.isValid(xpub))
@ -287,12 +298,12 @@ class HDAccountsHelper {
/**
* Derives an address for an hd account
* @param {int} chain - chain to be derived
* @param {number} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 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>}
* @param {bitcoin.bip32.BIP32Interface} chainNode - Parent bip32 used for derivation
* @param {number} index - index to be derived
* @param {number} type - type of derivation
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>, address: string }
*/
async deriveAddress(chain, chainNode, index, type) {
// Derive M/chain/index
@ -300,7 +311,7 @@ class HDAccountsHelper {
const addr = {
chain: chain,
index: index
index: index,
}
switch (type) {
@ -321,11 +332,11 @@ class HDAccountsHelper {
/**
* Derives addresses for an hd account
* @param {string} xpub - hd account to be derived
* @param {int} chain - chain to be derived
* @param {number} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {int[]} indices - array of indices to be derived
* @param {int} type - type of derivation
* @returns {Promise - object[]} array of {address: '...', chain: <int>, index: <int>}
* @param {number[]} indices - array of indices to be derived
* @param {number} type - type of derivation
* @returns {Promise<{ chain: number, index: number, publicKey: Buffer, address: string }[]>} array of address objects
*/
async deriveAddresses(xpub, chain, indices, type) {
const ret = []
@ -355,10 +366,21 @@ class HDAccountsHelper {
) {
// Few addresses to be derived or external derivation deactivated
// Let's do it here
let promises = indices.map(index => {
const promises = indices.map(index => {
return this.deriveAddress(chain, chainNode, index, info.type)
})
return Promise.all(promises)
// Generate additional change address types for postmix account
if (this.isPostmixAcct(node) && chain === 1) {
indices.forEach((index) => {
promises.push(this.deriveAddress(chain, chainNode, index, this.BIP44))
promises.push(this.deriveAddress(chain, chainNode, index, this.BIP49))
})
}
const addresses = await Promise.all(promises)
return addresses;
} else {
// Many addresses to be derived
@ -369,12 +391,13 @@ class HDAccountsHelper {
xpub: this.xlatXPUB(xpub),
chain: chain,
indices: indices,
type: info.type
type: info.type,
isPostmixChange: this.isPostmixAcct(node) && chain === 1
}
const msg = await this.derivationPool.exec('deriveAddresses', [data])
if (msg.status = 'ok') {
if (msg.status === 'ok') {
resolve(msg.addresses)
} else {
Logger.error(null, 'HdAccountsHelper : A problem was met during parallel addresses derivation')
@ -393,6 +416,20 @@ class HDAccountsHelper {
}
}
/**
* @description Detect postmix account
* @param {[bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface]} node - array of BIP32 node interfaces
* @returns {boolean}
*/
isPostmixAcct(node) {
const index = node[2].index
const threshold = Math.pow(2,31)
const hardened = (index >= threshold)
const account = hardened ? (index - threshold) : index
return account === this.POSTMIX_ACCT
}
}
module.exports = new HDAccountsHelper()

69
lib/bitcoin/hd-accounts-service.js

@ -4,7 +4,7 @@
*/
'use strict'
const _ = require('lodash')
const util = require('../util')
const errors = require('../errors')
const Logger = require('../logger')
const db = require('../db/mysql-db-wrapper')
@ -29,7 +29,7 @@ class HDAccountsService {
/**
* Create a new hd account in db
* @param {string} xpub - xpub
* @param {int} scheme - derivation scheme
* @param {number} scheme - derivation scheme
* @returns {Promise} returns true if success, false otherwise
*/
async createHdAccount(xpub, scheme) {
@ -37,8 +37,8 @@ class HDAccountsService {
await this.newHdAccount(xpub, scheme)
return true
} catch(e) {
const isInvalidXpub = (e == errors.xpub.INVALID || e == errors.xpub.PRIVKEY)
const isLockedXpub = (e == errors.xpub.LOCKED)
const isInvalidXpub = (e === errors.xpub.INVALID || e === errors.xpub.PRIVKEY)
const isLockedXpub = (e === errors.xpub.LOCKED)
const err = (isInvalidXpub || isLockedXpub) ? e : errors.xpub.CREATE
Logger.error(e, 'HdAccountsService : createHdAccount()' + err)
return Promise.reject(err)
@ -49,7 +49,7 @@ class HDAccountsService {
/**
* Restore a hd account in db
* @param {string} xpub - xpub
* @param {int} scheme - derivation scheme
* @param {number} scheme - derivation scheme
* @param {bool} forceOverride - force override of scheme even if hd account is locked
* @returns {Promise}
*/
@ -96,7 +96,7 @@ class HDAccountsService {
return hdaHelper.typeString(type)
} catch(e) {
const err = (e == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
const err = (e === errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
return Promise.reject(err)
}
}
@ -110,7 +110,7 @@ class HDAccountsService {
try {
await db.deleteHDAccount(xpub)
} catch(e) {
const err = (e == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
const err = (e === errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
return Promise.reject(err)
}
}
@ -134,17 +134,17 @@ class HDAccountsService {
let segwit = ''
if (scheme == hdaHelper.BIP49)
if (scheme === hdaHelper.BIP49)
segwit = ' SegWit (BIP49)'
else if (scheme == hdaHelper.BIP84)
else if (scheme === hdaHelper.BIP84)
segwit = ' SegWit (BIP84)'
Logger.info(`HdAccountsService : Created HD Account: ${xpub}${segwit}`)
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, _.range(gap.external), scheme)
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, _.range(gap.internal), scheme)
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, util.range(0, gap.external), scheme)
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, util.range(0, gap.internal), scheme)
const addresses = _.flatten(await Promise.all([externalPrm, internalPrm]))
const addresses = (await Promise.all([externalPrm, internalPrm])).flat()
return db.addAddressesToHDAccount(xpub, addresses)
}
@ -152,8 +152,8 @@ class HDAccountsService {
/**
* Rescan the blockchain for a hd account
* @param {string} xpub - xpub
* @param {integer} gapLimit - (optional) gap limit for derivation
* @param {integer} startIndex - (optional) rescan shall start from this index
* @param {number=} gapLimit - (optional) gap limit for derivation
* @param {number=} startIndex - (optional) rescan shall start from this index
* @returns {Promise}
*/
async rescan(xpub, gapLimit, startIndex) {
@ -165,7 +165,7 @@ class HDAccountsService {
await remote.importHDAccount(xpub, account.hdType, gapLimit, startIndex)
} catch(e) {
return Promise.reject(e)
}
}
}
/**
@ -201,7 +201,7 @@ class HDAccountsService {
const info = hdaHelper.classify(account.hdType)
// If this account is already known in the database,
// check for a derivation scheme mismatch
if (info.type != scheme) {
if (info.type !== scheme) {
if (info.locked && !forceOverride) {
Logger.info(`HdAccountsService : Attempted override on locked account: ${xpub}`)
return Promise.reject(errors.xpub.LOCKED)
@ -224,7 +224,7 @@ class HDAccountsService {
* @param {string} address - address used to sign the message
* @param {string} sig - signature of the message
* @param {string} msg - signed message
* @param {integer} scheme - derivation scheme to be used for the xpub
* @param {number} scheme - derivation scheme to be used for the xpub
* @returns {Promise} returns the xpub if signature is valid, otherwise returns an error
*/
async verifyXpubSignature(xpub, address, sig, msg, scheme) {
@ -242,16 +242,45 @@ class HDAccountsService {
if (!addrHelper.verifySignature(msg, sigAddress, sig))
return Promise.reject(errors.sig.INVSIG)
// Check that adresses match
if (address != expectedAddress)
if (address !== expectedAddress)
return Promise.reject(errors.sig.INVADDR)
// Return the corresponding xpub
return xpub
} catch(err) {
const ret = (err == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
const ret = (err === errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
return Promise.reject(ret)
}
}
/**
* @description
* @param {string[]} xpubs - array of xpubs
* @returns {Promise<void>}
*/
async importPostmixLikeTypeChange(xpubs) {
const postmixAcct = xpubs.find((xpub) => {
const node = hdaHelper.getNode(xpub)
return hdaHelper.isPostmixAcct(node)
})
if (!postmixAcct) return Promise.resolve()
const postmixNode = hdaHelper.getNode(postmixAcct)
const [externalUnused, internalUnused] = await db.getHDAccountNextUnusedIndices(postmixAcct)
const deriveRange = util.range(Math.max(0, internalUnused - 50), internalUnused + gap.internal)
const likeTypeChangeAddresses = await Promise.all(deriveRange.map((index) => {
return [
hdaHelper.deriveAddress(1, postmixNode[1], index, hdaHelper.BIP44),
hdaHelper.deriveAddress(1, postmixNode[1], index, hdaHelper.BIP49)
]
}).flat())
await db.addAddressesToHDAccount(postmixAcct, likeTypeChangeAddresses)
}
}
module.exports = new HDAccountsService()

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

@ -20,12 +20,12 @@ const BIP84 = 2
/**
* Derives an address for an hd account
* @param {int} chain - chain to be derived
* @param {number} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 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>}
* @param {number} index - index to be derived
* @param {number} type - type of derivation
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>}
*/
async function deriveAddress(chain, chainNode, index, type) {
// Derive M/chain/index
@ -33,7 +33,7 @@ async function deriveAddress(chain, chainNode, index, type) {
const addr = {
chain: chain,
index: index
index: index,
}
switch (type) {
@ -54,7 +54,7 @@ async function deriveAddress(chain, chainNode, index, type) {
/**
* Derives a set of addresses for an hd account
* @param {object} msg - parameters used for the derivation
* @returns {Promise - object[]}
* @returns {Promise<object[]>}
*/
async function deriveAddresses(msg) {
try {
@ -62,6 +62,7 @@ async function deriveAddresses(msg) {
const chain = msg.chain
const indices = msg.indices
const type = msg.type
const isPostmixChange = msg.isPostmixChange
// Parse input as an HD Node. Throws if invalid
const node = bitcoin.bip32.fromBase58(xpub, activeNet)
@ -76,6 +77,14 @@ async function deriveAddresses(msg) {
return deriveAddress(chain, chainNode, index, type)
})
// Generate additional change address types for postmix account
if (isPostmixChange) {
indices.forEach((index) => {
promises.push(deriveAddress(chain, chainNode, index, BIP44))
promises.push(deriveAddress(chain, chainNode, index, BIP49))
})
}
const addresses = await Promise.all(promises)
// Send response to parent process

4
lib/bitcoind-rpc/fees.js

@ -34,7 +34,7 @@ class Fees {
/**
* Refresh and return the current fees
* @returns {Promise}
* @returns {Promise<object>}
*/
async getFees() {
try {
@ -50,7 +50,7 @@ class Fees {
/**
* Refresh the current fees
* @returns {Promise}
* @returns {Promise<void>}
*/
async refresh() {
await util.parallelCall(this.targets, async tgt => {

2
lib/bitcoind-rpc/headers.js

@ -19,7 +19,7 @@ class Headers {
*/
constructor() {
// Cache
this.headers = LRU({
this.headers = new LRU({
// Maximum number of headers to store in cache
max: 2016,
// Function used to compute length of item

2
lib/bitcoind-rpc/latest-block.js

@ -4,7 +4,7 @@
*/
'use strict'
const zmq = require('zeromq')
const zmq = require('zeromq/v5-compat')
const Logger = require('../logger')
const util = require('../util')
const network = require('../bitcoin/network')

7
lib/bitcoind-rpc/rpc-client.js

@ -14,12 +14,13 @@ const Logger = require('../logger')
/**
* Wrapper for bitcoind rpc client
*/
const createRpcClient = () => {
const createRpcClient = (options = {}) => {
return new RPCClient({
url: `http://${keys.bitcoind.rpc.host}`,
port: keys.bitcoind.rpc.port,
user: keys.bitcoind.rpc.user,
pass: keys.bitcoind.rpc.pass
pass: keys.bitcoind.rpc.pass,
...options
})
}
@ -30,7 +31,7 @@ const createRpcClient = () => {
* @returns {boolean} returns true if message related to a connection error
*/
const isConnectionError = (err) => {
if (typeof err != 'string')
if (typeof err !== 'string')
return false
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') !== -1)

14
lib/bitcoind-rpc/transactions.js

@ -23,7 +23,7 @@ class Transactions {
*/
constructor() {
// Caches
this.prevCache = LRU({
this.prevCache = new LRU({
// Maximum number of transactions to store
max: 20000,
// Function used to compute length of item
@ -41,7 +41,7 @@ class Transactions {
* Get the transactions for a given array of txids
* @param {string[]} txids - txids of the transaction to be retrieved
* @param {boolean} fees - true if fees must be computed, false otherwise
* @returns {Promise} return an array of transactions (object[])
* @returns {Promise<object[]>} return an array of transactions (object[])
*/
async getTransactions(txids, fees) {
try {
@ -76,7 +76,7 @@ class Transactions {
* Get the transaction for a given txid
* @param {string} txid - txid of the transaction to be retrieved
* @param {boolean} fees - true if fees must be computed, false otherwise
* @returns {Promise}
* @returns {Promise<object[]>}
*/
async getTransaction(txid, fees) {
try {
@ -92,7 +92,7 @@ class Transactions {
* Formats a transaction object returned by the RPC API
* @param {object} tx - transaction
* @param {boolean} fees - true if fees must be computed, false otherwise
* @returns {Promise} return an array of inputs (object[])
* @returns {Promise<object[]>} return an array of inputs (object[])
*/
async _prepareTxResult(tx, fees) {
const ret = {
@ -148,7 +148,7 @@ class Transactions {
* Extract information about the inputs of a transaction
* @param {object} tx - transaction
* @param {boolean} fees - true if fees must be computed, false otherwise
* @returns {Promise} return an array of inputs (object[])
* @returns {Promise<object[]>} return an array of inputs (object[])
*/
async _getInputs(tx, fees) {
const inputs = []
@ -202,7 +202,7 @@ class Transactions {
/**
* Extract information about the outputs of a transaction
* @param {object} tx - transaction
* @returns {Promise} return an array of outputs (object[])
* @returns {Promise<object[]>} return an array of outputs (object[])
*/
async _getOutputs(tx) {
const outputs = []
@ -220,7 +220,7 @@ class Transactions {
}
if (pk.addresses) {
if (pk.addresses.length == 1)
if (pk.addresses.length === 1)
o.address = pk.addresses[0]
else
o.addresses = pk.addresses

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

@ -11,8 +11,8 @@ const errors = require('../errors')
const hdaHelper = require('../bitcoin/hd-accounts-helper')
const network = require('../bitcoin/network')
const keys = require('../../keys/')[network.key]
const debug = !!(process.argv.indexOf('db-debug') > -1)
const queryDebug = !!(process.argv.indexOf('dbquery-debug') > -1)
const debug = process.argv.indexOf('db-debug') > -1
const queryDebug = process.argv.indexOf('dbquery-debug') > -1
/**
@ -387,9 +387,9 @@ class MySqlDbWrapper {
this.pool.query(query, null, async (err, result, fields) => {
if (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)
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)
@ -426,7 +426,7 @@ class MySqlDbWrapper {
/**
* Get the ID of an address
* @param {string} address - bitcoin address
* @returns {integer} returns the address id
* @returns {number} returns the address id
*/
async getAddressId(address) {
const sqlQuery = 'SELECT `addrID` FROM `addresses` WHERE `addrAddress` = ?'
@ -443,7 +443,7 @@ class MySqlDbWrapper {
/**
* Get the ID of an Address. Ensures that the address exists.
* @param {string} address - bitcoin address
* @returns {integer} returns the address id
* @returns {number} returns the address id
*/
async ensureAddressId(address) {
const sqlQuery = 'SELECT `addrID` FROM `addresses` WHERE `addrAddress` = ?'
@ -469,7 +469,7 @@ class MySqlDbWrapper {
async getAddressesIds(addresses) {
const ret = {}
if (addresses.length == 0)
if (addresses.length === 0)
return ret
const sqlQuery = 'SELECT * FROM `addresses` WHERE `addrAddress` IN (?)'
@ -488,7 +488,7 @@ class MySqlDbWrapper {
* @param {string[]} addresses - array of bitcoin addresses
*/
async addAddresses(addresses) {
if (addresses.length == 0)
if (addresses.length === 0)
return []
const sqlQuery = 'INSERT IGNORE INTO `addresses` (addrAddress) VALUES ?'
@ -502,7 +502,7 @@ class MySqlDbWrapper {
* @param {string[]} addresses - array of bitcoin addresses
*/
async getAddresses(addresses) {
if (addresses.length == 0)
if (addresses.length === 0)
return []
const sqlQuery = 'SELECT * FROM `addresses` WHERE `addrAddress` IN (?)'
@ -514,7 +514,7 @@ class MySqlDbWrapper {
/**
* Get address balance.
* @param {string} address - bitcoin address
* @returns {integer} returns the balance of the address
* @returns {number} returns the balance of the address
*/
async getAddressBalance(address) {
if (address == null)
@ -532,7 +532,7 @@ class MySqlDbWrapper {
const query = mysql.format(sqlQuery, params)
const results = await this._query(query)
if (results.length == 1) {
if (results.length === 1) {
const balance = results[0].balance
return (balance == null) ? 0 : balance
}
@ -543,7 +543,7 @@ class MySqlDbWrapper {
/**
* Get the number of transactions for an address.
* @param {string} address - bitcoin address
* @returns {integer} returns the number of transactions for the address
* @returns {number} returns the number of transactions for the address
*/
async getAddressNbTransactions(address) {
if(address == null)
@ -571,7 +571,7 @@ class MySqlDbWrapper {
const query = mysql.format(sqlQuery, params)
const results = await this._query(query)
if (results.length == 1) {
if (results.length === 1) {
const nbTxs = results[0].nbTxs
return (nbTxs == null) ? 0 : nbTxs
}
@ -582,7 +582,7 @@ class MySqlDbWrapper {
/**
* Get an HD account.
* @param {string} xpub - xpub
* @returns {integer} returns {hdID, hdXpub, hdCreated, hdType}
* @returns {number} returns {hdID, hdXpub, hdCreated, hdType}
* throws if no record of xpub
*/
async getHDAccount(xpub) {
@ -600,7 +600,7 @@ class MySqlDbWrapper {
/**
* Get the ID of an HD account
* @param {string} xpub - xpub
* @returns {integer} returns the id of the hd account
* @returns {number} returns the id of the hd account
*/
async getHDAccountId(xpub) {
const sqlQuery = 'SELECT `hdID` FROM `hd` WHERE `hdXpub` = ?'
@ -618,7 +618,7 @@ class MySqlDbWrapper {
* Get the ID of an HD account. Ensures that the account exists.
* @param {string} xpub - xpub
* @param {string} type - hd account type
* @returns {integer} returns the id of the hd account
* @returns {number} returns the id of the hd account
*/
async ensureHDAccountId(xpub, type) {
const info = hdaHelper.classify(type)
@ -650,15 +650,14 @@ class MySqlDbWrapper {
/**
* Lock the type of a hd account
* @param {string} xpub - xpub
* @param {boolean} locked - true for locking, false for unlocking
* @returns {boolean} locked - true for locking, false for unlocking
*/
async setLockHDAccountType(xpub, locked) {
locked = !!locked
const account = await this.getHDAccount(xpub)
const info = hdaHelper.classify(account.hdType)
if (info.locked == locked)
if (info.locked === locked)
return true
const hdType = hdaHelper.makeType(account.hdType, locked)
@ -700,7 +699,7 @@ class MySqlDbWrapper {
* Add an address a hd account
* @param {string} address - bitcoin address
* @param {string} xpub - xpub
* @param {integer} chain - derivation chain
* @param {number} chain - derivation chain
* @param {index} index - derivation index for the address
*/
async addAddressToHDAccount(address, xpub, chain, index) {
@ -733,7 +732,7 @@ class MySqlDbWrapper {
* which is the output of the HDAccountsHelper.deriveAddresses()
*/
async addAddressesToHDAccount(xpub, addressData) {
if (addressData.length == 0)
if (addressData.length === 0)
return
const addresses = addressData.map(d => d.address)
@ -769,7 +768,7 @@ class MySqlDbWrapper {
* @returns {object[]}
*/
async getUngroupedHDAccountsByAddresses(addresses) {
if (addresses.length == 0) return {}
if (addresses.length === 0) return {}
const sqlQuery = 'SELECT \
`hd`.`hdID`, \
@ -814,7 +813,7 @@ class MySqlDbWrapper {
loose: []
}
if (addresses.length == 0)
if (addresses.length === 0)
return ret
const sqlQuery = 'SELECT \
@ -865,7 +864,7 @@ class MySqlDbWrapper {
/**
* Get an HD account balance
* @param {string} xpub - xpub
* @returns {integer} returns the balance of the hd account
* @returns {number} returns the balance of the hd account
*/
async getHDAccountBalance(xpub) {
const sqlQuery = 'SELECT \
@ -882,7 +881,7 @@ class MySqlDbWrapper {
const query = mysql.format(sqlQuery, params)
const results = await this._query(query)
if (results.length == 1)
if (results.length === 1)
return (results[0].balance == null) ? 0 : results[0].balance
return null
@ -891,7 +890,7 @@ class MySqlDbWrapper {
/**
* Get next unused address indices for each HD chain of an account
* @param {string} xpub - xpub
* @returns {integer[]} returns an array of unused indices
* @returns {number[]} returns an array of unused indices
* [M/0/X, M/1/Y] -> [X,Y]
*/
async getHDAccountNextUnusedIndices(xpub) {
@ -921,7 +920,7 @@ class MySqlDbWrapper {
/**
* Get the maximum derived address index for each HD chain of an account
* @param {string} xpub - xpub
* @returns {integer[]} returns an array of derived indices
* @returns {number[]} returns an array of derived indices
* [M/0/X, M/1/Y] -> [X,Y]
*/
async getHDAccountDerivedIndices(xpub) {
@ -950,10 +949,10 @@ class MySqlDbWrapper {
/**
* Get the number of indices derived in an interval for a HD chain
* @param {string} xpub - xpub
* @param {integer} chain - HD chain (0 or 1)
* @param {integer} minIdx - min index of derivation
* @param {integer} maxIdx - max index of derivation
* @returns {integer[]} returns an array of number of derived indices
* @param {number} chain - HD chain (0 or 1)
* @param {number} minIdx - min index of derivation
* @param {number} maxIdx - max index of derivation
* @returns {number[]} returns an array of number of derived indices
*/
async getHDAccountNbDerivedIndices(xpub, chain, minIdx, maxIdx) {
const sqlQuery = 'SELECT \
@ -969,7 +968,7 @@ class MySqlDbWrapper {
const query = mysql.format(sqlQuery, params)
const results = await this._query(query)
if (results.length == 1) {
if (results.length === 1) {
const nbDerivedIndices = results[0].nbDerivedIndices
return (nbDerivedIndices == null) ? 0 : nbDerivedIndices
}
@ -980,7 +979,7 @@ class MySqlDbWrapper {
/**
* Get the number of transactions for an HD account
* @param {string} xpub - xpub
* @returns {integer} returns the balance of the hd account
* @returns {number} returns the balance of the hd account
*/
async getHDAccountNbTransactions(xpub) {
const sqlQuery = 'SELECT COUNT(DISTINCT `r`.`txnTxid`) AS nbTxs \
@ -1007,7 +1006,7 @@ class MySqlDbWrapper {
const query = mysql.format(sqlQuery, params)
const results = await this._query(query)
if (results.length == 1)
if (results.length === 1)
return (results[0].nbTxs == null) ? 0 : results[0].nbTxs
return null
@ -1017,12 +1016,12 @@ class MySqlDbWrapper {
* Get the number of transactions for a list of addresses and HD accounts
* @param {string[]} addresses - array of bitcoin addresses
* @param {string[]} xpubs - array of xpubs
* @returns {int} returns the number of transactions
* @returns {number} returns the number of transactions
*/
async getAddrAndXpubsNbTransactions(addresses, xpubs) {
if (
(!addresses || addresses.length == 0)
&& (!xpubs || xpubs.length == 0)
(!addresses || addresses.length === 0)
&& (!xpubs || xpubs.length === 0)
) {
return []
}
@ -1052,7 +1051,7 @@ class MySqlDbWrapper {
let query = mysql.format(sqlQuery)
const results = await this._query(query)
if (results.length == 1)
if (results.length === 1)
return (results[0].nbTxs == null) ? 0 : results[0].nbTxs
return null
@ -1062,12 +1061,14 @@ class MySqlDbWrapper {
* Get the transactions for a list of addresses and HD accounts
* @param {string[]} addresses - array of bitcoin addresses
* @param {string[]} xpubs - array of xpubs
* @param {number=} page - page
* @param {number=} nbTxsPerPage - number of transactions per page
* @returns {object[]} returns an array of transactions
*/
async getTxsByAddrAndXpubs(addresses, xpubs, page, nbTxsPerPage) {
if (
(!addresses || addresses.length == 0)
&& (!xpubs || xpubs.length == 0)
(!addresses || addresses.length === 0)
&& (!xpubs || xpubs.length === 0)
) {
return []
}
@ -1117,7 +1118,7 @@ class MySqlDbWrapper {
const txsIds = txs.map(t => t.txnID)
if (txsIds.length == 0)
if (txsIds.length === 0)
return []
// Prepares subqueries for
@ -1189,7 +1190,7 @@ class MySqlDbWrapper {
}
}
if (input.hdXpub && input.hdXpub !== null) {
if (input.hdXpub) {
ret.prev_out.xpub = {
m: input.hdXpub,
path: ['M', input.hdAddrChain, input.hdAddrIndex].join('/')
@ -1211,7 +1212,7 @@ class MySqlDbWrapper {
addr: output.outAddress
}
if (output.hdXpub && output.hdXpub !== null) {
if (output.hdXpub) {
ret.xpub = {
m: output.hdXpub,
path: ['M', output.hdAddrChain, output.hdAddrIndex].join('/')
@ -1294,7 +1295,7 @@ class MySqlDbWrapper {
async getXpubByAddresses(addresses) {
const ret = {}
if (addresses.length == 0)
if (addresses.length === 0)
return ret
const sqlQuery = 'SELECT `hd`.`hdXpub`, `addresses`.`addrAddress` \
@ -1316,7 +1317,7 @@ class MySqlDbWrapper {
/**
* Get the mysql ID of a transaction. Ensures that the transaction exists.
* @param {string} txid - txid of a transaction
* @returns {integer} returns the transaction id (mysql id)
* @returns {number} returns the transaction id (mysql id)
*/
async ensureTransactionId(txid) {
const sqlQuery = 'INSERT IGNORE INTO `transactions` SET ?'
@ -1346,14 +1347,14 @@ class MySqlDbWrapper {
/**
* Get the mysql ID of a transaction
* @param {string} txid - txid of a transaction
* @returns {integer} returns the transaction id (mysql id)
* @returns {number} returns the transaction id (mysql id)
*/
async getTransactionId(txid) {
const sqlQuery = 'SELECT `txnID` FROM `transactions` WHERE `txnTxid` = ?'
const params = txid
const query = mysql.format(sqlQuery, params)
const result = await this._query(query)
return (result.length == 0) ? null : result[0].txnID
return (result.length === 0) ? null : result[0].txnID
}
/**
@ -1362,7 +1363,7 @@ class MySqlDbWrapper {
* @returns {object[]} returns an array of {txnTxid: txnId}
*/
async getTransactionsIds(txids) {
if (txids.length == 0)
if (txids.length === 0)
return []
const sqlQuery = 'SELECT `txnID`, `txnTxid` FROM `transactions` WHERE `txnTxid` IN (?)'
@ -1378,11 +1379,11 @@ class MySqlDbWrapper {
/**
* Get the mysql IDs of a set of transactions
* @param {string[]} txid - array of transactions txids
* @returns {integer[]} returns an array of transaction ids (mysql ids)
* @param {string[]} txnIDs - array of transactions txids
* @returns {number[]} returns an array of transaction ids (mysql ids)
*/
async getTransactionsById(txnIDs) {
if (txnIDs.length == 0)
if (txnIDs.length === 0)
return []
const sqlQuery = 'SELECT * FROM `transactions` WHERE `txnID` IN (?)'
@ -1419,7 +1420,7 @@ class MySqlDbWrapper {
* @param {object[]} txs - array of {txid, version, locktime}
*/
async addTransactions(txs) {
if (txs.length == 0)
if (txs.length === 0)
return
const sqlQuery = 'INSERT INTO `transactions` \
@ -1567,10 +1568,10 @@ class MySqlDbWrapper {
/**
* Batch confirm txids in a block
* @param {string[]} txnTxidArray - array of transaction txids
* @param {integer} blockID - mysql id of the blck
* @param {number} blockID - mysql id of the blck
*/
async confirmTransactions(txnTxidArray, blockID) {
if (txnTxidArray.length == 0)
if (txnTxidArray.length === 0)
return
const sqlQuery = 'UPDATE `transactions` SET `blockID` = ? WHERE `txnTxid` IN (?)'
@ -1581,8 +1582,8 @@ class MySqlDbWrapper {
/**
* Get the transactions confirmed after a given height
* @param {integer]} height - block height
* @param {object[]} returns an array of transactions
* @param {number} height - block height
* @returns {object[]} returns an array of transactions
*/
async getTransactionsConfirmedAfterHeight(height) {
const sqlQuery = 'SELECT `transactions`.* FROM `transactions` \
@ -1595,7 +1596,7 @@ class MySqlDbWrapper {
/**
* Delete the transactions confirmed after a given height
* @param {integer]} height - block height
* @param {number} height - block height
*/
async deleteTransactionsConfirmedAfterHeight(height) {
const sqlQuery = 'DELETE `transactions`.* FROM `transactions` \
@ -1611,7 +1612,7 @@ class MySqlDbWrapper {
* @param {string[]} txnTxidArray - array of transaction txids
*/
async unconfirmTransactions(txnTxidArray) {
if (txnTxidArray.length == 0)
if (txnTxidArray.length === 0)
return
const sqlQuery = 'UPDATE `transactions` SET `blockID` = NULL WHERE `txnTxid` IN (?)'
@ -1633,10 +1634,10 @@ class MySqlDbWrapper {
/**
* Delete a set of transactions identified by their mysql ids
* @param {integer[]} txnIDs - mysql ids of the transactions
* @param {number[]} txnIDs - mysql ids of the transactions
*/
async deleteTransactionsByID(txnIDs) {
if (txnIDs.length == 0)
if (txnIDs.length === 0)
return []
const sqlQuery = 'DELETE `transactions`.* FROM `transactions` WHERE `transactions`.`txnID` in (?)'
@ -1650,7 +1651,7 @@ class MySqlDbWrapper {
* @param {object[]} outputs - array of {txnID, addrID, outIndex, outAmount, outScript}
*/
async addOutputs(outputs) {
if (outputs.length == 0)
if (outputs.length === 0)
return
const sqlQuery = 'INSERT IGNORE INTO `outputs` \
@ -1671,7 +1672,7 @@ class MySqlDbWrapper {
* {addrAddress, outID, outAmount, txnTxid, outIndex, spendingTxnID/null, spendingInID/null}
*/
async getOutputSpends(spends) {
if (spends.length == 0)
if (spends.length === 0)
return []
const whereClauses =
@ -1703,7 +1704,7 @@ class MySqlDbWrapper {
* {outID, txnTxid, outIndex}
*/
async getOutputIds(spends) {
if (spends.length == 0)
if (spends.length === 0)
return []
const whereClauses =
@ -1730,7 +1731,7 @@ class MySqlDbWrapper {
* {txnTxid, outIndex, outAmount, outScript, addrAddress}
*/
async getUnspentOutputs(addresses) {
if (addresses.length == 0)
if (addresses.length === 0)
return []
const sqlQuery = 'SELECT \
@ -1761,7 +1762,7 @@ class MySqlDbWrapper {
* {txnID, outID, inIndex, inSequence}
*/
async addInputs(inputs) {
if (inputs.length == 0)
if (inputs.length === 0)
return
const sqlQuery = 'INSERT INTO `inputs` \
@ -1811,7 +1812,7 @@ class MySqlDbWrapper {
const params = hash
const query = mysql.format(sqlQuery, params)
const result = await this._query(query)
return (result.length == 1) ? result[0] : null
return (result.length === 1) ? result[0] : null
}
/**
@ -1820,7 +1821,7 @@ class MySqlDbWrapper {
* @returns {object[]} returns the blocks
*/
async getBlocksByHashes(hashes) {
if (hashes.length == 0)
if (hashes.length === 0)
return []
const sqlQuery = 'SELECT * FROM `blocks` WHERE `blockHash` IN (?)'
@ -1831,7 +1832,7 @@ class MySqlDbWrapper {
/**
* Get details about all blocks at a given block height
* @param {integer} height - block height
* @param {number} height - block height
* @returns {object[]} returns an array of blocks
*/
async getBlocksByHeight(height) {
@ -1843,7 +1844,7 @@ class MySqlDbWrapper {
/**
* Delete the blocks after a given block height
* @param {integer} height - block height
* @param {number} height - block height
*/
async deleteBlocksAfterHeight(height) {
const sqlQuery = 'DELETE FROM `blocks` WHERE `blockHeight` > ?'
@ -1854,12 +1855,12 @@ class MySqlDbWrapper {
/**
* Gets the last block
* @returns {object} returns the last block
* @returns {object | null} returns the last block
*/
async getHighestBlock() {
try {
const results = await this.getLastBlocks(1)
if (results == null || results.length == 0)
if (results == null || results.length === 0)
return {
blockID: null,
blockHeight: 0
@ -1873,7 +1874,7 @@ class MySqlDbWrapper {
/**
* Gets the N last blocks
* @param {integer} n - number of blocks to be retrieved
* @param {number} n - number of blocks to be retrieved
* @returns {object[]} returns an array of the n last blocks
*/
async getLastBlocks(n) {
@ -1896,14 +1897,14 @@ class MySqlDbWrapper {
/**
* Get the mysql ID of a scheduled transaction
* @param {string} txid - txid of a scheduled transaction
* @returns {integer} returns the scheduled transaction id (mysql id)
* @returns {number} returns the scheduled transaction id (mysql id)
*/
async getScheduledTransactionId(txid) {
const sqlQuery = 'SELECT `schID` FROM `scheduled_transactions` WHERE `schTxid` = ?'
const params = txid
const query = mysql.format(sqlQuery, params)
const result = await this._query(query)
return (result.length == 0) ? null : result[0].txnID
return (result.length === 0) ? null : result[0].txnID
}
/**
@ -1952,7 +1953,7 @@ class MySqlDbWrapper {
/**
* Get scheduled transactions
* with a trigger lower than a given block height
* @param {integer} height - block height
* @param {number} height - block height
*/
async getActivatedScheduledTransactions(height) {
const sqlQuery = 'SELECT * FROM `scheduled_transactions` \
@ -1964,7 +1965,7 @@ class MySqlDbWrapper {
/**
* Get the scheduled transaction having a given parentID
* @param {integer} parentId - parent ID
* @param {number} parentId - parent ID
* @returns {object[]} returns an array of scheduled transactions
*/
async getNextScheduledTransactions(parentId) {
@ -1978,8 +1979,8 @@ class MySqlDbWrapper {
/**
* Update the trigger of a scheduled transaction
* identified by its ID
* @param {integer} id - id of the scheduled transaction
* @param {integer} trigger - new trigger
* @param {number} id - id of the scheduled transaction
* @param {number} trigger - new trigger
*/
async updateTriggerScheduledTransaction(id, trigger) {
const sqlQuery = 'UPDATE `scheduled_transactions` \

33
lib/http-server/http-server.js

@ -7,8 +7,11 @@
const { App } = require('@tinyhttp/app')
const sirv = require('sirv')
const helmet = require('helmet')
const nocache = require('nocache')
const Logger = require('../logger')
const errors = require('../errors');
const errors = require('../errors')
const network = require("../../lib/bitcoin/network")
const keys = require('../../keys')[network.key]
/**
@ -18,7 +21,7 @@ class HttpServer {
/**
* Constructor
* @param {int} port - port used by the http server
* @param {number} port - port used by the http server
* @param {string} host - host exposing the http server
*/
constructor(port, host) {
@ -31,13 +34,17 @@ class HttpServer {
// Initialize the tiny-http app
this.app = new App({
noMatchHandler: (req, res) => {
Logger.error(null, `HttpServer : 404 - Not found: ${req.method} - ${req.hostname}${req.path}`)
HttpServer.sendError(res, {status: '404 - Not found'}, 404)
},
// Error handler
onError: (err, req, res) => {
// Detect if this is auth error
if (Object.values(errors.auth).includes(err)) {
HttpServer.sendError(res, err, 401)
} else {
Logger.error(err.stack, 'HttpServer : general error')
Logger.error(err.stack || err, 'HttpServer : general error')
const ret = {status: 'Server error'}
HttpServer.sendError(res, ret, 500)
}
@ -48,9 +55,15 @@ class HttpServer {
this.app.use(HttpServer.requestLogger)
this.app.use(HttpServer.setCrossOrigin)
this.app.use(helmet(HttpServer.HELMET_POLICY))
this.app.use(nocache())
this.app.use('/static', sirv('../static'));
this.app.use((req, res, next) => {
res.append('X-Dojo-Version', keys.dojoVersion)
next()
})
this.app.use(HttpServer.setJSONResponse)
this.app.use(HttpServer.setConnection)
}
@ -62,9 +75,9 @@ class HttpServer {
*/
start() {
// Start a http server
this.server = this.app.listen(this.port, this.host, () => {
this.server = this.app.listen(this.port, () => {
Logger.info(`HttpServer : Listening on ${this.host}:${this.port}`)
})
}, this.host)
this.server.timeout = 600 * 1000
// @see https://github.com/nodejs/node/issues/13391
@ -77,8 +90,8 @@ class HttpServer {
* Stop the http server
*/
stop() {
if (this.app === null) return
this.app.close()
if (this.server == null) return
this.server.close()
}
/**
@ -93,6 +106,7 @@ class HttpServer {
/**
* Return a http response without status
* @param {object} res - http response object
* @param {object} data
*/
static sendOkDataOnly(res, data) {
res.status(200).json(data)
@ -218,16 +232,11 @@ HttpServer.HELMET_POLICY = {
'style-src': ["'self'", "https:", "'unsafe-inline'"],
'media-src': ["'self'", 'data:'],
},
'browserSniff': false,
'disableAndroid': true
},
'dnsPrefetchControl': true,
'frameguard': true,
'hidePoweredBy': true,
'hpkp': false,
'hsts': true,
'ieNoOpen': true,
'noCache': true,
'noSniff': true,
'referrerPolicy': true,
'xssFilter': true

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

@ -8,6 +8,7 @@ const net = require('net')
const makeConcurrent = require('make-concurrent')
const network = require('../bitcoin/network')
const keys = require('../../keys')[network.key]
const util = require('../util')
/**
@ -62,11 +63,11 @@ class RpcClient {
* @returns {boolean} returns true if message related to a connection error
*/
static isConnectionError(err) {
if (typeof err != 'string')
if (typeof err !== 'string')
return false
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') != -1)
const isConnRejected = (err.indexOf('Connection Rejected') != -1)
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') !== -1)
const isConnRejected = (err.indexOf('Connection Rejected') !== -1)
return (isTimeoutError || isConnRejected)
}
@ -121,6 +122,7 @@ class RpcClient {
/**
* Send requests (internal method)
* @param {Object[]} data - array of objects {method: ..., params: ...}
* @param {boolean=} batched - batch requests
* @returns {Promise}
*/
async _call(data, batched) {
@ -173,7 +175,7 @@ class RpcClient {
for (let c of chunk) {
response += c
// Detect the end of a response
if (c == '\n') {
if (c === '\n') {
try {
// Parse the response
let parsed = JSON.parse(response)
@ -189,7 +191,7 @@ class RpcClient {
response = ''
// If all responses have been received
// close the connection
if (responses.length == data.length)
if (responses.length === data.length)
conn.end()
} catch (err) {
reject(

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

@ -23,9 +23,9 @@ class BitcoindWrapper extends Wrapper {
* Constructor
*/
constructor() {
super(null, null)
super('bitcoind', null)
// RPC client
this.client = createRpcClient()
this.client = createRpcClient({ timeout: 5 * 60 * 1000 })
}
/**

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

@ -20,6 +20,8 @@ class EsploraWrapper extends Wrapper {
/**
* Constructor
* @constructor
* @param {string} url
*/
constructor(url) {
super(url, keys.indexer.socks5Proxy)
@ -87,7 +89,7 @@ class EsploraWrapper extends Wrapper {
while (true) {
const txids = await this._getTxsForAddress(address, lastSeenTxid)
if (txids.length == 0)
if (txids.length === 0)
// we have all the transactions
return ret

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

@ -104,7 +104,7 @@ class LocalIndexerWrapper extends Wrapper {
})
// Send the requests
const results = (keys.indexer.localIndexer.batchRequests == 'active')
const results = (keys.indexer.localIndexer.batchRequests === 'active')
? await this.client.sendBatch(commands)
: await this.client.sendRequests(commands)

2
lib/remote-importer/local-rest-indexer-wrapper.js

@ -22,6 +22,8 @@ class LocalRestIndexerWrapper extends Wrapper {
/**
* Constructor
* @constructor
* @param {string} url
*/
constructor(url) {
super(url, null)

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

@ -18,6 +18,8 @@ class OxtWrapper extends Wrapper {
/**
* Constructor
* @constructor
* @param {string} url
*/
constructor(url) {
super(url, keys.indexer.socks5Proxy)
@ -123,7 +125,7 @@ class OxtWrapper extends Wrapper {
async getChainTipHeight() {
let chainTipHeight = null
const result = await this._get(`/lastblock`)
if (result != null && result['data'].length == 1)
if (result != null && result['data'].length === 1)
chainTipHeight = parseInt(result['data'][0]['height'])
return {'chainTipHeight': chainTipHeight}
}

29
lib/remote-importer/remote-importer.js

@ -17,7 +17,7 @@ const gap = keys.gap
let Sources
if (network.key == 'bitcoin') {
if (network.key === 'bitcoin') {
Sources = require('./sources-mainnet')
} else {
Sources = require('./sources-testnet')
@ -64,8 +64,8 @@ class RemoteImporter {
* Import an HD account from remote sources
* @param {string} xpub - HD Account
* @param {string} type - type of HD Account
* @param {integer} gapLimit - (optional) gap limit for derivation
* @param {integer} startIndex - (optional) rescan shall start from this index
* @param {number} gapLimit - (optional) gap limit for derivation
* @param {number} startIndex - (optional) rescan shall start from this index
*/
async importHDAccount(xpub, type, gapLimit, startIndex) {
if (!hdaHelper.isValid(xpub))
@ -96,9 +96,18 @@ class RemoteImporter {
startIndex = (startIndex == null) ? -1 : startIndex - 1
try {
const results = await util.parallelCall(chains, chain => {
return this.xpubScan(xpub, chain, startIndex, startIndex, gaps[chain], type)
})
let results
// Call in series if bitcoind is used because scantxoutset can only process one import at a time
if (keys.indexer.active === 'local_bitcoind') {
results = await util.seriesCall(chains, chain => {
return this.xpubScan(xpub, chain, startIndex, startIndex, gaps[chain], type)
})
} else {
results = await util.parallelCall(chains, chain => {
return this.xpubScan(xpub, chain, startIndex, startIndex, gaps[chain], type)
})
}
// Accumulate addresses and transactions from all chains
const txns = results.map(r => r.transactions).flat()
@ -202,7 +211,7 @@ class RemoteImporter {
}
if (gotTransactions) {
const keyStatus = (c == 0) ? 'txs_ext' : 'txs_int'
const keyStatus = (c === 0) ? 'txs_ext' : 'txs_int'
this.importing[xpub][keyStatus] = Object.keys(txids).length
// We must go deeper
const result = await this.xpubScan(xpub, c, d, u, G, type, txids)
@ -236,7 +245,7 @@ class RemoteImporter {
const addresses = candidates.filter(c => !this.importing[c])
this.importing = addresses.reduce((m,a) => (m[a] = true, m), this.importing)
if (addresses.length == 0) return true
if (addresses.length === 0) return true
Logger.info(`Importer : Importing ${addresses.join(',')}`)
@ -318,8 +327,8 @@ class RemoteImporter {
const blocks = await db.getBlocksByHashes(blocksHashes)
return util.parallelCall(blocks, block => {
const filteredTxs = txs.filter(tx => (tx.block && tx.block.hash == block.blockHash))
if (filteredTxs.length == 0) return
const filteredTxs = txs.filter(tx => (tx.block && tx.block.hash === block.blockHash))
if (filteredTxs.length === 0) return
const txids = filteredTxs.map(tx => tx.txid)
return db.confirmTransactions(txids, block.blockID)
})

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

@ -31,17 +31,17 @@ class SourcesMainnet extends Sources {
* Initialize the external data source
*/
_initSource() {
if (keys.indexer.active == 'local_bitcoind') {
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('Importer : Activated Bitcoind as the data source for imports')
} else if (keys.indexer.active == 'local_indexer') {
} 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('Importer : Activated local indexer as the data source for imports')
} else if (keys.indexer.active == 'local_rest_indexer') {
} else if (keys.indexer.active === 'local_rest_indexer') {
// If local rest indexer option is activated
// we'll use the local indexer as our unique source
const uri = `http://${keys.indexer.localIndexer.host}:${keys.indexer.localIndexer.port}`

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

@ -32,17 +32,17 @@ class SourcesTestnet extends Sources {
* Initialize the external data source
*/
_initSource() {
if (keys.indexer.active == 'local_bitcoind') {
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('Importer : Activated Bitcoind as the data source for imports')
} else if (keys.indexer.active == 'local_indexer') {
} 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('Importer : Activated local indexer as the data source for imports')
} else if (keys.indexer.active == 'local_rest_indexer') {
} else if (keys.indexer.active === 'local_rest_indexer') {
// If local rest indexer option is activated
// we'll use the local indexer as our unique source
const uri = `http://${keys.indexer.localIndexer.host}:${keys.indexer.localIndexer.port}`

4
lib/remote-importer/sources.js

@ -48,7 +48,7 @@ class Sources {
ret.txids = result.txids
} catch(e) {
Logger.error(null, `Importer : Sources.getAddress() : ${address} from ${this.source.base}`)
Logger.error(e, `Importer : Sources.getAddress() : ${address} from ${this.source.base}`)
} finally {
return ret
}
@ -75,7 +75,7 @@ class Sources {
}
} catch(e) {
Logger.error(null, `Importer : Sources.getAddresses() : Error while requesting ${addresses} from ${this.source.base}`)
Logger.error(e, `Importer : Sources.getAddresses() : Error while requesting ${addresses} from ${this.source.base}`)
} finally {
return ret
}

3
lib/remote-importer/wrapper.js

@ -16,6 +16,9 @@ class Wrapper {
/**
* Constructor
* @constructor
* @param {string} url
* @param {string=} socks5Proxy
*/
constructor(url, socks5Proxy) {
this.base = url

131
lib/util.js

@ -5,18 +5,22 @@
'use strict'
/**
* Class providing utility functions as static methods
* @class Util
* @description Class providing utility functions as static methods
*/
class Util {
/**
* Constructor
* @constructor
*/
constructor() {}
/**
* Serialize a series of asynchronous calls to a function
* over a list of objects
* @description Serialize a series of asynchronous calls to a function over a list of objects
* @param {Array<any>} list
* @param {function} fn
* @returns {Promise<Array<any>>}
*/
static async seriesCall(list, fn) {
const results = []
@ -29,8 +33,10 @@ class Util {
}
/**
* Execute parallel asynchronous calls to a function
* over a list of objects
* @description Execute parallel asynchronous calls to a function over a list of objects
* @param {Array<any>} list
* @param {function} fn
* @returns {Promise<Array<any>>}
*/
static parallelCall(list, fn) {
const operations = list.map(item => { return fn(item) })
@ -38,16 +44,21 @@ class Util {
}
/**
* Delay the call to a function
* @description Delay the call to a function
* @param {number} ms
* @returns {Promise<void>}
*/
static delay(ms, v) {
static delay(ms) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, v), ms)
setTimeout(() => resolve(), ms)
})
}
/**
* Splits a list into a list of lists each with maximum length LIMIT
* @description Splits a list into a list of lists each with maximum length LIMIT
* @param {Array<any>} list
* @param {number} limit
* @returns {Array<Array<any>>}
*/
static splitList(list, limit) {
const lists = []
@ -57,7 +68,9 @@ class Util {
}
/**
* Check if a string is a valid hex value
* @description Check if a string is a valid hex value
* @param {string} hash
* @returns {boolean}
*/
static isHashStr(hash) {
const hexRegExp = new RegExp(/^[0-9a-f]*$/, 'i')
@ -65,49 +78,64 @@ class Util {
}
/**
* Check if a string is a well formed 256 bits hash
* @description Check if a string is a well formed 256 bits hash
* @param {string} hash
* @returns {boolean}
*/
static is256Hash(hash) {
return Util.isHashStr(hash) && hash.length == 64
return Util.isHashStr(hash) && hash.length === 64
}
/**
* Sum an array of values
* @description Sum an array of values
* @param {Array<number>} arr
* @returns {number}
*/
static sum(arr) {
return arr.reduce((memo, val) => { return memo + val }, 0)
}
/**
* Mean of an array of values
* @description Mean of an array of values
* @param {Array<number>} arr
* @returns {number}
*/
static mean(arr) {
if (arr.length == 0)
if (arr.length === 0)
return NaN
return sum(arr) / arr.length
}
/**
* Compare 2 values (asc order)
* @description Compare 2 values (asc order)
* @param {number} a
* @param {number} b
* @returns {number}
*/
static cmpAsc(a, b) {
return a - b
}
/**
* Compare 2 values (desc order)
* @description Compare 2 values (desc order)
* @param {number} a
* @param {number} b
* @returns {number}
*/
static cmpDesc(a,b) {
return b - a
}
/**
* Median of an array of values
* @description Median of an array of values
* @param {Array<number>} arr
* @param {boolean=} sorted
* @returns {number}
*/
static median(arr, sorted) {
if (arr.length == 0) return NaN
if (arr.length == 1) return arr[0]
if (arr.length === 0) return NaN
if (arr.length === 1) return arr[0]
if (!sorted)
arr.sort(Util.cmpAsc)
@ -124,7 +152,10 @@ class Util {
}
/**
* Median Absolute Deviation of an array of values
* @description Median Absolute Deviation of an array of values
* @param {Array<number>} arr
* @param {boolean=} sorted
* @returns {number}
*/
static mad(arr, sorted) {
const med = Util.median(arr, sorted)
@ -136,7 +167,10 @@ class Util {
}
/**
* Quartiles of an array of values
* @description Quartiles of an array of values
* @param {Array<number>} arr
* @param {boolean=} sorted
* @returns {Array<number>}
*/
static quartiles(arr, sorted) {
const q = [NaN,NaN,NaN]
@ -156,10 +190,10 @@ class Util {
const mod4 = arr.length % 4
const n = Math.floor(arr.length / 4)
if (mod4 == 1) {
if (mod4 === 1) {
q[0] = (arr[n-1] + 3 * arr[n]) / 4
q[2] = (3 * arr[3*n] + arr[3*n+1]) / 4
} else if (mod4 == 3) {
} else if (mod4 === 3) {
q[0] = (3 * arr[n] + arr[n+1]) / 4
q[2] = (arr[3*n+1] + 3 * arr[3*n+2]) / 4
}
@ -174,7 +208,11 @@ class Util {
}
/**
* Obtain the value of the PCT-th percentile, where PCT on [0,100]
* @description Obtain the value of the PCT-th percentile, where PCT on [0,100]
* @param {Array<number>} arr
* @param {number} pct
* @param {boolean=} sorted
* @returns {number}
*/
static percentile(arr, pct, sorted) {
if (arr.length < 2) return NaN
@ -210,28 +248,35 @@ class Util {
}
/**
* Convert bytes to Mb
* @description Convert bytes to MB
* @param {number} bytes
* @returns {number}
*/
static toMb(bytes) {
return +(bytes / Util.MB).toFixed(0)
}
/**
* Convert a date to a unix timestamp
* @description Convert a date to a unix timestamp
* @returns {number}
*/
static unix() {
return (Date.now() / 1000) | 0
}
/**
* Convert a value to a padded string (10 chars)
* @description Convert a value to a padded string (10 chars)
* @param {number} v
* @returns {string}
*/
static pad10(v) {
return (v < 10) ? `0${v}` : `${v}`
}
/**
* Convert a value to a padded string (100 chars)
* @description Convert a value to a padded string (100 chars)
* @param {number} v
* @returns {string}
*/
static pad100(v) {
if (v < 10) return `00${v}`
@ -240,7 +285,9 @@ class Util {
}
/**
* Convert a value to a padded string (1000 chars)
* @description Convert a value to a padded string (1000 chars)
* @param {number} v
* @returns {string}
*/
static pad1000(v) {
if (v < 10) return `000${v}`
@ -250,7 +297,11 @@ class Util {
}
/**
* Left pad
* @description Left pad
* @param {number} number
* @param {number} places
* @param {string=} fill
* @returns {string}
*/
static leftPad(number, places, fill) {
number = Math.round(number)
@ -271,7 +322,10 @@ class Util {
}
/**
* Display a time period, in seconds, as DDD:HH:MM:SS[.MS]
* @description Display a time period, in seconds, as DDD:HH:MM:SS[.MS]
* @param {number} period
* @param {number} milliseconds
* @returns {string}
*/
static timePeriod(period, milliseconds) {
milliseconds = !!milliseconds
@ -297,6 +351,19 @@ class Util {
}
}
/**
* @description Generate array of sequential numbers from start to stop
* @param {number} start
* @param {number} stop
* @param {number=} step
* @returns {number[]}
*/
static range(start, stop, step = 1) {
return Array(Math.ceil((stop - start) / step))
.fill(start)
.map((x, y) => x + y * step)
}
}
/**

34
lib/wallet/address-info.js

@ -9,13 +9,15 @@ const hdaHelper = require('../bitcoin/hd-accounts-helper')
/**
* A class storing information about the actibity of an address
* @class AddressInfo
* @description A class storing information about the actibity of an address
*/
class AddressInfo {
/**
* Constructor
* @param {object} address - bitcoin address
* @constructor
* @param {string} address - bitcoin address
*/
constructor(address) {
// Initializes properties
@ -25,7 +27,7 @@ class AddressInfo {
this.nTx = 0
this.unspentOutputs = []
this.tracked = false,
this.tracked = false
this.type = 'untracked'
this.xpub = null
this.path = null
@ -34,8 +36,8 @@ class AddressInfo {
}
/**
* Load information about the address
* @returns {Promise}
* @description Load information about the address
* @returns {Promise<void[]>}
*/
async loadInfo() {
return Promise.all([
@ -57,9 +59,8 @@ class AddressInfo {
}
/**
* Load information about the address
* (extended form)
* @returns {Promise}
* @description Load information about the address (extended form)
* @returns {Promise<void[]>}
*/
async loadInfoExtended() {
const res = await db.getHDAccountsByAddresses([this.address])
@ -75,7 +76,7 @@ class AddressInfo {
}
for (let a of res.loose) {
if (a.addrAddress == this.address) {
if (a.addrAddress === this.address) {
this.tracked = true
this.type = 'loose'
break
@ -86,9 +87,9 @@ class AddressInfo {
}
/**
* Loads a partial list of transactions for this address
* @param {integer} page - page index
* @param {integer} count - number of transactions per page
* @description Loads a partial list of transactions for this address
* @param {number} page - page index
* @param {number} count - number of transactions per page
* @returns {Promise}
*/
async loadTransactions(page, count) {
@ -96,8 +97,8 @@ class AddressInfo {
}
/**
* Load the utxos associated to the address
* @returns {Promise - object[]}
* @description Load the utxos associated to the address
* @returns {Promise<object[]>}
*/
async loadUtxos() {
this.unspentOutputs = []
@ -118,7 +119,7 @@ class AddressInfo {
}
/**
* Return a plain old js object with address properties
* @description Return a plain old js object with address properties
* @returns {object}
*/
toPojo() {
@ -135,8 +136,7 @@ class AddressInfo {
}
/**
* Return a plain old js object with address properties
* (extended version)
* @description Return a plain old js object with address properties (extended version)
* @returns {object}
*/
toPojoExtended() {

37
lib/wallet/hd-account-info.js

@ -12,13 +12,14 @@ const rpcLatestBlock = require('../bitcoind-rpc/latest-block')
/**
* A class storing information about the actibity of a hd account
* @class HdAccountInfo
* @description A class storing information about the actibity of a hd account
*/
class HdAccountInfo {
/**
* Constructor
* @param {object} xpub - xpub
* @param {string} xpub - xpub
*/
constructor(xpub) {
// Initializes properties
@ -39,9 +40,8 @@ class HdAccountInfo {
}
/**
* Ensure the hd account exists in database
* Otherwise, tries to import it with BIP44 derivation
* @returns {Promise - integer} return the internal id of the hd account
* @description Ensure the hd account exists in database. Otherwise, tries to import it with BIP44 derivation
* @returns {Promise<number | null>} return the internal id of the hd account
* or null if it doesn't exist
*/
async ensureHdAccount() {
@ -49,7 +49,7 @@ class HdAccountInfo {
const id = await db.getHDAccountId(this.xpub)
return id
} catch(e) {
if (e == errors.db.ERROR_NO_HD_ACCOUNT) {
if (e === errors.db.ERROR_NO_HD_ACCOUNT) {
try {
// Default to BIP44 import
return hdaService.restoreHdAccount(this.xpub, hdaHelper.BIP44)
@ -62,8 +62,8 @@ class HdAccountInfo {
}
/**
* Load information about the hd account
* @returns {Promise}
* @description Load information about the hd account
* @returns {Promise<boolean>}
*/
async loadInfo() {
try {
@ -93,29 +93,41 @@ class HdAccountInfo {
this.depth = node[2].depth
}
/**
* @returns {Promise<void>}
*/
async _loadBalance() {
this.finalBalance = await db.getHDAccountBalance(this.xpub)
}
/**
* @returns {Promise<void>}
*/
async _loadUnusedIndices() {
const unusedIdx = await db.getHDAccountNextUnusedIndices(this.xpub)
this.accountIndex = unusedIdx[0]
this.changeIndex = unusedIdx[1]
}
/**
* @returns {Promise<void>}
*/
async _loadDerivedIndices() {
const derivedIdx = await db.getHDAccountDerivedIndices(this.xpub)
this.accountDerivedIndex = derivedIdx[0]
this.changeDerivedIndex = derivedIdx[1]
}
/**
* @returns {Promise<void>}
*/
async _loadNbTransactions() {
this.nTx = await db.getHDAccountNbTransactions(this.xpub)
}
/**
* Load the utxos associated to the hd account
* @returns {Promise - object[]}
* @description Load the utxos associated to the hd account
* @returns {Promise<object[]>}
*/
async loadUtxos() {
this.unspentOutputs = []
@ -153,7 +165,7 @@ class HdAccountInfo {
}
/**
* Return a plain old js object with hd account properties
* @description Return a plain old js object with hd account properties
* @returns {object}
*/
toPojo() {
@ -169,8 +181,7 @@ class HdAccountInfo {
}
/**
* Return a plain old js object with hd account properties
* (extended version)
* @description Return a plain old js object with hd account properties (extended version)
* @returns {object}
*/
toPojoExtended() {

21
lib/wallet/wallet-entities.js

@ -6,13 +6,13 @@
/**
* A class storing entities (xpubs, addresses, pubkeys)
* defining a (full|partial) wallet
* @class WalletEntities
* @description A class storing entities (xpubs, addresses, pubkeys) defining a (full|partial) wallet
*/
class WalletEntities {
/**
* Constructor
* @constructor
*/
constructor() {
this.pubkeys = []
@ -23,8 +23,7 @@ class WalletEntities {
}
/**
* Add a new hd account
* with its translation as an xpub
* @description Add a new hd account with its translation as an xpub
* @param {string} xpub - xpub or tpub
* @param {string} ypub - ypub or upub or false
* @param {string} zpub - zpub or vpub or false
@ -36,7 +35,7 @@ class WalletEntities {
}
/**
* Add a new address/pubkey
* @description Add a new address/pubkey
* @param {string} address - bitcoin address
* @param {string} pubkey - pubkey associated to the address or false
*/
@ -46,7 +45,7 @@ class WalletEntities {
}
/**
* Update the pubkey associated to a given address
* @description Update the pubkey associated to a given address
* @param {string} address - bitcoin address
* @param {string} pubkey - public key
*/
@ -57,7 +56,7 @@ class WalletEntities {
}
/**
* Checks if a xpub is already listed
* @description Checks if a xpub is already listed
* @param {string} xpub
* @returns {boolean} returns true if the xpub is already listed, false otherwise
*/
@ -66,7 +65,7 @@ class WalletEntities {
}
/**
* Checks if an address is already listed
* @description Checks if an address is already listed
* @param {string} address - bitcoin address
* @returns {boolean} returns true if the address is already listed, false otherwise
*/
@ -75,7 +74,7 @@ class WalletEntities {
}
/**
* Checks if a pubkey is already listed
* @description Checks if a pubkey is already listed
* @param {string} pubkey - public key
* @returns {boolean} returns true if the pubkey is already listed, false otherwise
*/
@ -85,4 +84,4 @@ class WalletEntities {
}
module.exports = WalletEntities
module.exports = WalletEntities

48
lib/wallet/wallet-info.js

@ -16,11 +16,13 @@ const AddressInfo = require('./address-info')
/**
* A class storing information about a (full|partial) wallet
* Provides a set of methods allowing to retrieve specific information
* @class WalletInfo
*/
class WalletInfo {
/**
* Constructor
* @constructor
* @param {object} entities - wallet entities (hdaccounts, addresses, pubkeys)
*/
constructor(entities) {
@ -49,7 +51,7 @@ class WalletInfo {
/**
* Ensure hd accounts exist in database
* @returns {Promise}
* @returns {Promise<Array<any>>}
*/
async ensureHdAccounts() {
return util.parallelCall(this.entities.xpubs, async xpub => {
@ -60,7 +62,7 @@ class WalletInfo {
/**
* Load information about the hd accounts
* @returns {Promise}
* @returns {Promise<Array<any>>}
*/
async loadHdAccountsInfo() {
return util.parallelCall(this.entities.xpubs, async xpub => {
@ -91,7 +93,7 @@ class WalletInfo {
/**
* Filter addresses that belong to an active hd account
* @returns {Promise}
* @returns {Promise<void>}
*/
async filterAddresses() {
const res = await db.getXpubByAddresses(this.entities.addrs)
@ -110,7 +112,7 @@ class WalletInfo {
/**
* Load information about the addresses
* @returns {Promise}
* @returns {Promise<Array<void>>}
*/
async loadAddressesInfo() {
return util.parallelCall(this.entities.addrs, async address => {
@ -123,11 +125,11 @@ class WalletInfo {
/**
* Loads a partial list of transactions for this wallet
* @param {integer} page - page index
* @param {integer} count - number of transactions per page
* @param {boolean} txBalance - True if past wallet balance
* @param {number} page - page index
* @param {number} count - number of transactions per page
* @param {boolean=} txBalance - True if past wallet balance
* should be computed for each transaction
* @returns {Promise}
* @returns {Promise<void>}
*/
async loadTransactions(page, count, txBalance) {
this.txs = await db.getTxsByAddrAndXpubs(
@ -149,7 +151,7 @@ class WalletInfo {
/**
* Loads the number of transactions for this wallet
* @returns {Promise}
* @returns {Promise<void>}
*/
async loadNbTransactions() {
const nbTxs = await db.getAddrAndXpubsNbTransactions(
@ -163,7 +165,7 @@ class WalletInfo {
/**
* Loads tinfo about the fee rates
* @returns {Promise}
* @returns {Promise<void>}
*/
async loadFeesInfo() {
this.info.fees = await rpcFees.getFees()
@ -171,7 +173,7 @@ class WalletInfo {
/**
* Loads the list of unspent outputs for this wallet
* @returns {Promise}
* @returns {Promise<void>}
*/
async loadUtxos() {
// Load the utxos for the hd accounts
@ -212,7 +214,7 @@ class WalletInfo {
/**
* Post process addresses and public keys
*/
postProcessAddresses() {
async postProcessAddresses() {
for (let b = 0; b < this.entities.pubkeys.length; b++) {
const pk = this.entities.pubkeys[b]
@ -221,7 +223,7 @@ class WalletInfo {
// Add pubkeys in this.addresses
for (let c = 0; c < this.addresses.length; c++) {
if (address == this.addresses[c].address)
if (address === this.addresses[c].address)
this.addresses[c].pubkey = pk
}
@ -229,30 +231,32 @@ class WalletInfo {
for (let d = 0; d < this.txs.length; d++) {
// inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) {
if (address == this.txs[d].inputs[e].prev_out.addr)
if (address === this.txs[d].inputs[e].prev_out.addr)
this.txs[d].inputs[e].prev_out.pubkey = pk
}
// outputs
for (let e = 0; e < this.txs[d].out.length; e++) {
if (address == this.txs[d].out[e].addr)
if (address === this.txs[d].out[e].addr)
this.txs[d].out[e].pubkey = pk
}
}
// Add pubkeys in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) {
if (address == this.unspentOutputs[f].addr) {
if (address === this.unspentOutputs[f].addr) {
this.unspentOutputs[f].pubkey = pk
}
}
}
}
return Promise.resolve()
}
/**
* Post process hd accounts (xpubs translations)
*/
postProcessHdAccounts() {
async postProcessHdAccounts() {
for (let b = 0; b < this.entities.xpubs.length; b++) {
const entityXPub = this.entities.xpubs[b]
const entityYPub = this.entities.ypubs[b]
@ -263,7 +267,7 @@ class WalletInfo {
// Translate xpub => ypub/zpub in this.addresses
for (let c = 0; c < this.addresses.length; c++) {
if (entityXPub == this.addresses[c].address)
if (entityXPub === this.addresses[c].address)
this.addresses[c].address = tgtXPub
}
@ -272,14 +276,14 @@ class WalletInfo {
// inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) {
const xpub = this.txs[d].inputs[e].prev_out.xpub
if (xpub && (xpub.m == entityXPub))
if (xpub && (xpub.m === entityXPub))
this.txs[d].inputs[e].prev_out.xpub.m = tgtXPub
}
// outputs
for (let e = 0; e < this.txs[d].out.length; e++) {
const xpub = this.txs[d].out[e].xpub
if (xpub && (xpub.m == entityXPub))
if (xpub && (xpub.m === entityXPub))
this.txs[d].out[e].xpub.m = tgtXPub
}
}
@ -287,12 +291,14 @@ class WalletInfo {
// Translate xpub => ypub/zpub in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) {
const xpub = this.unspentOutputs[f].xpub
if (xpub && (xpub.m == entityXPub)) {
if (xpub && (xpub.m === entityXPub)) {
this.unspentOutputs[f].xpub.m = tgtXPub
}
}
}
}
return Promise.resolve()
}
/**

28
lib/wallet/wallet-service.js

@ -72,7 +72,7 @@ class WalletService {
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
if (keys.indexer.active !== 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses
@ -150,7 +150,7 @@ class WalletService {
await walletInfo.ensureAddresses()
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
if (keys.indexer.active !== 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the address and load them
await walletInfo.filterAddresses()
@ -252,7 +252,7 @@ class WalletService {
await walletInfo.ensureAddresses()
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
if (keys.indexer.active !== 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses
await walletInfo.filterAddresses()
@ -274,8 +274,8 @@ class WalletService {
/**
* Get a subset of wallet transactions
* @param {object} entities - mapping of active entities
* @param {integer} page - page of transactions to be returned
* @param {integer} count - number of transactions returned per page
* @param {number} page - page of transactions to be returned
* @param {number} count - number of transactions returned per page
* @returns {Promise}
*/
async getWalletTransactions(entities, page, count) {
@ -287,7 +287,7 @@ class WalletService {
}
// Check parameters
if (entities.xpubs.length == 0 && entities.addrs.length == 0)
if (entities.xpubs.length === 0 && entities.addrs.length === 0)
return ret
// Initialize a WalletInfo object
@ -344,13 +344,13 @@ class WalletService {
* @returns {boolean} return true if conditions are met, false otherwise
*/
_checkEntities(active, legacy, bip49, bip84, pubkeys) {
const allEmpty = active.xpubs.length == 0
&& active.addrs.length == 0
&& legacy.xpubs.length == 0
&& legacy.addrs.length == 0
&& pubkeys.addrs.length == 0
&& bip49.xpubs.length == 0
&& bip84.xpubs.length == 0
const allEmpty = active.xpubs.length === 0
&& active.addrs.length === 0
&& legacy.xpubs.length === 0
&& legacy.addrs.length === 0
&& pubkeys.addrs.length === 0
&& bip49.xpubs.length === 0
&& bip84.xpubs.length === 0
return !allEmpty
}
@ -375,7 +375,7 @@ class WalletService {
const pubkey = source.pubkeys[idxSource]
const idxActive = active.addrs.indexOf(addr)
if (idxActive == -1) {
if (idxActive === -1) {
active.addrs.push(addr)
active.pubkeys.push(pubkey)
} else if (pubkey) {

3341
package-lock.json

File diff suppressed because it is too large

30
package.json

@ -1,8 +1,11 @@
{
"name": "samourai-dojo",
"version": "1.10.1",
"version": "1.11.0",
"description": "Backend server for Samourai Wallet",
"main": "accounts/index.js",
"engines": {
"node": "14.x.x"
},
"scripts": {
"test": "mocha --recursive --reporter spec"
},
@ -14,31 +17,32 @@
"license": "AGPL-3.0-only",
"homepage": "https://code.samourai.io/dojo/samourai-dojo",
"dependencies": {
"@tinyhttp/app": "1.3.3",
"async-sema": "2.1.2",
"@tinyhttp/app": "1.3.13",
"async-sema": "3.1.0",
"axios": "0.21.1",
"bip39": "2.4.0",
"bip39": "3.0.4",
"bitcoinjs-lib": "5.2.0",
"bitcoinjs-message": "1.0.1",
"bitcoinjs-message": "2.2.0",
"body-parser": "1.19.0",
"helmet": "3.23.3",
"helmet": "4.6.0",
"jsonwebtoken": "8.5.1",
"lodash": "4.17.21",
"lru-cache": "4.0.2",
"lru-cache": "6.0.0",
"make-concurrent": "5.3.0",
"minimist": "1.2.5",
"mysql": "2.18.1",
"nocache": "3.0.1",
"passport": "0.4.1",
"passport-localapikey-update": "0.6.0",
"rpc-bitcoin": "2.0.0",
"sirv": "1.0.11",
"socks-proxy-agent": "4.0.1",
"validator": "10.8.0",
"sirv": "1.0.12",
"socks-proxy-agent": "6.0.0",
"validator": "13.6.0",
"websocket": "1.0.34",
"workerpool": "6.1.4",
"zeromq": "4.2.0"
"workerpool": "6.1.5",
"zeromq": "6.0.0-beta.5"
},
"devDependencies": {
"mocha": "^7.1.1"
"mocha": "9.0.3"
}
}

12
pushtx/orchestrator.js

@ -4,8 +4,8 @@
*/
'use strict'
const zmq = require('zeromq')
const Sema = require('async-sema')
const zmq = require('zeromq/v5-compat')
const { Sema } = require('async-sema')
const Logger = require('../lib/logger')
const db = require('../lib/db/mysql-db-wrapper')
const { createRpcClient, isConnectionError } = require('../lib/bitcoind-rpc/rpc-client')
@ -94,7 +94,7 @@ class Orchestrator {
break
for (let tx of txs) {
let hasParentTx = (tx.schParentTxid != null) && (tx.schParentTxid != '')
let hasParentTx = (tx.schParentTxid != null) && (tx.schParentTxid !== '')
let parentTx = null
// Check if previous transaction has been confirmed
@ -158,11 +158,11 @@ class Orchestrator {
/**
* Update triggers in chain of transactions
* following a transaction identified by its txid
* @param {integer} parentId - parent id
* @param {integer} shift - delta to be added to the triggers
* @param {number} parentId - parent id
* @param {number} shift - delta to be added to the triggers
*/
async updateTriggers(parentId, shift) {
if (shift == 0)
if (shift === 0)
return
const txs = await db.getNextScheduledTransactions(parentId)

8
pushtx/pushtx-processor.js

@ -5,7 +5,7 @@
'use strict'
const bitcoin = require('bitcoinjs-lib')
const zmq = require('zeromq')
const zmq = require('zeromq/v5-compat')
const Logger = require('../lib/logger')
const errors = require('../lib/errors')
const db = require('../lib/db/mysql-db-wrapper')
@ -17,7 +17,7 @@ const keys = require('../keys')[network.key]
const status = require('./status')
let Sources
if (network.key == 'bitcoin') {
if (network.key === 'bitcoin') {
Sources = require('../lib/remote-importer/sources-mainnet')
} else {
Sources = require('../lib/remote-importer/sources-testnet')
@ -46,7 +46,7 @@ class PushTxProcessor {
initNotifications(config) {
// Notification socket for the tracker
this.notifSock = zmq.socket('pub')
this.notifSock.bindSync(config.uriSocket)
this.notifSock.bind(config.uriSocket)
}
/**
@ -78,7 +78,7 @@ class PushTxProcessor {
}
// Checks with indexer if addresses are known and have been used
if (Object.keys(addrMap).length > 0) {
if (keys.indexer.active != 'local_bitcoind') {
if (keys.indexer.active !== 'local_bitcoind') {
const results = await this.sources.getAddresses(Object.keys(addrMap))
for (let r of results)
if (r.ntx > 0)

4
pushtx/pushtx-rest-api.js

@ -193,7 +193,7 @@ class PushTxRestApi {
HttpServer.sendOk(res)
} catch(e) {
// Returns code 200 if VIOLATION_STRICT_MODE_VOUTS
if (e.message && e.message.code && e.message.code == errors.pushtx.VIOLATION_STRICT_MODE_VOUTS) {
if (e.message && e.message.code && e.message.code === errors.pushtx.VIOLATION_STRICT_MODE_VOUTS) {
e.message = JSON.stringify(e.message)
this._traceError(res, e, 200)
} else {
@ -206,7 +206,7 @@ class PushTxRestApi {
* Trace an error during push
* @param {object} res - http response object
* @param {object} err - error object
* @param {int} errorCode - error code (optional)
* @param {number} errorCode - error code (optional)
*/
_traceError(res, err, errorCode) {
let ret = null

8
pushtx/transactions-scheduler.js

@ -68,13 +68,13 @@ class TransactionsScheduler {
// Decode the transaction
const tx = bitcoin.Transaction.fromHex(entry.tx)
// Check that nlocktimes are matching
if (!(tx.locktime && tx.locktime == entry.nlocktime)) {
if (!(tx.locktime && tx.locktime === entry.nlocktime)) {
const msg = `TransactionsScheduler.schedule() : nLockTime mismatch : ${tx.locktime} - ${entry.nlocktime}`
Logger.error(null, `PushTx : ${msg}`)
throw errors.pushtx.NLOCK_MISMATCH
}
// Check that order of hop and nlocktime values are consistent
if (entry.hop != lastHopProcessed) {
if (entry.hop !== lastHopProcessed) {
if (entry.nlocktime < lastLockTimeProcessed)
throw errors.pushtx.SCHEDULED_BAD_ORDER
}
@ -105,7 +105,7 @@ class TransactionsScheduler {
lastHopProcessed = entry.hop
lastLockTimeProcessed = entry.nlocktime
// Update scheduled height if needed
if (baseHeight != nltTx0)
if (baseHeight !== nltTx0)
entry.nlocktime = baseHeight + entry.delta
}
@ -123,7 +123,7 @@ class TransactionsScheduler {
let parentNlocktime = baseHeight
// Check if first transactions should be sent immediately
while ((script.length > 0) && (script[0].nlocktime <= lastHeight) && (script[0].delta == 0)) {
while ((script.length > 0) && (script[0].nlocktime <= lastHeight) && (script[0].delta === 0)) {
await pushTxProcessor.pushTx(script[0].tx)
const tx = bitcoin.Transaction.fromHex(script[0].tx)
parentTxid = tx.getId()

14
scripts/patches/revert-hd-accounts.js

@ -21,9 +21,9 @@ function xlatXPUB(xpub) {
let xlatVer = 0
if (ver == hdaHelper.MAGIC_XPUB || ver == hdaHelper.MAGIC_YPUB || ver == hdaHelper.MAGIC_ZPUB) {
xlatVer = hdaHelper.MAGIC_XPUB
} else if (ver == hdaHelper.MAGIC_TPUB || ver == hdaHelper.MAGIC_UPUB || ver == hdaHelper.MAGIC_VPUB) {
if (ver === hdaHelper.MAGIC_XPUB || ver === hdaHelper.MAGIC_YPUB || ver === hdaHelper.MAGIC_ZPUB) {
xlatVer = hdaHelper.MAGIC_XPUB
} else if (ver === hdaHelper.MAGIC_TPUB || ver === hdaHelper.MAGIC_UPUB || ver === hdaHelper.MAGIC_VPUB) {
xlatVer = hdaHelper.MAGIC_TPUB
}
@ -75,8 +75,8 @@ async function run() {
const xpub = account.hdXpub
const info = hdaHelper.classify(account.hdType)
const scheme = info.type
if ((scheme == hdaHelper.BIP49) || (scheme == hdaHelper.BIP84)) {
if ((scheme === hdaHelper.BIP49) || (scheme === hdaHelper.BIP84)) {
try {
const xlatedXpub = xlatXPUB(xpub)
await updateHdAccount(hdId, xlatedXpub)
@ -90,7 +90,7 @@ async function run() {
} catch(e) {
console.log('A problem was met')
console.log(e)
}
}
}
/**
@ -102,4 +102,4 @@ const startupTimeout = setTimeout(async function() {
return run().then(() => {
console.log('Process completed')
})
}, 1500)
}, 1500)

20
scripts/patches/translate-hd-accounts.js

@ -21,18 +21,18 @@ function xlatXPUB(xpub, targetType) {
let xlatVer = 0
if (ver == hdaHelper.MAGIC_XPUB) {
if (ver === hdaHelper.MAGIC_XPUB) {
if (targetType == hdaHelper.BIP49)
if (targetType === hdaHelper.BIP49)
xlatVer = hdaHelper.MAGIC_YPUB
else if (targetType == hdaHelper.BIP84)
else if (targetType === hdaHelper.BIP84)
xlatVer = hdaHelper.MAGIC_ZPUB
} else if (ver == hdaHelper.MAGIC_TPUB) {
} else if (ver === hdaHelper.MAGIC_TPUB) {
if (targetType == hdaHelper.BIP49)
if (targetType === hdaHelper.BIP49)
xlatVer = hdaHelper.MAGIC_UPUB
else if (targetType == hdaHelper.BIP84)
else if (targetType === hdaHelper.BIP84)
xlatVer = hdaHelper.MAGIC_VPUB
}
@ -84,8 +84,8 @@ async function run() {
const xpub = account.hdXpub
const info = hdaHelper.classify(account.hdType)
const scheme = info.type
if ((scheme == hdaHelper.BIP49) || (scheme == hdaHelper.BIP84)) {
if ((scheme === hdaHelper.BIP49) || (scheme === hdaHelper.BIP84)) {
const xlatedXpub = xlatXPUB(xpub, scheme)
await updateHdAccount(hdId, xlatedXpub)
console.log(`Updated ${hdId} (${xpub} => ${xlatedXpub})`)
@ -94,7 +94,7 @@ async function run() {
} catch(e) {
console.log('A problem was met')
console.log(e)
}
}
}
/**
@ -106,4 +106,4 @@ const startupTimeout = setTimeout(async function() {
return run().then(() => {
console.log('Process completed')
})
}, 1500)
}, 1500)

2
static/admin/dmt/index.js

@ -84,7 +84,7 @@ function preparePage() {
const activeTab = sessionStorage.getItem('activeTab')
for (let idxTab in tabs) {
const screen = screens[idxTab]
if (tabs[idxTab] == activeTab) {
if (tabs[idxTab] === activeTab) {
$(screen).show()
if (screenScripts.has(screen))
screenScripts.get(screen).preparePage()

10
static/admin/dmt/status/status.js

@ -95,7 +95,7 @@ const statusScript = {
this.chaintipBitcoind = data['bitcoind']['blocks']
$('#node-chaintip').text(data['bitcoind']['blocks'])
$('#node-version').text(data['bitcoind']['version'])
const network = data['bitcoind']['testnet'] == true ? 'testnet' : 'mainnet'
const network = data['bitcoind']['testnet'] === true ? 'testnet' : 'mainnet'
$('#node-network').text(network)
$('#node-conn').text(data['bitcoind']['conn'])
$('#node-relay-fee').text(data['bitcoind']['relayfee'])
@ -123,13 +123,13 @@ const statusScript = {
},
setStatusIndicator: function(id, status) {
if (status == 'ok') {
if (status === 'ok') {
$(id).html('&#10003;')
$(id).css('color', '#76d776')
} else if (status == 'ko') {
} else if (status === 'ko') {
$(id).html('X')
$(id).css('color', '#f77c7c')
} else if (status == 'desynchronized') {
} else if (status === 'desynchronized') {
$(id).html('&#10003;')
$(id).css('color', '#f0c649')
} else {
@ -140,4 +140,4 @@ const statusScript = {
}
screenScripts.set('#screen-status', statusScript)
screenScripts.set('#screen-status', statusScript)

2
static/admin/dmt/txs-tools/txs-tools.js

@ -59,7 +59,7 @@ const screenTxsToolsScript = {
$('#txid-value').text(this.currentTxid)
$('#txid-value').attr('href', txUrl)
const firstseen = lib_fmt.unixTsToLocaleString(txInfo['created'])
const firstseen = txInfo['created'] ? lib_fmt.unixTsToLocaleString(txInfo['created']) : '--'
$('#tx-firstseen').text(firstseen)
if (txInfo.hasOwnProperty('block'))

8
static/admin/dmt/xpubs-tools/xpubs-tools.js

@ -31,8 +31,8 @@ const screenXpubsToolsScript = {
preparePage: function() {
// Disable custom lookahead if data source is a third party explorer
const isTPE = sessionStorage.getItem('indexerType') == 'third_party_explorer'
const isLRI = sessionStorage.getItem('indexerType') == 'local_rest_indexer'
const isTPE = sessionStorage.getItem('indexerType') === 'third_party_explorer'
const isLRI = sessionStorage.getItem('indexerType') === 'local_rest_indexer'
const disableLookahead = isTPE || isLRI
$('#rescan-lookahead').prop('disabled', disableLookahead)
@ -99,9 +99,9 @@ const screenXpubsToolsScript = {
}
const derivType = $('#import-deriv-type').val()
if (derivType == 'bip49' || derivType == 'bip84') {
if (derivType === 'bip49' || derivType === 'bip84') {
jsonData['segwit'] = derivType
} else if (derivType == 'auto') {
} else if (derivType === 'auto') {
if (this.currentXpub.startsWith('ypub'))
jsonData['segwit'] = 'bip49'
else if (this.currentXpub.startsWith('zpub'))

4
static/admin/lib/auth-utils.js

@ -98,7 +98,7 @@ const lib_auth = {
isAuthenticated: function() {
// Checks that an access token is stored in session storage
let token = this.getAccessToken()
return (token && (token != 'null')) ? true : false
return Boolean(token && (token !== 'null'))
},
/*
@ -128,7 +128,7 @@ const lib_auth = {
const payload = this.getPayloadAccessToken(token)
if (!payload)
return false
return (('prf' in payload) && (payload['prf'] == this.TOKEN_PROFILE_ADMIN))
return (('prf' in payload) && (payload['prf'] === this.TOKEN_PROFILE_ADMIN))
},
/*

8
static/admin/lib/common-script.js

@ -27,9 +27,9 @@ const lib_cmn = {
getExplorerTxUrl: function(txid, explorerInfo) {
if (explorerInfo == null)
return null
else if (explorerInfo['pairing']['type'] == 'explorer.oxt')
else if (explorerInfo['pairing']['type'] === 'explorer.oxt')
return `${explorerInfo['pairing']['url']}/transaction/${txid}`
else if (explorerInfo['pairing']['type'] == 'explorer.btc_rpc_explorer')
else if (explorerInfo['pairing']['type'] === 'explorer.btc_rpc_explorer')
return `http://${explorerInfo['pairing']['url']}/tx/${txid}`
else
return null
@ -46,7 +46,7 @@ const lib_cmn = {
if (file) {
xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.readyState === 4 && this.status === 200) {
elmnt.innerHTML = this.responseText
elmnt.removeAttribute('include-html')
self.includeHTML(cb)
@ -72,7 +72,7 @@ const lib_cmn = {
if (file) {
xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.readyState === 4 && this.status === 200) {
const newElmnt = document.createElement('script')
newElmnt.textContent = this.responseText
if (elmnt.parentNode) {

2
static/admin/lib/errors-utils.js

@ -14,7 +14,7 @@ const lib_errors = {
processError: function(e) {
const errorMsg = this.extractJqxhrErrorMsg(e)
// Redirect to sign in page if authentication error
if (errorMsg == 'Invalid JSON Web Token' || errorMsg == 'Missing JSON Web Token') {
if (errorMsg === 'Invalid JSON Web Token' || errorMsg === 'Missing JSON Web Token') {
lib_auth.logout()
} else {
lib_msg.displayErrors(errorMsg)

2
static/admin/lib/format-utils.js

@ -52,7 +52,7 @@ const lib_fmt = {
* Format a unix timestamp into a readable date/hour
*/
formatUnixTs: function(ts) {
if (ts == null || ts == 0)
if (ts == null || ts === 0)
return '-'
let tmpDate = new Date(ts*1000),

2
test/lib/bitcoin/addresses-helper-test.js

@ -149,7 +149,7 @@ describe('AddressesHelper', function() {
const targetSig = Buffer.from(stc[1], 'hex')
const expectedResult = stc[2]
const sig = btcMessage.sign(msg, prefix, privKey, true)
const sig = btcMessage.sign(msg, privKey, true, prefix)
// Check that library returns valid result
assert((sig.compare(targetSig) == 0) == expectedResult)

38
test/lib/bitcoin/hd-accounts-helper-test.js

@ -19,6 +19,8 @@ const XPUB = 'tpubDDDAe7GgFT4fzEzKwWVA4BWo8fiJXQeGEYDTexzo2w6CK1iDoLPYkpEisXo623
const YPUB = 'upub5ELkCsSF68UnAZE7zF9CDztvHeBJiAAhwa4VxEFzZ1CfQRbpy93mkBbUZsqYVpoeEHFwY3fGh9bfftH79ZwbhjUEUBAxQj551TMxVyny4UX'
const ZPUB = 'vpub5ZB1WY7AEp2G1rREpbvpS5zRTcKkenACrgaijd9sw1aYTXR4DoDLNFFcb5o8VjTZdvNkHXFq9oxDZAtfsGMcVy9qLWsNzdtZHBRbtXe87LB'
const POSTMIX_ZPUB = 'vpub5Y6cjg7GbwSLRu33XB76n3EoJZscmYSVEToLSMqD6ugAcm4rof8E9yvDiaFfhGEuyL95P9VD4A9W3JrBTZhzWSXiRyYvWFnUBAZc67X32wh'
const BIP44_VECTORS = [
[0, 0, 'mmZ5FRccGAkwfKme4JkrsmurnimDLdfmNL'],
[0, 1, 'n3yomLicyrSULiNWFKHsK8erntSpJZEPV6'],
@ -61,6 +63,27 @@ const BIP84_VECTORS = [
[1, 4, 'tb1qjrnw8u2pvspm6hq3aa83ff93wevq2zyxqczewy']
]
const POSTMIX_VECTORS = [
[1, 0, 'tb1qv3laps2vues6nh9fkxpds3wxd0cttd9jnr0772'],
[1, 1, 'tb1qz538rwwchv2unf97g4pugv3wjwxxjaypnwz8sk'],
[1, 2, 'tb1qdm3hfvw3knzujxx24g05e30kpe7vk0ez3dk0h8'],
[1, 3, 'tb1qxn4jgg5hgl3eggvt4alvraladpwq9pj30fy5ze'],
[1, 4, 'tb1qw2ghyxhqv5ysyehq9p9xwux4zqaf0mcwm29agh'],
[1, 0, 'mpgLz1YXDU9buy7Zn8w9w9mJtrGghiXotH'],
[1, 1, 'mhShkJxHHgzJd2WcqeaKL4spqBMe1wcaK5'],
[1, 2, 'mqdH74foDiN8hV2mmFSHnceCm7vgErd4A2'],
[1, 3, 'mkLm7vUy1rij3YicskkQJxGovnGDG6G2oj'],
[1, 4, 'mqxjZfjdSdUmecTVALzhoQBPFRNvLViMBr'],
[1, 0, '2N5UxwLfWexxHDm5MKHoyitRLWEK8x25tiA'],
[1, 1, '2N8wnnGoJujWGrM5YLs1nC1TFuszx2vJVA9'],
[1, 2, '2NA6Ja6PM6YMuQpSQdeWofKRV9pcBbz4aii'],
[1, 3, '2NFLd63BqGzh5BtfxobuU4dpoThg9sxMPth'],
[1, 4, '2NEeziC2dc3nbf9k3fyUWBzLWbn4MTrR2mm']
]
const HD_TYPES_VECTORS = [
// unlocked
[0, hdaHelper.BIP44, false],
@ -74,7 +97,7 @@ const HD_TYPES_VECTORS = [
describe('HdAccountsHelper', function() {
describe('isXpub()', function() {
it('should successfully detect a XPUB', function() {
assert(hdaHelper.isXpub(XPUB))
@ -117,7 +140,7 @@ describe('HdAccountsHelper', function() {
const ret = hdaHelper.classify(v[0])
assert(ret.type == v[1])
assert(ret.locked == v[2])
}
}
})
})
@ -127,7 +150,7 @@ describe('HdAccountsHelper', function() {
for (const v of HD_TYPES_VECTORS) {
const ret = hdaHelper.makeType(v[1], v[2])
assert(ret == v[0])
}
}
})
})
@ -153,6 +176,15 @@ describe('HdAccountsHelper', function() {
assert(addresses[0].address == v[2])
}
})
it('should successfully derive additional change address types for postmix account', async () => {
const addresses = await hdaHelper.deriveAddresses(POSTMIX_ZPUB, 1, [0, 1, 2, 3, 4], hdaHelper.BIP84)
POSTMIX_VECTORS.forEach((vector) => {
assert(addresses.find((addr) => addr.index === vector[1]))
assert(addresses.find((addr) => addr.address === vector[2]))
})
})
})

10
tracker/block-worker.js

@ -61,22 +61,22 @@ async function processMessage(msg) {
try {
switch(msg.op) {
case OP_INIT:
if (status != IDLE)
if (status !== IDLE)
throw 'Operation not allowed'
res = await initBlock(msg.header)
break
case OP_PROCESS_OUTPUTS:
if (status != INITIALIZED)
if (status !== INITIALIZED)
throw 'Operation not allowed'
res = await processOutputs()
break
case OP_PROCESS_INPUTS:
if (status != OUTPUTS_PROCESSED)
if (status !== OUTPUTS_PROCESSED)
throw 'Operation not allowed'
res = await processInputs()
break
case OP_CONFIRM:
if (status != INPUTS_PROCESSED)
if (status !== INPUTS_PROCESSED)
throw 'Operation not allowed'
res = await confirmTransactions(msg.blockId)
break
@ -130,7 +130,7 @@ async function processInputs() {
/**
* Confirm the transactions
* @param {integer} blockId - id of the block in db
* @param {number} blockId - id of the block in db
*/
async function confirmTransactions(blockId) {
status = TXS_CONFIRMED

16
tracker/block.js

@ -44,7 +44,7 @@ class Block extends TransactionsBundle {
* @dev This method isn't used anymore.
* It has been replaced by a parallel processing of blocks.
* (see blocks-processor and block-worker)
* @returns {Promise - object[]} returns an array of transactions to be broadcast
* @returns {Promise<object[]>} returns an array of transactions to be broadcast
*/
async processBlock() {
Logger.info('Tracker : Beginning to process new block.')
@ -77,7 +77,7 @@ class Block extends TransactionsBundle {
/**
* Process the transaction outputs
* @returns {Promise - object[]} returns an array of transactions to be broadcast
* @returns {Promise<object[]>} returns an array of transactions to be broadcast
*/
async processOutputs() {
const txsForBroadcast = new Set()
@ -93,7 +93,7 @@ class Block extends TransactionsBundle {
/**
* Process the transaction inputs
* @returns {Promise - object[]} returns an array of transactions to be broadcast
* @returns {Promise<object[]>} returns an array of transactions to be broadcast
*/
async processInputs() {
const txsForBroadcast = new Set()
@ -109,7 +109,7 @@ class Block extends TransactionsBundle {
/**
* Store the block in db
* @returns {Promise - int} returns the id of the block
* @returns {Promise<number>} returns the id of the block
*/
async registerBlock() {
const prevBlock = await db.getBlockByHash(this.header.previousblockhash)
@ -130,8 +130,8 @@ class Block extends TransactionsBundle {
/**
* Confirm the transactions in db
* @param {Set} txs - set of transactions stored in db
* @param {int} blockId - id of the block
* r@returns {Promise}
* @param {number} blockId - id of the block
* @returns {Promise}
*/
async confirmTransactions(txs, blockId) {
const txids = txs.map(t => t.getId())
@ -141,8 +141,8 @@ class Block extends TransactionsBundle {
/**
* Register the block header
* @param {int} prevBlockID - id of previous block
* @returns {Promise}
* @param {number} prevBlockID - id of previous block
* @returns {Promise<number>}
*/
async checkBlockHeader(prevBlockID) {
Logger.info('Tracker : Beginning to process new block header.')

32
tracker/blockchain-processor.js

@ -5,8 +5,8 @@
'use strict'
const _ = require('lodash')
const zmq = require('zeromq')
const Sema = require('async-sema')
const zmq = require('zeromq/v5-compat')
const { Sema } = require('async-sema')
const util = require('../lib/util')
const Logger = require('../lib/logger')
const db = require('../lib/db/mysql-db-wrapper')
@ -43,7 +43,7 @@ class BlockchainProcessor {
/**
* Start processing the blockchain
* @returns {Promise}
* @returns {Promise<void>}
*/
async start() {
await this.catchup()
@ -57,14 +57,14 @@ class BlockchainProcessor {
/**
* Tracker process startup
* @returns {Promise}
* @returns {Promise<void>}
*/
async catchup() {
const [highest, info] = await Promise.all([db.getHighestBlock(), this.client.getblockchaininfo()])
const daemonNbHeaders = info.headers
// Consider that we are in IBD mode if Dojo is far in the past (> 13,000 blocks)
this.isIBD = (highest.blockHeight < 655000) || (highest.blockHeight < daemonNbHeaders - 13000)
this.isIBD = (highest.blockHeight < 681000) || (highest.blockHeight < daemonNbHeaders - 13000)
if (this.isIBD)
return this.catchupIBDMode()
@ -78,7 +78,7 @@ class BlockchainProcessor {
* 2. Pull all block headers after database last known height
* 3. Process those block headers
*
* @returns {Promise}
* @returns {Promise<void>}
*/
async catchupIBDMode() {
try {
@ -93,7 +93,7 @@ class BlockchainProcessor {
let prevBlockId = highest.blockID
// If no header or block loaded by bitcoind => try later
if (daemonNbHeaders == 0 || daemonNbBlocks == 0) {
if (daemonNbHeaders === 0 || daemonNbBlocks === 0) {
Logger.info('Tracker : New attempt scheduled in 30s (waiting for block headers)')
return util.delay(30000).then(() => {
return this.catchupIBDMode()
@ -148,7 +148,7 @@ class BlockchainProcessor {
* 2. Pull all block headers after database last known height
* 3. Process those block headers
*
* @returns {Promise}
* @returns {Promise<void>}
*/
async catchupNormalMode() {
try {
@ -159,7 +159,7 @@ class BlockchainProcessor {
const daemonNbBlocks = info.blocks
if (highest == null) return null
if (daemonNbBlocks == highest.blockHeight) return null
if (daemonNbBlocks === highest.blockHeight) return null
const blockRange = _.range(highest.blockHeight, daemonNbBlocks + 1)
@ -225,7 +225,7 @@ class BlockchainProcessor {
* block confirmation.
*
* @param {Buffer} buf - block
* @returns {Promise}
* @returns {Promise<void>}
*/
async onBlockHash(buf) {
try {
@ -298,8 +298,8 @@ class BlockchainProcessor {
/**
* Cancel confirmation of transactions
* and delete blocks after a given height
* @param {integer} height - height of last block maintained
* @returns {Promise}
* @param {number} height - height of last block maintained
* @returns {Promise<void>}
*/
async rewind(height) {
// Retrieve transactions confirmed in reorg'd blocks
@ -317,8 +317,8 @@ class BlockchainProcessor {
/**
* Rescan a range of blocks
* @param {integer} fromHeight - height of first block
* @param {integer} toHeight - height of last block
* @param {number} fromHeight - height of first block
* @param {number} toHeight - height of last block
* @returns {Promise}
*/
async rescanBlocks(fromHeight, toHeight) {
@ -356,7 +356,7 @@ class BlockchainProcessor {
/**
* Process a range of blocks
* @param {int[]} heights - a range of block heights
* @param {number[]} heights - a range of block heights
*/
async processBlockRange(heights) {
const chunks = util.splitList(heights, blocksProcessor.nbWorkers)
@ -373,7 +373,7 @@ class BlockchainProcessor {
/**
* Process a block header
* @param {object} header - block header
* @param {int} prevBlockID - id of previous block
* @param {number} prevBlockID - id of previous block
* @returns {Promise}
*/
async processBlockHeader(header, prevBlockID) {

10
tracker/blocks-processor.js

@ -5,7 +5,7 @@
'use strict'
const os = require('os')
const Sema = require('async-sema')
const { Sema } = require('async-sema')
const { Worker } = require('worker_threads')
const Logger = require('../lib/logger')
const util = require('../lib/util')
@ -90,11 +90,11 @@ async function processWorkerMessage(msg) {
if (!msg.status) {
Logger.error(msg.res, 'Tracker : processWorkerMessage()')
} else if (msg.op == blockWorker.OP_CONFIRM) {
} else if (msg.op === blockWorker.OP_CONFIRM) {
txsForBroadcast = txsForBroadcast.concat(msg.res)
}
if (nbTasksCompleted == nbTasksEnqueued) {
if (nbTasksCompleted === nbTasksEnqueued) {
switch (msg.op) {
case blockWorker.OP_INIT:
// Process the transaction outputs
@ -139,7 +139,7 @@ async function processWorkerMessage(msg) {
/**
* Execute an operation processing a block
* @param {integer} op - operation
* @param {number} op - operation
* @param {*} args
*/
function processTask(op, args) {
@ -204,7 +204,7 @@ function notifyBlock(header) {
/**
* Store a block in db
* @param {object} header - block header
* @returns {Promise - int} returns the id of the block
* @returns {Promise<number>} returns the id of the block
*/
async function registerBlock(header) {
const prevBlock = await dbProcessor.getBlockByHash(header.previousblockhash)

64
tracker/mempool-processor.js

@ -5,7 +5,7 @@
'use strict'
const _ = require('lodash')
const zmq = require('zeromq')
const zmq = require('zeromq/v5-compat')
const bitcoin = require('bitcoinjs-lib')
const util = require('../lib/util')
const Logger = require('../lib/logger')
@ -47,7 +47,7 @@ class MempoolProcessor {
/**
* Start processing the mempool
* @returns {Promise}
* @returns {Promise<void>}
*/
async start() {
this.checkUnconfirmedId = setInterval(
@ -143,7 +143,7 @@ class MempoolProcessor {
/**
* Process transactions from the mempool buffer
* @returns {Promise}
* @returns {Promise<void>}
*/
async processMempool() {
// Refresh the isActive flag
@ -181,7 +181,7 @@ class MempoolProcessor {
/**
* On reception of a new transaction from bitcoind mempool
* @param {Buffer} buf - transaction
* @returns {Promise}
* @returns {Promise<void>}
*/
async onTx(buf) {
if (this.isActive) {
@ -201,7 +201,7 @@ class MempoolProcessor {
/**
* On reception of a new transaction from /pushtx
* @param {Buffer} buf - transaction
* @returns {Promise}
* @returns {Promise<void>}
*/
async onPushTx(buf) {
try {
@ -249,7 +249,7 @@ class MempoolProcessor {
/**
* Check unconfirmed transactions
* @returns {Promise}
* @returns {Promise<void>}
*/
async checkUnconfirmed() {
const t0 = Date.now()
@ -259,40 +259,46 @@ class MempoolProcessor {
const unconfirmedTxs = await db.getUnconfirmedTransactions()
if (unconfirmedTxs.length > 0) {
await util.parallelCall(unconfirmedTxs, tx => {
try {
return this.client.getrawtransaction( { txid: tx.txnTxid, verbose: true })
.then(async rtx => {
if (!rtx.blockhash) return null
// Transaction is confirmed
const block = await db.getBlockByHash(rtx.blockhash)
if (block && block.blockID) {
Logger.info(`Tracker : Marking TXID ${tx.txnTxid} confirmed`)
return db.confirmTransactions([tx.txnTxid], block.blockID)
const unconfirmedTxLists = util.splitList(unconfirmedTxs, 10)
await util.seriesCall(unconfirmedTxLists, async (txList) => {
return await util.parallelCall(txList, tx => {
try {
return this.client.getrawtransaction( { txid: tx.txnTxid, verbose: true })
.then(async rtx => {
if (!rtx.blockhash) return null
// Transaction is confirmed
const block = await db.getBlockByHash(rtx.blockhash)
if (block && block.blockID) {
Logger.info(`Tracker : Marking TXID ${tx.txnTxid} confirmed`)
return db.confirmTransactions([tx.txnTxid], block.blockID)
}
},
(e) => {
Logger.error(e, 'Tracker : MempoolProcessor.checkUnconfirmed()')
// Transaction not in mempool. Update LRU cache and database
TransactionsBundle.cache.del(tx.txnTxid)
// TODO: Notify clients of orphaned transaction
return db.deleteTransaction(tx.txnTxid)
}
},
() => {
// Transaction not in mempool. Update LRU cache and database
TransactionsBundle.cache.del(tx.txnTxid)
// TODO: Notify clients of orphaned transaction
return db.deleteTransaction(tx.txnTxid)
}
)
} catch(e) {
Logger.error(e, 'Tracker : MempoolProcessor.checkUnconfirmed()')
}
)
} catch(e) {
Logger.error(e, 'Tracker : MempoolProcessor.checkUnconfirmed()')
}
})
})
}
// Logs
const ntx = unconfirmedTxs.length
const dt = ((Date.now() - t0) / 1000).toFixed(1)
const per = (ntx == 0) ? 0 : ((Date.now() - t0) / ntx).toFixed(0)
const per = (ntx === 0) ? 0 : ((Date.now() - t0) / ntx).toFixed(0)
Logger.info(`Tracker : Finished processing unconfirmed transactions ${dt}s, ${ntx} tx, ${per}ms/tx`)
}
/**
* Sets the isActive flag
* @private
*/
async _refreshActiveStatus() {
// Get highest header in the blockchain
@ -300,7 +306,7 @@ class MempoolProcessor {
const [highestBlock, info] = await Promise.all([db.getHighestBlock(), this.client.getblockchaininfo()])
const highestHeader = info.headers
if (highestBlock == null || highestBlock.blockHeight == 0) {
if (highestBlock == null || highestBlock.blockHeight === 0) {
this.isActive = false
return
}

30
tracker/tracker.js

@ -4,11 +4,12 @@
*/
'use strict'
const zmq = require('zeromq')
const zmq = require('zeromq/v5-compat')
const network = require('../lib/bitcoin/network')
const keys = require('../keys')[network.key]
const BlockchainProcessor = require('./blockchain-processor')
const MempoolProcessor = require('./mempool-processor')
const util = require('../lib/util')
/**
@ -22,23 +23,28 @@ class Tracker {
constructor() {
// Notification socket for client events
this.notifSock = zmq.socket('pub')
this.notifSock.bindSync(`tcp://127.0.0.1:${keys.ports.tracker}`)
// Initialize the blockchain processor
// and the mempool buffer
this.blockchainProcessor = new BlockchainProcessor(this.notifSock)
this.mempoolProcessor = new MempoolProcessor(this.notifSock)
this.notifSock.bind(`tcp://127.0.0.1:${keys.ports.tracker}`, () => {
// Initialize the blockchain processor
// and the mempool buffer
this.initialized = true
this.blockchainProcessor = new BlockchainProcessor(this.notifSock)
this.mempoolProcessor = new MempoolProcessor(this.notifSock)
})
}
/**
* Start the tracker
* @returns {Promise}
* @returns {Promise<void>}
*/
async start() {
this.startupTimeout = setTimeout(async function() {
await this.blockchainProcessor.start()
await this.mempoolProcessor.start()
}.bind(this), 1500)
if (!this.initialized) {
await util.delay(1000)
return this.start()
}
await this.blockchainProcessor.start()
await this.mempoolProcessor.start()
}
/**

19
tracker/transaction.js

@ -96,7 +96,7 @@ class Transaction {
// Check if we find some inputs of interest
const results = await db.getOutputSpends(spends)
if (results.length == 0)
if (results.length === 0)
return null
// Flag the transaction for broadcast
@ -122,7 +122,7 @@ class Transaction {
})
// Detect potential double spends
if (r.spendingTxnID !== null && r.spendingTxnID != this.storedTxnID) {
if (r.spendingTxnID !== null && r.spendingTxnID !== this.storedTxnID) {
Logger.info(`Tracker : DOUBLE SPEND of ${r.txnTxid}-${r.outIndex} by ${this.txid}!`)
// Delete the existing transaction that has been double-spent:
// since the deepest block keeps its transactions, this will
@ -190,7 +190,7 @@ class Transaction {
const aHdAcctAddr = await this._processOutputsHdAccounts(result.hd, indexedOutputs)
fundedAddresses = fundedAddresses.concat(aHdAcctAddr)
if (fundedAddresses.length == 0)
if (fundedAddresses.length === 0)
return null
// Flag the transaction for broadcast
@ -219,7 +219,7 @@ class Transaction {
* Process outputs sending to tracked loose addresses
* @param {object[]} addresses - array of address objects
* @param {object} indexedOutputs - outputs indexed by address
* @returns {Promise - object[]} return an array of funded addresses
* @returns {Promise<object[]>} return an array of funded addresses
* {addrID: ..., outIndex: ..., outAmount: ..., outScript: ...}
*/
async _processOutputsLooseAddresses(addresses, indexedOutputs) {
@ -247,7 +247,7 @@ class Transaction {
* Process outputs sending to tracked hd accounts
* @param {object[]} hdAccounts - array of hd account objects
* @param {object} indexedOutputs - outputs indexed by address
* @returns {Promise - object[]} return an array of funded addresses
* @returns {Promise<object[]>} return an array of funded addresses
* {addrID: ..., outIndex: ..., outAmount: ..., outScript: ...}
*/
async _processOutputsHdAccounts(hdAccounts, indexedOutputs) {
@ -297,7 +297,7 @@ class Transaction {
* @param {string} xpub
* @param {object} hdAccount - hd account object
* @param {object} indexedOutputs - outputs indexed by address
* @returns {Promise - object[]} returns an array of the new addresses used
* @returns {Promise<object[]>} returns an array of the new addresses used
*/
async _deriveNewAddresses(xpub, hdAccount, indexedOutputs) {
const hdType = hdAccount.hdType
@ -317,10 +317,10 @@ class Transaction {
for (let chain of [0,1]) {
// Get addresses for this account that are on this chain
const chainAddresses = _.filter(hdAccount.addresses, v => {
return v.hdAddrChain == chain
return v.hdAddrChain === chain
})
if (chainAddresses.length == 0)
if (chainAddresses.length === 0)
continue
// Get the maximum used address on this chain
@ -365,7 +365,8 @@ class Transaction {
const indices = _.range(minIdx, maxIdx)
const derived = await hdaHelper.deriveAddresses(xpub, chain, indices, hdType)
Array.prototype.push.apply(newAddresses, derived)
newAddresses.push(...derived)
Logger.info(`Tracker : Derived hdID(${hdAccount.hdID}) M/${chain}/${indices.join(',')}`)

19
tracker/transactions-bundle.js

@ -18,7 +18,8 @@ class TransactionsBundle {
/**
* Constructor
* @param {object[]} txs - array of bitcoin transaction objects
* @constructor
* @param {object[]=} txs - array of bitcoin transaction objects
*/
constructor(txs) {
// List of transactions
@ -52,7 +53,7 @@ class TransactionsBundle {
/**
* Get the size of the bundle
* @returns {integer} return the number of transactions stored in the bundle
* @returns {number} return the number of transactions stored in the bundle
*/
size() {
return this.transactions.length
@ -61,7 +62,7 @@ class TransactionsBundle {
/**
* Find the transactions of interest
* based on theirs inputs
* @returns {object[]} returns an array of transactions objects
* @returns {Promise<object[]>} returns an array of transactions objects
*/
async prefilterByInputs() {
// Process transactions by slices of 5000 transactions
@ -74,7 +75,7 @@ class TransactionsBundle {
/**
* Find the transactions of interest
* based on theirs outputs
* @returns {object[]} returns an array of transactions objects
* @returns {Promise<object[]>} returns an array of transactions objects
*/
async prefilterByOutputs() {
// Process transactions by slices of 5000 transactions
@ -88,7 +89,7 @@ class TransactionsBundle {
* Find the transactions of interest
* based on theirs outputs (internal implementation)
* @params {object[]} txs - array of transactions objects
* @returns {object[]} returns an array of transactions objects
* @returns {Promise<object[]>} returns an array of transactions objects
*/
async _prefilterByOutputs(txs) {
let addresses = []
@ -122,7 +123,7 @@ class TransactionsBundle {
const idxTxs = indexedOutputs[key]
if (idxTxs) {
for (const idxTx of idxTxs)
if (filteredIdxTxs.indexOf(idxTx) == -1)
if (filteredIdxTxs.indexOf(idxTx) === -1)
filteredIdxTxs.push(idxTx)
}
}
@ -134,7 +135,7 @@ class TransactionsBundle {
* Find the transactions of interest
* based on theirs inputs (internal implementation)
* @params {object[]} txs - array of transactions objects
* @returns {object[]} returns an array of transactions objects
* @returns {Promise<object[]>} returns an array of transactions objects
*/
async _prefilterByInputs(txs) {
let inputs = []
@ -169,7 +170,7 @@ class TransactionsBundle {
const idxTxs = indexedInputs[key]
if (idxTxs) {
for (const idxTx of idxTxs)
if (filteredIdxTxs.indexOf(idxTx) == -1)
if (filteredIdxTxs.indexOf(idxTx) === -1)
filteredIdxTxs.push(idxTx)
}
}
@ -187,7 +188,7 @@ class TransactionsBundle {
* Additionally, the transaction comes in a block
* Orphaned transactions are deleted during the routine check
*/
TransactionsBundle.cache = LRU({
TransactionsBundle.cache = new LRU({
// Maximum number of txids to store in cache
max: 100000,
// Function used to compute length of item

Loading…
Cancel
Save