Browse Source

Merge branch 'develop' into 'master'

merge develop into master for v1.8.0

See merge request dojo/samourai-dojo!164
umbrel v1.8.0
kenshin-samourai 4 years ago
parent
commit
ab7a174579
  1. 2
      .gitignore
  2. 17
      .vscode/launch.json
  3. 119
      RELEASES.md
  4. 2
      accounts/index.js
  5. 1
      accounts/multiaddr-rest-api.js
  6. 14
      accounts/notifications-service.js
  7. 12
      accounts/support-rest-api.js
  8. 1
      accounts/unspent-rest-api.js
  9. 136
      accounts/wallet-rest-api.js
  10. 43
      accounts/xpub-rest-api.js
  11. 2
      doc/GET_multiaddr.md
  12. 2
      doc/GET_unspent.md
  13. 165
      doc/GET_wallet.md
  14. 37
      doc/GET_xpub_import_status.md
  15. 10
      docker/my-dojo/.env
  16. 8
      docker/my-dojo/bitcoin/Dockerfile
  17. 1
      docker/my-dojo/bitcoin/restart.sh
  18. 17
      docker/my-dojo/conf/docker-bitcoind.conf.tpl
  19. 35
      docker/my-dojo/dojo.sh
  20. 13
      docker/my-dojo/tor/Dockerfile
  21. 13
      docker/my-dojo/whirlpool/Dockerfile
  22. 10
      keys/index-example.js
  23. 10
      lib/bitcoin/hd-accounts-service.js
  24. 10
      lib/remote-importer/remote-importer.js
  25. 11
      lib/wallet/wallet-info.js
  26. 91
      lib/wallet/wallet-service.js
  27. 653
      package-lock.json
  28. 8
      package.json
  29. 6
      pushtx/pushtx-rest-api.js
  30. 0
      restart-example.sh
  31. 4
      static/admin/conf/index-mainnet.js
  32. 4
      static/admin/conf/index-testnet.js
  33. 587
      static/admin/css/bootstrap-theme.css
  34. 6757
      static/admin/css/bootstrap.css
  35. 697
      static/admin/css/style.css
  36. 147
      static/admin/dmt/addresses-tools/addresses-tools.html
  37. 228
      static/admin/dmt/addresses-tools/addresses-tools.js
  38. 22
      static/admin/dmt/blocks-rescan/blocks-rescan.html
  39. 45
      static/admin/dmt/blocks-rescan/blocks-rescan.js
  40. 152
      static/admin/dmt/index.html
  41. 116
      static/admin/dmt/index.js
  42. 7
      static/admin/dmt/msg-box/msg-box.html
  43. 33
      static/admin/dmt/pairing/pairing.html
  44. 65
      static/admin/dmt/pairing/pairing.js
  45. 44
      static/admin/dmt/pushtx/pushtx.html
  46. 90
      static/admin/dmt/pushtx/pushtx.js
  47. 92
      static/admin/dmt/status/status.html
  48. 68
      static/admin/dmt/status/status.js
  49. 101
      static/admin/dmt/txs-tools/txs-tools.html
  50. 117
      static/admin/dmt/txs-tools/txs-tools.js
  51. 42
      static/admin/dmt/welcome/welcome.html
  52. 183
      static/admin/dmt/xpubs-tools/xpubs-tools.html
  53. 259
      static/admin/dmt/xpubs-tools/xpubs-tools.js
  54. BIN
      static/admin/icons/samourai-logo-loading.png
  55. 17
      static/admin/index.html
  56. 50
      static/admin/index.js
  57. 152
      static/admin/lib/api-wrapper.js
  58. 72
      static/admin/lib/auth-utils.js
  59. 2377
      static/admin/lib/bootstrap.js
  60. 124
      static/admin/lib/common-script.js
  61. 49
      static/admin/lib/format-utils.js
  62. 4
      static/admin/lib/jquery-3.2.1.min.js
  63. 2
      static/admin/lib/jquery-3.5.1.min.js
  64. 44
      static/admin/lib/messages.js
  65. 135
      static/admin/tool/index.html
  66. 300
      static/admin/tool/index.js

2
.gitignore

@ -9,4 +9,6 @@ keys/sslcert/
node_modules/
private-tests/
static/admin/conf/index.js
static/admin-legacy/
*.log
static/admin-legacy

17
.vscode/launch.json

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/accounts/index.js"
}
]
}

119
RELEASES.md

