...
 
Commits (19)
......@@ -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)
......
......@@ -17,6 +17,7 @@ const debugApi = !!(process.argv.indexOf('api-debug') > -1)
/**
* Multiaddr API endpoints
* @deprecated
*/
class MultiaddrRestApi {
......
......@@ -17,6 +17,7 @@ const debugApi = !!(process.argv.indexOf('api-debug') > -1)
/**
* Unspent API endpoints
* @deprecated
*/
class UnspentRestApi {
......
/*!
* 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
......@@ -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
......
# 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).
......
# 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).
......
# 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)
# 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,10 +10,10 @@
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.7.0
DOJO_NODEJS_VERSION_TAG=1.8.0
DOJO_NGINX_VERSION_TAG=1.5.0
DOJO_TOR_VERSION_TAG=1.4.0
DOJO_EXPLORER_VERSION_TAG=1.3.0
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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()),
......
......@@ -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
......
This diff is collapsed.
{
"name": "samourai-dojo",
"version": "1.7.0",
"version": "1.8.0",
"description": "Backend server for Samourai Wallet",
"main": "accounts/index.js",
"scripts": {
......@@ -24,7 +24,7 @@
"express-jwt": "5.3.1",
"generic-pool": "3.4.2",
"helmet": "3.12.1",
"lodash": "4.17.14",
"lodash": "4.17.19",
"lru-cache": "4.0.2",
"minimist": "1.2.2",
"mysql": "2.16.0",
......
......@@ -119,18 +119,10 @@ var lib_api = {
},
/**
* Multiaddr
* Wallet
*/
getMultiaddr: function(arguments) {
let uri = this.baseUri + '/multiaddr';
return this.sendGetUriEncoded(uri, arguments);
},
/**
* Unspent
*/
getUnspent: function(arguments) {
let uri = this.baseUri + '/unspent';
getWallet: function(arguments) {
let uri = this.baseUri + '/wallet';
return this.sendGetUriEncoded(uri, arguments);
},
......
......@@ -64,11 +64,8 @@
<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 id="link-wallet">
<a href="#">WALLET</a>
</li>
<li id="link-tx">
<a href="#">TX</a>
......
......@@ -62,8 +62,7 @@ function initTabs() {
'#link-info-address',
'#link-rescan-address',
'#link-rescan-blocks',
'#link-multiaddr',
'#link-unspent',
'#link-wallet',
'#link-tx'
];
......@@ -142,10 +141,8 @@ function preparePage() {
$("#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-wallet') {
placeholder = 'ENTER /WALLET 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') {
......@@ -231,10 +228,8 @@ function processAction(activeTab, args, args2, args3) {
jsonData[aArg[0]] = aArg[1];
}
if (activeTab == '#link-multiaddr')
return lib_api.getMultiaddr(jsonData);
else if (activeTab == '#link-unspent')
return lib_api.getUnspent(jsonData);
if (activeTab == '#link-wallet')
return lib_api.getWallet(jsonData);
else if (activeTab == '#link-xpub')
return lib_api.postXpub(jsonData);
}
......