/* Blockbook API route */ 'use strict' const express = require('express') const axios = require('axios') const wlogger = require('../../util/winston-logging') const RouteUtils = require('../../util/route-utils') const routeUtils = new RouteUtils() // Library for easily switching the API paths to use different instances of // Blockbook. const BlockbookPath = require('../../util/blockbook-path') const BLOCKBOOKPATH = new BlockbookPath() // BLOCKBOOKPATH.toOpenBazaar() const router = express.Router() // Used for processing error messages before sending them to the user. const util = require('util') util.inspect.defaultOptions = { depth: 1 } const BCHJS = require('@psf/bch-js') const bchjs = new BCHJS() let _this class Blockbook { constructor () { _this = this _this.axios = axios _this.routeUtils = routeUtils _this.bchjs = bchjs _this.BLOCKBOOKPATH = BLOCKBOOKPATH _this.router = router _this.router.get('/', _this.root) _this.router.get('/balance/:address', _this.balanceSingle) _this.router.post('/balance', _this.balanceBulk) _this.router.get('/utxos/:address', _this.utxosSingle) _this.router.post('/utxos', _this.utxosBulk) _this.router.get('/tx/:txid', _this.txSingle) _this.router.post('/tx', _this.txBulk) } // DRY error handler. errorHandler (err, res) { // Attempt to decode the error message. const { msg, status } = _this.routeUtils.decodeError(err) if (msg) { res.status(status) return res.json({ error: msg }) } res.status(500) return res.json({ error: util.inspect(err) }) } // Root API endpoint. Simply acknowledges that it exists. root (req, res, next) { return res.json({ status: 'address' }) } // Query the Blockbook Node API for a balance on a single BCH address. // Returns a Promise. async balanceFromBlockbook (thisAddress) { try { // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) // Convert the address to a cashaddr without a prefix. const addr = _this.bchjs.Address.toCashAddress(thisAddress) const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}` // console.log(`path: ${path}`) // Query the Blockbook Node API. const options = { method: 'get', baseURL: path } const axiosResponse = await _this.axios.request(options) const retData = axiosResponse.data // console.log(`retData: ${util.inspect(retData)}`) return retData } catch (err) { // Dev Note: Do not log error messages here. Throw them instead and let the // parent function handle it. wlogger.debug('Error in blockbook.js/balanceFromBlockbook()') throw err } } async balanceSingle (req, res, next) { try { const address = req.params.address if (!address || address === '') { res.status(400) return res.json({ error: 'address can not be empty' }) } // Reject if address is an array. if (Array.isArray(address)) { res.status(400) return res.json({ error: 'address can not be an array. Use POST for bulk upload.' }) } wlogger.debug( 'Executing blockbook/balanceSingle with this address: ', address ) // Ensure the input is a valid BCH address. try { // const legacyAddr = bchjs.Address.toLegacyAddress(address) _this.bchjs.Address.toLegacyAddress(address) } catch (err) { res.status(400) return res.json({ error: `Invalid BCH address. Double check your address is valid: ${address}` }) } // Prevent a common user error. Ensure they are using the correct network address. const networkIsValid = _this.routeUtils.validateNetwork(address) if (!networkIsValid) { res.status(400) return res.json({ error: 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } // Query the Blockbook Node API. const retData = await _this.balanceFromBlockbook(address) // Return the retrieved address information. res.status(200) return res.json(retData) } catch (err) { // Write out error to error log. wlogger.error('Error in blockbook.js/balanceSingle().', err) return _this.errorHandler(err, res) } } // POST handler for bulk queries on address details async balanceBulk (req, res, next) { try { let addresses = req.body.addresses // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 // Reject if addresses is not an array. if (!Array.isArray(addresses)) { res.status(400) return res.json({ error: 'addresses needs to be an array. Use GET for single address.' }) } // Enforce array size rate limits if (!_this.routeUtils.validateArraySize(req, addresses)) { res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ error: 'Array too large.' }) } wlogger.debug( 'Executing blockbook.js/balanceBulk with these addresses: ', addresses ) // Validate each element in the address array. for (let i = 0; i < addresses.length; i++) { const thisAddress = addresses[i] // Ensure the input is a valid BCH address. try { _this.bchjs.Address.toLegacyAddress(thisAddress) } catch (err) { res.status(400) return res.json({ error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` }) } // Prevent a common user error. Ensure they are using the correct network address. const networkIsValid = _this.routeUtils.validateNetwork(thisAddress) if (!networkIsValid) { res.status(400) return res.json({ error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` }) } } // Loops through each address and creates an array of Promises, querying // Insight API in parallel. addresses = addresses.map(async (address, index) => // console.log(`address: ${address}`) _this.balanceFromBlockbook(address) ) // Wait for all parallel Insight requests to return. const result = await _this.axios.all(addresses) // Return the array of retrieved address information. res.status(200) return res.json(result) } catch (err) { wlogger.error('Error in blockbook.js/balanceBulk().', err) return _this.errorHandler(err, res) } } // Query the Blockbook API for utxos associated with a BCH address. // Returns a Promise. async utxosFromBlockbook (thisAddress) { try { // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) // Convert the address to a cashaddr without a prefix. const addr = _this.bchjs.Address.toCashAddress(thisAddress) const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}` // console.log(`path: ${path}`) // Query the Blockbook API. // Query the Blockbook Node API. const options = { method: 'get', baseURL: path } const axiosResponse = await _this.axios.request(options) const retData = axiosResponse.data // console.log(`retData: ${util.inspect(retData)}`) // Add the satoshis property to each UTXO. for (let i = 0; i < retData.length; i++) { retData[i].satoshis = Number(retData[i].value) } return retData } catch (err) { // Dev Note: Do not log error messages here. Throw them instead and let the // parent function handle it. wlogger.debug('Error in blockbook.js/utxosFromBlockbook()') throw err } } // GET handler for single balance async utxosSingle (req, res, next) { try { const address = req.params.address if (!address || address === '') { res.status(400) return res.json({ error: 'address can not be empty' }) } // Reject if address is an array. if (Array.isArray(address)) { res.status(400) return res.json({ error: 'address can not be an array. Use POST for bulk upload.' }) } wlogger.debug( 'Executing blockbook/utxosSingle with this address: ', address ) // Ensure the input is a valid BCH address. try { // const legacyAddr = bchjs.Address.toLegacyAddress(address) _this.bchjs.Address.toLegacyAddress(address) } catch (err) { res.status(400) return res.json({ error: `Invalid BCH address. Double check your address is valid: ${address}` }) } // Prevent a common user error. Ensure they are using the correct network address. const networkIsValid = _this.routeUtils.validateNetwork(address) if (!networkIsValid) { res.status(400) return res.json({ error: 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } // Query the Blockbook API. const retData = await _this.utxosFromBlockbook(address) // Return the retrieved address information. res.status(200) return res.json(retData) } catch (err) { // Write out error to error log. wlogger.error('Error in blockbook.js/utxosSingle().', err) return _this.errorHandler(err, res) } } // POST handler for bulk queries on address utxos async utxosBulk (req, res, next) { try { let addresses = req.body.addresses // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 // Reject if addresses is not an array. if (!Array.isArray(addresses)) { res.status(400) return res.json({ error: 'addresses needs to be an array. Use GET for single address.' }) } // Enforce array size rate limits if (!_this.routeUtils.validateArraySize(req, addresses)) { res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ error: 'Array too large.' }) } wlogger.debug( 'Executing blockbook.js/utxosBulk with these addresses: ', addresses ) // Validate each element in the address array. for (let i = 0; i < addresses.length; i++) { const thisAddress = addresses[i] // Ensure the input is a valid BCH address. try { _this.bchjs.Address.toLegacyAddress(thisAddress) } catch (err) { res.status(400) return res.json({ error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` }) } // Prevent a common user error. Ensure they are using the correct network address. const networkIsValid = _this.routeUtils.validateNetwork(thisAddress) if (!networkIsValid) { res.status(400) return res.json({ error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` }) } } // Loops through each address and creates an array of Promises, querying // Insight API in parallel. addresses = addresses.map(async (address, index) => // console.log(`address: ${address}`) _this.utxosFromBlockbook(address) ) // Wait for all parallel Insight requests to return. const result = await _this.axios.all(addresses) // Return the array of retrieved address information. res.status(200) return res.json(result) } catch (err) { wlogger.error('Error in blockbook.js/utxosBulk().', err) return _this.errorHandler(err, res) } } // Query the Blockbook Node API for transactions on a single TXID. // Returns a Promise. async transactionsFromBlockbook (txid) { try { // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) const path = `${_this.BLOCKBOOKPATH.txPath}${txid}` // console.log(`path: ${path}`) // Query the Blockbook Node API. const options = { method: 'get', baseURL: path } const axiosResponse = await _this.axios.request(options) const retPromise = axiosResponse.data // console.log(`retData: ${util.inspect(retData)}`) return retPromise } catch (err) { // Dev Note: Do not log error messages here. Throw them instead and let the // parent function handle it. wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()') throw err } } // GET handler for single transaction details. async txSingle (req, res, next) { try { const txid = req.params.txid if (!txid || txid === '') { res.status(400) return res.json({ error: 'txid can not be empty' }) } // Reject if address is an array. if (Array.isArray(txid)) { res.status(400) return res.json({ error: 'txid can not be an array. Use POST for bulk upload.' }) } // TODO: Add regex comparison of txid to ensure it's valid. if (txid.length !== 64) { res.status(400) return res.json({ error: `txid must be of length 64 (not ${txid.length})` }) } wlogger.debug('Executing blockbook/txSingle with this txid: ', txid) // Query the Blockbook Node API. const retData = await _this.transactionsFromBlockbook(txid) // Return the retrieved address information. res.status(200) return res.json(retData) } catch (err) { // Write out error to error log. wlogger.error('Error in blockbook.js/txSingle().', err) return _this.errorHandler(err, res) } } // POST handler for bulk queries on tx details async txBulk (req, res, next) { try { let txids = req.body.txids // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 // Reject if txids is not an array. if (!Array.isArray(txids)) { res.status(400) return res.json({ error: 'txids need to be an array. Use GET for single address.' }) } // Enforce array size rate limits if (!_this.routeUtils.validateArraySize(req, txids)) { res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ error: 'Array too large.' }) } wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids) // Validate each element in the txids array. for (let i = 0; i < txids.length; i++) { const thisTxid = txids[i] if (!thisTxid || thisTxid === '') { res.status(400) return res.json({ error: 'txid can not be empty' }) } // TODO: Add regex comparison of txid to ensure it's valid. if (thisTxid.length !== 64) { res.status(400) return res.json({ error: `txid must be of length 64 (not ${thisTxid.length})` }) } } // Loops through each address and creates an array of Promises, querying // Insight API in parallel. txids = txids.map(async (txid, index) => // console.log(`address: ${address}`) _this.transactionsFromBlockbook(txid) ) // Wait for all parallel Insight requests to return. const result = await _this.axios.all(txids) // Return the array of retrieved address information. res.status(200) return res.json(result) } catch (err) { wlogger.error('Error in blockbook.js/txBulk().', err) return _this.errorHandler(err, res) } } } module.exports = Blockbook