/*! * accounts/support-rest-api.js * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. */ 'use strict' const fs = require('fs') const validator = require('validator') const bodyParser = require('body-parser') const errors = require('../lib/errors') const Logger = require('../lib/logger') const authMgr = require('../lib/auth/authorizations-manager') const HttpServer = require('../lib/http-server/http-server') const network = require('../lib/bitcoin/network') const hdaService = require('../lib/bitcoin/hd-accounts-service') const addrService = require('../lib/bitcoin/addresses-service') const HdAccountInfo = require('../lib/wallet/hd-account-info') const AddressInfo = require('../lib/wallet/address-info') const apiHelper = require('./api-helper') const keys = require('../keys')[network.key] const debugApi = process.argv.indexOf('api-debug') > -1 /** * Support API endpoints */ class SupportRestApi { /** * Constructor * @param {pushtx.HttpServer} httpServer - HTTP server */ constructor(httpServer) { this.httpServer = httpServer // Establish routes const urlencodedParser = bodyParser.urlencoded({ extended: true }) this.httpServer.app.get( `/${keys.prefixes.support}/address/:addr/info`, authMgr.checkHasAdminProfile.bind(authMgr), this.validateAddress.bind(this), this.getAddressInfo.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/address/:addr/rescan`, authMgr.checkHasAdminProfile.bind(authMgr), this.validateAddress.bind(this), this.getAddressRescan.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/xpub/:xpub/info`, authMgr.checkHasAdminProfile.bind(authMgr), this.validateArgsGetXpubInfo.bind(this), this.getXpubInfo.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/xpub/:xpub/rescan`, authMgr.checkHasAdminProfile.bind(authMgr), this.validateArgsGetXpubRescan.bind(this), this.getXpubRescan.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/xpub/:xpub/delete`, authMgr.checkHasAdminProfile.bind(authMgr), this.validateArgsGetXpubDelete.bind(this), this.getXpubDelete.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/pairing/explorer`, authMgr.checkHasAdminProfile.bind(authMgr), this.getPairingExplorer.bind(this), ) this.httpServer.app.get( `/${keys.prefixes.support}/pairing`, authMgr.checkHasAdminProfile.bind(authMgr), this.getPairing.bind(this), ) } /** * Retrieve information for a given address * @param {object} req - http request object * @param {object} res - http response object */ async getAddressInfo(req, res) { try { // Parse the entities passed as url params const entities = apiHelper.parseEntities(req.params.addr).addrs if (entities.length === 0) return HttpServer.sendError(res, errors.address.INVALID) const address = entities[0] const info = new AddressInfo(address) await info.loadInfoExtended() await info.loadTransactions() await info.loadUtxos() const ret = this._formatAddressInfoResult(info) HttpServer.sendRawData(res, ret) } catch(e) { HttpServer.sendError(res, errors.generic.GEN) } finally { debugApi && Logger.info(`API : Completed GET /support/address/${req.params.addr}/info`) } } /** * Format response to be returned * for calls to getAddressInfo * @param {AddressInfo} info * @returns {string} return the json to be sent as a response */ _formatAddressInfoResult(info) { const res = info.toPojoExtended() return JSON.stringify(res, null, 2) } /** * Rescan the blockchain for a given address * @param {object} req - http request object * @param {object} res - http response object */ async getAddressRescan(req, res) { try { // Parse the entities passed as url params const entities = apiHelper.parseEntities(req.params.addr).addrs if (entities.length === 0) return HttpServer.sendError(res, errors.address.INVALID) const address = entities[0] const ret = { status: 'Rescan complete', } await addrService.rescan(address) HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } catch(e) { HttpServer.sendError(res, errors.generic.GEN) } finally { debugApi && Logger.info(`API : Completed GET /support/address/${req.params.addr}/rescan`) } } /** * Retrieve information for a given hd account * @param {object} req - http request object * @param {object} res - http response object */ async getXpubInfo(req, res) { try { // Parse the entities passed as url params const entities = apiHelper.parseEntities(req.params.xpub).xpubs if (entities.length === 0) return HttpServer.sendError(res, errors.xpub.INVALID) const xpub = entities[0] let info try { info = new HdAccountInfo(xpub) await info.loadInfo() const ret = this._formatXpubInfoResult(info) HttpServer.sendRawData(res, ret) } catch(e) { if(e === errors.db.ERROR_NO_HD_ACCOUNT) { const ret = this._formatXpubInfoResult(info) HttpServer.sendRawData(res, ret) } else { HttpServer.sendError(res, errors.generic.GEN) } } } catch(e) { HttpServer.sendError(res, errors.generic.GEN) } finally { debugApi && Logger.info(`API : Completed GET /support/xpub/${req.params.xpub}/info`) } } /** * Format response to be returned * for calls to getXpubInfo * @param {HdAccountInfo} info * @returns {string} return the json to be sent as a response */ _formatXpubInfoResult(info) { const res = info.toPojoExtended() return JSON.stringify(res, null, 2) } /** * Rescan the blockchain for a given address * @param {object} req - http request object * @param {object} res - http response object */ async getXpubRescan(req, res) { try { // Parse the entities passed as url params const entities = apiHelper.parseEntities(req.params.xpub).xpubs if (entities.length === 0) return HttpServer.sendError(res, errors.xpub.INVALID) const xpub = entities[0] const ret = { status: 'Rescan complete', } const gapLimit = req.query.gap != null ? parseInt(req.query.gap) : 0 const startIndex = req.query.startidx != null ? parseInt(req.query.startidx) : 0 try { await hdaService.rescan(xpub, gapLimit, startIndex) HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } catch(e) { if (e === errors.db.ERROR_NO_HD_ACCOUNT) { ret.status = 'Error: Not tracking xpub' HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } else if (e === errors.xpub.OVERLAP) { ret.status = 'Error: Rescan in progress' HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } else { ret.status = 'Rescan Error' Logger.error(e, 'API : SupportRestApi.getXpubRescan() : Support rescan error') HttpServer.sendError(res, JSON.stringify(ret, null, 2)) } } } catch(e) { HttpServer.sendError(res, errors.generic.GEN) } finally { debugApi && Logger.info(`API : Completed GET /support/xpub/${req.params.xpub}/rescan`) } } /** * Delete all data related to a hd account * @param {object} req - http request object * @param {object} res - http response object */ async getXpubDelete(req, res) { try { // Parse the entities passed as url params const entities = apiHelper.parseEntities(req.params.xpub).xpubs if (entities.length === 0) return HttpServer.sendError(res, errors.xpub.INVALID) const xpub = entities[0] try { await hdaService.deleteHdAccount(xpub) HttpServer.sendOk(res) } catch(e) { HttpServer.sendError(res, e) } } catch(e) { HttpServer.sendError(res, errors.generic.GEN) } finally { debugApi && Logger.info(`API : Completed GET /support/xpub/${req.params.xpub}/delete`) } } /** * Get pairing info */ async getPairing(req, res) { try { const ret = { 'pairing': { 'type': 'dojo.api', 'version': keys.dojoVersion, 'apikey': keys.auth.strategies.localApiKey.apiKeys[0] } } HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } catch(e) { const ret = { status: 'error' } Logger.error(e, 'API : SupportRestApi.getPairing() : Support pairing error') HttpServer.sendError(res, JSON.stringify(ret, null, 2)) } finally { debugApi && Logger.info(`API : Completed GET /pairing`) } } /** * Get pairing info for the local block explorer */ async getPairingExplorer(req, res) { try { const ret = { 'pairing': { 'type': `explorer.${keys.explorer.active}`, 'url': keys.explorer.uri, 'key': keys.explorer.password } } HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) } catch(e) { const ret = { status: 'error' } Logger.error(e, 'API : SupportRestApi.getPairingExplorer() : Support pairing error') HttpServer.sendError(res, JSON.stringify(ret, null, 2)) } finally { debugApi && Logger.info(`API : Completed GET /pairing/explorer`) } } /** * Validate arguments related to GET xpub info requests * @param {object} req - http request object * @param {object} res - http response object * @param {function} next - next tiny-http middleware */ validateArgsGetXpubInfo(req, res, next) { const isValidXpub = validator.isAlphanumeric(req.params.xpub) if (!isValidXpub) { HttpServer.sendError(res, errors.body.INVDATA) Logger.error(null, `API : SupportRestApi.validateArgsGetXpubInfo() : Invalid xpub ${req.params.xpub}`) } else { next() } } /** * Validate arguments related to GET xpub rescan requests * @param {object} req - http request object * @param {object} res - http response object * @param {function} next - next tiny-http middleware */ validateArgsGetXpubRescan(req, res, next) { const isValidXpub = validator.isAlphanumeric(req.params.xpub) const isValidGap = !req.query.gap || validator.isInt(req.query.gap) if (!(isValidXpub && isValidGap)) { HttpServer.sendError(res, errors.body.INVDATA) Logger.error(null, 'API : SupportRestApi.validateArgsGetXpubRescan() : Invalid arguments') } else { next() } } /** * Validate arguments related to GET xpub delete requests * @param {object} req - http request object * @param {object} res - http response object * @param {function} next - next tiny-http middleware */ validateArgsGetXpubDelete(req, res, next) { const isValidXpub = validator.isAlphanumeric(req.params.xpub) if (!isValidXpub) { HttpServer.sendError(res, errors.body.INVDATA) Logger.error(null, `API : SupportRestApi.validateArgsGetXpubDelete() : Invalid xpub ${req.params.xpub}`) } else { next() } } /** * Validate arguments related to addresses requests * @param {object} req - http request object * @param {object} res - http response object * @param {function} next - next tiny-http middleware */ validateAddress(req, res, next) { const isValidAddress = validator.isAlphanumeric(req.params.addr) if (!isValidAddress) { HttpServer.sendError(res, errors.body.INVDATA) Logger.error(null, `API : SupportRestApi.validateAddress() : Invalid address ${req.params.addr}`) } else { next() } } } module.exports = SupportRestApi