@ -3,6 +3,7 @@
## Releases ##
- [v1.8.0](#1_8_0)
- [v1.7.0](#1_7_0)
- [v1.6.0](#1_6_0)
- [v1.5.0](#1_5_0)
@ -13,6 +14,94 @@
- [v1.1.0](#1_1_0)
<a name="1_8_0"/>
## Samourai Dojo v1.8.0 ##
### Notable changes ###
#### New version of the Maintenance Tool ####
This release introduces a new version of Dojo Maintenance Tool (DMT).
The DMT has been revamped in order to provide a more user-friendly experience.
#### New configuration property BITCOIND_RPC_WORK_QUEUE ####
This new configuration property added to docker-bitcoind.conf allows to set a custom max depth for the RPC work queue of the full node.
Increasing the value set for this property may help users running Dojo on slower devices when recurring "work queue depth exceeded" errors appear in the logs.
#### New configuration property BITCOIND_SHUTDOWN_DELAY ####
This new configuration property added to docker-bitcoind.conf allows to set a custom delay before Dojo forces the shutdown of its full node (default delay is 180 seconds).
Increasing the value set for this property may help users running Dojo on slower devices requiring a longer delay for a clean shutdown of the full node.
#### Automatic fallback to a mirror of the Tor archive ####
If Dojo fails to contact the Tor servers (archive.torproject.org) during an installation or an upgrade, it will automatically try to download Tor source code from a mirror hosted by the EFF (tor.eff.org).
#### Upgrade of bitcoind to v0.20.1 ####
Upgrade to Bitcoin Core v0.20.1
#### New /wallet API endpoint ####
This new API endpoint combines the results previously returned by the /multiaddr, /unspent and /fees endpoints. See this [doc](https://github.com/Samourai-Wallet/samourai-dojo/blob/master/doc/GET_wallet.md) for more details.
Starting with this version, the /multiaddr and /unspent endpoints are marked as deprecated.
### Change log ###
#### MyDojo ####
- [#mr151](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/151) add new /wallet api endpoint
- [#mr153](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/153) add new BITCOIND_RPC_WORK_QUEUE parameter to docker-bitcoind.conf.tpl
- [#mr154](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/154) add new /xpub/impot/status endpoint
- [#mr155](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/155) upgrade bitcoind to bitcoin core 0.20.1
- [#mr156](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/156) automatic fallback to mirror of tor archive
- [#mr157](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/157) add new config property BITCOIND_SHUTDOWN_DELAY
- [#mr160](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/160) new version of the maintenance tool
- [#mr161](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/161) improve the xpub tools screen
- [#mr162](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/162) rework response returned by dojo.sh onion
- [#mr163](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/163) improve presentation of response returned by dojo.sh onion
- [a548bce6](https://code.samourai.io/dojo/samourai-dojo/-/commit/a548bce6dea78297f21368c1e04ee1a021f1f524) bump dojo version in index-example.js
#### Bug fixes ####
- [#mr158](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/158) fix parsing of message in notification service
- [dbf61217](https://code.samourai.io/dojo/samourai-dojo/-/commit/dbf6121779385f19e99167298ac8a6bf3411422a) fix presentation of message returned by dojo.sh onion
- [5d960071](https://code.samourai.io/dojo/samourai-dojo/-/commit/5d960071cb4832a348e1883057be3d35c7ff747e) update presentation of response returned by dojo.sh onion
#### Security ####
- [#mr152](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/152) update nodejs modules
- [#mr159](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/159) update version of minimist and helmet
#### Credits ###
- Crazyk031
- kenshin-samourai
- LaurentMT
- RockyRococo
- sarath
- SatoshiThreepwood
- zeroleak
<a name="1_7_0"/>
## Samourai Dojo v1.7.0 ##
@ -25,7 +114,7 @@
A new optional "strict mode" is added to the /pushtx and /pushtx/schedule endpoints of the API.
This strict mode enforces a few additional checks on a selected subset of the outputs of a transaction before it's pushed on the P2P network or before it's scheduled for a delayed push.
This strict mode enforces a few additional checks on a selected subset of the outputs of a transaction before it's pushed on the P2P network or before it's scheduled for a delayed push.
See this [doc](https://code.samourai.io/dojo/samourai-dojo/-/blob/develop/doc/POST_pushtx.md) for detailed information.
@ -42,14 +131,14 @@ A new config parameter `WHIRLPOOL_RESYNC` is added to docker-whirlpool.conf. Whe
#### MyDojo ####
- [#mr142](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/142) add setup of explorer in keys.index.js
- [#mr143](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/143) update doc and package.json with url of new repository
- [#mr144](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/144) switch addrindexrs repo to gitlab
- [#mr142](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/142) add setup of explorer in keys.index.js
- [#mr143](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/143) update doc and package.json with url of new repository
- [#mr144](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/144) switch addrindexrs repo to gitlab
- [#mr145](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/145) explicitely set algo used for jwt signatures
- [#mr146](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/146) upgrade whirlpool to whirlpool-cli 0.10.7
- [#mr147](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/147) add new optional strict_mode_vouts to pushtx endpoints
- [#mr148](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/148) status code pushtx endpoints
- [#mr149](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/149) upgrade whirlpool to whirlpool-cli 0.10.8
- [#mr149](https://code.samourai.io/dojo/samourai-dojo/-/merge_requests/149) upgrade whirlpool to whirlpool-cli 0.10.8
#### Credits ###
@ -115,7 +204,7 @@ Upgrade to [addrindexrs](https://github.com/Samourai-Wallet/addrindexrs) v0.3.0
- [#128](https://github.com/Samourai-Wallet/samourai-dojo/pull/128) drop unneeded reversebuffer util method
- [#142](https://github.com/Samourai-Wallet/samourai-dojo/pull/142) modify results returned by dojo.sh onion
- [#143](https://github.com/Samourai-Wallet/samourai-dojo/pull/143) improve display of dojo version
- [#143](https://github.com/Samourai-Wallet/samourai-dojo/pull/143) improve display of dojo version
- [#144](https://github.com/Samourai-Wallet/samourai-dojo/pull/144) add dynamic switch of startup mode
- [#147](https://github.com/Samourai-Wallet/samourai-dojo/pull/147) increase control over ports exposed by dojo
- [#148](https://github.com/Samourai-Wallet/samourai-dojo/pull/148) upgrade explorer to btc-rpc-explorer 2.0.0
@ -123,7 +212,7 @@ Upgrade to [addrindexrs](https://github.com/Samourai-Wallet/addrindexrs) v0.3.0
- [#152](https://github.com/Samourai-Wallet/samourai-dojo/pull/152) add new optional whirlpool container
- [#154](https://github.com/Samourai-Wallet/samourai-dojo/pull/154) manage all logs with docker log system
- [#156](https://github.com/Samourai-Wallet/samourai-dojo/pull/156) upgrade indexer to addrindexrs v0.2.0
- [#157](https://github.com/Samourai-Wallet/samourai-dojo/pull/157) clean-up of log files
- [#157](https://github.com/Samourai-Wallet/samourai-dojo/pull/157) clean-up of log files
- [#158](https://github.com/Samourai-Wallet/samourai-dojo/pull/158) misc improvements in bitcoind rpc transactions class
- [#159](https://github.com/Samourai-Wallet/samourai-dojo/pull/159) upgrade indexer to rust 1.42.0 slim buster
- [#160](https://github.com/Samourai-Wallet/samourai-dojo/pull/160) upgrade bitcoind to bitcoin core 0.20.0
@ -216,7 +305,7 @@ Upgrade to Bitcoin Core v0.19.1
- [#118](https://github.com/Samourai-Wallet/samourai-dojo/pull/118) add support of local indexers as the data source of imports and rescans
- [#119](https://github.com/Samourai-Wallet/samourai-dojo/pull/119) improve performances of dojo upgrades
- [#120](https://github.com/Samourai-Wallet/samourai-dojo/pull/120) upgrade btc-rpc-explorer to v1.1.8
- [#120](https://github.com/Samourai-Wallet/samourai-dojo/pull/120) upgrade btc-rpc-explorer to v1.1.8
- [#121](https://github.com/Samourai-Wallet/samourai-dojo/pull/121) add controls and confirmations before reinstalls and uninstalls
- [#124](https://github.com/Samourai-Wallet/samourai-dojo/pull/124) upgrade bitcoin v0.19.1
- [#125](https://github.com/Samourai-Wallet/samourai-dojo/pull/125) improve support of --auto option in dojo.sh
@ -272,13 +361,13 @@ This release removes automatic restarts of the bitcoind container when bitcoind
#### Bug fixes ####
- [0ff045d](https://github.com/Samourai-Wallet/samourai-dojo/commit/0ff045d1495807902e9fd7dcfbd2fdb4dc21c608) keep bitcoind container up if bitcoind exits with an error
- [bd43526](https://github.com/Samourai-Wallet/samourai-dojo/commit/bd43526bca1f36a1ada07ad799c87b11a897e873) fix for dojo hanging on shutdown
- [bd43526](https://github.com/Samourai-Wallet/samourai-dojo/commit/bd43526bca1f36a1ada07ad799c87b11a897e873) fix for dojo hanging on shutdown
- [3ee85db](https://github.com/Samourai-Wallet/samourai-dojo/commit/3ee85db3bf69f4312204e502c98d414a4180dc53) force kill of docker exec used for testing bitcoind shutdown if command hangs more than 12s
#### Misc. ####
- [21925f7](https://github.com/Samourai-Wallet/samourai-dojo/commit/21925f7c321974ef7eb55c1ad897a5e02ef52bee) bump versions of dojo and bitcoind container
- [21925f7](https://github.com/Samourai-Wallet/samourai-dojo/commit/21925f7c321974ef7eb55c1ad897a5e02ef52bee) bump versions of dojo and bitcoind container
- [08342e3](https://github.com/Samourai-Wallet/samourai-dojo/commit/08342e3995c473b589bb2a517e5bc30cf5f7dc9a) add trace in stop() function of dojo.sh
@ -407,7 +496,7 @@ This version introduces a new "Blocks Rescan" feature accessible from the Mainte
#### Add Esplora as the new external data source for testnet ####
The testnet version of Dojo now relies on the Esplora API as its external data source for imports and rescans.
The testnet version of Dojo now relies on the Esplora API as its external data source for imports and rescans.
Previously used API (BTC.COM and Insight) have been removed.
@ -505,7 +594,7 @@ See [issue #59](https://github.com/Samourai-Wallet/samourai-dojo/issues/59).
- [#46](https://github.com/Samourai-Wallet/samourai-dojo/pull/46) add testnet support to my-dojo
- [#49](https://github.com/Samourai-Wallet/samourai-dojo/pull/49) add support of auth token passed through the authorization http header
- [#54](https://github.com/Samourai-Wallet/samourai-dojo/pull/54) remove /dump/heap endpoint and dependency on heapdump package
- [#55](https://github.com/Samourai-Wallet/samourai-dojo/pull/55) upgrade bitcoind to bitcoin core 0.18.1
- [#55](https://github.com/Samourai-Wallet/samourai-dojo/pull/55) upgrade bitcoind to bitcoin core 0.18.1
- [#60](https://github.com/Samourai-Wallet/samourai-dojo/pull/55) fix for #59 (dojo with exposed bitcoind ports doesn't start)
@ -600,7 +689,7 @@ Added a new [doc](./doc/DOCKER_mac_setup.MD) for MacOS users.
- [#27](https://github.com/Samourai-Wallet/samourai-dojo/pull/27) rework external loop of Orchestrator
- [#28](https://github.com/Samourai-Wallet/samourai-dojo/pull/28) rework RemoteImporter
- [#32](https://github.com/Samourai-Wallet/samourai-dojo/pull/32) change the conditions switching the startup mode of the tracker
- [#33](https://github.com/Samourai-Wallet/samourai-dojo/pull/33) check authentication with admin key
- [#33](https://github.com/Samourai-Wallet/samourai-dojo/pull/33) check authentication with admin key
- [#37](https://github.com/Samourai-Wallet/samourai-dojo/pull/37) automatic redirect of onion address to maintenance tool
- [#38](https://github.com/Samourai-Wallet/samourai-dojo/pull/38) dojo shutdown - replace sleep with static delay by docker wait
@ -616,7 +705,7 @@ Added a new [doc](./doc/DOCKER_mac_setup.MD) for MacOS users.
- [#13](https://github.com/Samourai-Wallet/samourai-dojo/pull/13) included Mac instructions
- [92097d8](https://github.com/Samourai-Wallet/samourai-dojo/commit/92097d8ec7f9488ce0318c452356994315f4be72) doc
- [de4c9b5](https://github.com/Samourai-Wallet/samourai-dojo/commit/de4c9b5e5078b673c7b199503d48e7ceca328285) doc - minor updates
- [de4c9b5](https://github.com/Samourai-Wallet/samourai-dojo/commit/de4c9b5e5078b673c7b199503d48e7ceca328285) doc - minor updates
- [fead0bb](https://github.com/Samourai-Wallet/samourai-dojo/commit/fead0bb4b2b6174e637f5cb8c57edd9b55c3a1c7) doc - add link to MacOS install doc
- [#42](https://github.com/Samourai-Wallet/samourai-dojo/pull/42) fix few typos, add backticks for config values
- [#43](https://github.com/Samourai-Wallet/samourai-dojo/pull/43) add missing `d` in `docker-bitcoind.conf`
@ -624,7 +713,7 @@ Added a new [doc](./doc/DOCKER_mac_setup.MD) for MacOS users.
#### Misc ####
- [a382e42](https://github.com/Samourai-Wallet/samourai-dojo/commit/a382e42469b884d2eda9fa6f5a3c8ce93a7cd39a) add sql scripts and config files to gitignore
- [a382e42](https://github.com/Samourai-Wallet/samourai-dojo/commit/a382e42469b884d2eda9fa6f5a3c8ce93a7cd39a) add sql scripts and config files to gitignore
### Credits ###

2
accounts/index.js

@ -20,6 +20,7 @@
const TransactionsRestApi = require('./transactions-rest-api')
const StatusRestApi = require('./status-rest-api')
const notifServer = require('./notifications-server')
const WalletRestApi = require('./wallet-rest-api')
const MultiaddrRestApi = require('./multiaddr-rest-api')
const UnspentRestApi = require('./unspent-rest-api')
const SupportRestApi = require('./support-rest-api')
@ -63,6 +64,7 @@
const headersRestApi = new HeadersRestApi(httpServer)
const transactionsRestApi = new TransactionsRestApi(httpServer)
const statusRestApi = new StatusRestApi(httpServer)
const walletRestApi = new WalletRestApi(httpServer)
const multiaddrRestApi = new MultiaddrRestApi(httpServer)
const unspentRestApi = new UnspentRestApi(httpServer)
const supportRestApi = new SupportRestApi(httpServer)

1
accounts/multiaddr-rest-api.js

@ -17,6 +17,7 @@ const debugApi = !!(process.argv.indexOf('api-debug') > -1)
/**
* Multiaddr API endpoints
* @deprecated
*/
class MultiaddrRestApi {

14
accounts/notifications-service.js

@ -80,7 +80,7 @@ class NotificationsService {
})
conn.on('message', msg => {
if (msg.type == 'utf8')
if (msg.type == 'utf8')
this._handleWSMessage(msg.utf8Data, conn)
else
this._closeWSConnection(conn, true)
@ -95,7 +95,7 @@ class NotificationsService {
}
})
}
/**
* Close a web sockets connection
* @param {object} conn - web socket connection
@ -157,11 +157,11 @@ class NotificationsService {
// Check authentication (if needed)
if (authMgr.authActive && authMgr.isMandatory) {
try {
authMgr.isAuthenticated(msg.at)
authMgr.isAuthenticated(data.at)
} catch(e) {
this.notifyAuthError(e, conn.id)
return
}
}
}
switch(data.op) {
@ -236,7 +236,7 @@ class NotificationsService {
return false
const index = this.subs[topic].indexOf(cid)
if (index < 0)
if (index < 0)
return false
this.subs[topic].splice(index, 1)
@ -261,7 +261,7 @@ class NotificationsService {
return
for (let cid of this.subs[topic]) {
if (!this.conn[cid])
if (!this.conn[cid])
continue
try {
@ -469,7 +469,7 @@ class NotificationsService {
Logger.error(e, `API : NotificationsService.notifyAuthError() : Trouble sending authentication error to client ${cid}`)
}
}
}

12
accounts/support-rest-api.js

@ -44,7 +44,7 @@ class SupportRestApi {
this.getAddressInfo.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.get(
`/${keys.prefixes.support}/address/:addr/rescan`,
authMgr.checkHasAdminProfile.bind(authMgr),
@ -52,7 +52,7 @@ class SupportRestApi {
this.getAddressRescan.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.get(
`/${keys.prefixes.support}/xpub/:xpub/info`,
authMgr.checkHasAdminProfile.bind(authMgr),
@ -60,7 +60,7 @@ class SupportRestApi {
this.getXpubInfo.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.get(
`/${keys.prefixes.support}/xpub/:xpub/rescan`,
authMgr.checkHasAdminProfile.bind(authMgr),
@ -140,7 +140,7 @@ class SupportRestApi {
url: `/${keys.prefixes.support}/xpub/${info.xpub}/rescan`
})
}*/
return JSON.stringify(res, null, 2)
}
@ -167,7 +167,7 @@ class SupportRestApi {
url: `/${keys.prefixes.support}/address/${address}/info`
}]*/
}
await addrService.rescan(address)
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2))
@ -229,7 +229,7 @@ class SupportRestApi {
task: 'Rescan the whole HD account from remote sources',
url: `/${keys.prefixes.support}/xpub/${info.xpub}/rescan`
}]*/
return JSON.stringify(res, null, 2)
}

1
accounts/unspent-rest-api.js

@ -17,6 +17,7 @@ const debugApi = !!(process.argv.indexOf('api-debug') > -1)
/**
* Unspent API endpoints
* @deprecated
*/
class UnspentRestApi {

136
accounts/wallet-rest-api.js

@ -0,0 +1,136 @@
/*!
* accounts/wallet-rest-api.js
* Copyright © 2019 Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const bodyParser = require('body-parser')
const Logger = require('../lib/logger')
const errors = require('../lib/errors')
const walletService = require('../lib/wallet/wallet-service')
const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const debugApi = !!(process.argv.indexOf('api-debug') > -1)
/**
* Wallet API endpoints
*/
class WalletRestApi {
/**
* Constructor
* @param {pushtx.HttpServer} httpServer - HTTP server
*/
constructor(httpServer) {
this.httpServer = httpServer
// Establish routes
const urlencodedParser = bodyParser.urlencoded({ extended: true })
this.httpServer.app.get(
'/wallet',
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.getWallet.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.post(
'/wallet',
urlencodedParser,
authMgr.checkAuthentication.bind(authMgr),
apiHelper.validateEntitiesParams.bind(apiHelper),
this.postWallet.bind(this),
HttpServer.sendAuthError
)
}
/**
* Handle wallet GET request
* @param {object} req - http request object
* @param {object} res - http response object
*/
async getWallet(req, res) {
try {
// Check request params
if (!apiHelper.checkEntitiesParams(req.query))
return HttpServer.sendError(res, errors.multiaddr.NOACT)
// Parse params
const entities = apiHelper.parseEntitiesParams(req.query)
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
)
const ret = JSON.stringify(result, null, 2)
HttpServer.sendRawData(res, ret)
} catch(e) {
HttpServer.sendError(res, e)
} finally {
if (debugApi) {
const strParams =
`${req.query.active ? req.query.active : ''} \
${req.query.new ? req.query.new : ''} \
${req.query.pubkey ? req.query.pubkey : ''} \
${req.query.bip49 ? req.query.bip49 : ''} \
${req.query.bip84 ? req.query.bip84 : ''}`
Logger.info(`API : Completed GET /wallet ${strParams}`)
}
}
}
/**
* Handle wallet POST request
* @param {object} req - http request object
* @param {object} res - http response object
*/
async postWallet(req, res) {
try {
// Check request params
if (!apiHelper.checkEntitiesParams(req.body))
return HttpServer.sendError(res, errors.multiaddr.NOACT)
// Parse params
const entities = apiHelper.parseEntitiesParams(req.body)
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,
entities.bip49,
entities.bip84,
entities.pubkey
)
HttpServer.sendOkDataOnly(res, result)
} catch(e) {
HttpServer.sendError(res, e)
} finally {
if (debugApi) {
const strParams =
`${req.body.active ? req.body.active : ''} \
${req.body.new ? req.body.new : ''} \
${req.body.pubkey ? req.body.pubkey : ''} \
${req.body.bip49 ? req.body.bip49 : ''} \
${req.body.bip84 ? req.body.bip84 : ''}`
Logger.info(`API : Completed POST /wallet ${strParams}`)
}
}
}
}
module.exports = WalletRestApi

43
accounts/xpub-rest-api.js

@ -48,6 +48,14 @@ class XPubRestApi {
HttpServer.sendAuthError
)
this.httpServer.app.get(
'/xpub/:xpub/import/status',
authMgr.checkAuthentication.bind(authMgr),
this.validateArgsGetXpub.bind(this),
this.getXpubImportStatus.bind(this),
HttpServer.sendAuthError
)
this.httpServer.app.get(
'/xpub/:xpub',
authMgr.checkAuthentication.bind(authMgr),
@ -202,6 +210,41 @@ class XPubRestApi {
}
}
/**
* Handle xPub/import/status GET request
* @param {object} req - http request object
* @param {object} res - http response object
*/
async getXpubImportStatus(req, res) {
try {
let xpub
// Extracts arguments
const argXpub = req.params.xpub
// Translate xpub if needed
try {
const xlatXpub = this.xlatHdAccount(argXpub)
xpub = xlatXpub.xpub
} catch(e) {
return HttpServer.sendError(res, e)
}
const ret = {
import_in_progress: hdaService.importInProgress(xpub)
}
HttpServer.sendOkData(res, ret)
} catch(e) {
Logger.error(e, 'API : XpubRestApi.getXpubImportStatus()')
HttpServer.sendError(res, e)
} finally {
debugApi && Logger.info(`API : Completed GET /xpub/${req.params.xpub}/import/status`)
}
}
/**
* Handle Lock XPub POST request
* @param {object} req - http request object

2
doc/GET_multiaddr.md

@ -1,5 +1,7 @@
# Get Multiaddr
Note: Starting with Dojo 1.8.0, this API endpoint is deprecated. See the new [/wallet endpoint](./GET_wallet.md)
Request details about a collection of HD accounts and/or loose addresses and/or pubkeys (derived in 3 formats P2PKH, P2WPKH/P2SH, P2WPKH Bech32).

2
doc/GET_unspent.md

@ -1,5 +1,7 @@
# Get Unspent
Note: Starting with Dojo 1.8.0, this API endpoint is deprecated. See the new [/wallet endpoint](./GET_wallet.md)
Request a list of unspent transaction outputs from a collection of HD accounts and/or loose addresses and/or pubkeys (derived in 3 formats P2PKH, P2WPKH/P2SH, P2WPKH Bech32).

165
doc/GET_wallet.md

@ -0,0 +1,165 @@
# Get Wallet
Request details about a collection of HD accounts and/or loose addresses and/or pubkeys (derived in 3 formats P2PKH, P2WPKH/P2SH, P2WPKH Bech32) including a list of unspent transaction outputs.
This endpoint merges the deprecated /multiaddr and /unspent endpoints augmented with feerates info provided by the /fees endpoint.
## Behavior of the active parameter
If accounts passed to `?active` do not exist, they will be created with a relayed call to the [POST /xpub](./POST_xpub.md) mechanics if new or will be imported from external data sources.
If loose addresses passed to `?active` do not exist, they will be imported from external data sources.
If addresses derived from pubkeys passed to `?active` do not exist, they will be imported from external data sources.
## Declaration of new entities
Instruct the server that [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) entities are new with `?new=xpub1|addr2|addr3` in the query parameters, and the server will skip importing for those entities.
SegWit support via [BIP49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) is activated for new ypubs and new P2WPKH/P2SH loose addresses with `?bip49=xpub3|xpub4`.
SegWit support via [BIP84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) is activated for new zpubs and new P2WPKH Bech32 loose addresses with `?bip84=xpub3|xpub4`.
Support of [BIP47](https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki) with addresses derived in 3 formats (P2PKH, P2WPKH/P2SH, P2WPKH Bech32) is activated for new pubkeys with `?pubkey=pubkey1|pubkey2`.
Note that loose addresses that are also part of one of the HD accounts requested will be ignored. Their balances and transactions are listed as part of the HD account result.
The `POST` version of `/wallet` is identical, except the parameters are in the POST body.
```
GET /wallet?active=...[&new=...][&bip49=...][&bip84=...][&pubkey=...]
```
## Parameters
* **active** - `string` - A pipe-separated list of extended public keys and/or loose addresses and/or pubkeys (`xpub1|address1|address2|pubkey1|...`)
* **new** - `string` - A pipe-separated list of **new** extended public keys to be derived via [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) and/or new P2PKH loose addresses
* **bip49** - `string` - A pipe-separated list of **new** extended public keys to be derived via [BIP49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) and/or new P2WPKH/P2SH loose addresses
* **bip84** - `string` - A pipe-separated list of **new** extended public keys to be derived via [BIP84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) and/or new P2WPKH Bech32 loose addresses
* **pubkey** - `string` - A pipe-separated list of **new** public keys to be derived as P2PKH, P2WPKH/P2SH, P2WPKH Bech32 addresses
* **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme).
### Examples
```
GET /wallet?active=xpub0123456789&new=address2|address3&pubkey=pubkey4
GET /wallet?active=xpub0123456789|address1|address2
GET /wallet?bip49=xpub0123456789
GET /wallet?bip84=xpub0123456789
GET /wallet?pubkey=0312345678901
```
#### Success
Status code 200 with JSON response:
```json
{
"wallet": {
"final_balance": 100000000
},
"info": {
"latest_block": {
"height": 100000,
"hash": "abcdef",
"time": 1000000000
},
"fees": {
"2": 181,
"4": 150,
"6": 150,
"12": 111,
"24": 62
}
},
"addresses": [
{
"address": "xpubABCDEF -or- 1xAddress",
"pubkey": "04Pubkey -or- inexistant attribute"
"final_balance": 100000000,
"account_index": 0,
"change_index": 0,
"n_tx": 0
}
],
"txs": [
{
"block_height": 100000,
"hash": "abcdef",
"version": 1,
"locktime": 0,
"result": -10000,
"balance": 90000,
"time": 1400000000,
"inputs": [
{
"vin": 1,
"prev_out": {
"txid": "abcdef",
"vout": 2,
"value": 20000,
"xpub": {
"m": "xpubABCDEF",
"path": "M/0/3"
},
"addr": "1xAddress",
"pubkey": "04Pubkey"
},
"sequence": 4294967295
}
],
"out": [
{
"n": 2,
"value": 10000,
"addr": "1xAddress",
"pubkey": "03Pubkey"
"xpub": {
"m": "xpubABCDEF",
"path": "M/1/5"
}
}
]
}
],
"unspent_outputs": [
{
"tx_hash": "abcdef",
"tx_output_n": 2,
"tx_version": 1,
"tx_locktime": 0,
"value": 10000,
"script": "abcdef",
"addr": "1xAddress",
"pubkey": "03Pubkey -or- inexistant attribute"
"confirmations": 10000,
"xpub": {
"m": "xpubABCDEF",
"path": "M/1/5"
}
}
]
}
```
**Notes**
* The transaction `inputs` and `out` arrays are for known addresses only and do not reflect the full input and output list of the transaction on the blockchain
* `result.addresses[i].n_tx` used by BIP47 logic to detemine unused index
* `result.txs[i].block_height` should not be present for unconfirmed transactions
* `result.txs[i].result` is the change in value for the "wallet" as defined by all entries on the `active` query parameter
* `result.txs[i].inputs[j].prev_out.addr` should be present for BIP47-related addresses but may be `null` if the previous output address is unknown
* `result.txs[i].out[j].addr` should be present for BIP47-related addresses
#### Failure
Status code 400 with JSON response:
```json
{
"status": "error",
"error": "<error message>"
}
```
## Notes
Wallet response is consumed by the wallet in the [APIFactory](https://code.samourai.io/wallet/samourai-wallet-android/-/blob/master/app/src/main/java/com/samourai/wallet/api/APIFactory.java)

37
doc/GET_xpub_import_status.md

@ -0,0 +1,37 @@
# Get import status for a HD Account
Check if an import or a rescan is currently processed by Dojo for a given HD Account.
```
GET /xpub/:xpub/import/status
```
## Parameters
* **:xpub** - `string` - The extended public key for the HD Account
* **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme).
### Example
```
GET /xpub/xpub0123456789/import/status
```
#### Success
Status code 200 with JSON response:
```json
{
"status": "ok",
"data": {
"import_in_progress": false
}
}
```
#### Failure
Status code 400 with JSON response:
```json
{
"status": "error",
"error": "<error message>"
}
```

10
docker/my-dojo/.env

@ -10,15 +10,15 @@
COMPOSE_CONVERT_WINDOWS_PATHS=1
DOJO_VERSION_TAG=1.7.0
DOJO_VERSION_TAG=1.8.0
DOJO_DB_VERSION_TAG=1.2.0
DOJO_BITCOIND_VERSION_TAG=1.6.0
DOJO_NODEJS_VERSION_TAG=1.7.0
DOJO_BITCOIND_VERSION_TAG=1.8.0
DOJO_NODEJS_VERSION_TAG=1.8.0
DOJO_NGINX_VERSION_TAG=1.5.0
DOJO_TOR_VERSION_TAG=1.4.0
DOJO_TOR_VERSION_TAG=1.5.0
DOJO_EXPLORER_VERSION_TAG=1.3.0
DOJO_INDEXER_VERSION_TAG=1.1.0
DOJO_WHIRLPOOL_VERSION_TAG=1.1.0
DOJO_WHIRLPOOL_VERSION_TAG=1.2.0
#########################################

8
docker/my-dojo/bitcoin/Dockerfile

@ -5,10 +5,10 @@ FROM debian:buster
# INSTALL BITCOIN
#################################################################
ENV BITCOIN_HOME /home/bitcoin
ENV BITCOIN_VERSION 0.20.0
ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-0.20.0/bitcoin-0.20.0-x86_64-linux-gnu.tar.gz
ENV BITCOIN_SHA256 35ec10f87b6bc1e44fd9cd1157e5dfa483eaf14d7d9a9c274774539e7824c427
ENV BITCOIN_ASC_URL https://bitcoincore.org/bin/bitcoin-core-0.20.0/SHA256SUMS.asc
ENV BITCOIN_VERSION 0.20.1
ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz
ENV BITCOIN_SHA256 376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397
ENV BITCOIN_ASC_URL https://bitcoincore.org/bin/bitcoin-core-0.20.1/SHA256SUMS.asc
ENV BITCOIN_PGP_KS_URI hkp://keyserver.ubuntu.com:80
ENV BITCOIN_PGP_KEY 01EA5486DE18A882D4C2684590C8019E36C2E964

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

@ -24,6 +24,7 @@ bitcoind_options=(
-rpcpassword=$BITCOIND_RPC_PASSWORD
-rpcport=28256
-rpcthreads=$BITCOIND_RPC_THREADS
-rpcworkqueue=$BITCOIND_RPC_WORK_QUEUE
-rpcuser=$BITCOIND_RPC_USER
-server=1
-txindex=1

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

@ -26,6 +26,10 @@ BITCOIND_DB_CACHE=1024
# Type: integer
BITCOIND_RPC_THREADS=6
# RPC Work queue size
# Type: integer
BITCOIND_RPC_WORK_QUEUE=16
# Mempool expiry in hours
# Defines how long transactions stay in your local mempool before expiring
# Type: integer
@ -100,4 +104,15 @@ BITCOIND_ZMQ_RAWTXS=9501
# Port exposing ZMQ notifications for block hashes
# Set value to 9502 if BITCOIND_INSTALL is set to 'on'
# Type: integer
BITCOIND_ZMQ_BLK_HASH=9502
BITCOIND_ZMQ_BLK_HASH=9502
#
# SHUTDOWN
#
# Max delay for bitcoind shutdown (expressed in seconds)
# Defines how long Dojo waits for a clean shutdown of bitcoind before shutting down the bitcoind container
# This parameter is inactive if BITCOIND_INSTALL is set to 'off'
# Type: integer
BITCOIND_SHUTDOWN_DELAY=180

35
docker/my-dojo/dojo.sh

@ -57,7 +57,7 @@ docker_up() {
yamlFiles=$(select_yaml_files)
eval "docker-compose $yamlFiles up $1 -d"
}
# Start
start() {
# Check if dojo is running (check the db container)
@ -96,7 +96,8 @@ stop() {
# Check if the bitcoin daemon is still up
# wait 3mn max
i="0"
while [ $i -lt 18 ]
nbIters=$(( $BITCOIND_SHUTDOWN_DELAY / 10 ))
while [ $i -lt $nbIters ]
do
echo "Waiting for shutdown of Bitcoin server."
# Check if bitcoind rpc api is responding
@ -115,7 +116,7 @@ stop() {
done
# Bitcoin daemon is still up
# => force close
if [ $i -eq 18 ]; then
if [ $i -eq $nbIters ]; then
echo "Force shutdown of Bitcoin server."
fi
fi
@ -255,7 +256,7 @@ uninstall() {
del_images_for() {
# $1: image name
# $2: most recent version of the image (do not delete this one)
docker image ls | grep "$1" | sed "s/ \+/,/g" | cut -d"," -f2 | while read -r version ; do
docker image ls | grep "$1" | sed "s/ \+/,/g" | cut -d"," -f2 | while read -r version ; do
if [ "$2" != "$version" ]; then
docker image rm "$1:$version"
fi
@ -336,22 +337,32 @@ upgrade() {
# Display the onion address
onion() {
echo " "
echo "WARNING: Do not share these onion addresses with anyone!"
echo " To allow another person to use this Dojo with their Samourai Wallet,"
echo " you should share the QRCodes provided by the Maintenance Tool."
echo " "
V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname )
echo "Dojo API and Maintenance Tool = $V3_ADDR"
echo " "
if [ "$EXPLORER_INSTALL" == "on" ]; then
V3_ADDR_EXPLORER=$( docker exec -it tor cat /var/lib/tor/hsv3explorer/hostname )
echo "Explorer hidden service address = $V3_ADDR_EXPLORER"
echo "Block Explorer = $V3_ADDR_EXPLORER"
echo " "
fi
V3_ADDR=$( docker exec -it tor cat /var/lib/tor/hsv3dojo/hostname )
echo "Maintenance Tool hidden service address = $V3_ADDR"
if [ "$WHIRLPOOL_INSTALL" == "on" ]; then
V3_ADDR_WHIRLPOOL=$( docker exec -it tor cat /var/lib/tor/hsv3whirlpool/hostname )
echo "Whirlpool API hidden service address = $V3_ADDR_WHIRLPOOL"
echo "Your private Whirlpool client (do not share) = $V3_ADDR_WHIRLPOOL"
echo " "
fi
if [ "$BITCOIND_INSTALL" == "on" ]; then
V2_ADDR_BTCD=$( docker exec -it tor cat /var/lib/tor/hsv2bitcoind/hostname )
echo "bitcoind hidden service address = $V2_ADDR_BTCD"
echo "Your local bitcoind (do not share) = $V2_ADDR_BTCD"
echo " "
fi
}
@ -390,7 +401,7 @@ display_logs() {
docker-compose $yamlFiles logs --tail=50 --follow $1
else
docker-compose $yamlFiles logs --tail=$2 $1
fi
fi
}
logs() {
@ -433,7 +444,7 @@ logs() {
fi
;;
* )
services="nginx node tor db"
services="nginx node tor db"
if [ "$BITCOIND_INSTALL" == "on" ]; then
services="$services bitcoind"
fi

13
docker/my-dojo/tor/Dockerfile

@ -2,6 +2,7 @@ FROM debian:buster
ENV TOR_HOME /var/lib/tor
ENV TOR_URL https://archive.torproject.org/tor-package-archive
ENV TOR_MIRROR_URL https://tor.eff.org/dist
ENV TOR_VERSION 0.4.2.7
ENV TOR_GPG_KS_URI hkp://keyserver.ubuntu.com:80
ENV TOR_GPG_KEY1 0xEB5A896A28988BF5
@ -23,8 +24,16 @@ RUN set -ex && \
apt-get install -y git libevent-dev zlib1g-dev libssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils gpg wget && \
mkdir -p /usr/local/src/ && \
cd /usr/local/src && \
wget -qO "tor-$TOR_VERSION.tar.gz" "$TOR_URL/tor-$TOR_VERSION.tar.gz" && \
wget -qO "tor-$TOR_VERSION.tar.gz.asc" "$TOR_URL/tor-$TOR_VERSION.tar.gz.asc" && \
res=0; \
wget -qO "tor-$TOR_VERSION.tar.gz" "$TOR_URL/tor-$TOR_VERSION.tar.gz" || res=$?; \
if [ $res -gt 0 ]; then \
wget -qO "tor-$TOR_VERSION.tar.gz" "$TOR_MIRROR_URL/tor-$TOR_VERSION.tar.gz"; \
fi && \
res=0; \
wget -qO "tor-$TOR_VERSION.tar.gz.asc" "$TOR_URL/tor-$TOR_VERSION.tar.gz.asc" || res=$?; \
if [ $res -gt 0 ]; then \
wget -qO "tor-$TOR_VERSION.tar.gz.asc" "$TOR_MIRROR_URL/tor-$TOR_VERSION.tar.gz.asc"; \
fi && \
gpg --keyserver "$TOR_GPG_KS_URI" --recv-keys "$TOR_GPG_KEY1" && \
gpg --keyserver "$TOR_GPG_KS_URI" --recv-keys "$TOR_GPG_KEY2" && \
gpg --keyserver "$TOR_GPG_KS_URI" --recv-keys "$TOR_GPG_KEY3" && \

13
docker/my-dojo/whirlpool/Dockerfile

@ -20,6 +20,7 @@ RUN set -ex && \
# Install Tor
ENV WHIRLPOOL_TOR_URL https://archive.torproject.org/tor-package-archive
ENV WHIRLPOOL_TOR_MIRROR_URL https://tor.eff.org/dist
ENV WHIRLPOOL_TOR_VERSION 0.4.2.7
ENV WHIRLPOOL_TOR_GPG_KS_URI hkp://keyserver.ubuntu.com:80
ENV WHIRLPOOL_TOR_GPG_KEY1 0xEB5A896A28988BF5
@ -30,8 +31,16 @@ ENV WHIRLPOOL_TOR_GPG_KEY4 0x6AFEE6D49E92B601
RUN set -ex && \
mkdir -p /usr/local/src/ && \
cd /usr/local/src && \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz" "$WHIRLPOOL_TOR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz" && \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" "$WHIRLPOOL_TOR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" && \
res=0; \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz" "$WHIRLPOOL_TOR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz" || res=$?; \
if [ $res -gt 0 ]; then \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz" "$WHIRLPOOL_TOR_MIRROR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz"; \
fi && \
res=0; \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" "$WHIRLPOOL_TOR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" || res=$?; \
if [ $res -gt 0 ]; then \
wget -qO "tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" "$WHIRLPOOL_TOR_MIRROR_URL/tor-$WHIRLPOOL_TOR_VERSION.tar.gz.asc" ; \
fi && \
gpg --keyserver "$WHIRLPOOL_TOR_GPG_KS_URI" --recv-keys "$WHIRLPOOL_TOR_GPG_KEY1" && \
gpg --keyserver "$WHIRLPOOL_TOR_GPG_KS_URI" --recv-keys "$WHIRLPOOL_TOR_GPG_KEY2" && \
gpg --keyserver "$WHIRLPOOL_TOR_GPG_KS_URI" --recv-keys "$WHIRLPOOL_TOR_GPG_KEY3" && \

10
keys/index-example.js

@ -15,7 +15,7 @@ module.exports = {
/*
* Dojo version
*/
dojoVersion: '1.6.0',
dojoVersion: '1.8.0',
/*
* Bitcoind
*/
@ -36,7 +36,7 @@ module.exports = {
// ZMQ Block notifications
zmqBlk: 'tcp://127.0.0.1:9502',
// Fee type (estimatesmartfee)
feeType: 'ECONOMICAL'
feeType: 'ECONOMICAL'
},
/*
* MySQL database
@ -201,10 +201,10 @@ module.exports = {
minNbChildren: 2,
// Max number of child processes allowed
maxNbChildren: 2,
// Max duration
// Max duration
acquireTimeoutMillis: 60000,
// Parallel derivation threshold
// (use parallel derivation if number of addresses to be derived
// (use parallel derivation if number of addresses to be derived
// is greater than thresholdParalleDerivation)
thresholdParallelDerivation: 10
},
@ -232,7 +232,7 @@ module.exports = {
* Testnet parameters
*/
testnet: {
dojoVersion: '1.6.0',
dojoVersion: '1.8.0',
bitcoind: {
rpc: {
user: 'user',

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

@ -171,6 +171,16 @@ class HDAccountsService {
}
}
/**
* Check if a xpub is currently being imported or rescanned by Dojo
* Returns true if import/rescan is in progress, otherwise returns false
* @param {string} xpub - xpub
* @returns {Promise}
*/
importInProgress(xpub) {
return remote.importInProgress(xpub)
}
/**
* Check if we try to override an existing xpub
* Delete the old xpub from db if it's the case

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

@ -48,6 +48,16 @@ class RemoteImporter {
delete this.importing[xpub]
}
/**
* Check if a xpub is currently being imported or rescanned by Dojo
* Returns true if import/rescan is in progress, otherwise returns false
* @param {string} xpub - xpub
* @returns {boolean}
*/
importInProgress(xpub) {
return this.importing[xpub] ? true : false
}
/**
* Process the relations between a list of transactions
* @param {object[]} txs - array of transaction objects

11
lib/wallet/wallet-info.js

@ -7,6 +7,7 @@
const db = require('../db/mysql-db-wrapper')
const util = require('../util')
const rpcLatestBlock = require('../bitcoind-rpc/latest-block')
const rpcFees = require('../bitcoind-rpc/fees')
const addrService = require('../bitcoin/addresses-service')
const HdAccountInfo = require('./hd-account-info')
const AddressInfo = require('./address-info')
@ -31,6 +32,7 @@ class WalletInfo {
}
this.info = {
fees: {},
latestBlock: {
height: rpcLatestBlock.height,
hash: rpcLatestBlock.hash,
@ -159,6 +161,14 @@ class WalletInfo {
this.nTx = nbTxs
}
/**
* Loads tinfo about the fee rates
* @returns {Promise}
*/
async loadFeesInfo() {
this.info.fees = await rpcFees.getFees()
}
/**
* Loads the list of unspent outputs for this wallet
* @returns {Promise}
@ -295,6 +305,7 @@ class WalletInfo {
final_balance: this.wallet.finalBalance
},
info: {
fees: this.info.fees,
latest_block: this.info.latestBlock
},
addresses: this.addresses.map(a => a.toPojo()),

91
lib/wallet/wallet-service.js

@ -25,8 +25,75 @@ class WalletService {
*/
constructor() {}
/**
* Get full wallet information
* @param {object} active - mapping of active entities
* @param {object} legacy - mapping of new legacy addresses
* @param {object} bip49 - mapping of new bip49 addresses
* @param {object} bip84 - mapping of new bip84 addresses
* @param {object} pubkeys - mapping of new pubkeys/addresses
* @returns {Promise}
*/
async getFullWalletInfo(active, legacy, bip49, bip84, pubkeys) {
// Check parameters
const validParams = this._checkEntities(active, legacy, bip49, bip84, pubkeys)
if (!validParams) {
const info = new WalletInfo()
const ret = this._formatGetFullWalletInfoResult(info)
return Promise.resolve(ret)
}
// Merge all entities into active mapping
active = this._mergeEntities(active, legacy, bip49, bip84, pubkeys)
// Initialize a WalletInfo object
const walletInfo = new WalletInfo(active)
try {
// Add the new xpubs
await util.seriesCall(legacy.xpubs, this._newBIP44)
await util.seriesCall(bip49.xpubs, this._newBIP49)
await util.seriesCall(bip84.xpubs, this._newBIP84)
// Load hd accounts info
await walletInfo.ensureHdAccounts()
await walletInfo.loadHdAccountsInfo()
// Add the new addresses
await db.addAddresses(legacy.addrs)
await db.addAddresses(bip49.addrs)
await db.addAddresses(bip84.addrs)
await db.addAddresses(pubkeys.addrs)
// Ensure addresses exist
await walletInfo.ensureAddresses()
// Force import of addresses associated to paynyms
// if dojo relies on a local index
if (keys.indexer.active != 'third_party_explorer')
await this._forceEnsureAddressesForActivePubkeys(active)
// Filter the addresses
await walletInfo.filterAddresses()
// Load the utxos
await walletInfo.loadUtxos()
// Load the addresses
await walletInfo.loadAddressesInfo()
// Load the most recent transactions
await walletInfo.loadTransactions(0, null, true)
// Load feerates
await walletInfo.loadFeesInfo()
// Postprocessing
await walletInfo.postProcessAddresses()
await walletInfo.postProcessHdAccounts()
// Format the result
return this._formatGetFullWalletInfoResult(walletInfo)
} catch(e) {
Logger.error(e, 'WalletService.getWalletInfo()')
return Promise.reject({status:'error', error:'internal server error'})
}
}
/**
* Get wallet information
* @deprecated
* @param {object} active - mapping of active entities
* @param {object} legacy - mapping of new legacy addresses
* @param {object} bip49 - mapping of new bip49 addresses
@ -86,8 +153,28 @@ class WalletService {
}
}
/**
* Prepares the result to be returned by getFullWalletInfo()
* @param {WalletInfo} info
* @returns {object}
*/
_formatGetFullWalletInfoResult(info) {
let ret = info.toPojo()
delete ret['n_tx']
ret.addresses = ret.addresses.map(x => {
delete x['derivation']
delete x['created']
return x
})
return ret
}
/**
* Prepares the result to be returned by getWalletInfo()
* @deprecated
* @param {WalletInfo} info
* @returns {object}
*/
@ -96,6 +183,7 @@ class WalletService {
delete ret['n_tx']
delete ret['unspent_outputs']
delete ret['info']['fees']
ret.addresses = ret.addresses.map(x => {
delete x['derivation']
@ -108,6 +196,7 @@ class WalletService {
/**
* Get wallet unspent outputs
* @deprecated
* @param {object} active - mapping of active entities
* @param {object} legacy - mapping of new legacy addresses
* @param {object} bip49 - mapping of new bip49 addresses
@ -167,7 +256,7 @@ class WalletService {
}
/**
* Get a subset of wallet transaction
* Get a subset of wallet transactions
* @param {object} entities - mapping of active entities
* @param {integer} page - page of transactions to be returned
* @param {integer} count - number of transactions returned per page

653
package-lock.json

File diff suppressed because it is too large

8
package.json

@ -1,6 +1,6 @@
{
"name": "samourai-dojo",
"version": "1.7.0",
"version": "1.8.0",
"description": "Backend server for Samourai Wallet",
"main": "accounts/index.js",
"scripts": {
@ -23,10 +23,10 @@
"express": "4.16.3",
"express-jwt": "5.3.1",
"generic-pool": "3.4.2",
"helmet": "3.12.1",
"lodash": "4.17.14",
"helmet": "3.23.3",
"lodash": "4.17.19",
"lru-cache": "4.0.2",
"minimist": "1.2.2",
"minimist": "1.2.3",
"mysql": "2.16.0",
"passport": "0.4.0",
"passport-localapikey-update": "0.6.0",

6
pushtx/pushtx-rest-api.js

@ -94,7 +94,7 @@ class PushTxRestApi {
* Handle Status GET request
* @param {object} req - http request object
* @param {object} res - http response object
*/
*/
async getStatus(req, res) {
try {
const currStatus = await status.getCurrent()
@ -108,7 +108,7 @@ class PushTxRestApi {
* Handle status/schedule GET request
* @param {object} req - http request object
* @param {object} res - http response object
*/
*/
async getStatusSchedule(req, res) {
try {
const ret = await status.getScheduledTransactions()
@ -178,7 +178,7 @@ class PushTxRestApi {
HttpServer.sendOkData(res, txid)
} catch(e) {
this._traceError(res, e)
}
}
})
}

0
restart-example.sh

4
static/admin/conf/index-mainnet.js

@ -1,4 +1,4 @@
var conf = {
const conf = {
// Admin tool
adminTool: {
@ -22,4 +22,4 @@ var conf = {
statusPushtx: 'status'
}
};
}

4
static/admin/conf/index-testnet.js

@ -1,4 +1,4 @@
var conf = {
const conf = {
// Admin tool
adminTool: {
@ -22,4 +22,4 @@ var conf = {
statusPushtx: 'status'
}
};
}

587
static/admin/css/bootstrap-theme.css

@ -1,587 +0,0 @@
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-default.disabled,
.btn-primary.disabled,
.btn-success.disabled,
.btn-info.disabled,
.btn-warning.disabled,
.btn-danger.disabled,
.btn-default[disabled],
.btn-primary[disabled],
.btn-success[disabled],
.btn-info[disabled],
.btn-warning[disabled],
.btn-danger[disabled],
fieldset[disabled] .btn-default,
fieldset[disabled] .btn-primary,
fieldset[disabled] .btn-success,
fieldset[disabled] .btn-info,
fieldset[disabled] .btn-warning,
fieldset[disabled] .btn-danger {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-default .badge,
.btn-primary .badge,
.btn-success .badge,
.btn-info .badge,
.btn-warning .badge,
.btn-danger .badge {
text-shadow: none;
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-default.disabled,
.btn-default[disabled],
fieldset[disabled] .btn-default,
.btn-default.disabled:hover,
.btn-default[disabled]:hover,
fieldset[disabled] .btn-default:hover,
.btn-default.disabled:focus,
.btn-default[disabled]:focus,
fieldset[disabled] .btn-default:focus,
.btn-default.disabled.focus,
.btn-default[disabled].focus,
fieldset[disabled] .btn-default.focus,
.btn-default.disabled:active,
.btn-default[disabled]:active,
fieldset[disabled] .btn-default:active,
.btn-default.disabled.active,
.btn-default[disabled].active,
fieldset[disabled] .btn-default.active {
background-color: #e0e0e0;
background-image: none;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #245580;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #265a88;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #265a88;
border-color: #245580;
}
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled.focus,
.btn-primary[disabled].focus,
fieldset[disabled] .btn-primary.focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active {
background-color: #265a88;
background-image: none;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-success.disabled,
.btn-success[disabled],
fieldset[disabled] .btn-success,
.btn-success.disabled:hover,
.btn-success[disabled]:hover,
fieldset[disabled] .btn-success:hover,
.btn-success.disabled:focus,
.btn-success[disabled]:focus,
fieldset[disabled] .btn-success:focus,
.btn-success.disabled.focus,
.btn-success[disabled].focus,
fieldset[disabled] .btn-success.focus,
.btn-success.disabled:active,
.btn-success[disabled]:active,
fieldset[disabled] .btn-success:active,
.btn-success.disabled.active,
.btn-success[disabled].active,
fieldset[disabled] .btn-success.active {
background-color: #419641;
background-image: none;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-info.disabled,
.btn-info[disabled],
fieldset[disabled] .btn-info,
.btn-info.disabled:hover,
.btn-info[disabled]:hover,
fieldset[disabled] .btn-info:hover,
.btn-info.disabled:focus,
.btn-info[disabled]:focus,
fieldset[disabled] .btn-info:focus,
.btn-info.disabled.focus,
.btn-info[disabled].focus,
fieldset[disabled] .btn-info.focus,
.btn-info.disabled:active,
.btn-info[disabled]:active,
fieldset[disabled] .btn-info:active,
.btn-info.disabled.active,
.btn-info[disabled].active,
fieldset[disabled] .btn-info.active {
background-color: #2aabd2;
background-image: none;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-warning.disabled,
.btn-warning[disabled],
fieldset[disabled] .btn-warning,
.btn-warning.disabled:hover,
.btn-warning[disabled]:hover,
fieldset[disabled] .btn-warning:hover,
.btn-warning.disabled:focus,
.btn-warning[disabled]:focus,
fieldset[disabled] .btn-warning:focus,
.btn-warning.disabled.focus,
.btn-warning[disabled].focus,
fieldset[disabled] .btn-warning.focus,
.btn-warning.disabled:active,
.btn-warning[disabled]:active,
fieldset[disabled] .btn-warning:active,
.btn-warning.disabled.active,
.btn-warning[disabled].active,
fieldset[disabled] .btn-warning.active {
background-color: #eb9316;
background-image: none;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.btn-danger.disabled,
.btn-danger[disabled],
fieldset[disabled] .btn-danger,
.btn-danger.disabled:hover,
.btn-danger[disabled]:hover,
fieldset[disabled] .btn-danger:hover,
.btn-danger.disabled:focus,
.btn-danger[disabled]:focus,
fieldset[disabled] .btn-danger:focus,
.btn-danger.disabled.focus,
.btn-danger[disabled].focus,
fieldset[disabled] .btn-danger.focus,
.btn-danger.disabled:active,
.btn-danger[disabled]:active,
fieldset[disabled] .btn-danger:active,
.btn-danger.disabled.active,
.btn-danger[disabled].active,
fieldset[disabled] .btn-danger.active {
background-color: #c12e2a;
background-image: none;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #2e6da4;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
}
.navbar-inverse .navbar-nav > .open > a,
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
@media (max-width: 767px) {
.navbar .navbar-nav .open .dropdown-menu > .active > a,
.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #fff;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-striped {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #286090;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
background-repeat: repeat-x;
border-color: #2b669a;
}
.list-group-item.active .badge,
.list-group-item.active:hover .badge,
.list-group-item.active:focus .badge {
text-shadow: none;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
background-repeat: repeat-x;
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
background-repeat: repeat-x;
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */

6757
static/admin/css/bootstrap.css

File diff suppressed because it is too large

697
static/admin/css/style.css

@ -13,7 +13,6 @@ h1 {
input, select {
padding: 5px;
border: 0;
margin-bottom: 16px;
width: 100%;
border: 1px solid #bfbfbf;
background-color: #1a1d1f;
@ -36,6 +35,31 @@ a:hover {
color: #2196f3;
}
.row {
margin-left: 0;
margin-right: 0;
}
.small {
font-size: 11px;
}
.beta {
font-size: 11px;
color: #9f9f9f;
}
/* RAW TX */
pre.raw-tx {
white-space: pre-wrap;
max-width: 500px;
color: #e0e0e3;
background: #1a1d1f;
border-radius: 0;
border: 0;
}
/* ICONS */
.mini-icon {
height: 16px;
width: 16px;
@ -46,7 +70,6 @@ a:hover {
width: 32px;
}
/* ALIGNMENTS */
.left {
text-align: left!important;
@ -64,17 +87,32 @@ a:hover {
text-align: justify!important;
}
/* BOXES WIDTHS */
.halfwidth-left {
width: 49%;
min-width: 49%;
margin-left: 0;
margin-right: 0.7%;
}
.small {
font-size: 11px;
.halfwidth-right {
width: 49%;
min-width: 49%;
margin-left: 0.7%;
margin-right: 0;
}
.beta {
font-size: 11px;
color: #9f9f9f;
.fullwidth {
width: 100%;
min-width: 100%;
}
/* BUTTONS*/
.btn {
font-size: 12px;
padding: 4px 12px;
}
/* BUTTON SUCCESS */
.btn-success {
background-image: -webkit-linear-gradient(top, #2196f3 0%, #1186e3 100%);
background-image: -o-linear-gradient(top, #2196f3 0%, #1186e3 100%);
@ -121,236 +159,281 @@ fieldset[disabled] .btn-success.active {
background-image: none;
}
/* TABLES */
table.spaced tr th,
table.spaced tr td {
padding: 5px;
}
/* SEARCH BOX */
.search-input {
width: 500px;
td.table-label {
font-weight: bold;
padding-top: 2px;
padding-bottom: 2px;
vertical-align: top;
}
.search-btn-icon {
background-image: url('/icons/ic_search_white_24dp_2x.png');
background-clip: padding-box;
background-size: cover;
width: 28px;
height: 28px;
vertical-align: middle;
margin: 0!important;
color: transparent;
background-color: transparent;
border-color: transparent;
box-shadow: none;
webkit-box-shadow: none;
text-shadow: none;
webkit-text-shadow: none;
td.table-value {
padding-left: 15px;
padding-top: 2px;
padding-bottom: 2px;
vertical-align: top;
}
.search-btn-icon:hover,
.search-btn-icon:focus {
outline: 0;
/* BOXES */
.two-columns-left {
vertical-align: top;
display: inline-block;
padding: 0;
background-color: transparent;
width: 49%;
min-width: 49%;
margin-left: 0;
margin-right: 0.7%;
}
table.spaced tr th,
table.spaced tr td {
padding: 5px;
.two-columns-right {
vertical-align: top;
display: inline-block;
padding: 0;
background-color: transparent;
width: 49%;
min-width: 49%;
margin-left: 0.7%;
margin-right: 0;
}
.two-columns-left .box,
.two-columns-right .box {
margin-bottom: 15px;
}
/* COMMON PAGES */
.container #welcome-msg {
text-align: center;
.box {
display: inline-block;
padding: 10px 10px 10px 10px;
background-color: rgba(255, 255, 255, 0.1);
}
.container h3 {
margin-bottom: 20px;
margin-top: 0;
.box-header {
width: 100%;
font-size: 12px;
font-weight: bold;
}
.container span {
display: block;
margin-left: auto;
margin-right: auto;
.box-body {
width: 100%;
}
.container span.label-field {
.box-context {
width: 100%;
font-size: 12px;
margin-bottom: 2px;
padding-left: 2px;
font-style: italic;
margin: 0 10px 10px 0;
}
.container button {
display: inline-block;
margin-left: 8px;
margin-right: 8px;
margin-bottom: 8px;
.box-main {
margin: 20px 0;
}
.container #welcome-msg {
margin-bottom: 60px;
padding-bottom: 10px;
#box-msg {
text-align: center;
position: fixed;
bottom: 0;
right: 0;
left: 0;
z-index: 100;
}
.container #welcome-msg h1 {
color: #e0e0e3;
/* MESSAGE BOX */
.msg, .msg-error, .msg-info {
color: #505050;
font-weight: bold;
padding: 0;
}
.container div.box-content {
color: #e0e0e3;
.msg {
background: #81b6e2;
}
.msg-error {
background: #ca7c7c;
}
.msg-info {
background: #8caf8c;
}
/* PAGES - COMMONS */
body.dmt {
min-height: 100vh;
background-image: url("../icons/samourai-logo-loading.png");
background-repeat: no-repeat;
background-position: center;
}
#body {
padding: 0;
color: #efefef;
}
#body,
#form {
padding-top: 20px;
}
#body #main > div {
min-height: 80vh;
background-color: #1a1d1f;
}
#body #main .title {
margin: 0;
background-color: rgba(255, 255, 255, 0.1);
text-align: left;
padding: 30px;
}
.container div.box-content-transp {
color: #e0e0e3;
background: transparent;
text-align: left;
padding: 30px;
#body #main h1 {
font-size: 24px;
margin: 0 0 20px 0;
padding: 0;
}
.container div.title-section {
color: #e0e0e3;
background: transparent;
text-align: left;
h3 {
margin-bottom: 20px;
border-bottom: 1px solid #bfbfbf;
margin-top: 0;
}
.container div.box-actions {
margin-top: 10px;
text-align: center;
span {
display: block;
margin-left: auto;
margin-right: auto;
}
.container .optional-actions {
margin-top: 30px;
text-align: center;
font-size: 11px;
button {
display: inline-block;
margin-left: 8px;
margin-right: 8px;
}
.container .optional-actions a {
margin: 0 5px;
.box-content {
color: #e0e0e3;
background-color: rgba(255, 255, 255, 0.1);
text-align: left;
padding: 30px;
}
.box-actions {
margin-top: 10px;
text-align: center;
}
.container #body,
.container #form {
padding-top: 20px;
background-color: rgba(255, 255, 255, 0.1);
.amount-sent {
color: #f77c7c;
}
.container #body {
border-bottom: 1px solid #bfbfbf;
}
.amount-received {
color: #76d776;
}
/* Navigation tab menu */
.container #tab-menu div {
/* NAVIGATION MENU*/
#body #menu {
padding-left: 0;
padding-right: 0;
}
.container .nav-pills {
/*border-bottom: 1px solid #bfbfbf;*/
#body #menu .title {
margin: 0;
background-color: rgba(255, 255, 255, 0.1);
}
#body #menu .title h1 {
font-size: 16px;
margin: 0 0 5px 0;
padding: 5px;
}
.nav-pills {
color: #e0e0e3;
display: flex;
overflow: hidden;
}
.container .nav-pills > li {
.nav-pills > li {
padding-left: 0;
padding-right: 0;
border: none;
}
.container .nav-pills > li > a {
.nav-pills > li > a {
color: #cfd8dc;
border-radius: 0;
margin-left: 0;
border: none;
margin-left: 5px;
text-decoration: none;
cursor: pointer;
padding: 6px;
padding: 6px 4px;
}
.container .nav-pills > li > a:hover {
.nav-pills > li > a:hover {
color: #fff;
background-color: transparent;
background-color: transparent!important;
}
.container .nav-pills > li.active > a,
.container .nav-pills > li.active > a:hover {
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover {
color: #fff;
border-top: 1px solid #fff;
background-color: rgba(255, 255, 255, 0.1);
outline: none;
background-color: transparent!important;
font-weight: 600;
text-decoration: none;
cursor: default;
}
/* HEADER */
.container #header {
#header {
height: 60px;
border-bottom-width: 3px;
border-bottom-color: #b0bec5;
border-bottom-style: solid;
display: flex;
display: -ms-flexbox;
align-items: center;
-ms-flex-align: center;
}
.container #header .title {
color: #e0e0e3;
margin-left: 0!important;
#header div {
padding-left: 0;
padding-right: 0;
}
.container #header .login-box {
text-align: right;
#header span {
display:inline;
}
.container #header .login-box a,
.container #header .login-box .login,
.container #header .login-box .wallet-blc {
#header .title {
color: #e0e0e3;
font-size: 12px;
margin-left: 0!important;
}
.container #header .login-box a,
.container #header .login-box .login {
display: inline-block;
#header .login-box {
text-align: right;
}
.container #header .login-box a {
vertical-align: middle;
#header .login-box a {
color: #e0e0e3;
font-size: 12px;
display: inline-block;
vertical-align: middle;
}
.container #header span {
display:inline;
/* PAGES - HOME */
#login-page {
padding: 100px 0;
}
/* MESSAGES */
.container div.msg-boxes {
#login-page #welcome-msg {
text-align: center;
margin-top: 20px;
font-size: 14px;
margin-bottom: 60px;
padding-bottom: 10px;
}
.container div.msg-boxes .msg {
#login-page #welcome-msg h1 {
color: #e0e0e3;
}
.container div.msg-boxes .msg-error {
color: #c76464;
}
.container div.msg-boxes .msg-info {
color: #52c152;
}
/* LOGIN PAGE */
#login-page {
padding: 100px 0;
}
#login-page #signin {
margin-top: 10px;
}
@ -363,13 +446,14 @@ table.spaced tr td {
display:inline;
}
#body {
padding: 40px;
color: #efefef;
/* PAGES - STATUS */
#tor-status-ind,
#nginx-status-ind,
#nodejs-status-ind {
color: #76d776;
}
/* PAIRING */
/* PAGES - PAIRING */
#qr-label,
#qr-explorer-label {
margin: 0 0 20px 0;
@ -391,64 +475,299 @@ table.spaced tr td {
margin: auto;
}
/* FORM FIED*/
#cell-args,
#cell-args2,
#cell-args3 {
/* PAGES - BLOCKS RESCAN */
#blocks-rescan-form span {
display: inline;
}
#blocks-rescan-form .box-body {
text-align: center;
}
#blocks-rescan-form input {
width: 60px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
.halfwidth {
width: 49%;
min-width: 49%;
/* PAGES - XPUBS TOOL */
#xpubs-tool-search-form span {
display: inline;
}
.fullwidth {
#xpubs-tool-search-form .box-body {
text-align: center;
}
#xpubs-tool-search-form input {
width: 400px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#xpubs-tool-details {
width: 100%;
min-width: 100%;
}
#cell-args2,
#cell-args3 {
width: 24%;
min-width: 24%;
#xpubs-tool-header {
margin: 0 0 20px 0;
}
/* JSON DATA */
.json-data-container {
max-width: 100%;
word-wrap: break-word;
overflow: visible;
margin-top: 20px;
#xpubs-tool-actions {
text-align: center;
}
#json-data {
text-align: left;
min-height: 400px;
max-width: 945px;
outline: 1px solid #252525;
border: none;
padding: 5px;
margin: 5px;
color: lightgreen;
background-color: #252525;
#xpubs-rescans-actions span {
display: inline;
}
#xpubs-rescans-actions input {
width: 50px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#xpubs-tool-details #xpub-value {
overflow: hidden;
}
#xpubs-tool-details-row1 table {
width: 100%;
}
#xpubs-tool-details-row1 table .table-label {
width: 15%;
}
#xpubs-tool-details-row1 table .table-value {
width: 35%;
}
#xpubs-tool-details-row2 table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
#xpubs-tool-details-row2 table tbody tr:first-child {
height: 0;
}
#xpubs-tool-details-row2 table tbody tr:first-child td:first-child {
width: 80px;
}
#xpubs-tool-details-row2 tbody tr td:last-child {
overflow: hidden;
white-space: nowrap;
}
#xpubs-tool-details-row2 table a {
font-weight: bold;
text-decoration: underline;
color: #efefef;
}
#xpubs-tool-details-row2 table .table-label {
width: 30%;
}
#xpubs-tool-details-row2 table .table-value {
width: 70%;
}
#xpubs-tool-import {
text-align: center;
}
#xpubs-tool-import span {
display: inline;
}
#xpubs-tool-import select {
width: 80px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#xpubs-tool-import #import-xpub {
font-weight: bold;
}
/* PAGES - ADDRESSES TOOL */
#addresses-tool-search-form span {
display: inline;
}
#addresses-tool-search-form .box-body {
text-align: center;
}
#addresses-tool-search-form input {
width: 280px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#addresses-tool-details {
width: 100%;
}
#json-data span {
display: inline;
max-width: 800px;
word-wrap: break-word;
overflow: visible;
#addresses-tool-header {
margin: 0 0 20px 0;
}
#addresses-tool-actions {
text-align: center;
}
#addresses-rescans-actions span {
display: inline;
}
#addresses-rescans-actions input {
width: 50px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#addresses-tool-details-row1 table,
#addresses-tool-details-row2 table {
width: 100%;
}
#addresses-tool-details-row1 table .table-label,
#addresses-tool-details-row2 table .table-label {
width: 110px;
}
#addresses-tool-details-row2 #addr-xpub {
overflow: hidden;
white-space: nowrap;
max-width: 200px;
}
#json-data .string { color: lightgreen; }
#json-data .number { color: lightgreen; }
#json-data .boolean { color: lightgreen; }
#json-data .null { color: lightgreen; }
#json-data .key { color: lightgreen; }
#json-data .info { color: lightskyblue; }
#json-data .error { color: orangered; }
#addresses-tool-details-row3 table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
#addresses-tool-details-row3 table tbody tr:first-child {
height: 0;
}
#addresses-tool-details-row3 table tbody tr:first-child td:first-child {
width: 80px;
}
#addresses-tool-details-row3 tbody tr td:last-child {
overflow: hidden;
white-space: nowrap;
}
#addresses-tool-details-row3 table a {
font-weight: bold;
text-decoration: underline;
color: #efefef;
}
#addresses-tool-details-row3 table .table-label {
width: 30%;
}
#addresses-tool-details-row3 table .table-value {
width: 70%;
}
#addresses-tool-import {
text-align: center;
}
#addresses-tool-import span {
display: inline;
}
#addresses-tool-import select {
width: 80px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#addresses-tool-import #import-address {
font-weight: bold;
}
/* PAGES - TRANSACTIONS TOOL */
#txs-tool-search-form span {
display: inline;
}
#txs-tool-search-form .box-body {
text-align: center;
}
#txs-tool-search-form input {
width: 400px;
margin-left: 5px;
margin-right: 5px;
display: inline-block;
}
#txs-tool-details {
width: 100%;
}
#txs-tool-header {
margin: 0 0 20px 0;
}
#txs-tool-actions {
text-align: center;
}
#txs-tool-details #txid-value {
overflow: hidden;
}
#txs-tool-details-row1 table {
width: 100%;
}
#txs-tool-details-row1 table .table-label {
width: 15%;
}
#txs-tool-details-row1 table .table-value {
width: 35%;
}
/* PAGES - HELP DMT */
#welcome span {
margin: 20px 0;
}
#welcome .items-category {
margin: 20px 0 10px 0;
font-weight: bold;
font-size: 16px;
}
#welcome .item {
margin: 10px 0 0 10px;
font-weight: bold;
}
#welcome .item-descr {
margin: 5px 0 10px 10px;
}
/* SPACERS */

147
static/admin/dmt/addresses-tools/addresses-tools.html

@ -0,0 +1,147 @@
<div id="addresses-tool">
<h1>ADDRESSES TOOL</h1>
<div class="box-context">Check if an address is tracked by your Dojo. Import and track a new address. Rescan the full history of an address.</div>
<div class="row box-main">
<!-- ADDRESS SEARCH FORM -->
<div id="addresses-tool-search-form" class="fullwidth box">
<div class="box-body">
<span>Check if </span>
<input id="address" type="text" placeholder="address">
<span> is tracked by your Dojo </span>
<button id="btn-address-search-go" class="btn btn-success" type="button">GO</button>
</div>
</div>
<!-- ADDRESS IMPORT -->
<div id="addresses-tool-import" class="fullwidth box">
<div class="box-body">
<div>
<span>This address isn't tracked by your Dojo.</span>
</div>
<div class="spacer20"></div>
<div>
<span>Do you want to import </span>
<span id="import-address"></span>
<span> and track its activity?</span>
<button id="btn-address-import-go" class="btn btn-success" type="button">IMPORT</button>
<button id="btn-address-import-cancel" class="btn btn-success" type="button">CANCEL</button>
</div>
</div>
</div>
<!-- ADDRESS DETAILS -->
<div id="addresses-tool-details">
<div id="addresses-tool-header" class="row box-main">
<div class="fullwidth box">
<div id="addr-value" class="box-body center"></div>
</div>
</div>
<div id="addresses-tool-actions" class="row box-main">
<div class="center">
<button id="btn-address-details-rescan" class="btn btn-success" type="button">RESCAN THIS ADDRESS</button>
<button id="btn-address-details-reset" class="btn btn-success" type="button">SEARCH ANOTHER ADDRESS</button>
</div>
</div>
<div id="addresses-rescans-actions" class="row box-main">
<div class="center">
<span>Do you want to rescan this address?</span>
<button id="btn-address-rescan-go" class="btn btn-success" type="button">RESCAN</button>
<button id="btn-address-rescan-cancel" class="btn btn-success" type="button">CANCEL</button>
</div>
</div>
<div id="addresses-tool-details-row1" class="row box-main">
<!-- GENERAL INFO -->
<div id="box-general" class="fullwidth box">
<div class="box-header">GENERAL INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Balance</td>
<td class="table-value" id="addr-balance"></td>
</tr>
<tr>
<td class="table-label">Number of Txs</td>
<td class="table-value" id="addr-nb-txs"></td>
</tr>
<tr>
<td class="table-label">Number of UTXOs</td>
<td class="table-value" id="addr-nb-utxos"></td>
</tr>
<tr>
<td class="table-label">Segwit</td>
<td class="table-value" id="addr-segwit"></td>
</tr>
<tr>
<td class="table-label">Address Type</td>
<td class="table-value" id="addr-type"></td>
</tr>
</table>
</div>
</div>
</div>
<div id="addresses-tool-details-row2" class="row box-main">
<!-- HD ADDRESS INFO -->
<div id="box-hd" class="fullwidth box">
<div class="box-header">DERIVATION INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Derivation path</td>
<td class="table-value" id="addr-deriv-path"></td>
</tr>
<tr>
<td class="table-label" colspan="2">Derived from</td>
</tr>
<tr>
<td id="addr-xpub" colspan="2"></td>
</tr>
</table>
</div>
</div>
</div>
<div id="addresses-tool-details-row3" class="row box-main">
<!-- TXS LIST -->
<div id="box-txs" class="halfwidth-left box">
<div class="box-header">MOST RECENT TRANSACTIONS</div>
<div class="spacer10"></div>
<div class="box-body">
<table id="addr-table-list-txs">
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- UTXOS LIST -->
<div id="box-utxos" class="halfwidth-right box">
<div class="box-header">UNSPENT TRANSACTION OUTPUTS</div>
<div class="spacer10"></div>
<div class="box-body">
<table id="addr-table-list-utxos">
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script include-js="addresses-tools/addresses-tools.js"></script>

228
static/admin/dmt/addresses-tools/addresses-tools.js

@ -0,0 +1,228 @@
const screenAddressesToolsScript = {
explorerInfo: null,
currentAddress: null,
initPage: function() {
this.getExplorerInfo()
// Sets the event handlers
$('#btn-address-search-go').click(() => {this.searchAddress()})
$('#btn-address-details-reset').click(() => {this.showSearchForm()})
$('#btn-address-details-rescan').click(() => {this.showRescanForm()})
$('#btn-address-rescan-go').click(() => {this.rescanAddress()})
$('#btn-address-rescan-cancel').click(() => {this.hideRescanForm()})
$('#btn-address-import-go').click(() => {this.importAddress()})
$('#btn-address-import-cancel').click(() => {this.showSearchForm()})
$('#addresses-tool').keyup(evt => {
if (evt.keyCode === 13) {
this.searchAddress()
}
})
},
preparePage: function() {
this.hideRescanForm()
this.showSearchForm()
$("#address").focus()
},
getExplorerInfo: function() {
lib_api.getExplorerPairingInfo().then(explorerInfo => {
this.explorerInfo = explorerInfo
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
searchAddress: function() {
lib_msg.displayMessage('Search in progress...');
const address = $('#address').val()
this.currentAddress = address
return this._searchAddress(address).then(() => {
lib_msg.cleanMessagesUi()
})
},
_searchAddress: function(address) {
return lib_api.getAddressInfo(address).then(addressInfo => {
if (addressInfo && addressInfo['tracked']) {
this.setAddressDetails(addressInfo)
this.showAddressDetails()
const jsonData = {'active': address}
return lib_api.getWallet(jsonData).then(walletInfo => {
// Display the txs
const txs = walletInfo['txs']
for (let tx of txs)
this.setTxDetails(tx)
// Display the UTXOs
const utxos = walletInfo['unspent_outputs'].sort((a,b) => {
return a['confirmations'] - b['confirmations']
})
$('#addr-nb-utxos').text(utxos.length)
for (let utxo of utxos)
this.setUtxoDetails(utxo)
})
} else {
lib_msg.displayErrors('address not found')
this.showImportForm(false)
}
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
throw e
})
},
importAddress: function() {
lib_msg.displayMessage('Processing address import...');
const jsonData = {'active': this.currentAddress}
return lib_api.getWallet(jsonData)
.then(result => {
this._searchAddress(this.currentAddress).then(() => {
lib_msg.displayInfo('Import complete')
})
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
rescanAddress: function() {
lib_msg.displayMessage('Processing address rescan...');
return lib_api.getAddressRescan(this.currentAddress)
.then(result => {
this.hideRescanForm()
this._searchAddress(this.currentAddress).then(() => {
lib_msg.displayInfo('Rescan complete')
})
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
setAddressDetails: function(addressInfo) {
$('tr.tx-row').remove()
$('tr.utxo-row').remove()
$('#addr-value').text(this.currentAddress)
$('#addr-nb-txs').text(addressInfo['n_tx'])
$('#addr-nb-utxos').text('-')
const balance = parseInt(addressInfo['balance']) / 100000000
$('#addr-balance').text(`${balance} BTC`)
const addrType = (addressInfo['type'] == 'hd') ? 'Derived from an XPUB' : 'Loose address'
$('#addr-type').text(addrType)
if (addressInfo['segwit']) {
$('#addr-segwit').html('&#10003;')
$('#addr-segwit').css('color', '#76d776')
} else {
$('#addr-segwit').text('-')
$('#addr-segwit').css('color', '#f77c7c')
}
if (addressInfo['type'] == 'hd') {
$('#addr-xpub').text(addressInfo['xpub'])
$('#addr-deriv-path').text(addressInfo['path'])
$('#addresses-tool-details-row2').show()
} else {
$('#addresses-tool-details-row2').hide()
}
},
setTxDetails: function(tx) {
const txid = tx['hash']
const txidDisplay = `${txid.substring(0,50)}...`
const amount = parseInt(tx['result']) / 100000000
const amountLabel = amount < 0 ? amount : `+${amount}`
const amountStyle = amount < 0 ? 'amount-sent' : 'amount-received'
const date = lib_fmt.unixTsToLocaleString(tx['time'])
const txUrl = lib_cmn.getExplorerTxUrl(txid, this.explorerInfo)
const newRow = `<tr class="tx-row"><td colspan="2">&nbsp;</td></tr>
<tr class="tx-row">
<td class="table-label" colspan="2">
<a href="${txUrl}" target="_blank">${txidDisplay}</a>
</td>
</tr>
<tr class="tx-row">
<td class="table-label">Amount</td>
<td class="table-value ${amountStyle}">${amountLabel} BTC</td>
</tr>
<tr class="tx-row">
<td class="table-label">Block height</td>
<td class="table-value">${tx['block_height']}</td>
</tr>
<tr class="tx-row">
<td class="table-label">Date</td>
<td class="table-value">${date}</td>
</tr>`
$('#addr-table-list-txs tr:last').after(newRow)
},
setUtxoDetails: function(utxo) {
const txid = utxo['tx_hash']
const txidVout = `${txid.substring(0,50)}...:${utxo['tx_output_n']}`
const amount = parseInt(utxo['value']) / 100000000
const txUrl = lib_cmn.getExplorerTxUrl(txid, this.explorerInfo)
const newRow = `<tr class="utxo-row"><td colspan="2">&nbsp;</td></tr>
<tr class="utxo-row">
<td class="table-label" colspan="2">
<a href="${txUrl}" target="_blank">${txidVout}</a>
</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Amount</td>
<td class="table-value">${amount} BTC</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Address</td>
<td class="table-value">${utxo['addr']}</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Confirmations</td>
<td class="table-value">${utxo['confirmations']}</td>
</tr>`
$('#addr-table-list-utxos tr:last').after(newRow)
},
showSearchForm: function() {
$('#addresses-tool-details').hide()
$('#addresses-tool-import').hide()
$('#address').val('')
$('#addresses-tool-search-form').show()
lib_msg.cleanMessagesUi()
},
showImportForm: function() {
$('#addresses-tool-search-form').hide()
$('#addresses-tool-details').hide()
$('#import-address').text(this.currentAddress)
$('#addresses-tool-import').show()
},
showAddressDetails: function() {
$('#addresses-tool-search-form').hide()
$('#addresses-tool-import').hide()
$('#addresses-tool-details').show()
},
showRescanForm: function() {
$('#addresses-tool-actions').hide()
$('#addresses-rescans-actions').show()
lib_msg.cleanMessagesUi()
},
hideRescanForm: function() {
$('#addresses-rescans-actions').hide()
$('#addresses-tool-actions').show()
},
}
screenScripts.set('#screen-addresses-tools', screenAddressesToolsScript)

22
static/admin/dmt/blocks-rescan/blocks-rescan.html

@ -0,0 +1,22 @@
<div id="blocks-rescan">
<h1>BLOCKS RESCAN</h1>
<div class="box-context">Force the Tracker to rescan a range of blocks.</div>
<div class="row box-main">
<div id="blocks-rescan-form" class="box fullwidth">
<div class="box-body">
<span>Rescan blocks between</span>
<input id="rescan-from-height" type="text" placeholder="height">
<span> and </span>
<input id="rescan-to-height" type="text" placeholder="height">
<button id="btn-rescan-go"
class="btn btn-success"
type="button">GO</button>
</div>
</div>
</div>
</div>
<script include-js="blocks-rescan/blocks-rescan.js"></script>

45
static/admin/dmt/blocks-rescan/blocks-rescan.js

@ -0,0 +1,45 @@
const screenBlocksRescanScript = {
initPage: function() {
// Sets the event handlers
$('#btn-rescan-go').click(() => {
this.processRescan()
})
$('#blocks-rescan').keyup(evt => {
if (evt.keyCode === 13) {
this.processRescan()
}
})
},
preparePage: function() {
$("#rescan-from-height").focus()
},
processRescan: function() {
lib_msg.displayMessage('Processing...');
let fromHeight = $("#rescan-from-height").val()
let toHeight = $("#rescan-to-height").val()
fromHeight = parseInt(fromHeight)
toHeight = (toHeight) ? parseInt(toHeight) : fromHeight;
lib_api.getBlocksRescan(fromHeight, toHeight).then(result => {
if (!result)
return
const fromHeightRes = result['fromHeight']
const toHeightRes = result['toHeight']
const msg = `successfully rescanned blocks between height ${fromHeightRes} and height ${toHeightRes}`
lib_msg.displayInfo(msg)
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
}).then(() => {
$('#rescan-from-height').val('')
$('#rescan-to-height').val('')
})
},
}
screenScripts.set('#screen-blocks-rescan', screenBlocksRescanScript)

152
static/admin/dmt/index.html

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DOJO // MAINTENANCE TOOL</title>
<link rel="stylesheet" type="text/css" href="../css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="../css/style.css">
<script src="../lib/jquery-3.5.1.min.js"></script>
<script src="../lib/jquery.qrcode.min.js"></script>
<script src="../conf/index.js"></script>
<script src="../lib/common-script.js"></script>
<script src="../lib/api-wrapper.js"></script>
<script src="../lib/auth-utils.js"></script>
<script src="../lib/format-utils.js"></script>
<script src="../lib/messages.js"></script>
<script src="index.js"></script>
</head>
<body class="dmt">
<div id="top-container" class="container" style="display: none">
<!-- HEADER -->
<div id="header" class="row">
<div class="col-xs-9">
<h1 class="title"><span>DOJO // MAINTENANCE TOOL</span> <span id="dojo-version" class="beta">beta</span></h1>
</div>
<div class="col-xs-3 login-box">
<a id="btn-logout" style="display: inline;" href="#" title="DISCONNECT">
<img src="../icons/ic_power_settings_new_white_24dp_1x.png" class="mini-icon"/>
</a>
</div>
</div>
<div class="spacer30"></div>
<!-- BODY -->
<div id="body" class="row">
<!-- MENU -->
<div id="menu" class="col-xs-2">
<div class="title">
<h1>MONITORING</h1>
</div>
<ul id="tab-menu_list" class="nav nav-pills nav-stacked">
<li id="link-welcome" style="display: none;">
<a href="#">WELCOME</a>
</li>
<li id="link-status">
<a href="#">DOJO STATUS</a>
</li>
<li id="link-pushtx">
<a href="#">PUSHTX STATUS</a>
</li>
</ul>
<div class="spacer20"></div>
<div class="title">
<h1>TOOLS</h1>
</div>
<ul id="tab-menu_list2" class="nav nav-pills nav-stacked">
<li id="link-pairing">
<a href="#">PAIRING</a>
</li>
<li id="link-xpubs-tools">
<a href="#">XPUBS TOOL</a>
</li>
<li id="link-addresses-tools">
<a href="#">ADDRESSES TOOL</a>
</li>
<li id="link-txs-tools">
<a href="#">TRANSACTIONS TOOL</a>
</li>
<li id="link-blocks-rescan">
<a href="#">BLOCKS RESCAN</a>
</li>
</ul>
<div class="spacer20"></div>
<div class="title">
<h1>HELP</h1>
</div>
<ul id="tab-menu_list3" class="nav nav-pills nav-stacked">
<li id="link-help-dmt">
<a href="#">HELP DMT</a>
</li>
<li id="link-dojo-telegram">
<a href="https://t.me/samourai_dojo" target="_blank">DOJO TELEGRAM CHAT</a>
</li>
<li id="link-wp-telegram">
<a href="https://t.me/whirlpool_trollbox" target="_blank">WHIRLPOOL TELEGRAM CHAT</a>
</li>
<li id="link-sw-support">
<a href="https://t.me/SamouraiWallet" target="_blank">SAMOURAI TELEGRAM CHAT</a>
</li>
</ul>
</div>
<div class="col-xs-1"></div>
<!-- MAIN AREA -->
<div id="main" class="col-xs-9">
<!-- WELCOME -->
<div id="screen-welcome"
include-html="welcome/welcome.html"
style="display: none">
</div>
<!-- STATUS -->
<div id="screen-status"
include-html="status/status.html"
style="display: none">
</div>
<!-- PUSH TX -->
<div id="screen-pushtx"
include-html="pushtx/pushtx.html"
style="display: none">
</div>
<!-- PAIRING -->
<div id="screen-pairing"
include-html="pairing/pairing.html"
style="display: none">
</div>
<!-- XPUBS TOOLS -->
<div id="screen-xpubs-tools"
include-html="xpubs-tools/xpubs-tools.html"
style="display: none">
</div>
<!-- ADDRESSES TOOLS -->
<div id="screen-addresses-tools"
include-html="addresses-tools/addresses-tools.html"
style="display: none">
</div>
<!-- TRANSACTIONS TOOLS -->
<div id="screen-txs-tools"
include-html="txs-tools/txs-tools.html"
style="display: none">
</div>
<!-- BLOCKS RESCAN -->
<div id="screen-blocks-rescan"
include-html="blocks-rescan/blocks-rescan.html"
style="display: none">
</div>
<!-- HELP DMT -->
<div id="screen-help-dmt"
include-html="welcome/welcome.html"
style="display: none">
</div>
</div>
</div>
</div>
<!-- MSG BOX -->
<div id="box-msg"
include-html="msg-box/msg-box.html">
</div>
</body>
</html>

116
static/admin/dmt/index.js

@ -0,0 +1,116 @@
/**
* Global obkjects
*/
// Ordered list of screens
const screens = [
'#screen-welcome',
'#screen-status',
'#screen-pushtx',
'#screen-pairing',
'#screen-xpubs-tools',
'#screen-addresses-tools',
'#screen-txs-tools',
'#screen-blocks-rescan',
'#screen-help-dmt'
]
// Ordered list of menu items
const tabs = [
'#link-welcome',
'#link-status',
'#link-pushtx',
'#link-pairing',
'#link-xpubs-tools',
'#link-addresses-tools',
'#link-txs-tools',
'#link-blocks-rescan',
'#link-help-dmt'
]
// Mapping of scripts associaed to screens
const screenScripts = new Map()
/**
* UI initialization
*/
function initTabs() {
// Activates the current tab
let currentTab = sessionStorage.getItem('activeTab')
if (!currentTab)
currentTab = '#link-status'
$(currentTab).addClass('active')
// Sets event handlers
for (let tab of tabs) {
$(tab).click(function() {
$(sessionStorage.getItem('activeTab')).removeClass('active')
sessionStorage.setItem('activeTab', tab)
$(tab).addClass('active')
preparePage()
})
}
}
function initPages() {
// Dynamic loading of screens and scripts
lib_cmn.includeHTML(_initPages)
// Dojo version
let lblVersion = sessionStorage.getItem('lblVersion')
if (lblVersion == null) {
lib_api.getPairingInfo().then(apiInfo => {
lblVersion = 'v' + apiInfo['pairing']['version'] + ' beta'
sessionStorage.setItem('lblVersion', lblVersion)
$('#dojo-version').text(lblVersion)
})
} else {
$('#dojo-version').text(lblVersion)
}
}
function _initPages() {
for (let screen of screens) {
const screenScript = screenScripts.get(screen)
if (screenScript)
screenScript.initPage()
}
preparePage()
$('#top-container').show()
}
function preparePage() {
lib_msg.cleanMessagesUi()
const activeTab = sessionStorage.getItem('activeTab')
for (let idxTab in tabs) {
const screen = screens[idxTab]
if (tabs[idxTab] == activeTab) {
$(screen).show()
if (screenScripts.has(screen))
screenScripts.get(screen).preparePage()
} else {
$(screen).hide()
}
}
}
/**
* Processing on loading completed
*/
$(document).ready(function() {
// Refresh the access token
lib_auth.refreshAccessToken()
setInterval(() => {
lib_auth.refreshAccessToken()
}, 300000)
// Inits menu and pages
initTabs()
initPages()
// Set event handlers
$('#btn-logout').click(function() {
lib_auth.logout()
})
})

7
static/admin/dmt/msg-box/msg-box.html

@ -0,0 +1,7 @@
<div class="row box-msg">
<div class="col-xs-12">
<div id="msg" class="msg"></div>
<div id="errors" class="msg-error"></div>
<div id="info" class="msg-info"></div>
</div>
</div>

33
static/admin/dmt/pairing/pairing.html

@ -0,0 +1,33 @@
<div id="pairing">
<h1>PAIRING</h1>
<div class="box-context">Pair your wallet to your Dojo and to your Block Explorer with a simple QRCode.</div>
<div class="row box-main">
<div id="dojo-pairing" class="halfwidth-left box">
<div class="box-header">DOJO</div>
<div class="spacer10"></div>
<div class="box-body" id="qr-container">
<div class="center">Scan this QRCode with your wallet</div>
<div class="spacer10"></div>
<div id="qr-pairing"></div>
<div class="spacer10"></div>
</div>
</div>
<div id="explorer-pairing" class="halfwidth-right box">
<div class="box-header">BLOCK EXPLORER</div>
<div class="spacer10"></div>
<div class="box-body" id="qr-explorer-container">
<div class="center">Scan this QRCode with your wallet</div>
<div class="spacer10"></div>
<div id="qr-explorer-pairing"></div>
<div class="spacer10"></div>
</div>
</div>
</div>
</div>
<script include-js="pairing/pairing.js"></script>

65
static/admin/dmt/pairing/pairing.js

@ -0,0 +1,65 @@
const screenPairingScript = {
initPage: function() {},
preparePage: function() {
this.displayQRPairing()
},
loadPairingPayloads: function() {
let result = {
'api': null,
'explorer': null
}
lib_msg.displayMessage('Loading pairing payloads...');
return lib_api.getPairingInfo().then(apiInfo => {
if (apiInfo) {
apiInfo['pairing']['url'] = window.location.protocol + '//' + window.location.host + conf['api']['baseUri']
result['api'] = apiInfo
}
}).then(() => {
return lib_api.getExplorerPairingInfo()
}).then(explorerInfo => {
if (explorerInfo)
result['explorer'] = explorerInfo
lib_msg.cleanMessagesUi()
return result
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
return result
})
},
displayQRPairing: function() {
this.loadPairingPayloads().then(
function (result) {
if (result) {
if (result['api']) {
const textJson = JSON.stringify(result['api'], null, 4)
$("#qr-pairing").html('') // clear qrcode first
$('#qr-pairing').qrcode({width: 256, height: 256, text: textJson})
}
if (result['explorer'] && result['explorer']['pairing']['url']) {
const textJson = JSON.stringify(result['explorer'], null, 4)
$("#qr-explorer-pairing").html('') // clear qrcode first
$('#qr-explorer-pairing').qrcode({width: 256, height: 256, text: textJson})
} else {
$("#qr-label").removeClass('halfwidth')
$("#qr-label").addClass('fullwidth')
$("#qr-container").removeClass('halfwidth')
$("#qr-container").addClass('fullwidth')
$("#qr-explorer-label").hide()
$("#qr-explorer-container").hide()
}
}
},
function (jqxhr) {}
);
}
}
screenScripts.set('#screen-pairing', screenPairingScript)

44
static/admin/dmt/pushtx/pushtx.html

@ -0,0 +1,44 @@
<div id="pushtx-status">
<h1>PUSHTX STATUS</h1>
<div class="box-context">Monitor the transactions pushed through your Dojo.</div>
<div class="row box-main">
<div id="txs-pushed" class="box fullwidth">
<div class="box-header">TRANSACTIONS PUSHED</div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Uptime</td>
<td class="table-value" id="pushed-uptime"></td>
</tr>
<tr>
<td class="table-label">Number of Transactions</td>
<td class="table-value" id="pushed-count"></td>
</tr>
<tr>
<td class="table-label">Total Amount</td>
<td class="table-value" id="pushed-amount"></td>
</tr>
</table>
</div>
</div>
</div>
<div class="row box-main">
<div id="txs-scheduled" class="box fullwidth">
<div class="box-header">TRANSACTIONS SCHEDULED</div>
<div class="box-body">
<table id="table-scheduled-txs">
<thead>
<td colspan="2"></td>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script include-js="pushtx/pushtx.js"></script>

90
static/admin/dmt/pushtx/pushtx.js

@ -0,0 +1,90 @@
const pushtxScript = {
processedSchedTxs: new Set(),
initPage: function() {
// Refresh PushTx status
setInterval(() => {this.refreshPushTxStatus()}, 60000)
// Refresh ScheduledTxs list
setInterval(() => {this.refreshScheduledTxsList()}, 60000)
},
preparePage: function() {
this.refreshPushTxStatus()
this.refreshScheduledTxsList()
},
refreshPushTxStatus: function() {
lib_msg.displayMessage('Loading PushTx status info...');
lib_api.getPushtxStatus().then(pushTxStatus => {
if (pushTxStatus) {
const data = pushTxStatus['data']
const uptime = lib_cmn.timePeriod(data['uptime'])
$('#pushed-uptime').text(uptime)
$('#pushed-count').text(data['push']['count'])
$('#pushed-amount').text(data['push']['amount'])
lib_msg.cleanMessagesUi()
}
}).catch(e => {
$('#pushed-uptime').text('-')
$('#pushed-count').text('-')
$('#pushed-amount').text('-')
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
refreshScheduledTxsList: function() {
lib_msg.displayMessage('Loading PushTx orchestrator status info...');
lib_api.getOrchestratorStatus().then(orchestrStatus => {
if(orchestrStatus) {
const data = orchestrStatus['data']
for (let tx of data['txs']) {
if (!this.processedSchedTxs.has(tx['schTxid'])) {
this.displayScheduledTx(tx)
this.processedSchedTxs.add(tx['schTxid'])
}
}
lib_msg.cleanMessagesUi()
}
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
displayScheduledTx: function(tx) {
const newRow = `<tr><td colspan="2">&nbsp;</td></tr>
<tr class="table-value">
<td class="table-label">TXID</td>
<td class="table-value" id="scheduled-txid">${tx['schTxid']}</td>
</tr>
<tr class="table-value">
<td class="table-label">Schedule Id</td>
<td class="table-value" id="scheduled-txid">${tx['schID']}</td>
</tr>
<tr class="table-value">
<td class="table-label">Scheduled for block</td>
<td class="table-value" id="scheduled-trigger">${tx['schTrigger']}</td>
</tr>
<tr class="table-value">
<td class="table-label">Created on</td>
<td class="table-value" id="scheduled-created">${lib_fmt.unixTsToLocaleString(tx['schCreated'])}</td>
</tr>
<tr class="table-value">
<td class="table-label">Parent TXID</td>
<td class="table-value" id="scheduled-parent-txid">${tx['schParentTxid']}</td>
</tr>
<tr class="table-value">
<td class="table-label">Raw Transaction</td>
<td class="table-value" id="scheduled-tx">
<pre class="raw-tx">${tx['schRaw']}</pre>
</td>
</tr>`
$('#table-scheduled-txs tr:last').after(newRow)
},
}
screenScripts.set('#screen-pushtx', pushtxScript)

92
static/admin/dmt/status/status.html

@ -0,0 +1,92 @@
<div id="status">
<h1>DOJO STATUS</h1>
<div class="box-context">Monitor the health of some core components of your Dojo.</div>
<div class="row box-main">
<div id="left-column" class="two-columns-left">
<div id="bitcoind-status" class="fullwidth box">
<div class="box-header">FULL NODE</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Status</td>
<td class="table-value" id="node-status-ind"></td>
</tr>
<tr>
<td class="table-label">Uptime</td>
<td class="table-value" id="node-uptime"></td>
</tr>
<tr>
<td class="table-label">Latest block</td>
<td class="table-value" id="node-chaintip"></td>
</tr>
<tr>
<td class="table-label">Bitcoind version</td>
<td class="table-value" id="node-version"></td>
</tr>
<tr>
<td class="table-label">Network</td>
<td class="table-value" id="node-network"></td>
</tr>
<tr>
<td class="table-label">Connected nodes</td>
<td class="table-value" id="node-conn"></td>
</tr>
<tr>
<td class="table-label">Network relay fee</td>
<td class="table-value" id="node-relay-fee"></td>
</tr>
</table>
</div>
</div>
</div>
<div id="right-column" class="two-columns-right">
<div id="tracker-status" class="fullwidth box">
<div class="box-header">TRACKER</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Status</td>
<td class="table-value" id="tracker-status-ind"></td>
</tr>
<tr>
<td class="table-label">Uptime</td>
<td class="table-value" id="tracker-uptime"></td>
</tr>
<tr>
<td class="table-label">Latest block</td>
<td class="table-value" id="tracker-chaintip"></td>
</tr>
</table>
</div>
</div>
<div id="web-status" class="fullwidth box">
<div class="box-header">WEB</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Tor status</td>
<td class="table-value" id="tor-status-ind">&#10003;</td>
</tr>
<tr>
<td class="table-label">Nginx status</td>
<td class="table-value" id="nginx-status-ind">&#10003;</td>
</tr>
<tr>
<td class="table-label">Node.js status</td>
<td class="table-value" id="nodejs-status-ind">&#10003;</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<script include-js="status/status.js"></script>

68
static/admin/dmt/status/status.js

@ -0,0 +1,68 @@
const statusScript = {
initPage: function() {
// Refresh API status
setInterval(() => {this.refreshApiStatus()}, 60000)
// Refresh PushTx status
setInterval(() => {this.refreshPushTxStatus()}, 60000)
},
preparePage: function() {
this.refreshApiStatus()
this.refreshPushTxStatus()
},
refreshApiStatus: function() {
lib_msg.displayMessage('Loading API status info...');
return lib_api.getApiStatus().then(apiStatus => {
if (apiStatus) {
$('#tracker-status-ind').html('&#10003;')
$('#tracker-status-ind').css('color', '#76d776')
$('#tracker-uptime').text(apiStatus['uptime'])
$('#tracker-chaintip').text(apiStatus['blocks'])
lib_msg.cleanMessagesUi()
}
}).catch(e => {
$('#tracker-status-ind').text('X')
$('#tracker-status-ind').css('color', '#f77c7c')
$('#tracker-uptime').text('-')
$('#tracker-chaintip').text('-')
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
refreshPushTxStatus: function() {
lib_msg.displayMessage('Loading Tracker status info...');
lib_api.getPushtxStatus().then(pushTxStatus => {
if (pushTxStatus) {
const data = pushTxStatus['data']
$('#node-status-ind').html('&#10003;')
$('#node-status-ind').css('color', '#76d776')
const uptime = lib_cmn.timePeriod(data['uptime'])
$('#node-uptime').text(uptime)
$('#node-chaintip').text(data['bitcoind']['blocks'])
$('#node-version').text(data['bitcoind']['version'])
const network = data['bitcoind']['testnet'] == true ? 'testnet' : 'mainnet'
$('#node-network').text(network)
$('#node-conn').text(data['bitcoind']['conn'])
$('#node-relay-fee').text(data['bitcoind']['relayfee'])
lib_msg.cleanMessagesUi()
}
}).catch(e => {
$('#node-status-ind').text('-')
$('#node-status-ind').css('color', '#f77c7c')
$('#node-uptime').text('-')
$('#node-chaintip').text('-')
$('#node-version').text('-')
$('#node-network').text('-')
$('#node-conn').text('-')
$('#node-relay-fee').text('-')
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
}
screenScripts.set('#screen-status', statusScript)

101
static/admin/dmt/txs-tools/txs-tools.html

@ -0,0 +1,101 @@
<div id="txs-tool">
<h1>TRANSACTIONS TOOL</h1>
<div class="box-context">Check if a transaction is found in a block or in the mempool of your full node.</div>
<div class="row box-main">
<!-- TRANSACTION SEARCH FORM -->
<div id="txs-tool-search-form" class="fullwidth box">
<div class="box-body">
<span>Search transaction with this </span>
<input id="txid" type="text" placeholder="TXID">
<button id="btn-tx-search-go" class="btn btn-success" type="button">GO</button>
</div>
</div>
<!-- TRANSACTION DETAILS -->
<div id="txs-tool-details">
<div id="txs-tool-header" class="row box-main">
<div class="fullwidth box">
<div class="box-body center">
<a id="txid-value" href="" target="_blank"></a>
</div>
</div>
</div>
<div id="txs-tool-actions" class="row box-main">
<div class="center">
<button id="btn-txs-details-reset" class="btn btn-success" type="button">SEARCH ANOTHER TRANSACTION</button>
</div>
</div>
<div id="txs-tool-details-row1" class="row box-main">
<!-- GENERAL INFO -->
<div id="box-general" class="halfwidth-left box">
<div class="box-header">GENERAL INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">First-seen date</td>
<td class="table-value" id="tx-firstseen"></td>
</tr>
<tr>
<td class="table-label">Found in</td>
<td class="table-value" id="tx-location"></td>
</tr>
<tr>
<td class="table-label">Amount</td>
<td class="table-value" id="tx-amount"></td>
</tr>
<tr>
<td class="table-label">Fees</td>
<td class="table-value" id="tx-fees"></td>
</tr>
<tr>
<td class="table-label">Feerate</td>
<td class="table-value" id="tx-vfeerate"></td>
</tr>
<tr>
<td class="table-label">Number of inputs</td>
<td class="table-value" id="tx-nb-inputs"></td>
</tr>
<tr>
<td class="table-label">Number of outputs</td>
<td class="table-value" id="tx-nb-outputs"></td>
</tr>
</table>
</div>
</div>
<!-- TECHNICAL INFO -->
<div id="box-technical" class="halfwidth-right box">
<div class="box-header">TECHNICAL INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Virtual size</td>
<td class="table-value" id="tx-vsize"></td>
</tr>
<tr>
<td class="table-label">Raw size</td>
<td class="table-value" id="tx-size"></td>
</tr>
<tr>
<td class="table-label">Transaction version</td>
<td class="table-value" id="tx-version"></td>
</tr>
<tr>
<td class="table-label">nLockTime</td>
<td class="table-value" id="tx-nlocktime"></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script include-js="txs-tools/txs-tools.js"></script>

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

@ -0,0 +1,117 @@
const screenTxsToolsScript = {
explorerInfo: null,
currentTxid: null,
initPage: function() {
this.getExplorerInfo()
// Sets the event handlers
$('#btn-tx-search-go').click(() => {this.searchTx()})
$('#btn-txs-details-reset').click(() => {this.showSearchForm()})
$('#txs-tool').keyup(evt => {
if (evt.keyCode === 13) {
this.searchTx()
}
})
},
preparePage: function() {
this.showSearchForm()
$("#txid").focus()
},
getExplorerInfo: function() {
lib_api.getExplorerPairingInfo().then(explorerInfo => {
this.explorerInfo = explorerInfo
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
searchTx: function() {
lib_msg.displayMessage('Search in progress...');
const txid = $('#txid').val()
this.currentTxid = txid
return this._searchTx(txid).then(() => {
lib_msg.cleanMessagesUi()
})
},
_searchTx: function(txid) {
return lib_api.getTransaction(txid).then(txInfo => {
if (txInfo) {
console.log(txInfo)
this.setTxDetails(txInfo)
this.showTxDetails()
}
}).catch(e => {
lib_msg.displayErrors('No transaction found')
console.log(e)
throw e
})
},
setTxDetails: function(txInfo) {
$('tr.input-row').remove()
$('tr.output-row').remove()
const txUrl = lib_cmn.getExplorerTxUrl(this.currentTxid, this.explorerInfo)
$('#txid-value').text(this.currentTxid)
$('#txid-value').attr('href', txUrl)
const firstseen = lib_fmt.unixTsToLocaleString(txInfo['created'])
$('#tx-firstseen').text(firstseen)
if (txInfo.hasOwnProperty('block'))
$('#tx-location').text(` Block ${txInfo['block']['height']}`)
else
$('#tx-location').text(' Mempool')
const nbInputs = txInfo['inputs'].length
$('#tx-nb-inputs').text(nbInputs)
const nbOutputs = txInfo['outputs'].length
$('#tx-nb-outputs').text(nbOutputs)
$('#tx-vfeerate').text(`${txInfo['vfeerate']} sats/vbyte`)
const fees = parseInt(txInfo['fees'])
$('#tx-fees').text(`${fees} sats`)
let amount = fees
for (let o of txInfo['outputs']) {
amount += parseInt(o['value'])
}
amount = amount / 100000000
$('#tx-amount').text(`${amount} BTC`)
$('#tx-size').text(`${txInfo['size']} bytes`)
$('#tx-vsize').text(`${txInfo['vsize']} vbytes`)
$('#tx-version').text(txInfo['version'])
let nlocktime = parseInt(txInfo['locktime'])
if (nlocktime < 500000000) {
$('#tx-nlocktime').text(`Block ${nlocktime}`)
} else {
locktime = lib_fmt.unixTsToLocaleString(locktime)
$('#tx-nlocktime').text(locktime)
}
},
showSearchForm: function() {
$('#txs-tool-details').hide()
$('#txid').val('')
$('#txs-tool-search-form').show()
lib_msg.cleanMessagesUi()
},
showTxDetails: function() {
$('#txs-tool-search-form').hide()
$('#txs-tool-details').show()
},
}
screenScripts.set('#screen-txs-tools', screenTxsToolsScript)

42
static/admin/dmt/welcome/welcome.html

@ -0,0 +1,42 @@
<div id="welcome">
<h1>WELCOME!</h1>
<span>The Dojo's Maintenance Tool (DMT for short) provides a set of tools for monitoring and maintaining your Dojo.</span>
<span class="items-category ">MONITORING</span>
<span class="item">DOJO STATUS</span>
<span class="item-descr">A dashboard for monitoring the health of some components of your Dojo.</span>
<span class="item">PUSHTX STATUS</span>
<span class="item-descr">A dashboard for monitoring the transactions pushed through your Dojo.</span>
<span class="items-category ">TOOLS</span>
<span class="item">PAIRING</span>
<span class="item-descr">Pair your wallet to your Dojo by scanning a QRCode.</span>
<span class="item">XPUBS TOOL</span>
<span class="item-descr">Everything you need to manage your XPUBs manually.<br/>Check if a XPUB is tracked by your Dojo. Import and track a XPUB. Rescan the full history of a XPUB.</span>
<span class="item">ADDRESSES TOOL</span>
<span class="item-descr">Everything you need to manage your addresses manually.<br/>Check if an address is tracked by your Dojo. Import and track an address. Rescan the full history of an address.</span>
<span class="item">TRANSACTIONS TOOL</span>
<span class="item-descr">Check if a transaction is found in a block or in the mempool of your full node.</span>
<span class="item">BLOCKS RESCAN</span>
<span class="item-descr">Rescan the transactions confirmed by the blocks in a given range.</span>
<span class="items-category ">HELP</span>
<span class="item">DOJO TELEGRAM CHAT</span>
<span class="item-descr">Get support from the community for all things related to your Dojo (requires Telegram).</span>
<span class="item">WHIRLPOOL TELEGRAM CHAT</span>
<span class="item-descr">Get support from the community for all things related to Whirlpool (requires Telegram).</span>
<span class="item">SW TELEGRAM CHAT</span>
<span class="item-descr">Get support from the community for all things related to your Samourai Wallet (requires Telegram).</span>
</div>

183
static/admin/dmt/xpubs-tools/xpubs-tools.html

@ -0,0 +1,183 @@
<div id="xpubs-tool">
<h1>XPUBS TOOL</h1>
<div class="box-context">Check if a XPUB is tracked by your Dojo. Import and track a new XPUB. Rescan the full history of a XPUB.</div>
<div class="row box-main">
<!-- XPUB SEARCH FORM -->
<div id="xpubs-tool-search-form" class="fullwidth box">
<div class="box-body">
<span>Check if </span>
<input id="xpub" type="text" placeholder="XPUB">
<span> is tracked by your Dojo </span>
<button id="btn-xpub-search-go"
class="btn btn-success"
type="button">GO</button>
</div>
</div>
<!-- XPUB IMPORT -->
<div id="xpubs-tool-import" class="fullwidth box">
<div class="box-body">
<div id="import-deriv-first-import-msg">
<span>This XPUB isn't tracked by your Dojo. Do you want to import it and track its activity?</span>
</div>
<div id="import-deriv-reimport-msg">
<span>Do you want to reimport this XPUB with a new derivation type?</span>
<br/><br/>
<span>WARNING: Are you sure you need to retype this XPUB? Generally, the 'auto' derivation will type your XPUB correctly.</span>
<br/>
<span>Retyping your XPUB is reserved for very specific circumstances, and should not be taken lightly.</span>
<br/>
<span>If in doubt, contact <a href="mailto:support@samouraiwallet.com">support@samouraiwallet.com</a></span>
</div>
<div class="spacer20"></div>
<div>
<span>Import </span>
<span id="import-xpub"></span>
<span> with a </span>
<select id="import-deriv-type" type="select" value="auto">
<option value="auto" selected>auto</option>
<option value="bip44">BIP44</option>
<option value="bip49">BIP49</option>
<option value="bip84">BIP84</option>
</select>
<span> derivation</span>
<button id="btn-xpub-import-go" class="btn btn-success" type="button">IMPORT</button>
<button id="btn-xpub-import-cancel" class="btn btn-success" type="button">CANCEL</button>
</div>
</div>
</div>
<!-- XPUB DETAILS -->
<div id="xpubs-tool-details">
<div id="xpubs-tool-header" class="row box-main">
<div class="fullwidth box">
<div id="xpub-value" class="box-body center"></div>
</div>
</div>
<div id="xpubs-tool-actions" class="row box-main">
<div class="center">
<button id="btn-xpub-details-rescan" class="btn btn-success" type="button">RESCAN THIS XPUB</button>
<button id="btn-xpub-details-retype" class="btn btn-success" type="button">RETYPE THIS XPUB</button>
<button id="btn-xpub-details-reset" class="btn btn-success" type="button">SEARCH ANOTHER XPUB</button>
</div>
</div>
<div id="xpubs-rescans-actions" class="row box-main">
<div class="center">
<span>Rescan this xpub starting at index</span>
<input id="rescan-start-idx" type="text" value="0" placeholder="index">
<span> with a lookahead of </span>
<input id="rescan-lookahead" type="text" value="100" placeholder="#addresses">
<span> addresses</span>
<button id="btn-xpub-rescan-go" class="btn btn-success" type="button">RESCAN</button>
<button id="btn-xpub-rescan-cancel" class="btn btn-success" type="button">CANCEL</button>
</div>
</div>
<div id="xpubs-tool-details-row1" class="row box-main">
<!-- GENERAL INFO -->
<div id="box-general" class="halfwidth-left box">
<div class="box-header">GENERAL INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Derivation Type</td>
<td class="table-value" id="xpub-deriv-type"></td>
</tr>
<tr>
<td class="table-label">Balance</td>
<td class="table-value" id="xpub-balance"></td>
</tr>
<tr>
<td class="table-label">Number of Txs</td>
<td class="table-value" id="xpub-nb-txs"></td>
</tr>
<tr>
<td class="table-label">Number of UTXOs</td>
<td class="table-value" id="xpub-nb-utxos"></td>
</tr>
<tr>
<td class="table-label">Tracked since</td>
<td class="table-value" id="xpub-import-date"></td>
</tr>
</table>
</div>
</div>
<!-- DERIVATION INFO -->
<div id="box-derivation" class="halfwidth-right box">
<div class="box-header">XPUB DERIVATION INFO</div>
<div class="spacer10"></div>
<div class="box-body">
<table>
<tr>
<td class="table-label">Account</td>
<td class="table-value" id="xpub-deriv-account"></td>
<td class="table-label">Depth</td>
<td class="table-value" id="xpub-deriv-depth"></td>
</tr>
</table>
<div class="spacer10"></div>
<table id="table-deriv-idx">
<tr>
<td class="table-label" colspan="2">First unused indices</td>
<td class="table-label" colspan="2">Last derived indices</td>
</tr>
<tr>
<td class="table-label">External</td>
<td class="table-value" id="xpub-idx-unused-ext"></td>
<td class="table-label">External</td>
<td class="table-value" id="xpub-idx-derived-ext"></td>
</tr>
<tr>
<td class="table-label">Internal</td>
<td class="table-value" id="xpub-idx-unused-int"></td>
<td class="table-label">Internal</td>
<td class="table-value" id="xpub-idx-derived-int"></td>
</tr>
</table>
<div class="spacer10"></div>
</div>
</div>
</div>
<div id="xpubs-tool-details-row2" class="row box-main">
<!-- TXS LIST -->
<div id="box-txs" class="halfwidth-left box">
<div class="box-header">MOST RECENT TRANSACTIONS</div>
<div class="spacer10"></div>
<div class="box-body">
<table id="xpub-table-list-txs">
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- UTXOS LIST -->
<div id="box-utxos" class="halfwidth-right box">
<div class="box-header">UNSPENT TRANSACTION OUTPUTS</div>
<div class="spacer10"></div>
<div class="box-body">
<table id="xpub-table-list-utxos">
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script include-js="xpubs-tools/xpubs-tools.js"></script>

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

@ -0,0 +1,259 @@
const screenXpubsToolsScript = {
explorerInfo: null,
currentXpub: null,
isReimport: false,
initPage: function() {
this.getExplorerInfo()
// Sets the event handlers
$('#btn-xpub-search-go').click(() => {this.searchXpub()})
$('#btn-xpub-details-reset').click(() => {this.showSearchForm()})
$('#btn-xpub-details-rescan').click(() => {this.showRescanForm()})
$('#btn-xpub-rescan-go').click(() => {this.rescanXpub()})
$('#btn-xpub-rescan-cancel').click(() => {this.hideRescanForm()})
$('#btn-xpub-import-go').click(() => {this.importXpub()})
$('#btn-xpub-details-retype').click(() => {this.showImportForm(true)})
$('#btn-xpub-import-cancel').click(() => {this.hideImportForm(this.isReimport)})
$('#xpubs-tool').keyup(evt => {
if (evt.keyCode === 13) {
this.searchXpub()
}
})
},
preparePage: function() {
this.hideRescanForm()
this.showSearchForm()
$("#xpub").focus()
},
getExplorerInfo: function() {
lib_api.getExplorerPairingInfo().then(explorerInfo => {
this.explorerInfo = explorerInfo
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
searchXpub: function() {
lib_msg.displayMessage('Search in progress...');
const xpub = $('#xpub').val()
this.currentXpub = xpub
return this._searchXpub(xpub).then(() => {
lib_msg.cleanMessagesUi()
})
},
_searchXpub: function(xpub) {
return lib_api.getXpubInfo(xpub).then(xpubInfo => {
if (xpubInfo && xpubInfo['tracked']) {
this.setXpubDetails(xpubInfo)
this.showXpubDetails()
const jsonData = {'active': xpub}
return lib_api.getWallet(jsonData).then(walletInfo => {
// Display the txs
const txs = walletInfo['txs']
for (let tx of txs)
this.setTxDetails(tx)
// Display the UTXOs
const utxos = walletInfo['unspent_outputs'].sort((a,b) => {
return a['confirmations'] - b['confirmations']
})
$('#xpub-nb-utxos').text(utxos.length)
for (let utxo of utxos)
this.setUtxoDetails(utxo)
})
} else {
lib_msg.displayErrors('xpub not found')
this.showImportForm(false)
}
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
throw e
})
},
importXpub: function() {
lib_msg.displayMessage('Processing xpub import...');
const jsonData = {
'xpub': this.currentXpub,
'type': 'restore',
'force': true
}
const derivType = $('#import-deriv-type').val()
if (derivType == 'bip49' || derivType == 'bip84') {
jsonData['segwit'] = derivType
} else if (derivType == 'auto') {
if (this.currentXpub.startsWith('ypub'))
jsonData['segwit'] = 'bip49'
else if (this.currentXpub.startsWith('zpub'))
jsonData['segwit'] = 'bip84'
}
return lib_api.postXpub(jsonData)
.then(result => {
this._searchXpub(this.currentXpub).then(() => {
lib_msg.displayInfo('Import complete')
})
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
rescanXpub: function() {
lib_msg.displayMessage('Processing xpub rescan...');
let startIdx = $('#rescan-start-idx').val()
startIdx = (startIdx == null) ? 0 : parseInt(startIdx)
let lookahead = $('#rescan-lookahead').val()
lookahead = (lookahead == null) ? 100 : parseInt(lookahead)
return lib_api.getXpubRescan(this.currentXpub, lookahead, startIdx)
.then(result => {
this.hideRescanForm()
this._searchXpub(this.currentXpub).then(() => {
lib_msg.displayInfo('Rescan complete')
})
}).catch(e => {
lib_msg.displayErrors(lib_msg.extractJqxhrErrorMsg(e))
console.log(e)
})
},
setXpubDetails: function(xpubInfo) {
$('tr.tx-row').remove()
$('tr.utxo-row').remove()
$('#xpub-value').text(this.currentXpub)
$('#xpub-import-date').text(xpubInfo['created'])
$('#xpub-deriv-type').text(xpubInfo['derivation'])
$('#xpub-nb-txs').text(xpubInfo['n_tx'])
$('#xpub-nb-utxos').text('-')
const balance = parseInt(xpubInfo['balance']) / 100000000
$('#xpub-balance').text(`${balance} BTC`)
$('#xpub-deriv-account').text(xpubInfo['account'])
$('#xpub-deriv-depth').text(xpubInfo['depth'])
$('#xpub-idx-unused-ext').text(xpubInfo['unused']['external'])
$('#xpub-idx-derived-ext').text(xpubInfo['derived']['external'])
$('#xpub-idx-unused-int').text(xpubInfo['unused']['internal'])
$('#xpub-idx-derived-int').text(xpubInfo['derived']['internal'])
},
setTxDetails: function(tx) {
const txid = tx['hash']
const txidDisplay = `${txid.substring(0,50)}...`
const amount = parseInt(tx['result']) / 100000000
const amountLabel = amount < 0 ? amount : `+${amount}`
const amountStyle = amount < 0 ? 'amount-sent' : 'amount-received'
const date = lib_fmt.unixTsToLocaleString(tx['time'])
const txUrl = lib_cmn.getExplorerTxUrl(txid, this.explorerInfo)
const newRow = `<tr class="tx-row"><td colspan="2">&nbsp;</td></tr>
<tr class="tx-row">
<td class="table-label" colspan="2">
<a href="${txUrl}" target="_blank">${txidDisplay}</a>
</td>
</tr>
<tr class="tx-row">
<td class="table-label">Amount</td>
<td class="table-value ${amountStyle}">${amountLabel} BTC</td>
</tr>
<tr class="tx-row">
<td class="table-label">Block height</td>
<td class="table-value">${tx['block_height']}</td>
</tr>
<tr class="tx-row">
<td class="table-label">Date</td>
<td class="table-value">${date}</td>
</tr>`
$('#xpub-table-list-txs tr:last').after(newRow)
},
setUtxoDetails: function(utxo) {
const txid = utxo['tx_hash']
const txidVout = `${txid.substring(0,50)}...:${utxo['tx_output_n']}`
const amount = parseInt(utxo['value']) / 100000000
const txUrl = lib_cmn.getExplorerTxUrl(txid, this.explorerInfo)
const newRow = `<tr class="utxo-row"><td colspan="2">&nbsp;</td></tr>
<tr class="utxo-row">
<td class="table-label" colspan="2">
<a href="${txUrl}" target="_blank">${txidVout}</a>
</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Amount</td>
<td class="table-value">${amount} BTC</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Address</td>
<td class="table-value">${utxo['addr']}</td>
</tr>
<tr class="utxo-row">
<td class="table-label">Confirmations</td>
<td class="table-value">${utxo['confirmations']}</td>
</tr>`
$('#xpub-table-list-utxos tr:last').after(newRow)
},
showSearchForm: function() {
$('#xpubs-tool-details').hide()
$('#xpubs-tool-import').hide()
$('#xpub').val('')
$('#xpubs-tool-search-form').show()
lib_msg.cleanMessagesUi()
},
showImportForm: function(isReimport) {
this.isReimport = isReimport
$('#xpubs-tool-search-form').hide()
$('#xpubs-tool-details').hide()
if (isReimport) {
$('#import-deriv-first-import-msg').hide()
$('#import-deriv-reimport-msg').show()
} else {
$('#import-deriv-reimport-msg').hide()
$('#import-deriv-first-import-msg').show()
}
const xpubLen = this.currentXpub.length
const xpubShortLbl = `"${this.currentXpub.substring(0, 20)}...${this.currentXpub.substring(xpubLen-20, xpubLen)}"`
$('#import-xpub').text(xpubShortLbl)
$('#xpubs-tool-import').show()
},
hideImportForm: function(isReimport) {
if (isReimport)
this.showXpubDetails()
else
this.showSearchForm()
},
showXpubDetails: function() {
$('#xpubs-tool-search-form').hide()
$('#xpubs-tool-import').hide()
$('#xpubs-tool-details').show()
},
showRescanForm: function() {
$('#xpubs-tool-actions').hide()
$('#xpubs-rescans-actions').show()
lib_msg.cleanMessagesUi()
},
hideRescanForm: function() {
$('#xpubs-rescans-actions').hide()
$('#xpubs-tool-actions').show()
},
}
screenScripts.set('#screen-xpubs-tools', screenXpubsToolsScript)

BIN
static/admin/icons/samourai-logo-loading.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

17
static/admin/index.html

@ -8,7 +8,7 @@
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
<script src="lib/jquery-3.2.1.min.js"></script>
<script src="lib/jquery-3.5.1.min.js"></script>
<script src="conf/index.js"></script>
<script src="lib/common-script.js"></script>
<script src="lib/api-wrapper.js"></script>
@ -40,17 +40,10 @@
</div>
<div class="col-xs-4"></div>
</div>
<!-- MESSAGES -->
<div class="row msg-boxes">
<div class="col-xs-4"></div>
<div class="col-xs-4">
<div id="msg" class="msg"></div>
<div id="errors" class="msg-error"></div>
<div id="info" class="msg-info"></div>
</div>
<div class="col-xs-4"></div>
</div>
</div>
<!-- MSG BOX -->
<div id="box-msg"
include-html="dmt/msg-box/msg-box.html">
</div>
</body>

50
static/admin/index.js

@ -2,54 +2,56 @@
* Signin
*/
function login() {
let apiKey = $('#apikey').val();
let apiKey = $('#apikey').val()
let dataJson = {
'apikey': apiKey
};
}
// Checks input fields
if (!apiKey) {
lib_msg.displayErrors('Admin key is mandatory');
return;
lib_msg.displayErrors('Admin key is mandatory')
return
}
lib_msg.displayMessage('Processing...');
let deferred = lib_api.signin(dataJson);
lib_msg.displayMessage('Processing...')
let deferred = lib_api.signin(dataJson)
deferred.then(
function (result) {
const auth = result['authorizations'];
const accessToken = auth['access_token'];
const auth = result['authorizations']
const accessToken = auth['access_token']
if (lib_auth.isAdmin(accessToken)) {
lib_auth.setAccessToken(accessToken);
const refreshToken = auth['refresh_token'];
lib_auth.setRefreshToken(refreshToken);
sessionStorage.setItem('activeTab', '');
lib_msg.displayInfo('Successfully connected to your backend');
lib_auth.setAccessToken(accessToken)
const refreshToken = auth['refresh_token']
lib_auth.setRefreshToken(refreshToken)
sessionStorage.setItem('activeTab', '')
lib_msg.displayInfo('Successfully connected to your backend')
// Redirection to default page
lib_cmn.goToDefaultPage();
lib_cmn.goToDefaultPage()
} else {
lib_msg.displayErrors('You must sign in with the admin key');
lib_msg.displayErrors('You must sign in with the admin key')
}
},
function (jqxhr) {
let msg = lib_msg.extractJqxhrErrorMsg(jqxhr);
lib_msg.displayErrors(msg);
let msg = lib_msg.extractJqxhrErrorMsg(jqxhr)
lib_msg.displayErrors(msg)
}
);
)
}
/*
* onPageLoaded
*/
$(document).ready(function() {
$(document).ready(function() {
// Dynamic loading of html and scripts
lib_cmn.includeHTML()
// Sets the event handlers
$('#apikey').keyup(function(evt) {
if (evt.keyCode === 13) {
login();
login()
}
});
$('#signin').click(function() {
login();
});
});
login()
})
})

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

@ -1,5 +1,5 @@
var lib_api = {
const lib_api = {
/**
* Base URI
*/
@ -9,164 +9,160 @@ var lib_api = {
* Authentication
*/
signin: function(data) {
let uri = this.baseUri + '/auth/login';
return this.sendPostUriEncoded(uri, data);
let uri = this.baseUri + '/auth/login'
return this.sendPostUriEncoded(uri, data)
},
/**
* Gets a new access token
* Gets a new access token
*/
refreshToken: function(data) {
let uri = this.baseUri + '/auth/refresh';
return this.sendPostUriEncoded(uri, data);
let uri = this.baseUri + '/auth/refresh'
return this.sendPostUriEncoded(uri, data)
},
/**
* API Status
*/
getApiStatus: function() {
let prefix = conf['prefixes']['status'];
let uri = this.baseUri + '/' + prefix;
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['status']
let uri = this.baseUri + '/' + prefix
return this.sendGetUriEncoded(uri, {})
},
/**
* Get pairing info
*/
getPairingInfo: function() {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/pairing';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/pairing'
return this.sendGetUriEncoded(uri, {})
},
/**
* Get block explorer pairing info
*/
getExplorerPairingInfo: function() {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/pairing/explorer';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/pairing/explorer'
return this.sendGetUriEncoded(uri, {})
},
/**
* PushTx Status
*/
getPushtxStatus: function() {
let prefix = conf['prefixes']['statusPushtx'];
let uri = this.baseUri + '/pushtx/' + prefix;
//let uri = 'http://127.0.0.1:8081/' + prefix;
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['statusPushtx']
let uri = this.baseUri + '/pushtx/' + prefix
//let uri = 'http://127.0.0.1:8081/' + prefix
return this.sendGetUriEncoded(uri, {})
},
/**
* Orchestrztor Status
*/
getOrchestratorStatus: function() {
let prefix = conf['prefixes']['statusPushtx'];
let uri = this.baseUri + '/pushtx/' + prefix + '/schedule';
//let uri = 'http://127.0.0.1:8081/' + prefix + '/schedule';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['statusPushtx']
let uri = this.baseUri + '/pushtx/' + prefix + '/schedule'
//let uri = 'http://127.0.0.1:8081/' + prefix + '/schedule'
return this.sendGetUriEncoded(uri, {})
},
/**
* Gets information about an address
*/
getAddressInfo: function(address) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/address/' + address + '/info';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/address/' + address + '/info'
return this.sendGetUriEncoded(uri, {})
},
/**
* Rescans an address
*/
getAddressRescan: function(address) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/address/' + address + '/rescan';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/address/' + address + '/rescan'
return this.sendGetUriEncoded(uri, {})
},
/**
* Gets information about a xpub
*/
getXpubInfo: function(xpub) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/xpub/' + xpub + '/info';
return this.sendGetUriEncoded(uri, {});
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/xpub/' + xpub + '/info'
return this.sendGetUriEncoded(uri, {})
},
/**
* Rescans a xpub
*/
getXpubRescan: function(xpub, nbAddr, startIdx) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/' + prefix + '/xpub/' + xpub + '/rescan';
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/' + prefix + '/xpub/' + xpub + '/rescan'
return this.sendGetUriEncoded(
uri,
{
'gap': nbAddr,
'startidx': startIdx
}
);
)
},
/**
* Notifies the server of the new HD account for tracking.
*/
postXpub: function(arguments) {
let uri = this.baseUri + '/xpub';
return this.sendPostUriEncoded(uri, arguments);
},
/**
* Multiaddr
*/
getMultiaddr: function(arguments) {
let uri = this.baseUri + '/multiaddr';
return this.sendGetUriEncoded(uri, arguments);
let uri = this.baseUri + '/xpub'
return this.sendPostUriEncoded(uri, arguments)
},
/**
* Unspent
* Wallet
*/
getUnspent: function(arguments) {
let uri = this.baseUri + '/unspent';
return this.sendGetUriEncoded(uri, arguments);
getWallet: function(arguments) {
let uri = this.baseUri + '/wallet'
return this.sendGetUriEncoded(uri, arguments)
},
/**
* Transaction
*/
getTransaction: function(txid) {
let uri = this.baseUri + '/tx/' + txid;
return this.sendGetUriEncoded(uri, {});
let uri = this.baseUri + '/tx/' + txid
return this.sendGetUriEncoded(
uri,
{
'fees': 1
}
)
},
/**
* Rescans a range of blocks
*/
getBlocksRescan: function(fromHeight, toHeight) {
let prefix = conf['prefixes']['support'];
let uri = this.baseUri + '/tracker/' + prefix + '/rescan';
//let uri = 'http://127.0.0.1:8082/' + prefix + '/rescan';
let prefix = conf['prefixes']['support']
let uri = this.baseUri + '/tracker/' + prefix + '/rescan'
//let uri = 'http://127.0.0.1:8082/' + prefix + '/rescan'
return this.sendGetUriEncoded(
uri,
{
'fromHeight': fromHeight,
'toHeight': toHeight
}
);
)
},
/**
* HTTP requests methods
*/
sendGetUriEncoded: function(uri, data) {
data['at'] = lib_auth.getAccessToken();
data['at'] = lib_auth.getAccessToken()
let deferred = $.Deferred(),
dataString = $.param(data);
dataString = $.param(data)
$.when($.ajax({
url: uri,
@ -175,20 +171,20 @@ var lib_api = {
contentType: "application/x-www-form-urlencoded; charset=utf-8"
}))
.done(function (result) {
deferred.resolve(result);
deferred.resolve(result)
})
.fail(function (jqxhr, textStatus, error) {
deferred.reject(jqxhr);
});
deferred.reject(jqxhr)
})
return deferred.promise();
return deferred.promise()
},
sendPostUriEncoded: function(uri, data) {
data['at'] = lib_auth.getAccessToken();
data['at'] = lib_auth.getAccessToken()
let deferred = $.Deferred(),
dataString = $.param(data);
dataString = $.param(data)
$.when($.ajax({
url: uri,
@ -197,19 +193,19 @@ var lib_api = {
contentType: "application/x-www-form-urlencoded; charset=utf-8"
}))
.done(function (result) {
deferred.resolve(result);
deferred.resolve(result)
})
.fail(function (jqxhr, textStatus, error) {
deferred.reject(jqxhr);
deferred.reject(jqxhr)
});
return deferred.promise();
return deferred.promise()
},
sendGetJson: function(uri, data) {
data['at'] = lib_auth.getAccessToken();
data['at'] = lib_auth.getAccessToken()
let deferred = $.Deferred();
let deferred = $.Deferred()
$.when($.ajax({
url: uri,
@ -217,21 +213,21 @@ var lib_api = {
data: data,
}))
.done(function (result) {
deferred.resolve(result);
deferred.resolve(result)
})
.fail(function (jqxhr, textStatus, error) {
deferred.reject(jqxhr);
deferred.reject(jqxhr)
});
return deferred.promise();
return deferred.promise()
},
sendPostJson: function(uri, data) {
data['at'] = lib_auth.getAccessToken();
data['at'] = lib_auth.getAccessToken()
let deferred = $.Deferred(),
dataString = JSON.stringify(data);
dataString = JSON.stringify(data)
$.when($.ajax({
url: uri,
@ -241,13 +237,13 @@ var lib_api = {
dataType: 'json'
}))
.done(function (result) {
deferred.resolve(result);
deferred.resolve(result)
})
.fail(function (jqxhr, textStatus, error) {
deferred.reject(jqxhr);
deferred.reject(jqxhr)
});
return deferred.promise();
return deferred.promise()
}
}

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

@ -1,4 +1,4 @@
var lib_auth = {
const lib_auth = {
/* SessionStorage Key used for access token */
SESSION_STORE_ACCESS_TOKEN: 'access_token',
@ -16,34 +16,34 @@ var lib_auth = {
TOKEN_PROFILE_ADMIN: 'admin',
/*
/*
* Retrieves access token from session storage
*/
getAccessToken: function() {
return sessionStorage.getItem(this.SESSION_STORE_ACCESS_TOKEN);
return sessionStorage.getItem(this.SESSION_STORE_ACCESS_TOKEN)
},
/*
/*
* Stores access token in session storage
*/
setAccessToken: function(token) {
const now = new Date();
sessionStorage.setItem(this.SESSION_STORE_ACCESS_TOKEN_TS, now.getTime());
sessionStorage.setItem(this.SESSION_STORE_ACCESS_TOKEN, token);
sessionStorage.setItem(this.SESSION_STORE_ACCESS_TOKEN_TS, now.getTime())
sessionStorage.setItem(this.SESSION_STORE_ACCESS_TOKEN, token)
},
/*
/*
* Retrieves refresh token from session storage
*/
getRefreshToken: function() {
return sessionStorage.getItem(this.SESSION_STORE_REFRESH_TOKEN);
return sessionStorage.getItem(this.SESSION_STORE_REFRESH_TOKEN)
},
/*
/*
* Stores refresh token in session storage
*/
setRefreshToken: function(token) {
sessionStorage.setItem(this.SESSION_STORE_REFRESH_TOKEN, token);
sessionStorage.setItem(this.SESSION_STORE_REFRESH_TOKEN, token)
},
/*
@ -51,28 +51,28 @@ var lib_auth = {
*/
refreshAccessToken: function() {
if (!this.isAuthenticated()) {
return;
return
}
const now = new Date();
const atts = sessionStorage.getItem(this.SESSION_STORE_ACCESS_TOKEN_TS);
const timeElapsed = (now.getTime() - atts) / 1000;
const atts = sessionStorage.getItem(this.SESSION_STORE_ACCESS_TOKEN_TS)
const timeElapsed = (now.getTime() - atts) / 1000
// Refresh the access token if more than 10mn
if (timeElapsed > 600) {
// Refresh the access token if more than 5mn
if (timeElapsed > 300) {
const dataJson = {
'rt': this.getRefreshToken()
};
}
let self = this;
let self = this
let deferred = lib_api.refreshToken(dataJson);
let deferred = lib_api.refreshToken(dataJson)
deferred.then(
function (result) {
const auth = result['authorizations'];
const accessToken = auth['access_token'];
self.setAccessToken(accessToken);
const auth = result['authorizations']
const accessToken = auth['access_token']
self.setAccessToken(accessToken)
},
function (jqxhr) {
// Do nothing
@ -86,8 +86,8 @@ var lib_auth = {
*/
isAuthenticated: function() {
// Checks that an access token is stored in session storage
let token = this.getAccessToken();
return (token && (token != 'null')) ? true : false;
let token = this.getAccessToken()
return (token && (token != 'null')) ? true : false
},
/*
@ -96,17 +96,17 @@ var lib_auth = {
*/
getPayloadAccessToken: function(token) {
if (!token)
token = this.getAccessToken();
token = this.getAccessToken()
if (!token)
return null;
return null
try {
const payloadBase64 = token.split('.')[1];
const payloadUtf8 = atob(payloadBase64);
return JSON.parse(payloadUtf8);
const payloadBase64 = token.split('.')[1]
const payloadUtf8 = atob(payloadBase64)
return JSON.parse(payloadUtf8)
} catch {
return null;
return null
}
},
@ -114,10 +114,10 @@ var lib_auth = {
* Check if user has admin profile
*/
isAdmin: function(token) {
const payload = this.getPayloadAccessToken(token);
const payload = this.getPayloadAccessToken(token)
if (!payload)
return false;
return (('prf' in payload) && (payload['prf'] == this.TOKEN_PROFILE_ADMIN));
return false
return (('prf' in payload) && (payload['prf'] == this.TOKEN_PROFILE_ADMIN))
},
/*
@ -125,10 +125,10 @@ var lib_auth = {
*/
logout: function() {
// Clears session storage
this.setRefreshToken(null);
this.setAccessToken(null);
sessionStorage.setItem('activeTab', '');
lib_cmn.goToHomePage();
this.setRefreshToken(null)
this.setAccessToken(null)
sessionStorage.setItem('activeTab', '')
lib_cmn.goToHomePage()
}
}

2377
static/admin/lib/bootstrap.js

File diff suppressed because it is too large

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

@ -1,51 +1,125 @@
lib_cmn = {
const lib_cmn = {
// Utils functions
hasProperty: function(obj, propName) {
/* Checks if an object has a property with given name */
if ( (obj == null) || (!propName) )
return false;
if ( (obj == null) || (!propName) )
return false
else if (obj.hasOwnProperty('propName') || propName in obj)
return true;
return true
else
return false;
return false
},
// Go to default page
goToDefaultPage: function() {
const baseUri = conf['adminTool']['baseUri'];
sessionStorage.setItem('activeTab', '#link-pairing');
window.location = baseUri + '/tool/';
const baseUri = conf['adminTool']['baseUri']
sessionStorage.setItem('activeTab', '#link-status')
window.location = baseUri + '/dmt/'
},
// Go to home page
goToHomePage: function() {
sessionStorage.setItem('activeTab', null);
window.location = conf['adminTool']['baseUri'] + '/';
sessionStorage.setItem('activeTab', null)
window.location = conf['adminTool']['baseUri'] + '/'
},
// Loads html snippet
// Get Transaction url on selected explorer
getExplorerTxUrl: function(txid, explorerInfo) {
if (explorerInfo == null)
return null
else if (explorerInfo['pairing']['type'] == 'explorer.oxt')
return `${explorerInfo['pairing']['url']}/transaction/${txid}`
else if (explorerInfo['pairing']['type'] == 'explorer.btc_rpc_explorer')
return `http://${explorerInfo['pairing']['url']}/tx/${txid}`
else
return null
},
// Loads html snippets
includeHTML: function(cb) {
let self = this;
let z, i, elmnt, file, xhttp;
z = document.getElementsByTagName('*');
let self = this
let z, i, elmnt, file, xhttp
z = document.getElementsByTagName('*')
for (i = 0; i < z.length; i++) {
elmnt = z[i]
file = elmnt.getAttribute('include-html')
if (file) {
xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
elmnt.innerHTML = this.responseText
elmnt.removeAttribute('include-html')
self.includeHTML(cb)
self.includeJs(elmnt)
}
}
xhttp.open('GET', file, true)
xhttp.send()
return
}
}
if (cb) cb()
},
// Loads js snippets
includeJs: function(element) {
let self = this
let z, i, elmnt, file, xhttp
z = element.querySelectorAll('script')
for (i = 0; i < z.length; i++) {
elmnt = z[i];
file = elmnt.getAttribute('include-html');
elmnt = z[i]
file = elmnt.getAttribute('include-js')
if (file) {
xhttp = new XMLHttpRequest();
xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
elmnt.innerHTML = this.responseText;
elmnt.removeAttribute('include-html');
self.includeHTML(cb);
const newElmnt = document.createElement('script')
newElmnt.textContent = this.responseText
if (elmnt.parentNode) {
elmnt.parentNode.insertBefore(newElmnt, elmnt.nextSibling)
elmnt.parentNode.removeChild(elmnt)
}
}
}
xhttp.open('GET', file, true);
xhttp.send();
return;
}
xhttp.open('GET', file, true)
xhttp.send()
return
}
}
if (cb) cb();
},
pad10: function(v) {
return (v < 10) ? `0${v}` : `${v}`
},
pad100: function(v) {
if (v < 10) return `00${v}`
if (v < 100) return `0${v}`
return `${v}`
},
timePeriod: function(period, milliseconds) {
milliseconds = !!milliseconds
const whole = Math.floor(period)
const ms = 1000*(period - whole)
const s = whole % 60
const m = (whole >= 60) ? Math.floor(whole / 60) % 60 : 0
const h = (whole >= 3600) ? Math.floor(whole / 3600) % 24 : 0
const d = (whole >= 86400) ? Math.floor(whole / 86400) : 0
const parts = [this.pad10(h), this.pad10(m), this.pad10(s)]
if (d > 0)
parts.splice(0, 0, this.pad100(d))
const str = parts.join(':')
if (milliseconds) {
return str + '.' + this.pad100(ms)
} else {
return str
}
}
}

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

@ -1,13 +1,13 @@
lib_fmt = {
const lib_fmt = {
/*
* Returns a stringified version of a cleaned json object
*/
cleanJson: function(json) {
let jsonText = JSON.stringify(json);
jsonText = jsonText.replace(/'/g, '"').replace(/False/g, 'false').replace(/True/g, 'true');
jsonText = jsonText.replace(/(Decimal\(")([0-9.E\-,]*)("\))/g, '"$2"');
return jsonText;
let jsonText = JSON.stringify(json)
jsonText = jsonText.replace(/'/g, '"').replace(/False/g, 'false').replace(/True/g, 'true')
jsonText = jsonText.replace(/(Decimal\(")([0-9.E\-,]*)("\))/g, '"$2"')
return jsonText
},
/*
@ -15,48 +15,49 @@ lib_fmt = {
*/
jsonSyntaxHighlight: function(json) {
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 2);
json = JSON.stringify(json, undefined, 2)
}
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
function (match) {
let cls = 'number';
let cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
cls = 'key'
} else {
cls = 'string';
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null';
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>';
return '<span class="' + cls + '">' + match + '</span>'
}
);
)
},
/*
* Format a unix timestamp to locale date string
*/
unixTsToLocaleString: function(ts) {
let tmpDate = new Date(ts*1000);
return tmpDate.toLocaleString();
let tmpDate = new Date(ts*1000)
return tmpDate.toLocaleString()
},
/*
* Format a unix timestamp into a readable date/hour
*/
formatUnixTs: function(ts) {
if (ts == null || ts == 0)
return '-';
return '-'
let tmpDate = new Date(ts*1000),
options = {hour: '2-digit', minute: '2-digit', hour12: false};
return tmpDate.toLocaleDateString('fr-FR', options);
options = {hour: '2-digit', minute: '2-digit', hour12: false}
return tmpDate.toLocaleDateString('fr-FR', options)
}
}

4
static/admin/lib/jquery-3.2.1.min.js

File diff suppressed because one or more lines are too long

2
static/admin/lib/jquery-3.5.1.min.js

File diff suppressed because one or more lines are too long

44
static/admin/lib/messages.js

@ -1,39 +1,41 @@
var lib_msg = {
const lib_msg = {
// Extracts jqxhr error message
extractJqxhrErrorMsg: function(jqxhr) {
let hasErrorMsg = ('responseJSON' in jqxhr) &&
(jqxhr['responseJSON'] != null) &&
('message' in jqxhr['responseJSON']);
let hasErrorMsg = ('responseJSON' in jqxhr) &&
(jqxhr['responseJSON'] != null) &&
('error' in jqxhr['responseJSON'])
return hasErrorMsg ? jqxhr['responseJSON']['message'] : jqxhr.statusText;
return hasErrorMsg ? jqxhr['responseJSON']['error'] : jqxhr.statusText
},
// UI functions
addTextinID: function(text, id){
$(id).html(text.toUpperCase());
$(id).html(text.toUpperCase())
},
displayMessage: function(text){
this.addTextinID('', '#errors');
this.addTextinID('', '#info');
this.addTextinID(text, '#msg');
this.addTextinID('', '#errors')
this.addTextinID('', '#info')
this.addTextinID(text, '#msg')
},
displayErrors: function(text){
this.addTextinID('', '#msg');
this.addTextinID('', '#info');
this.addTextinID(text, '#errors');
this.addTextinID('', '#msg')
this.addTextinID('', '#info')
this.addTextinID(text, '#errors')
},
displayInfo: function(text){
this.addTextinID('', '#msg');
this.addTextinID('', '#errors');
this.addTextinID(text, '#info');
this.addTextinID('', '#msg')
this.addTextinID('', '#errors')
this.addTextinID(text, '#info')
},
cleanMessagesUi: function() {
this.addTextinID('', '#msg');
this.addTextinID('', '#errors');
this.addTextinID('', '#info');
this.addTextinID('', '#msg')
this.addTextinID('', '#errors')
this.addTextinID('', '#info')
}
}
}

135
static/admin/tool/index.html

@ -1,135 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DOJO // MAINTENANCE TOOL</title>
<link rel="stylesheet" type="text/css" href="../css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="../css/style.css">
<script src="../lib/jquery-3.2.1.min.js"></script>
<script src="../lib/jquery.qrcode.min.js"></script>
<script src="../conf/index.js"></script>
<script src="../lib/common-script.js"></script>
<script src="../lib/api-wrapper.js"></script>
<script src="../lib/auth-utils.js"></script>
<script src="../lib/format-utils.js"></script>
<script src="index.js"></script>
</head>
<body>
<div id="info-xpub" class="container">
<!-- HEADER -->
<div id="header" class="row">
<div class="col-xs-9">
<h1 class="title"><span>DOJO // MAINTENANCE TOOL</span> <span id="dojo-version" class="beta">beta</span></h1>
</div>
<div class="col-xs-3 login-box">
<a id="btn-logout" style="display: inline;" href="#" title="DISCONNECT">
<img src="../icons/ic_power_settings_new_white_24dp_1x.png" class="mini-icon"/>
</a>
</div>
</div>
<div class="spacer60"></div>
<!-- TAB MENU -->
<div id="tab-menu" class="row">
<div class="col-xs-12" >
<ul id="tab-menu_list" class="nav nav-pills">
<li id="link-pairing">
<a href="#">PAIRING</a>
</li>
<li id="link-status-api">
<a href="#">API</a>
</li>
<li id="link-status-pushtx">
<a href="#">PUSHTX</a>
</li>
<li id="link-orchestrator">
<a href="#">ORCHESTRATOR</a>
</li>
<li id="link-info-xpub">
<a href="#">XPUB INFO</a>
</li>
<li id="link-rescan-xpub">
<a href="#">XPUB RESCAN</a>
</li>
<li id="link-xpub">
<a href="#">XPUB</a>
</li>
<li id="link-info-address">
<a href="#">ADDR. INFO</a>
</li>
<li id="link-rescan-address">
<a href="#">ADDR. RESCAN</a>
</li>
<li id="link-multiaddr">
<a href="#">MULTIADDR</a>
</li>
<li id="link-unspent">
<a href="#">UNSPENT</a>
</li>
<li id="link-tx">
<a href="#">TX</a>
</li>
<li id="link-rescan-blocks">
<a href="#">BLOCKS RESCAN</a>
</li>
</ul>
</div>
</div>
<!-- BODY -->
<div id="body" class="row">
<div class="col-xs-1"></div>
<div class="col-xs-10 json-data-container">
<!-- PAIRING -->
<div id="screen-pairing">
<div class="row">
<div id="qr-label" class="halfwidth">
PAIR YOUR WALLET WITH YOUR DOJO
</div>
<div id="qr-explorer-label" class="halfwidth">
PAIR YOUR WALLET WITH YOUR BLOCK EXPLORER
</div>
</div>
<div class="row">
<div id="qr-container" class="halfwidth">
<div id="qr-pairing"></div>
</div>
<div id="qr-explorer-container" class="halfwidth">
<div id="qr-explorer-pairing"></div>
</div>
</div>
</div>
<!-- MAINTENANCE -->
<div id="form-maintenance">
<div id="row-form-field">
<div id="cell-args">
<input type="text" id="args" placeholder="">
</div>
<div id="cell-args2">
<input type="text" id="args2" placeholder="">
</div>
<div id="cell-args3">
<input type="text" id="args3" placeholder="">
</div>
</div>
<div id="row-form-button" class="center">
<button id="btn-go"
class="btn btn-success"
type="button">GO</button>
</div>
<div class="center">
<pre id="json-data" style="min-height: 300px"></pre>
</div>
</div>
</div>
<div class="col-xs-1"></div>
</div>
</div>
</body>
</html>

300
static/admin/tool/index.js

@ -1,300 +0,0 @@
/**
* Display Messages
*/
function displayInfoMsg(msg) {
const htmlMsg = '<span class="info">' + msg + '</span>';
$('#json-data').html(htmlMsg);
}
function displayErrorMsg(msg) {
const htmlMsg = '<span class="error">' + msg + '</span>';
$('#json-data').html(htmlMsg);
}
function displayQRPairing() {
const activeTab = sessionStorage.getItem('activeTab');
processAction(activeTab).then(
function (result) {
if (result) {
if (result['api']) {
const textJson = JSON.stringify(result['api'], null, 4);
$("#qr-pairing").html('') // clear qrcode first
$('#qr-pairing').qrcode({width: 256, height: 256, text: textJson});
}
if (result['explorer'] && result['explorer']['pairing']['url']) {
const textJson = JSON.stringify(result['explorer'], null, 4);
$("#qr-explorer-pairing").html('') // clear qrcode first
$('#qr-explorer-pairing').qrcode({width: 256, height: 256, text: textJson});
} else {
$("#qr-label").removeClass('halfwidth');
$("#qr-label").addClass('fullwidth');
$("#qr-container").removeClass('halfwidth');
$("#qr-container").addClass('fullwidth');
$("#qr-explorer-label").hide();
$("#qr-explorer-container").hide();
}
}
},
function (jqxhr) {}
);
}
/**
* On tab switched
*/
function initTabs() {
// Activates the current tab
let currentTab = sessionStorage.getItem('activeTab');
if (!currentTab) {
currentTab = '#link-pairing';
}
$(currentTab).addClass('active');
const tabs = [
'#link-pairing',
'#link-status-api',
'#link-status-pushtx',
'#link-orchestrator',
'#link-info-xpub',
'#link-rescan-xpub',
'#link-xpub',
'#link-info-address',
'#link-rescan-address',
'#link-rescan-blocks',
'#link-multiaddr',
'#link-unspent',
'#link-tx'
];
// Sets event handlers
for (let tab of tabs) {
$(tab).click(function() {
$(sessionStorage.getItem('activeTab')).removeClass('active');
sessionStorage.setItem('activeTab', tab);
$(tab).addClass('active');
preparePage();
});
}
}
/**
* Prepares the page content
*/
function preparePage() {
const activeTab = sessionStorage.getItem('activeTab');
// Dojo version
let lblVersion = sessionStorage.getItem('lblVersion');
if (lblVersion == null) {
lib_api.getPairingInfo().then(apiInfo => {
lblVersion = 'v' + apiInfo['pairing']['version'] + ' beta';
sessionStorage.setItem('lblVersion', lblVersion);
$('#dojo-version').text(lblVersion);
});
} else {
$('#dojo-version').text(lblVersion);
}
// Pairing
if (activeTab == '#link-pairing') {
$('#screen-pairing').show();
$('#form-maintenance').hide();
displayQRPairing();
// Maintenance screens
} else {
$('#form-maintenance').show();
$('#screen-pairing').hide();
let placeholder = '',
placeholder2 = '',
placeholder3 = '';
$("#cell-args").removeClass('halfwidth');
$("#cell-args").addClass('fullwidth');
$("#cell-args2").hide();
$("#cell-args3").hide();
if (activeTab == '#link-status-api' ||
activeTab == '#link-status-pushtx' ||
activeTab == '#link-orchestrator'
) {
$("#row-form-field").hide();
$("#row-form-button").hide();
processGo();
} else {
$("#row-form-field").show();
$("#row-form-button").show();
}
if (activeTab == '#link-info-xpub') {
placeholder = 'ENTER A XPUB, YPUB OR ZPUB';
} else if (activeTab == '#link-xpub') {
placeholder = 'ENTER /XPUB URL ARGUMENTS (e.g.: xpub=xpub0123456789&segwit=bip84&type=restore&force=true)';
} else if (activeTab == '#link-info-address') {
placeholder = 'ENTER A BITCOIN ADDRESS';
} else if (activeTab == '#link-rescan-address') {
placeholder = 'ENTER A BITCOIN ADDRESS';
} else if (activeTab == '#link-rescan-blocks') {
$("#cell-args").removeClass('fullwidth');
$("#cell-args").addClass('halfwidth');
$("#cell-args2").show();
placeholder = 'RESCAN BLOCKS FROM HEIGHT...';
placeholder2 = '...TO HEIGHT (OPTIONAL)';
} else if (activeTab == '#link-multiaddr') {
placeholder = 'ENTER /MULTIADDR URL ARGUMENTS (e.g.: active=xpub0123456789&new=address2|address3&pubkey=pubkey4)';
} else if (activeTab == '#link-unspent') {
placeholder = 'ENTER /UNSPENT URL ARGUMENTS (e.g.: active=xpub0123456789&new=address2|address3&pubkey=pubkey4)';
} else if (activeTab == '#link-tx') {
placeholder = 'ENTER A TRANSACTION TXID';
} else if (activeTab == '#link-rescan-xpub') {
$("#cell-args").removeClass('fullwidth');
$("#cell-args").addClass('halfwidth');
$("#cell-args2").show();
$("#cell-args3").show();
placeholder = 'ENTER A XPUB, YPUB OR ZPUB';
placeholder2 = 'ENTER #ADDR. (DEFAULT=100)';
placeholder3 = 'ENTER START INDEX (DEFAULT=0)';
}
$("#args").attr('placeholder', placeholder);
$('#args').val('');
$("#args2").attr('placeholder', placeholder2);
$('#args2').val('');
$("#args3").attr('placeholder', placeholder3);
$('#args3').val('');
$('#json-data').html('');
}
}
/**
* Process action (api calls)
*/
function processAction(activeTab, args, args2, args3) {
if (activeTab == '#link-pairing') {
//return lib_api.getPairingInfo();
let result = {
'api': null,
'explorer': null
};
return lib_api.getPairingInfo().then(apiInfo => {
if (apiInfo) {
apiInfo['pairing']['url'] = window.location.protocol + '//' + window.location.host + conf['api']['baseUri'];
result['api'] = apiInfo;
}
}).then(() => {
return lib_api.getExplorerPairingInfo();
}).then(explorerInfo => {
if (explorerInfo)
result['explorer'] = explorerInfo;
return result
}).catch(e => {
console.log(e);
return result;
});
} else if (activeTab == '#link-status-api') {
return lib_api.getApiStatus();
} else if (activeTab == '#link-status-pushtx') {
return lib_api.getPushtxStatus();
} else if (activeTab == '#link-orchestrator') {
return lib_api.getOrchestratorStatus();
}
if (args == '') {
alert('Argument is mandatory');
return;
}
if (activeTab == '#link-info-xpub') {
return lib_api.getXpubInfo(args);
} else if (activeTab == '#link-rescan-xpub') {
const nbAddr = (!args2) ? 100 : parseInt(args2);
const startIdx = (!args3) ? 0 : parseInt(args3);
return lib_api.getXpubRescan(args, nbAddr, startIdx);
} else if (activeTab == '#link-info-address') {
return lib_api.getAddressInfo(args);
} else if (activeTab == '#link-rescan-address') {
return lib_api.getAddressRescan(args);
} else if (activeTab == '#link-rescan-blocks') {
const fromHeight = parseInt(args);
const toHeight = (args2) ? parseInt(args2) : fromHeight;
return lib_api.getBlocksRescan(fromHeight, toHeight);
} else if (activeTab == '#link-tx') {
return lib_api.getTransaction(args);
}
const jsonData = {};
const aArgs = args.split('&');
for (let arg of aArgs) {
const aArg = arg.split('=');
jsonData[aArg[0]] = aArg[1];
}
if (activeTab == '#link-multiaddr')
return lib_api.getMultiaddr(jsonData);
else if (activeTab == '#link-unspent')
return lib_api.getUnspent(jsonData);
else if (activeTab == '#link-xpub')
return lib_api.postXpub(jsonData);
}
/**
* Retrieve information about the xpub
*/
function processGo() {
const activeTab = sessionStorage.getItem('activeTab');
const args = $("#args").val();
const args2 = $("#args2").val();
const args3 = $("#args3").val();
displayInfoMsg('Processing...');
let deferred = processAction(activeTab, args, args2, args3);
deferred.then(
function (result) {
if (!result)
return;
let textJson = lib_fmt.cleanJson(result);
textJson = JSON.stringify(JSON.parse(textJson), null, 4);
textJson = lib_fmt.jsonSyntaxHighlight(textJson);
$('#json-data').html(textJson);
},
function (jqxhr) {
let hasErrorMsg =
('responseJSON' in jqxhr) &&
(jqxhr['responseJSON'] != null) &&
('message' in jqxhr['responseJSON']);
const msg = hasErrorMsg ? jqxhr['responseJSON']['message'] : jqxhr.statusText;
displayErrorMsg(msg);
}
);
}
/**
* Processing on loading completed
*/
$(document).ready(function() {
// Refresh the access token if needed
setInterval(() => {
lib_auth.refreshAccessToken();
}, 300000);
initTabs();
preparePage();
// Sets the event handlers
$('#args').keyup(function(evt) {
if (evt.keyCode === 13) {
processGo();
}
});
$('#btn-go').click(function() {
processGo();
});
$('#btn-logout').click(function() {
lib_auth.logout();
});
});
Loading…
Cancel
Save