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 ## ## Releases ##
- [v1.11.0](#1_11_0)
- [v1.10.1](#1_10_1) - [v1.10.1](#1_10_1)
- [v1.10.0](#1_10_0) - [v1.10.0](#1_10_0)
- [v1.9.0](#1_9_0) - [v1.9.0](#1_9_0)
@ -17,6 +18,28 @@
- [v1.2.0](#1_2_0) - [v1.2.0](#1_2_0)
- [v1.1.0](#1_1_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"/> <a name="1_10_1"/>
## Samourai Dojo v1.10.1 ## ## Samourai Dojo v1.10.1 ##

4
accounts/api-helper.js

@ -70,7 +70,9 @@ class ApiHelper {
item = item.toLowerCase() item = item.toLowerCase()
ret.addAddress(item, false) ret.addAddress(item, false)
} }
} catch(e) {} } catch(e) {
Logger.error(e, 'API : ApiHelper.parseEntities() : Invalid arguments')
}
} }
return ret 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 authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server') 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), authMgr.checkAuthentication.bind(authMgr),
this.getFees.bind(this), this.getFees.bind(this),
) )
this.httpServer.app.post(
'/fees',
authMgr.checkAuthentication.bind(authMgr),
this.getFees.bind(this),
)
// Refresh the network fees // Refresh the network fees
rpcFees.refresh() 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 HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper') 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 HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper') 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 }) const urlencodedParser = bodyParser.urlencoded({ extended: true })
this.httpServer.app.get( this.httpServer.app.get(
'/multiaddr', '/multiaddr',
authMgr.checkAuthentication.bind(authMgr), authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper), apiHelper.validateEntitiesParams.bind(apiHelper),
this.getMultiaddr.bind(this), this.getMultiaddr.bind(this),
HttpServer.sendAuthError
) )
this.httpServer.app.post( this.httpServer.app.post(
'/multiaddr', '/multiaddr',
urlencodedParser, urlencodedParser,
authMgr.checkAuthentication.bind(authMgr), authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper), apiHelper.validateEntitiesParams.bind(apiHelper),
this.postMultiaddr.bind(this), this.postMultiaddr.bind(this),
HttpServer.sendAuthError
) )
} }
@ -57,16 +59,19 @@ class MultiaddrRestApi {
// Check request params // Check request params
if (!apiHelper.checkEntitiesParams(req.query)) if (!apiHelper.checkEntitiesParams(req.query))
return HttpServer.sendError(res, errors.multiaddr.NOACT) return HttpServer.sendError(res, errors.multiaddr.NOACT)
//return HttpServer.sendError(res, '')
// Parse params // Parse params
const entities = apiHelper.parseEntitiesParams(req.query) 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( const result = await walletService.getWalletInfo(
entities.active, entities.active,
entities.legacy, entities.legacy,
entities.bip49, entities.bip49,
entities.bip84, entities.bip84,
entities.pubkey entities.pubkey
) )
const ret = JSON.stringify(result, null, 2) const ret = JSON.stringify(result, null, 2)
@ -78,7 +83,7 @@ class MultiaddrRestApi {
} finally { } finally {
if (debugApi) { if (debugApi) {
const strParams = const strParams =
`${req.query.active ? req.query.active : ''} \ `${req.query.active ? req.query.active : ''} \
${req.query.new ? req.query.new : ''} \ ${req.query.new ? req.query.new : ''} \
${req.query.pubkey ? req.query.pubkey : ''} \ ${req.query.pubkey ? req.query.pubkey : ''} \
${req.query.bip49 ? req.query.bip49 : ''} \ ${req.query.bip49 ? req.query.bip49 : ''} \
@ -104,11 +109,11 @@ class MultiaddrRestApi {
const entities = apiHelper.parseEntitiesParams(req.body) const entities = apiHelper.parseEntitiesParams(req.body)
const result = await walletService.getWalletInfo( const result = await walletService.getWalletInfo(
entities.active, entities.active,
entities.legacy, entities.legacy,
entities.bip49, entities.bip49,
entities.bip84, entities.bip84,
entities.pubkey entities.pubkey
) )
HttpServer.sendOkDataOnly(res, result) HttpServer.sendOkDataOnly(res, result)
@ -119,7 +124,7 @@ class MultiaddrRestApi {
} finally { } finally {
if (debugApi) { if (debugApi) {
const strParams = const strParams =
`${req.body.active ? req.body.active : ''} \ `${req.body.active ? req.body.active : ''} \
${req.body.new ? req.body.new : ''} \ ${req.body.new ? req.body.new : ''} \
${req.body.pubkey ? req.body.pubkey : ''} \ ${req.body.pubkey ? req.body.pubkey : ''} \
${req.body.bip49 ? req.body.bip49 : ''} \ ${req.body.bip49 ? req.body.bip49 : ''} \

13
accounts/notifications-server.js

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

16
accounts/notifications-service.js

@ -14,7 +14,7 @@ const apiHelper = require('./api-helper')
const status = require('./status') const status = require('./status')
const authMgr = require('../lib/auth/authorizations-manager') 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 // Cache registering the most recent subscriptions received
// Used to filter multiple subscriptions sent by external apps. // Used to filter multiple subscriptions sent by external apps.
this.cacheSubs = LRU({ this.cacheSubs = new LRU({
// Maximum number of subscriptions to store in cache // Maximum number of subscriptions to store in cache
// Estimate: 1000 clients with an average of 5 subscriptions // Estimate: 1000 clients with an average of 5 subscriptions
max: 5000, max: 5000,
@ -80,7 +80,7 @@ class NotificationsService {
}) })
conn.on('message', msg => { conn.on('message', msg => {
if (msg.type == 'utf8') if (msg.type === 'utf8')
this._handleWSMessage(msg.utf8Data, conn) this._handleWSMessage(msg.utf8Data, conn)
else else
this._closeWSConnection(conn, true) this._closeWSConnection(conn, true)
@ -229,7 +229,7 @@ class NotificationsService {
/** /**
* Unsubscribe from a topic * Unsubscribe from a topic
* @param {string} topic - topic * @param {string} topic - topic
* @param {int} cid - client id * @param {number} cid - client id
*/ */
_unsub(topic, cid) { _unsub(topic, cid) {
if (!this.subs[topic]) if (!this.subs[topic])
@ -241,7 +241,7 @@ class NotificationsService {
this.subs[topic].splice(index, 1) this.subs[topic].splice(index, 1)
if (this.subs[topic].length == 0) { if (this.subs[topic].length === 0) {
delete this.subs[topic] delete this.subs[topic]
if (this.cachePubKeys.hasOwnProperty(topic)) if (this.cachePubKeys.hasOwnProperty(topic))
delete this.cachePubKeys[topic] delete this.cachePubKeys[topic]
@ -391,7 +391,7 @@ class NotificationsService {
for (let cid of this.subs[topic]) { for (let cid of this.subs[topic]) {
if (!clients[cid]) if (!clients[cid])
clients[cid] = [] clients[cid] = []
if (clients[cid].indexOf(topic) == -1) if (clients[cid].indexOf(topic) === -1)
clients[cid].push(topic) clients[cid].push(topic)
} }
} }
@ -429,7 +429,7 @@ class NotificationsService {
} }
// Move on if the custom transaction has no inputs or outputs // 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 continue
// Send custom transaction to client // Send custom transaction to client
@ -454,7 +454,7 @@ class NotificationsService {
/** /**
* Dispatch notification for an authentication error * Dispatch notification for an authentication error
* @param {string} err - error * @param {string} err - error
* @param {integer} cid - connection id * @param {number} cid - connection id
*/ */
notifyAuthError(err, cid) { notifyAuthError(err, cid) {
const data = { 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 HttpServer = require('../lib/http-server/http-server')
const status = require('./status') 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 * Get current status
* @returns {Promise - object} status object * @returns {Promise<object>} status object
*/ */
async getCurrent() { async getCurrent() {
const uptime = util.timePeriod((Date.now() - this.t0) / 1000, false) const uptime = util.timePeriod((Date.now() - this.t0) / 1000, false)
@ -49,8 +49,8 @@ class Status {
let indexerMaxHeight = null let indexerMaxHeight = null
let indexerUrl = null let indexerUrl = null
if (indexerType == 'third_party_explorer') { if (indexerType === 'third_party_explorer') {
indexerUrl = (network.key == 'bitcoin') indexerUrl = (network.key === 'bitcoin')
? keys.indexer.oxt ? keys.indexer.oxt
: keys.indexer.esplora : 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 apiHelper = require('./api-helper')
const keys = require('../keys')[network.key] 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 { try {
// Parse the entities passed as url params // Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.addr).addrs const entities = apiHelper.parseEntities(req.params.addr).addrs
if (entities.length == 0) if (entities.length === 0)
return HttpServer.sendError(res, errors.address.INVALID) return HttpServer.sendError(res, errors.address.INVALID)
const address = entities[0] const address = entities[0]
@ -133,7 +133,7 @@ class SupportRestApi {
try { try {
// Parse the entities passed as url params // Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.addr).addrs const entities = apiHelper.parseEntities(req.params.addr).addrs
if (entities.length == 0) if (entities.length === 0)
return HttpServer.sendError(res, errors.address.INVALID) return HttpServer.sendError(res, errors.address.INVALID)
const address = entities[0] const address = entities[0]
@ -162,7 +162,7 @@ class SupportRestApi {
try { try {
// Parse the entities passed as url params // Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0) if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID) return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0] const xpub = entities[0]
@ -174,7 +174,7 @@ class SupportRestApi {
const ret = this._formatXpubInfoResult(info) const ret = this._formatXpubInfoResult(info)
HttpServer.sendRawData(res, ret) HttpServer.sendRawData(res, ret)
} catch(e) { } catch(e) {
if(e == errors.db.ERROR_NO_HD_ACCOUNT) { if(e === errors.db.ERROR_NO_HD_ACCOUNT) {
const ret = this._formatXpubInfoResult(info) const ret = this._formatXpubInfoResult(info)
HttpServer.sendRawData(res, ret) HttpServer.sendRawData(res, ret)
} else { } else {
@ -210,7 +210,7 @@ class SupportRestApi {
try { try {
// Parse the entities passed as url params // Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0) if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID) return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0] const xpub = entities[0]
@ -226,10 +226,10 @@ class SupportRestApi {
await hdaService.rescan(xpub, gapLimit, startIndex) await hdaService.rescan(xpub, gapLimit, startIndex)
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} catch(e) { } catch(e) {
if (e == errors.db.ERROR_NO_HD_ACCOUNT) { if (e === errors.db.ERROR_NO_HD_ACCOUNT) {
ret.status = 'Error: Not tracking xpub' ret.status = 'Error: Not tracking xpub'
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) 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' ret.status = 'Error: Rescan in progress'
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
} else { } else {
@ -256,7 +256,7 @@ class SupportRestApi {
try { try {
// Parse the entities passed as url params // Parse the entities passed as url params
const entities = apiHelper.parseEntities(req.params.xpub).xpubs const entities = apiHelper.parseEntities(req.params.xpub).xpubs
if (entities.length == 0) if (entities.length === 0)
return HttpServer.sendError(res, errors.xpub.INVALID) return HttpServer.sendError(res, errors.xpub.INVALID)
const xpub = entities[0] 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 apiHelper = require('./api-helper')
const keys = require('../keys')[network.key] 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) const result = await walletService.getWalletTransactions(active, page, count)
if (excludeNullXfer) { if (excludeNullXfer) {
result.txs = result.txs.filter(tx => { 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 HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper') 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 authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server') const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper') 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 * Wallet API endpoints
@ -60,6 +60,10 @@ class WalletRestApi {
// Parse params // Parse params
const entities = apiHelper.parseEntitiesParams(req.query) const entities = apiHelper.parseEntitiesParams(req.query)
if (req.query.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo( const result = await walletService.getFullWalletInfo(
entities.active, entities.active,
entities.legacy, entities.legacy,
@ -102,6 +106,10 @@ class WalletRestApi {
// Parse params // Parse params
const entities = apiHelper.parseEntitiesParams(req.body) const entities = apiHelper.parseEntitiesParams(req.body)
if (req.body.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo( const result = await walletService.getFullWalletInfo(
entities.active, entities.active,
entities.legacy, entities.legacy,

16
accounts/xpub-rest-api.js

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

6
docker/my-dojo/.env

@ -10,12 +10,12 @@
COMPOSE_CONVERT_WINDOWS_PATHS=1 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_DB_VERSION_TAG=1.3.0
DOJO_BITCOIND_VERSION_TAG=1.12.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_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_EXPLORER_VERSION_TAG=1.7.0
DOJO_INDEXER_VERSION_TAG=1.3.0 DOJO_INDEXER_VERSION_TAG=1.3.0
DOJO_WHIRLPOOL_VERSION_TAG=1.4.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 && \ RUN set -ex && \
apt-get update && \ 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/* rm -rf /var/lib/apt/lists/*
# Build and install bitcoin binaries # 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 u+x /wait-for-it.sh && \
chmod g+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 EXPOSE 8333 9501 9502 28256
USER bitcoin USER bitcoin

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

@ -1,6 +1,9 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Generate RPC auth payload
BITCOIND_RPC_AUTH=$(./rpcauth.py $BITCOIND_RPC_USER $BITCOIND_RPC_PASSWORD)
echo "## Start bitcoind #############################" echo "## Start bitcoind #############################"
bitcoind_options=( bitcoind_options=(
@ -18,11 +21,10 @@ bitcoind_options=(
-proxy=$NET_DOJO_TOR_IPV4:9050 -proxy=$NET_DOJO_TOR_IPV4:9050
-rpcallowip=0.0.0.0/0 -rpcallowip=0.0.0.0/0
-rpcbind=$NET_DOJO_BITCOIND_IPV4 -rpcbind=$NET_DOJO_BITCOIND_IPV4
-rpcpassword=$BITCOIND_RPC_PASSWORD
-rpcport=28256 -rpcport=28256
-rpcthreads=$BITCOIND_RPC_THREADS -rpcthreads=$BITCOIND_RPC_THREADS
-rpcworkqueue=$BITCOIND_RPC_WORK_QUEUE -rpcworkqueue=$BITCOIND_RPC_WORK_QUEUE
-rpcuser=$BITCOIND_RPC_USER -rpcauth=$BITCOIND_RPC_AUTH
-server=1 -server=1
-txindex=1 -txindex=1
-zmqpubhashblock=tcp://0.0.0.0:9502 -zmqpubhashblock=tcp://0.0.0.0:9502
@ -32,7 +34,6 @@ bitcoind_options=(
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
bitcoind_options+=(-listen=1) bitcoind_options+=(-listen=1)
bitcoind_options+=(-bind="$NET_DOJO_BITCOIND_IPV4") 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)) bitcoind_options+=(-externalip=$(cat /var/lib/tor/hsv3bitcoind/hostname))
fi 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 # RPC Work queue size
# Type: integer # Type: integer
BITCOIND_RPC_WORK_QUEUE=16 BITCOIND_RPC_WORK_QUEUE=64
# Mempool expiry in hours # Mempool expiry in hours
# Defines how long transactions stay in your local mempool before expiring # 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 # 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' # This parameter is inactive if BITCOIND_INSTALL is set to 'off'
# Type: integer # 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 # Renewal of bitcoind onion address
if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
if [ "$BITCOIND_EPHEMERAL_HS" = "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 $( docker exec -it tor rm -rf /var/lib/tor/hsv3bitcoind ) &> /dev/null
fi fi
fi fi
@ -355,77 +354,35 @@ upgrade() {
# Display the onion addresses # Display the onion addresses
onion() { 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 " "
echo "WARNING: Do not share these onion addresses with anyone!" echo "WARNING: Do not share these onion addresses with anyone!"
echo " To allow another person to use this Dojo with their Samourai Wallet," 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 " you should share the QRCodes provided by the Maintenance Tool."
echo " " echo " "
if [ $version -eq 3 ]; then # V3 onion addresses
# V3 onion addresses V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname )
V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname ) echo "Dojo API and Maintenance Tool = $V3_ADDR"
echo "Dojo API and Maintenance Tool = $V3_ADDR" echo " "
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
else if [ "$EXPLORER_INSTALL" == "on" ]; then
# v2 onion addresses V3_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv3explorer/hostname )
V2_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv2dojo/hostname ) echo "Block Explorer = $V3_ADDR_EXPLORER"
echo "Dojo API and Maintenance Tool = $V2_ADDR"
echo " " echo " "
fi
if [ "$EXPLORER_INSTALL" == "on" ]; then if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
V2_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv2explorer/hostname ) V3_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv3whirlpool/hostname )
echo "Block Explorer = $V2_ADDR_EXPLORER" echo "Your private Whirlpool client (do not share) = $V3_ADDR_WHIRLPOOL"
echo " " echo " "
fi fi
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then if [ "$BITCOIND_INSTALL" == "on" ]; then
V2_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv2whirlpool/hostname ) if [ "$BITCOIND_LISTEN_MODE" == "on" ]; then
echo "Your private Whirlpool client (do not share) = $V2_ADDR_WHIRLPOOL" V3_ADDR_BTCD=$( docker exec -it tor cat /var/lib/tor/hsv3bitcoind/hostname )
echo "Your local bitcoind (do not share) = $V3_ADDR_BTCD"
echo " " echo " "
fi 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 fi
} }
@ -561,11 +518,7 @@ help() {
echo " Available options:" echo " Available options:"
echo " -n [VALUE] : display the last VALUE lines" echo " -n [VALUE] : display the last VALUE lines"
echo " " echo " "
echo " onion [version] Display the Tor onion addresses allowing your wallet to access your dojo." echo " onion 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 " " echo " "
echo " restart Restart your dojo." echo " restart Restart your dojo."
echo " " echo " "
@ -659,7 +612,7 @@ case "$subcommand" in
logs "$module" $numlines logs "$module" $numlines
;; ;;
onion ) onion )
onion "$@" onion
;; ;;
restart ) restart )
restart restart

2
docker/my-dojo/explorer/Dockerfile

@ -1,4 +1,4 @@
FROM node:12-alpine FROM node:14-alpine
ENV NODE_ENV production 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 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 explorerActive = 'oxt'
let explorerUrl = 'https://oxt.me' let explorerUrl = 'https://oxt.me'
let explorerPassword = '' let explorerPassword = ''
if (process.env.EXPLORER_INSTALL == 'on') { if (process.env.EXPLORER_INSTALL === 'on') {
try { try {
explorerUrl = fs.readFileSync('/var/lib/tor/hsv3explorer/hostname', 'utf8').replace('\n', '') explorerUrl = fs.readFileSync('/var/lib/tor/hsv3explorer/hostname', 'utf8').replace('\n', '')
explorerPassword = process.env.EXPLORER_KEY 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_HOME /var/lib/tor
ENV TOR_URL https://dist.torproject.org ENV TOR_URL https://dist.torproject.org
ENV TOR_MIRROR_URL https://tor.eff.org/dist 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_KS_URI hkp://keyserver.ubuntu.com:80
ENV TOR_GPG_KEY1 0xEB5A896A28988BF5 ENV TOR_GPG_KEY1 0xEB5A896A28988BF5
ENV TOR_GPG_KEY2 0xC218525819F78451 ENV TOR_GPG_KEY2 0xC218525819F78451
@ -11,8 +11,8 @@ ENV TOR_GPG_KEY3 0x21194EBB165733EA
ENV TOR_GPG_KEY4 0x6AFEE6D49E92B601 ENV TOR_GPG_KEY4 0x6AFEE6D49E92B601
ENV GOLANG_DL_URL https://dl.google.com/go ENV GOLANG_DL_URL https://dl.google.com/go
ENV GOLANG_ARCHIVE go1.16.4.linux-amd64.tar.gz ENV GOLANG_ARCHIVE go1.16.6.linux-amd64.tar.gz
ENV GOLANG_SHA256 7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59 ENV GOLANG_SHA256 be333ef18b3016e9d7cb7b1ff1fdb0cac800ca0be4cf2290fe613b3d069dfe0d
ENV OBFS4_URL https://github.com/Yawning/obfs4.git ENV OBFS4_URL https://github.com/Yawning/obfs4.git
ENV OBFS4_VERSION 0.0.11 ENV OBFS4_VERSION 0.0.11

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

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

2
docker/my-dojo/whirlpool/Dockerfile

@ -27,7 +27,7 @@ RUN set -ex && \
# Install Tor # Install Tor
ENV WHIRLPOOL_TOR_URL https://dist.torproject.org ENV WHIRLPOOL_TOR_URL https://dist.torproject.org
ENV WHIRLPOOL_TOR_MIRROR_URL https://tor.eff.org/dist 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_KS_URI hkp://keyserver.ubuntu.com:80
ENV WHIRLPOOL_TOR_GPG_KEY1 0xEB5A896A28988BF5 ENV WHIRLPOOL_TOR_GPG_KEY1 0xEB5A896A28988BF5
ENV WHIRLPOOL_TOR_GPG_KEY2 0xC218525819F78451 ENV WHIRLPOOL_TOR_GPG_KEY2 0xC218525819F78451

4
keys/index-example.js

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

6
lib/auth/authorizations-manager.js

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

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

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

38
lib/bitcoin/addresses-helper.js

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

12
lib/bitcoin/addresses-service.js

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

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

@ -4,7 +4,7 @@
*/ */
'use strict' 'use strict'
const _ = require('lodash') const util = require('../util')
const errors = require('../errors') const errors = require('../errors')
const Logger = require('../logger') const Logger = require('../logger')
const db = require('../db/mysql-db-wrapper') const db = require('../db/mysql-db-wrapper')
@ -29,7 +29,7 @@ class HDAccountsService {
/** /**
* Create a new hd account in db * Create a new hd account in db
* @param {string} xpub - xpub * @param {string} xpub - xpub
* @param {int} scheme - derivation scheme * @param {number} scheme - derivation scheme
* @returns {Promise} returns true if success, false otherwise * @returns {Promise} returns true if success, false otherwise
*/ */
async createHdAccount(xpub, scheme) { async createHdAccount(xpub, scheme) {
@ -37,8 +37,8 @@ class HDAccountsService {
await this.newHdAccount(xpub, scheme) await this.newHdAccount(xpub, scheme)
return true return true
} catch(e) { } catch(e) {
const isInvalidXpub = (e == errors.xpub.INVALID || e == errors.xpub.PRIVKEY) const isInvalidXpub = (e === errors.xpub.INVALID || e === errors.xpub.PRIVKEY)
const isLockedXpub = (e == errors.xpub.LOCKED) const isLockedXpub = (e === errors.xpub.LOCKED)
const err = (isInvalidXpub || isLockedXpub) ? e : errors.xpub.CREATE const err = (isInvalidXpub || isLockedXpub) ? e : errors.xpub.CREATE
Logger.error(e, 'HdAccountsService : createHdAccount()' + err) Logger.error(e, 'HdAccountsService : createHdAccount()' + err)
return Promise.reject(err) return Promise.reject(err)
@ -49,7 +49,7 @@ class HDAccountsService {
/** /**
* Restore a hd account in db * Restore a hd account in db
* @param {string} xpub - xpub * @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 * @param {bool} forceOverride - force override of scheme even if hd account is locked
* @returns {Promise} * @returns {Promise}
*/ */
@ -96,7 +96,7 @@ class HDAccountsService {
return hdaHelper.typeString(type) return hdaHelper.typeString(type)
} catch(e) { } 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) return Promise.reject(err)
} }
} }
@ -110,7 +110,7 @@ class HDAccountsService {
try { try {
await db.deleteHDAccount(xpub) await db.deleteHDAccount(xpub)
} catch(e) { } 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) return Promise.reject(err)
} }
} }
@ -134,17 +134,17 @@ class HDAccountsService {
let segwit = '' let segwit = ''
if (scheme == hdaHelper.BIP49) if (scheme === hdaHelper.BIP49)
segwit = ' SegWit (BIP49)' segwit = ' SegWit (BIP49)'
else if (scheme == hdaHelper.BIP84) else if (scheme === hdaHelper.BIP84)
segwit = ' SegWit (BIP84)' segwit = ' SegWit (BIP84)'
Logger.info(`HdAccountsService : Created HD Account: ${xpub}${segwit}`) Logger.info(`HdAccountsService : Created HD Account: ${xpub}${segwit}`)
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, _.range(gap.external), scheme) const externalPrm = hdaHelper.deriveAddresses(xpub, 0, util.range(0, gap.external), scheme)
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, _.range(gap.internal), 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) return db.addAddressesToHDAccount(xpub, addresses)
} }
@ -152,8 +152,8 @@ class HDAccountsService {
/** /**
* Rescan the blockchain for a hd account * Rescan the blockchain for a hd account
* @param {string} xpub - xpub * @param {string} xpub - xpub
* @param {integer} gapLimit - (optional) gap limit for derivation * @param {number=} gapLimit - (optional) gap limit for derivation
* @param {integer} startIndex - (optional) rescan shall start from this index * @param {number=} startIndex - (optional) rescan shall start from this index
* @returns {Promise} * @returns {Promise}
*/ */
async rescan(xpub, gapLimit, startIndex) { async rescan(xpub, gapLimit, startIndex) {
@ -165,7 +165,7 @@ class HDAccountsService {
await remote.importHDAccount(xpub, account.hdType, gapLimit, startIndex) await remote.importHDAccount(xpub, account.hdType, gapLimit, startIndex)
} catch(e) { } catch(e) {
return Promise.reject(e) return Promise.reject(e)
} }
} }
/** /**
@ -201,7 +201,7 @@ class HDAccountsService {
const info = hdaHelper.classify(account.hdType) const info = hdaHelper.classify(account.hdType)
// If this account is already known in the database, // If this account is already known in the database,
// check for a derivation scheme mismatch // check for a derivation scheme mismatch
if (info.type != scheme) { if (info.type !== scheme) {
if (info.locked && !forceOverride) { if (info.locked && !forceOverride) {
Logger.info(`HdAccountsService : Attempted override on locked account: ${xpub}`) Logger.info(`HdAccountsService : Attempted override on locked account: ${xpub}`)
return Promise.reject(errors.xpub.LOCKED) return Promise.reject(errors.xpub.LOCKED)
@ -224,7 +224,7 @@ class HDAccountsService {
* @param {string} address - address used to sign the message * @param {string} address - address used to sign the message
* @param {string} sig - signature of the message * @param {string} sig - signature of the message
* @param {string} msg - signed 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 * @returns {Promise} returns the xpub if signature is valid, otherwise returns an error
*/ */
async verifyXpubSignature(xpub, address, sig, msg, scheme) { async verifyXpubSignature(xpub, address, sig, msg, scheme) {
@ -242,16 +242,45 @@ class HDAccountsService {
if (!addrHelper.verifySignature(msg, sigAddress, sig)) if (!addrHelper.verifySignature(msg, sigAddress, sig))
return Promise.reject(errors.sig.INVSIG) return Promise.reject(errors.sig.INVSIG)
// Check that adresses match // Check that adresses match
if (address != expectedAddress) if (address !== expectedAddress)
return Promise.reject(errors.sig.INVADDR) return Promise.reject(errors.sig.INVADDR)
// Return the corresponding xpub // Return the corresponding xpub
return xpub return xpub
} catch(err) { } 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) 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() 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 * 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 * must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {bip32} chainNode - Parent bip32 used for derivation * @param {bip32} chainNode - Parent bip32 used for derivation
* @param {int} index - index to be derived * @param {number} index - index to be derived
* @param {int} type - type of derivation * @param {number} type - type of derivation
* @returns {Promise - object} returns an object {address: '...', chain: <int>, index: <int>} * @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>}
*/ */
async function deriveAddress(chain, chainNode, index, type) { async function deriveAddress(chain, chainNode, index, type) {
// Derive M/chain/index // Derive M/chain/index
@ -33,7 +33,7 @@ async function deriveAddress(chain, chainNode, index, type) {
const addr = { const addr = {
chain: chain, chain: chain,
index: index index: index,
} }
switch (type) { switch (type) {
@ -54,7 +54,7 @@ async function deriveAddress(chain, chainNode, index, type) {
/** /**
* Derives a set of addresses for an hd account * Derives a set of addresses for an hd account
* @param {object} msg - parameters used for the derivation * @param {object} msg - parameters used for the derivation
* @returns {Promise - object[]} * @returns {Promise<object[]>}
*/ */
async function deriveAddresses(msg) { async function deriveAddresses(msg) {
try { try {
@ -62,6 +62,7 @@ async function deriveAddresses(msg) {
const chain = msg.chain const chain = msg.chain
const indices = msg.indices const indices = msg.indices
const type = msg.type const type = msg.type
const isPostmixChange = msg.isPostmixChange
// Parse input as an HD Node. Throws if invalid // Parse input as an HD Node. Throws if invalid
const node = bitcoin.bip32.fromBase58(xpub, activeNet) const node = bitcoin.bip32.fromBase58(xpub, activeNet)
@ -76,6 +77,14 @@ async function deriveAddresses(msg) {
return deriveAddress(chain, chainNode, index, type) 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) const addresses = await Promise.all(promises)
// Send response to parent process // Send response to parent process

4
lib/bitcoind-rpc/fees.js

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

2
lib/bitcoind-rpc/headers.js

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

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

@ -4,7 +4,7 @@
*/ */
'use strict' 'use strict'
const zmq = require('zeromq') const zmq = require('zeromq/v5-compat')
const Logger = require('../logger') const Logger = require('../logger')
const util = require('../util') const util = require('../util')
const network = require('../bitcoin/network') 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 * Wrapper for bitcoind rpc client
*/ */
const createRpcClient = () => { const createRpcClient = (options = {}) => {
return new RPCClient({ return new RPCClient({
url: `http://${keys.bitcoind.rpc.host}`, url: `http://${keys.bitcoind.rpc.host}`,
port: keys.bitcoind.rpc.port, port: keys.bitcoind.rpc.port,
user: keys.bitcoind.rpc.user, 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 * @returns {boolean} returns true if message related to a connection error
*/ */
const isConnectionError = (err) => { const isConnectionError = (err) => {
if (typeof err != 'string') if (typeof err !== 'string')
return false return false
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') !== -1) const isTimeoutError = (err.indexOf('connect ETIMEDOUT') !== -1)

14
lib/bitcoind-rpc/transactions.js

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

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

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

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

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

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

@ -23,9 +23,9 @@ class BitcoindWrapper extends Wrapper {
* Constructor * Constructor
*/ */
constructor() { constructor() {
super(null, null) super('bitcoind', null)
// RPC client // 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
* @constructor
* @param {string} url
*/ */
constructor(url) { constructor(url) {
super(url, keys.indexer.socks5Proxy) super(url, keys.indexer.socks5Proxy)
@ -87,7 +89,7 @@ class EsploraWrapper extends Wrapper {
while (true) { while (true) {
const txids = await this._getTxsForAddress(address, lastSeenTxid) const txids = await this._getTxsForAddress(address, lastSeenTxid)
if (txids.length == 0) if (txids.length === 0)
// we have all the transactions // we have all the transactions
return ret return ret

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

@ -104,7 +104,7 @@ class LocalIndexerWrapper extends Wrapper {
}) })
// Send the requests // 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.sendBatch(commands)
: await this.client.sendRequests(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
* @constructor
* @param {string} url
*/ */
constructor(url) { constructor(url) {
super(url, null) super(url, null)

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

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

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

@ -17,7 +17,7 @@ const gap = keys.gap
let Sources let Sources
if (network.key == 'bitcoin') { if (network.key === 'bitcoin') {
Sources = require('./sources-mainnet') Sources = require('./sources-mainnet')
} else { } else {
Sources = require('./sources-testnet') Sources = require('./sources-testnet')
@ -64,8 +64,8 @@ class RemoteImporter {
* Import an HD account from remote sources * Import an HD account from remote sources
* @param {string} xpub - HD Account * @param {string} xpub - HD Account
* @param {string} type - type of HD Account * @param {string} type - type of HD Account
* @param {integer} gapLimit - (optional) gap limit for derivation * @param {number} gapLimit - (optional) gap limit for derivation
* @param {integer} startIndex - (optional) rescan shall start from this index * @param {number} startIndex - (optional) rescan shall start from this index
*/ */
async importHDAccount(xpub, type, gapLimit, startIndex) { async importHDAccount(xpub, type, gapLimit, startIndex) {
if (!hdaHelper.isValid(xpub)) if (!hdaHelper.isValid(xpub))
@ -96,9 +96,18 @@ class RemoteImporter {
startIndex = (startIndex == null) ? -1 : startIndex - 1 startIndex = (startIndex == null) ? -1 : startIndex - 1
try { try {
const results = await util.parallelCall(chains, chain => { let results
return this.xpubScan(xpub, chain, startIndex, startIndex, gaps[chain], type)
}) // 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 // Accumulate addresses and transactions from all chains
const txns = results.map(r => r.transactions).flat() const txns = results.map(r => r.transactions).flat()
@ -202,7 +211,7 @@ class RemoteImporter {
} }
if (gotTransactions) { 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 this.importing[xpub][keyStatus] = Object.keys(txids).length
// We must go deeper // We must go deeper
const result = await this.xpubScan(xpub, c, d, u, G, type, txids) 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]) const addresses = candidates.filter(c => !this.importing[c])
this.importing = addresses.reduce((m,a) => (m[a] = true, m), this.importing) 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(',')}`) Logger.info(`Importer : Importing ${addresses.join(',')}`)
@ -318,8 +327,8 @@ class RemoteImporter {
const blocks = await db.getBlocksByHashes(blocksHashes) const blocks = await db.getBlocksByHashes(blocksHashes)
return util.parallelCall(blocks, block => { return util.parallelCall(blocks, block => {
const filteredTxs = txs.filter(tx => (tx.block && tx.block.hash == block.blockHash)) const filteredTxs = txs.filter(tx => (tx.block && tx.block.hash === block.blockHash))
if (filteredTxs.length == 0) return if (filteredTxs.length === 0) return
const txids = filteredTxs.map(tx => tx.txid) const txids = filteredTxs.map(tx => tx.txid)
return db.confirmTransactions(txids, block.blockID) 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 * Initialize the external data source
*/ */
_initSource() { _initSource() {
if (keys.indexer.active == 'local_bitcoind') { if (keys.indexer.active === 'local_bitcoind') {
// If local bitcoind option is activated // If local bitcoind option is activated
// we'll use the local node as our unique source // we'll use the local node as our unique source
this.source = new BitcoindWrapper() this.source = new BitcoindWrapper()
Logger.info('Importer : Activated Bitcoind as the data source for imports') 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 // If local indexer option is activated
// we'll use the local indexer as our unique source // we'll use the local indexer as our unique source
this.source = new LocalIndexerWrapper() this.source = new LocalIndexerWrapper()
Logger.info('Importer : Activated local indexer as the data source for imports') 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 // If local rest indexer option is activated
// we'll use the local indexer as our unique source // we'll use the local indexer as our unique source
const uri = `http://${keys.indexer.localIndexer.host}:${keys.indexer.localIndexer.port}` 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 * Initialize the external data source
*/ */
_initSource() { _initSource() {
if (keys.indexer.active == 'local_bitcoind') { if (keys.indexer.active === 'local_bitcoind') {
// If local bitcoind option is activated // If local bitcoind option is activated
// we'll use the local node as our unique source // we'll use the local node as our unique source
this.source = new BitcoindWrapper() this.source = new BitcoindWrapper()
Logger.info('Importer : Activated Bitcoind as the data source for imports') 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 // If local indexer option is activated
// we'll use the local indexer as our unique source // we'll use the local indexer as our unique source
this.source = new LocalIndexerWrapper() this.source = new LocalIndexerWrapper()
Logger.info('Importer : Activated local indexer as the data source for imports') 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 // If local rest indexer option is activated
// we'll use the local indexer as our unique source // we'll use the local indexer as our unique source
const uri = `http://${keys.indexer.localIndexer.host}:${keys.indexer.localIndexer.port}` 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 ret.txids = result.txids
} catch(e) { } 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 { } finally {
return ret return ret
} }
@ -75,7 +75,7 @@ class Sources {
} }
} catch(e) { } 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 { } finally {
return ret return ret
} }

3
lib/remote-importer/wrapper.js

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

131
lib/util.js

@ -5,18 +5,22 @@
'use strict' 'use strict'
/** /**
* Class providing utility functions as static methods * @class Util
* @description Class providing utility functions as static methods
*/ */
class Util { class Util {
/** /**
* Constructor * Constructor
* @constructor
*/ */
constructor() {} constructor() {}
/** /**
* Serialize a series of asynchronous calls to a function * @description Serialize a series of asynchronous calls to a function over a list of objects
* over a list of objects * @param {Array<any>} list
* @param {function} fn
* @returns {Promise<Array<any>>}
*/ */
static async seriesCall(list, fn) { static async seriesCall(list, fn) {
const results = [] const results = []
@ -29,8 +33,10 @@ class Util {
} }
/** /**
* Execute parallel asynchronous calls to a function * @description Execute parallel asynchronous calls to a function over a list of objects
* over a list of objects * @param {Array<any>} list
* @param {function} fn
* @returns {Promise<Array<any>>}
*/ */
static parallelCall(list, fn) { static parallelCall(list, fn) {
const operations = list.map(item => { return fn(item) }) 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 => { 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) { static splitList(list, limit) {
const lists = [] 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) { static isHashStr(hash) {
const hexRegExp = new RegExp(/^[0-9a-f]*$/, 'i') 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) { 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) { static sum(arr) {
return arr.reduce((memo, val) => { return memo + val }, 0) 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) { static mean(arr) {
if (arr.length == 0) if (arr.length === 0)
return NaN return NaN
return sum(arr) / arr.length 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) { static cmpAsc(a, b) {
return 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) { static cmpDesc(a,b) {
return b - a 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) { static median(arr, sorted) {
if (arr.length == 0) return NaN if (arr.length === 0) return NaN
if (arr.length == 1) return arr[0] if (arr.length === 1) return arr[0]
if (!sorted) if (!sorted)
arr.sort(Util.cmpAsc) 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) { static mad(arr, sorted) {
const med = Util.median(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) { static quartiles(arr, sorted) {
const q = [NaN,NaN,NaN] const q = [NaN,NaN,NaN]
@ -156,10 +190,10 @@ class Util {
const mod4 = arr.length % 4 const mod4 = arr.length % 4
const n = Math.floor(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[0] = (arr[n-1] + 3 * arr[n]) / 4
q[2] = (3 * arr[3*n] + arr[3*n+1]) / 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[0] = (3 * arr[n] + arr[n+1]) / 4
q[2] = (arr[3*n+1] + 3 * arr[3*n+2]) / 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) { static percentile(arr, pct, sorted) {
if (arr.length < 2) return NaN 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) { static toMb(bytes) {
return +(bytes / Util.MB).toFixed(0) 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() { static unix() {
return (Date.now() / 1000) | 0 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) { static pad10(v) {
return (v < 10) ? `0${v}` : `${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) { static pad100(v) {
if (v < 10) return `00${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) { static pad1000(v) {
if (v < 10) return `000${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) { static leftPad(number, places, fill) {
number = Math.round(number) 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) { static timePeriod(period, milliseconds) {
milliseconds = !!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 { class AddressInfo {
/** /**
* Constructor * Constructor
* @param {object} address - bitcoin address * @constructor
* @param {string} address - bitcoin address
*/ */
constructor(address) { constructor(address) {
// Initializes properties // Initializes properties
@ -25,7 +27,7 @@ class AddressInfo {
this.nTx = 0 this.nTx = 0
this.unspentOutputs = [] this.unspentOutputs = []
this.tracked = false, this.tracked = false
this.type = 'untracked' this.type = 'untracked'
this.xpub = null this.xpub = null
this.path = null this.path = null
@ -34,8 +36,8 @@ class AddressInfo {
} }
/** /**
* Load information about the address * @description Load information about the address
* @returns {Promise} * @returns {Promise<void[]>}
*/ */
async loadInfo() { async loadInfo() {
return Promise.all([ return Promise.all([
@ -57,9 +59,8 @@ class AddressInfo {
} }
/** /**
* Load information about the address * @description Load information about the address (extended form)
* (extended form) * @returns {Promise<void[]>}
* @returns {Promise}
*/ */
async loadInfoExtended() { async loadInfoExtended() {
const res = await db.getHDAccountsByAddresses([this.address]) const res = await db.getHDAccountsByAddresses([this.address])
@ -75,7 +76,7 @@ class AddressInfo {
} }
for (let a of res.loose) { for (let a of res.loose) {
if (a.addrAddress == this.address) { if (a.addrAddress === this.address) {
this.tracked = true this.tracked = true
this.type = 'loose' this.type = 'loose'
break break
@ -86,9 +87,9 @@ class AddressInfo {
} }
/** /**
* Loads a partial list of transactions for this address * @description Loads a partial list of transactions for this address
* @param {integer} page - page index * @param {number} page - page index
* @param {integer} count - number of transactions per page * @param {number} count - number of transactions per page
* @returns {Promise} * @returns {Promise}
*/ */
async loadTransactions(page, count) { async loadTransactions(page, count) {
@ -96,8 +97,8 @@ class AddressInfo {
} }
/** /**
* Load the utxos associated to the address * @description Load the utxos associated to the address
* @returns {Promise - object[]} * @returns {Promise<object[]>}
*/ */
async loadUtxos() { async loadUtxos() {
this.unspentOutputs = [] 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} * @returns {object}
*/ */
toPojo() { toPojo() {
@ -135,8 +136,7 @@ class AddressInfo {
} }
/** /**
* Return a plain old js object with address properties * @description Return a plain old js object with address properties (extended version)
* (extended version)
* @returns {object} * @returns {object}
*/ */
toPojoExtended() { 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 { class HdAccountInfo {
/** /**
* Constructor * Constructor
* @param {object} xpub - xpub * @param {string} xpub - xpub
*/ */
constructor(xpub) { constructor(xpub) {
// Initializes properties // Initializes properties
@ -39,9 +40,8 @@ class HdAccountInfo {
} }
/** /**
* Ensure the hd account exists in database * @description Ensure the hd account exists in database. Otherwise, tries to import it with BIP44 derivation
* Otherwise, tries to import it with BIP44 derivation * @returns {Promise<number | null>} return the internal id of the hd account
* @returns {Promise - integer} return the internal id of the hd account
* or null if it doesn't exist * or null if it doesn't exist
*/ */
async ensureHdAccount() { async ensureHdAccount() {
@ -49,7 +49,7 @@ class HdAccountInfo {
const id = await db.getHDAccountId(this.xpub) const id = await db.getHDAccountId(this.xpub)
return id return id
} catch(e) { } catch(e) {
if (e == errors.db.ERROR_NO_HD_ACCOUNT) { if (e === errors.db.ERROR_NO_HD_ACCOUNT) {
try { try {
// Default to BIP44 import // Default to BIP44 import
return hdaService.restoreHdAccount(this.xpub, hdaHelper.BIP44) return hdaService.restoreHdAccount(this.xpub, hdaHelper.BIP44)
@ -62,8 +62,8 @@ class HdAccountInfo {
} }
/** /**
* Load information about the hd account * @description Load information about the hd account
* @returns {Promise} * @returns {Promise<boolean>}
*/ */
async loadInfo() { async loadInfo() {
try { try {
@ -93,29 +93,41 @@ class HdAccountInfo {
this.depth = node[2].depth this.depth = node[2].depth
} }
/**
* @returns {Promise<void>}
*/
async _loadBalance() { async _loadBalance() {
this.finalBalance = await db.getHDAccountBalance(this.xpub) this.finalBalance = await db.getHDAccountBalance(this.xpub)
} }
/**
* @returns {Promise<void>}
*/
async _loadUnusedIndices() { async _loadUnusedIndices() {
const unusedIdx = await db.getHDAccountNextUnusedIndices(this.xpub) const unusedIdx = await db.getHDAccountNextUnusedIndices(this.xpub)
this.accountIndex = unusedIdx[0] this.accountIndex = unusedIdx[0]
this.changeIndex = unusedIdx[1] this.changeIndex = unusedIdx[1]
} }
/**
* @returns {Promise<void>}
*/
async _loadDerivedIndices() { async _loadDerivedIndices() {
const derivedIdx = await db.getHDAccountDerivedIndices(this.xpub) const derivedIdx = await db.getHDAccountDerivedIndices(this.xpub)
this.accountDerivedIndex = derivedIdx[0] this.accountDerivedIndex = derivedIdx[0]
this.changeDerivedIndex = derivedIdx[1] this.changeDerivedIndex = derivedIdx[1]
} }
/**
* @returns {Promise<void>}
*/
async _loadNbTransactions() { async _loadNbTransactions() {
this.nTx = await db.getHDAccountNbTransactions(this.xpub) this.nTx = await db.getHDAccountNbTransactions(this.xpub)
} }
/** /**
* Load the utxos associated to the hd account * @description Load the utxos associated to the hd account
* @returns {Promise - object[]} * @returns {Promise<object[]>}
*/ */
async loadUtxos() { async loadUtxos() {
this.unspentOutputs = [] 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} * @returns {object}
*/ */
toPojo() { toPojo() {
@ -169,8 +181,7 @@ class HdAccountInfo {
} }
/** /**
* Return a plain old js object with hd account properties * @description Return a plain old js object with hd account properties (extended version)
* (extended version)
* @returns {object} * @returns {object}
*/ */
toPojoExtended() { toPojoExtended() {

21
lib/wallet/wallet-entities.js

@ -6,13 +6,13 @@
/** /**
* A class storing entities (xpubs, addresses, pubkeys) * @class WalletEntities
* defining a (full|partial) wallet * @description A class storing entities (xpubs, addresses, pubkeys) defining a (full|partial) wallet
*/ */
class WalletEntities { class WalletEntities {
/** /**
* Constructor * @constructor
*/ */
constructor() { constructor() {
this.pubkeys = [] this.pubkeys = []
@ -23,8 +23,7 @@ class WalletEntities {
} }
/** /**
* Add a new hd account * @description Add a new hd account with its translation as an xpub
* with its translation as an xpub
* @param {string} xpub - xpub or tpub * @param {string} xpub - xpub or tpub
* @param {string} ypub - ypub or upub or false * @param {string} ypub - ypub or upub or false
* @param {string} zpub - zpub or vpub 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} address - bitcoin address
* @param {string} pubkey - pubkey associated to the address or false * @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} address - bitcoin address
* @param {string} pubkey - public key * @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 * @param {string} xpub
* @returns {boolean} returns true if the xpub is already listed, false otherwise * @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 * @param {string} address - bitcoin address
* @returns {boolean} returns true if the address is already listed, false otherwise * @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 * @param {string} pubkey - public key
* @returns {boolean} returns true if the pubkey is already listed, false otherwise * @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 * A class storing information about a (full|partial) wallet
* Provides a set of methods allowing to retrieve specific information * Provides a set of methods allowing to retrieve specific information
* @class WalletInfo
*/ */
class WalletInfo { class WalletInfo {
/** /**
* Constructor * Constructor
* @constructor
* @param {object} entities - wallet entities (hdaccounts, addresses, pubkeys) * @param {object} entities - wallet entities (hdaccounts, addresses, pubkeys)
*/ */
constructor(entities) { constructor(entities) {
@ -49,7 +51,7 @@ class WalletInfo {
/** /**
* Ensure hd accounts exist in database * Ensure hd accounts exist in database
* @returns {Promise} * @returns {Promise<Array<any>>}
*/ */
async ensureHdAccounts() { async ensureHdAccounts() {
return util.parallelCall(this.entities.xpubs, async xpub => { return util.parallelCall(this.entities.xpubs, async xpub => {
@ -60,7 +62,7 @@ class WalletInfo {
/** /**
* Load information about the hd accounts * Load information about the hd accounts
* @returns {Promise} * @returns {Promise<Array<any>>}
*/ */
async loadHdAccountsInfo() { async loadHdAccountsInfo() {
return util.parallelCall(this.entities.xpubs, async xpub => { return util.parallelCall(this.entities.xpubs, async xpub => {
@ -91,7 +93,7 @@ class WalletInfo {
/** /**
* Filter addresses that belong to an active hd account * Filter addresses that belong to an active hd account
* @returns {Promise} * @returns {Promise<void>}
*/ */
async filterAddresses() { async filterAddresses() {
const res = await db.getXpubByAddresses(this.entities.addrs) const res = await db.getXpubByAddresses(this.entities.addrs)
@ -110,7 +112,7 @@ class WalletInfo {
/** /**
* Load information about the addresses * Load information about the addresses
* @returns {Promise} * @returns {Promise<Array<void>>}
*/ */
async loadAddressesInfo() { async loadAddressesInfo() {
return util.parallelCall(this.entities.addrs, async address => { return util.parallelCall(this.entities.addrs, async address => {
@ -123,11 +125,11 @@ class WalletInfo {
/** /**
* Loads a partial list of transactions for this wallet * Loads a partial list of transactions for this wallet
* @param {integer} page - page index * @param {number} page - page index
* @param {integer} count - number of transactions per page * @param {number} count - number of transactions per page
* @param {boolean} txBalance - True if past wallet balance * @param {boolean=} txBalance - True if past wallet balance
* should be computed for each transaction * should be computed for each transaction
* @returns {Promise} * @returns {Promise<void>}
*/ */
async loadTransactions(page, count, txBalance) { async loadTransactions(page, count, txBalance) {
this.txs = await db.getTxsByAddrAndXpubs( this.txs = await db.getTxsByAddrAndXpubs(
@ -149,7 +151,7 @@ class WalletInfo {
/** /**
* Loads the number of transactions for this wallet * Loads the number of transactions for this wallet
* @returns {Promise} * @returns {Promise<void>}
*/ */
async loadNbTransactions() { async loadNbTransactions() {
const nbTxs = await db.getAddrAndXpubsNbTransactions( const nbTxs = await db.getAddrAndXpubsNbTransactions(
@ -163,7 +165,7 @@ class WalletInfo {
/** /**
* Loads tinfo about the fee rates * Loads tinfo about the fee rates
* @returns {Promise} * @returns {Promise<void>}
*/ */
async loadFeesInfo() { async loadFeesInfo() {
this.info.fees = await rpcFees.getFees() this.info.fees = await rpcFees.getFees()
@ -171,7 +173,7 @@ class WalletInfo {
/** /**
* Loads the list of unspent outputs for this wallet * Loads the list of unspent outputs for this wallet
* @returns {Promise} * @returns {Promise<void>}
*/ */
async loadUtxos() { async loadUtxos() {
// Load the utxos for the hd accounts // Load the utxos for the hd accounts
@ -212,7 +214,7 @@ class WalletInfo {
/** /**
* Post process addresses and public keys * Post process addresses and public keys
*/ */
postProcessAddresses() { async postProcessAddresses() {
for (let b = 0; b < this.entities.pubkeys.length; b++) { for (let b = 0; b < this.entities.pubkeys.length; b++) {
const pk = this.entities.pubkeys[b] const pk = this.entities.pubkeys[b]
@ -221,7 +223,7 @@ class WalletInfo {
// Add pubkeys in this.addresses // Add pubkeys in this.addresses
for (let c = 0; c < this.addresses.length; c++) { 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 this.addresses[c].pubkey = pk
} }
@ -229,30 +231,32 @@ class WalletInfo {
for (let d = 0; d < this.txs.length; d++) { for (let d = 0; d < this.txs.length; d++) {
// inputs // inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) { 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 this.txs[d].inputs[e].prev_out.pubkey = pk
} }
// outputs // outputs
for (let e = 0; e < this.txs[d].out.length; e++) { 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 this.txs[d].out[e].pubkey = pk
} }
} }
// Add pubkeys in this.unspentOutputs // Add pubkeys in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) { 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 this.unspentOutputs[f].pubkey = pk
} }
} }
} }
} }
return Promise.resolve()
} }
/** /**
* Post process hd accounts (xpubs translations) * Post process hd accounts (xpubs translations)
*/ */
postProcessHdAccounts() { async postProcessHdAccounts() {
for (let b = 0; b < this.entities.xpubs.length; b++) { for (let b = 0; b < this.entities.xpubs.length; b++) {
const entityXPub = this.entities.xpubs[b] const entityXPub = this.entities.xpubs[b]
const entityYPub = this.entities.ypubs[b] const entityYPub = this.entities.ypubs[b]
@ -263,7 +267,7 @@ class WalletInfo {
// Translate xpub => ypub/zpub in this.addresses // Translate xpub => ypub/zpub in this.addresses
for (let c = 0; c < this.addresses.length; c++) { 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 this.addresses[c].address = tgtXPub
} }
@ -272,14 +276,14 @@ class WalletInfo {
// inputs // inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) { for (let e = 0; e < this.txs[d].inputs.length; e++) {
const xpub = this.txs[d].inputs[e].prev_out.xpub 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 this.txs[d].inputs[e].prev_out.xpub.m = tgtXPub
} }
// outputs // outputs
for (let e = 0; e < this.txs[d].out.length; e++) { for (let e = 0; e < this.txs[d].out.length; e++) {
const xpub = this.txs[d].out[e].xpub 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 this.txs[d].out[e].xpub.m = tgtXPub
} }
} }
@ -287,12 +291,14 @@ class WalletInfo {
// Translate xpub => ypub/zpub in this.unspentOutputs // Translate xpub => ypub/zpub in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) { for (let f = 0; f < this.unspentOutputs.length; f++) {
const xpub = this.unspentOutputs[f].xpub const xpub = this.unspentOutputs[f].xpub
if (xpub && (xpub.m == entityXPub)) { if (xpub && (xpub.m === entityXPub)) {
this.unspentOutputs[f].xpub.m = tgtXPub 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 // Force import of addresses associated to paynyms
// if dojo relies on a local index // 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) await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses // Filter the addresses
@ -150,7 +150,7 @@ class WalletService {
await walletInfo.ensureAddresses() await walletInfo.ensureAddresses()
// Force import of addresses associated to paynyms // Force import of addresses associated to paynyms
// if dojo relies on a local index // 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) await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the address and load them // Filter the address and load them
await walletInfo.filterAddresses() await walletInfo.filterAddresses()
@ -252,7 +252,7 @@ class WalletService {
await walletInfo.ensureAddresses() await walletInfo.ensureAddresses()
// Force import of addresses associated to paynyms // Force import of addresses associated to paynyms
// if dojo relies on a local index // 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) await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses // Filter the addresses
await walletInfo.filterAddresses() await walletInfo.filterAddresses()
@ -274,8 +274,8 @@ class WalletService {
/** /**
* Get a subset of wallet transactions * Get a subset of wallet transactions
* @param {object} entities - mapping of active entities * @param {object} entities - mapping of active entities
* @param {integer} page - page of transactions to be returned * @param {number} page - page of transactions to be returned
* @param {integer} count - number of transactions returned per page * @param {number} count - number of transactions returned per page
* @returns {Promise} * @returns {Promise}
*/ */
async getWalletTransactions(entities, page, count) { async getWalletTransactions(entities, page, count) {
@ -287,7 +287,7 @@ class WalletService {
} }
// Check parameters // Check parameters
if (entities.xpubs.length == 0 && entities.addrs.length == 0) if (entities.xpubs.length === 0 && entities.addrs.length === 0)
return ret return ret
// Initialize a WalletInfo object // Initialize a WalletInfo object
@ -344,13 +344,13 @@ class WalletService {
* @returns {boolean} return true if conditions are met, false otherwise * @returns {boolean} return true if conditions are met, false otherwise
*/ */
_checkEntities(active, legacy, bip49, bip84, pubkeys) { _checkEntities(active, legacy, bip49, bip84, pubkeys) {
const allEmpty = active.xpubs.length == 0 const allEmpty = active.xpubs.length === 0
&& active.addrs.length == 0 && active.addrs.length === 0
&& legacy.xpubs.length == 0 && legacy.xpubs.length === 0
&& legacy.addrs.length == 0 && legacy.addrs.length === 0
&& pubkeys.addrs.length == 0 && pubkeys.addrs.length === 0
&& bip49.xpubs.length == 0 && bip49.xpubs.length === 0
&& bip84.xpubs.length == 0 && bip84.xpubs.length === 0
return !allEmpty return !allEmpty
} }
@ -375,7 +375,7 @@ class WalletService {
const pubkey = source.pubkeys[idxSource] const pubkey = source.pubkeys[idxSource]
const idxActive = active.addrs.indexOf(addr) const idxActive = active.addrs.indexOf(addr)
if (idxActive == -1) { if (idxActive === -1) {
active.addrs.push(addr) active.addrs.push(addr)
active.pubkeys.push(pubkey) active.pubkeys.push(pubkey)
} else if (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", "name": "samourai-dojo",
"version": "1.10.1", "version": "1.11.0",
"description": "Backend server for Samourai Wallet", "description": "Backend server for Samourai Wallet",
"main": "accounts/index.js", "main": "accounts/index.js",
"engines": {
"node": "14.x.x"
},
"scripts": { "scripts": {
"test": "mocha --recursive --reporter spec" "test": "mocha --recursive --reporter spec"
}, },
@ -14,31 +17,32 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"homepage": "https://code.samourai.io/dojo/samourai-dojo", "homepage": "https://code.samourai.io/dojo/samourai-dojo",
"dependencies": { "dependencies": {
"@tinyhttp/app": "1.3.3", "@tinyhttp/app": "1.3.13",
"async-sema": "2.1.2", "async-sema": "3.1.0",
"axios": "0.21.1", "axios": "0.21.1",
"bip39": "2.4.0", "bip39": "3.0.4",
"bitcoinjs-lib": "5.2.0", "bitcoinjs-lib": "5.2.0",
"bitcoinjs-message": "1.0.1", "bitcoinjs-message": "2.2.0",
"body-parser": "1.19.0", "body-parser": "1.19.0",
"helmet": "3.23.3", "helmet": "4.6.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"lru-cache": "4.0.2", "lru-cache": "6.0.0",
"make-concurrent": "5.3.0", "make-concurrent": "5.3.0",
"minimist": "1.2.5", "minimist": "1.2.5",
"mysql": "2.18.1", "mysql": "2.18.1",
"nocache": "3.0.1",
"passport": "0.4.1", "passport": "0.4.1",
"passport-localapikey-update": "0.6.0", "passport-localapikey-update": "0.6.0",
"rpc-bitcoin": "2.0.0", "rpc-bitcoin": "2.0.0",
"sirv": "1.0.11", "sirv": "1.0.12",
"socks-proxy-agent": "4.0.1", "socks-proxy-agent": "6.0.0",
"validator": "10.8.0", "validator": "13.6.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"workerpool": "6.1.4", "workerpool": "6.1.5",
"zeromq": "4.2.0" "zeromq": "6.0.0-beta.5"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^7.1.1" "mocha": "9.0.3"
} }
} }

12
pushtx/orchestrator.js

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

8
pushtx/pushtx-processor.js

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

4
pushtx/pushtx-rest-api.js

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

8
pushtx/transactions-scheduler.js

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

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

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

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

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

2
static/admin/dmt/index.js

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

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

@ -95,7 +95,7 @@ const statusScript = {
this.chaintipBitcoind = data['bitcoind']['blocks'] this.chaintipBitcoind = data['bitcoind']['blocks']
$('#node-chaintip').text(data['bitcoind']['blocks']) $('#node-chaintip').text(data['bitcoind']['blocks'])
$('#node-version').text(data['bitcoind']['version']) $('#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-network').text(network)
$('#node-conn').text(data['bitcoind']['conn']) $('#node-conn').text(data['bitcoind']['conn'])
$('#node-relay-fee').text(data['bitcoind']['relayfee']) $('#node-relay-fee').text(data['bitcoind']['relayfee'])
@ -123,13 +123,13 @@ const statusScript = {
}, },
setStatusIndicator: function(id, status) { setStatusIndicator: function(id, status) {
if (status == 'ok') { if (status === 'ok') {
$(id).html('&#10003;') $(id).html('&#10003;')
$(id).css('color', '#76d776') $(id).css('color', '#76d776')
} else if (status == 'ko') { } else if (status === 'ko') {
$(id).html('X') $(id).html('X')
$(id).css('color', '#f77c7c') $(id).css('color', '#f77c7c')
} else if (status == 'desynchronized') { } else if (status === 'desynchronized') {
$(id).html('&#10003;') $(id).html('&#10003;')
$(id).css('color', '#f0c649') $(id).css('color', '#f0c649')
} else { } 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').text(this.currentTxid)
$('#txid-value').attr('href', txUrl) $('#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) $('#tx-firstseen').text(firstseen)
if (txInfo.hasOwnProperty('block')) if (txInfo.hasOwnProperty('block'))

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

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

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

@ -98,7 +98,7 @@ const lib_auth = {
isAuthenticated: function() { isAuthenticated: function() {
// Checks that an access token is stored in session storage // Checks that an access token is stored in session storage
let token = this.getAccessToken() 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) const payload = this.getPayloadAccessToken(token)
if (!payload) if (!payload)
return false 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) { getExplorerTxUrl: function(txid, explorerInfo) {
if (explorerInfo == null) if (explorerInfo == null)
return null return null
else if (explorerInfo['pairing']['type'] == 'explorer.oxt') else if (explorerInfo['pairing']['type'] === 'explorer.oxt')
return `${explorerInfo['pairing']['url']}/transaction/${txid}` 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}` return `http://${explorerInfo['pairing']['url']}/tx/${txid}`
else else
return null return null
@ -46,7 +46,7 @@ const lib_cmn = {
if (file) { if (file) {
xhttp = new XMLHttpRequest() xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) { if (this.readyState === 4 && this.status === 200) {
elmnt.innerHTML = this.responseText elmnt.innerHTML = this.responseText
elmnt.removeAttribute('include-html') elmnt.removeAttribute('include-html')
self.includeHTML(cb) self.includeHTML(cb)
@ -72,7 +72,7 @@ const lib_cmn = {
if (file) { if (file) {
xhttp = new XMLHttpRequest() xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) { if (this.readyState === 4 && this.status === 200) {
const newElmnt = document.createElement('script') const newElmnt = document.createElement('script')
newElmnt.textContent = this.responseText newElmnt.textContent = this.responseText
if (elmnt.parentNode) { if (elmnt.parentNode) {

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

@ -14,7 +14,7 @@ const lib_errors = {
processError: function(e) { processError: function(e) {
const errorMsg = this.extractJqxhrErrorMsg(e) const errorMsg = this.extractJqxhrErrorMsg(e)
// Redirect to sign in page if authentication error // 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() lib_auth.logout()
} else { } else {
lib_msg.displayErrors(errorMsg) 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 * Format a unix timestamp into a readable date/hour
*/ */
formatUnixTs: function(ts) { formatUnixTs: function(ts) {
if (ts == null || ts == 0) if (ts == null || ts === 0)
return '-' return '-'
let tmpDate = new Date(ts*1000), 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 targetSig = Buffer.from(stc[1], 'hex')
const expectedResult = stc[2] 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 // Check that library returns valid result
assert((sig.compare(targetSig) == 0) == expectedResult) 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 YPUB = 'upub5ELkCsSF68UnAZE7zF9CDztvHeBJiAAhwa4VxEFzZ1CfQRbpy93mkBbUZsqYVpoeEHFwY3fGh9bfftH79ZwbhjUEUBAxQj551TMxVyny4UX'
const ZPUB = 'vpub5ZB1WY7AEp2G1rREpbvpS5zRTcKkenACrgaijd9sw1aYTXR4DoDLNFFcb5o8VjTZdvNkHXFq9oxDZAtfsGMcVy9qLWsNzdtZHBRbtXe87LB' const ZPUB = 'vpub5ZB1WY7AEp2G1rREpbvpS5zRTcKkenACrgaijd9sw1aYTXR4DoDLNFFcb5o8VjTZdvNkHXFq9oxDZAtfsGMcVy9qLWsNzdtZHBRbtXe87LB'
const POSTMIX_ZPUB = 'vpub5Y6cjg7GbwSLRu33XB76n3EoJZscmYSVEToLSMqD6ugAcm4rof8E9yvDiaFfhGEuyL95P9VD4A9W3JrBTZhzWSXiRyYvWFnUBAZc67X32wh'
const BIP44_VECTORS = [ const BIP44_VECTORS = [
[0, 0, 'mmZ5FRccGAkwfKme4JkrsmurnimDLdfmNL'], [0, 0, 'mmZ5FRccGAkwfKme4JkrsmurnimDLdfmNL'],
[0, 1, 'n3yomLicyrSULiNWFKHsK8erntSpJZEPV6'], [0, 1, 'n3yomLicyrSULiNWFKHsK8erntSpJZEPV6'],
@ -61,6 +63,27 @@ const BIP84_VECTORS = [
[1, 4, 'tb1qjrnw8u2pvspm6hq3aa83ff93wevq2zyxqczewy'] [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 = [ const HD_TYPES_VECTORS = [
// unlocked // unlocked
[0, hdaHelper.BIP44, false], [0, hdaHelper.BIP44, false],
@ -74,7 +97,7 @@ const HD_TYPES_VECTORS = [
describe('HdAccountsHelper', function() { describe('HdAccountsHelper', function() {
describe('isXpub()', function() { describe('isXpub()', function() {
it('should successfully detect a XPUB', function() { it('should successfully detect a XPUB', function() {
assert(hdaHelper.isXpub(XPUB)) assert(hdaHelper.isXpub(XPUB))
@ -117,7 +140,7 @@ describe('HdAccountsHelper', function() {
const ret = hdaHelper.classify(v[0]) const ret = hdaHelper.classify(v[0])
assert(ret.type == v[1]) assert(ret.type == v[1])
assert(ret.locked == v[2]) assert(ret.locked == v[2])
} }
}) })
}) })
@ -127,7 +150,7 @@ describe('HdAccountsHelper', function() {
for (const v of HD_TYPES_VECTORS) { for (const v of HD_TYPES_VECTORS) {
const ret = hdaHelper.makeType(v[1], v[2]) const ret = hdaHelper.makeType(v[1], v[2])
assert(ret == v[0]) assert(ret == v[0])
} }
}) })
}) })
@ -153,6 +176,15 @@ describe('HdAccountsHelper', function() {
assert(addresses[0].address == v[2]) 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 { try {
switch(msg.op) { switch(msg.op) {
case OP_INIT: case OP_INIT:
if (status != IDLE) if (status !== IDLE)
throw 'Operation not allowed' throw 'Operation not allowed'
res = await initBlock(msg.header) res = await initBlock(msg.header)
break break
case OP_PROCESS_OUTPUTS: case OP_PROCESS_OUTPUTS:
if (status != INITIALIZED) if (status !== INITIALIZED)
throw 'Operation not allowed' throw 'Operation not allowed'
res = await processOutputs() res = await processOutputs()
break break
case OP_PROCESS_INPUTS: case OP_PROCESS_INPUTS:
if (status != OUTPUTS_PROCESSED) if (status !== OUTPUTS_PROCESSED)
throw 'Operation not allowed' throw 'Operation not allowed'
res = await processInputs() res = await processInputs()
break break
case OP_CONFIRM: case OP_CONFIRM:
if (status != INPUTS_PROCESSED) if (status !== INPUTS_PROCESSED)
throw 'Operation not allowed' throw 'Operation not allowed'
res = await confirmTransactions(msg.blockId) res = await confirmTransactions(msg.blockId)
break break
@ -130,7 +130,7 @@ async function processInputs() {
/** /**
* Confirm the transactions * 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) { async function confirmTransactions(blockId) {
status = TXS_CONFIRMED status = TXS_CONFIRMED

16
tracker/block.js

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

32
tracker/blockchain-processor.js

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

10
tracker/blocks-processor.js

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

64
tracker/mempool-processor.js

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

30
tracker/tracker.js

@ -4,11 +4,12 @@
*/ */
'use strict' 'use strict'
const zmq = require('zeromq') const zmq = require('zeromq/v5-compat')
const network = require('../lib/bitcoin/network') const network = require('../lib/bitcoin/network')
const keys = require('../keys')[network.key] const keys = require('../keys')[network.key]
const BlockchainProcessor = require('./blockchain-processor') const BlockchainProcessor = require('./blockchain-processor')
const MempoolProcessor = require('./mempool-processor') const MempoolProcessor = require('./mempool-processor')
const util = require('../lib/util')
/** /**
@ -22,23 +23,28 @@ class Tracker {
constructor() { constructor() {
// Notification socket for client events // Notification socket for client events
this.notifSock = zmq.socket('pub') this.notifSock = zmq.socket('pub')
this.notifSock.bindSync(`tcp://127.0.0.1:${keys.ports.tracker}`) this.notifSock.bind(`tcp://127.0.0.1:${keys.ports.tracker}`, () => {
// Initialize the blockchain processor
// Initialize the blockchain processor // and the mempool buffer
// and the mempool buffer this.initialized = true
this.blockchainProcessor = new BlockchainProcessor(this.notifSock) this.blockchainProcessor = new BlockchainProcessor(this.notifSock)
this.mempoolProcessor = new MempoolProcessor(this.notifSock) this.mempoolProcessor = new MempoolProcessor(this.notifSock)
})
} }
/** /**
* Start the tracker * Start the tracker
* @returns {Promise} * @returns {Promise<void>}
*/ */
async start() { async start() {
this.startupTimeout = setTimeout(async function() { if (!this.initialized) {
await this.blockchainProcessor.start() await util.delay(1000)
await this.mempoolProcessor.start()
}.bind(this), 1500) 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 // Check if we find some inputs of interest
const results = await db.getOutputSpends(spends) const results = await db.getOutputSpends(spends)
if (results.length == 0) if (results.length === 0)
return null return null
// Flag the transaction for broadcast // Flag the transaction for broadcast
@ -122,7 +122,7 @@ class Transaction {
}) })
// Detect potential double spends // 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}!`) Logger.info(`Tracker : DOUBLE SPEND of ${r.txnTxid}-${r.outIndex} by ${this.txid}!`)
// Delete the existing transaction that has been double-spent: // Delete the existing transaction that has been double-spent:
// since the deepest block keeps its transactions, this will // since the deepest block keeps its transactions, this will
@ -190,7 +190,7 @@ class Transaction {
const aHdAcctAddr = await this._processOutputsHdAccounts(result.hd, indexedOutputs) const aHdAcctAddr = await this._processOutputsHdAccounts(result.hd, indexedOutputs)
fundedAddresses = fundedAddresses.concat(aHdAcctAddr) fundedAddresses = fundedAddresses.concat(aHdAcctAddr)
if (fundedAddresses.length == 0) if (fundedAddresses.length === 0)
return null return null
// Flag the transaction for broadcast // Flag the transaction for broadcast
@ -219,7 +219,7 @@ class Transaction {
* Process outputs sending to tracked loose addresses * Process outputs sending to tracked loose addresses
* @param {object[]} addresses - array of address objects * @param {object[]} addresses - array of address objects
* @param {object} indexedOutputs - outputs indexed by address * @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: ...} * {addrID: ..., outIndex: ..., outAmount: ..., outScript: ...}
*/ */
async _processOutputsLooseAddresses(addresses, indexedOutputs) { async _processOutputsLooseAddresses(addresses, indexedOutputs) {
@ -247,7 +247,7 @@ class Transaction {
* Process outputs sending to tracked hd accounts * Process outputs sending to tracked hd accounts
* @param {object[]} hdAccounts - array of hd account objects * @param {object[]} hdAccounts - array of hd account objects
* @param {object} indexedOutputs - outputs indexed by address * @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: ...} * {addrID: ..., outIndex: ..., outAmount: ..., outScript: ...}
*/ */
async _processOutputsHdAccounts(hdAccounts, indexedOutputs) { async _processOutputsHdAccounts(hdAccounts, indexedOutputs) {
@ -297,7 +297,7 @@ class Transaction {
* @param {string} xpub * @param {string} xpub
* @param {object} hdAccount - hd account object * @param {object} hdAccount - hd account object
* @param {object} indexedOutputs - outputs indexed by address * @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) { async _deriveNewAddresses(xpub, hdAccount, indexedOutputs) {
const hdType = hdAccount.hdType const hdType = hdAccount.hdType
@ -317,10 +317,10 @@ class Transaction {
for (let chain of [0,1]) { for (let chain of [0,1]) {
// Get addresses for this account that are on this chain // Get addresses for this account that are on this chain
const chainAddresses = _.filter(hdAccount.addresses, v => { const chainAddresses = _.filter(hdAccount.addresses, v => {
return v.hdAddrChain == chain return v.hdAddrChain === chain
}) })
if (chainAddresses.length == 0) if (chainAddresses.length === 0)
continue continue
// Get the maximum used address on this chain // Get the maximum used address on this chain
@ -365,7 +365,8 @@ class Transaction {
const indices = _.range(minIdx, maxIdx) const indices = _.range(minIdx, maxIdx)
const derived = await hdaHelper.deriveAddresses(xpub, chain, indices, hdType) 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(',')}`) Logger.info(`Tracker : Derived hdID(${hdAccount.hdID}) M/${chain}/${indices.join(',')}`)

19
tracker/transactions-bundle.js

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

Loading…
Cancel
Save