From a2447bd07c00604dc204da11fec05474af0c8ca9 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Jun 2019 14:11:16 -0700 Subject: [PATCH 1/4] fix(blockbook): Added blockbook API for balance and utxo --- src/routes/v3/blockbook.js | 382 +++++++++++++++++++++++ test/v3/blockbook.js | 600 +++++++++++++++++++++++++++++++++++++ 2 files changed, 982 insertions(+) create mode 100644 src/routes/v3/blockbook.js create mode 100644 test/v3/blockbook.js diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js new file mode 100644 index 0000000..eded070 --- /dev/null +++ b/src/routes/v3/blockbook.js @@ -0,0 +1,382 @@ +/* + Bitcore Node API route +*/ + +"use strict" + +const express = require("express") +const requestUtils = require("./services/requestUtils") +const axios = require("axios") +const routeUtils = require("./route-utils") +const wlogger = require("../../util/winston-logging") + +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 BITBOXJS = require("@chris.troutner/bitbox-js") +const BITBOX = new BITBOXJS() + +//const BITCORE_URL = process.env.BITCORE_URL + +// Connect the route endpoints to their handler functions. +router.get("/", root) +router.get("/balance/:address", balanceSingle) + +// Root API endpoint. Simply acknowledges that it exists. +function root(req, res, next) { + return res.json({ status: "address" }) +} + +// Query the Bitcore Node API for a balance on a single BCH address. +// Returns a Promise. +async function balanceFromBlockbook(thisAddress) { + try { + //console.log(`BITCORE_URL: ${BITCORE_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = BITBOX.Address.toCashAddress(thisAddress, false) + + // Determine if we are working with the testnet or mainnet networks. + let network = "mainnet" + if (process.env.NETWORK === "testnet") network = "testnet" + + const path = `${process.env.BLOCKBOOK_URL}api/v2/address/${addr}` + + // Query the Bitcore Node API. + const axiosResponse = await axios.get(path) + 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. + throw err + } +} + +// GET handler for single balance +async function 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 bitcore/balanceSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + const legacyAddr = BITBOX.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 = 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 Bitcore Node API. + const retData = await balanceFromBlockbook(address) + + // Return the retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + wlogger.error(`Error in blockbook.js/balanceSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} +/* +// POST handler for bulk queries on address details +async function 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 (!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 bitcore.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 { + BITBOX.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 = 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) => + balanceFromBitcore(address) + ) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in bitcore.js/balanceBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Query the Bitcore Node API for utxos associated with a BCH address. +// Returns a Promise. +async function utxosFromBitcore(thisAddress) { + try { + //console.log(`BITCORE_URL: ${BITCORE_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = BITBOX.Address.toCashAddress(thisAddress, false) + + // Determine if we are working with the testnet or mainnet networks. + let network = "mainnet" + if (process.env.NETWORK === "testnet") network = "testnet" + + const path = `${process.env.BITCORE_URL}api/BCH/${network}/address/${addr}/?unspent=true` + + // Query the Bitcore Node API. + const axiosResponse = await axios.get(path) + 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. + throw err + } +} + +// GET handler for single balance +async function 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 bitcore/balanceSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + const legacyAddr = BITBOX.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 = 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 Bitcore Node API. + const retData = await utxosFromBitcore(address) + + // Return the retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + wlogger.error(`Error in bitcore.js/utxosSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// POST handler for bulk queries on address utxos +async function 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 (!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 bitcore.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 { + BITBOX.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 = 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) => + utxosFromBitcore(address) + ) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in bitcore.js/utxosBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} +*/ +module.exports = { + router, + testableComponents: { + root, + balanceSingle + //balanceBulk, + //utxosSingle, + //utxosBulk + } +} diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js new file mode 100644 index 0000000..b8af026 --- /dev/null +++ b/test/v3/blockbook.js @@ -0,0 +1,600 @@ +/* + TESTS FOR THE BLOCKBOOK.JS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + To-Do: +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const blockbookRoute = require("../../src/routes/v3/blockbook") +const nock = require("nock") // HTTP mocking + +let originalUrl // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/bitcore-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Blockbook Router", () => { + let req, res, mockServerUrl + + before(() => { + originalUrl = process.env.BLOCKBOOK_URL + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + mockServerUrl = `http://fakeurl` + } + // console.log(`Testing type is: ${process.env.TEST}`) + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + process.env.BLOCKBOOK_URL = originalUrl + }) + + describe("#root", () => { + // root route handler. + const root = blockbookRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "address", "Returns static string") + }) + }) + + describe("#Balance Single", () => { + // details route handler. + const balanceSingle = blockbookRoute.testableComponents.balanceSingle + + it("should throw 400 if address is empty", async () => { + const result = await balanceSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await balanceSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await balanceSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await balanceSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BLOCKBOOK_URL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + + const result = await balanceSingle(req, res) + + // Restore the saved URL. + process.env.BLOCKBOOK_URL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BLOCKBOOK_URL = savedUrl + } + }) + + it("should get balance for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BLOCKBOOK_URL}`) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["confirmed", "unconfirmed", "balance"]) + assert.isNumber(result.confirmed) + assert.isNumber(result.unconfirmed) + assert.isNumber(result.balance) + }) + }) + /* + describe("#Balance Bulk", () => { + // details route handler. + const balanceBulk = blockbookRoute.testableComponents.balanceBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await balanceBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await balanceBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await balanceBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await balanceBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await balanceBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await balanceBulk(req, res) + //console.log(`network issue result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["confirmed", "unconfirmed", "balance"]) + assert.isNumber(result[0].confirmed) + assert.isNumber(result[0].unconfirmed) + assert.isNumber(result[0].balance) + }) + + it("should get details for multiple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("#UTXOs Single", () => { + // details route handler. + const utxosSingle = blockbookRoute.testableComponents.utxosSingle + + it("should throw 400 if address is empty", async () => { + const result = await utxosSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await utxosSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await utxosSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await utxosSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BLOCKBOOK_URL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + + const result = await utxosSingle(req, res) + + // Restore the saved URL. + process.env.BLOCKBOOK_URL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BLOCKBOOK_URL = savedUrl + } + }) + + it("should get utxos for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BLOCKBOOK_URL}`) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + }) + + describe("#UTXO Bulk", () => { + // details route handler. + const utxosBulk = blockbookRoute.testableComponents.utxosBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await utxosBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await utxosBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await utxosBulk(req, res) + //console.log(`network issue result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + + it("should get details for multiple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + }) + */ +}) From fca3e25b737f3efcf2273ae67bbc6ba333ec2a1b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Jun 2019 14:31:36 -0700 Subject: [PATCH 2/4] Added first unit tests for blockbook --- src/app.js | 4 +-- src/routes/v3/blockbook.js | 4 ++- test/v3/blockbook.js | 22 ++++++++++++----- test/v3/mocks/blockbook-mock.js | 43 +++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 test/v3/mocks/blockbook-mock.js diff --git a/src/app.js b/src/app.js index d9baa34..b9e77dd 100644 --- a/src/app.js +++ b/src/app.js @@ -193,9 +193,7 @@ io.on("connection", socket => { if (process.env.ZEROMQ_URL && process.env.ZEROMQ_PORT) { console.log( - `Connecting to BCH ZMQ at ${process.env.ZEROMQ_URL}:${ - process.env.ZEROMQ_PORT - }` + `Connecting to BCH ZMQ at ${process.env.ZEROMQ_URL}:${process.env.ZEROMQ_PORT}` ) const bitcoincashZmqDecoder = new BitcoinCashZMQDecoder(process.env.NETWORK) diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index eded070..a046a27 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -37,7 +37,7 @@ async function balanceFromBlockbook(thisAddress) { //console.log(`BITCORE_URL: ${BITCORE_URL}`) // Convert the address to a cashaddr without a prefix. - const addr = BITBOX.Address.toCashAddress(thisAddress, false) + const addr = BITBOX.Address.toCashAddress(thisAddress) // Determine if we are working with the testnet or mainnet networks. let network = "mainnet" @@ -107,6 +107,8 @@ async function balanceSingle(req, res, next) { res.status(200) return res.json(retData) } catch (err) { + console.log(`err: ${JSON.stringify(err, null, 2)}`) + // Attempt to decode the error message. const { msg, status } = routeUtils.decodeError(err) if (msg) { diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index b8af026..91d89a4 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -19,7 +19,7 @@ let originalUrl // Used during transition from integration to unit tests. // Mocking data. const { mockReq, mockRes } = require("./mocks/express-mocks") -const mockData = require("./mocks/bitcore-mock") +const mockData = require("./mocks/blockbook-mock") // Used for debugging. const util = require("util") @@ -157,12 +157,22 @@ describe("#Blockbook Router", () => { // Call the details API. const result = await balanceSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) - assert.hasAllKeys(result, ["confirmed", "unconfirmed", "balance"]) - assert.isNumber(result.confirmed) - assert.isNumber(result.unconfirmed) - assert.isNumber(result.balance) + assert.hasAnyKeys(result, [ + "page", + "totalPages", + "itemsOnPage", + "address", + "balance", + "totalReceived", + "totalSent", + "unconfirmedBalance", + "unconfirmedTxs", + "txs", + "txids" + ]) + assert.isArray(result.txids) }) }) /* diff --git a/test/v3/mocks/blockbook-mock.js b/test/v3/mocks/blockbook-mock.js new file mode 100644 index 0000000..2fe2b09 --- /dev/null +++ b/test/v3/mocks/blockbook-mock.js @@ -0,0 +1,43 @@ +/* + This library contains mocking data for running unit tests on the blockbook route. +*/ + +"use strict" + +const mockBalance = { + page: 1, + totalPages: 1, + itemsOnPage: 1000, + address: "bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4", + balance: "10000000", + totalReceived: "10000000", + totalSent: "0", + unconfirmedBalance: "0", + unconfirmedTxs: 0, + txs: 1, + txids: ["5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392"] +} + +const mockUtxos = [ + { + _id: "5cf2c31a33bd46a95ec7e730", + chain: "BCH", + network: "testnet", + coinbase: false, + mintIndex: 1, + spentTxid: "", + mintTxid: + "5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392", + mintHeight: 1265275, + spentHeight: -2, + address: "qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4", + script: "76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac", + value: 10000000, + confirmations: -1 + } +] + +module.exports = { + mockBalance, + mockUtxos +} From 07313784afc86deb406b7400ee39a0dc136ee62b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Jun 2019 14:45:28 -0700 Subject: [PATCH 3/4] Added unit and integration tests for balance endpoints with blockbook --- src/routes/v3/bitcore.js | 3 +++ src/routes/v3/blockbook.js | 35 ++++++++++++++++++----------------- test/v3/blockbook.js | 38 ++++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/src/routes/v3/bitcore.js b/src/routes/v3/bitcore.js index 873f399..54965f3 100644 --- a/src/routes/v3/bitcore.js +++ b/src/routes/v3/bitcore.js @@ -24,6 +24,9 @@ const BITBOX = new BITBOXJS() // Connect the route endpoints to their handler functions. router.get("/", root) router.get("/balance/:address", balanceSingle) +router.post("/balance", balanceBulk) +router.get("/utxos/:address", utxosSingle) +router.post("/utxos", utxosBulk) // Root API endpoint. Simply acknowledges that it exists. function root(req, res, next) { diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index a046a27..059685b 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -19,11 +19,14 @@ util.inspect.defaultOptions = { depth: 1 } const BITBOXJS = require("@chris.troutner/bitbox-js") const BITBOX = new BITBOXJS() -//const BITCORE_URL = process.env.BITCORE_URL +//const BLOCKBOOK_URL = process.env.BLOCKBOOK_URL // Connect the route endpoints to their handler functions. router.get("/", root) router.get("/balance/:address", balanceSingle) +router.post("/balance", balanceBulk) +//router.get("/utxos/:address", utxosSingle) +//router.post("/utxos", utxosBulk) // Root API endpoint. Simply acknowledges that it exists. function root(req, res, next) { @@ -34,7 +37,7 @@ function root(req, res, next) { // Returns a Promise. async function balanceFromBlockbook(thisAddress) { try { - //console.log(`BITCORE_URL: ${BITCORE_URL}`) + //console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) // Convert the address to a cashaddr without a prefix. const addr = BITBOX.Address.toCashAddress(thisAddress) @@ -107,8 +110,6 @@ async function balanceSingle(req, res, next) { res.status(200) return res.json(retData) } catch (err) { - console.log(`err: ${JSON.stringify(err, null, 2)}`) - // Attempt to decode the error message. const { msg, status } = routeUtils.decodeError(err) if (msg) { @@ -123,7 +124,7 @@ async function balanceSingle(req, res, next) { return res.json({ error: util.inspect(err) }) } } -/* + // POST handler for bulk queries on address details async function balanceBulk(req, res, next) { try { @@ -147,7 +148,7 @@ async function balanceBulk(req, res, next) { } wlogger.debug( - `Executing bitcore.js/balanceBulk with these addresses: `, + `Executing blockbook.js/balanceBulk with these addresses: `, addresses ) @@ -178,7 +179,7 @@ async function balanceBulk(req, res, next) { // Loops through each address and creates an array of Promises, querying // Insight API in parallel. addresses = addresses.map(async (address, index) => - balanceFromBitcore(address) + balanceFromBlockbook(address) ) // Wait for all parallel Insight requests to return. @@ -195,18 +196,18 @@ async function balanceBulk(req, res, next) { return res.json({ error: msg }) } - wlogger.error(`Error in bitcore.js/balanceBulk().`, err) + wlogger.error(`Error in blockbook.js/balanceBulk().`, err) res.status(500) return res.json({ error: util.inspect(err) }) } } - -// Query the Bitcore Node API for utxos associated with a BCH address. +/* +// Query the Blockbook API for utxos associated with a BCH address. // Returns a Promise. -async function utxosFromBitcore(thisAddress) { +async function utxosFromBlockbook(thisAddress) { try { - //console.log(`BITCORE_URL: ${BITCORE_URL}`) + //console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) // Convert the address to a cashaddr without a prefix. const addr = BITBOX.Address.toCashAddress(thisAddress, false) @@ -215,9 +216,9 @@ async function utxosFromBitcore(thisAddress) { let network = "mainnet" if (process.env.NETWORK === "testnet") network = "testnet" - const path = `${process.env.BITCORE_URL}api/BCH/${network}/address/${addr}/?unspent=true` + const path = `${process.env.BLOCKBOOK_URL}api/BCH/${network}/address/${addr}/?unspent=true` - // Query the Bitcore Node API. + // Query the Blockbook API. const axiosResponse = await axios.get(path) const retData = axiosResponse.data //console.log(`retData: ${util.inspect(retData)}`) @@ -249,7 +250,7 @@ async function utxosSingle(req, res, next) { } wlogger.debug( - `Executing bitcore/balanceSingle with this address: `, + `Executing blockbook/balanceSingle with this address: `, address ) @@ -376,8 +377,8 @@ module.exports = { router, testableComponents: { root, - balanceSingle - //balanceBulk, + balanceSingle, + balanceBulk //utxosSingle, //utxosBulk } diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index 91d89a4..4cabf5c 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -175,7 +175,7 @@ describe("#Blockbook Router", () => { assert.isArray(result.txids) }) }) - /* + describe("#Balance Bulk", () => { // details route handler. const balanceBulk = blockbookRoute.testableComponents.balanceBulk @@ -250,7 +250,7 @@ describe("#Blockbook Router", () => { }) it("should throw 500 when network issues", async () => { - const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl = process.env.BLOCKBOOK_URL try { req.body = { @@ -258,13 +258,13 @@ describe("#Blockbook Router", () => { } // Switch the Insight URL to something that will error out. - process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" const result = await balanceBulk(req, res) //console.log(`network issue result: ${util.inspect(result)}`) // Restore the saved URL. - process.env.BITCOINCOM_BASEURL = savedUrl + process.env.BLOCKBOOK_URL = savedUrl assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") //assert.include(result.error, "ENOTFOUND", "Error message expected") @@ -275,7 +275,7 @@ describe("#Blockbook Router", () => { ) } catch (err) { // Restore the saved URL. - process.env.BITCOINCOM_BASEURL = savedUrl + process.env.BLOCKBOOK_URL = savedUrl } }) @@ -296,10 +296,20 @@ describe("#Blockbook Router", () => { // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) - assert.hasAllKeys(result[0], ["confirmed", "unconfirmed", "balance"]) - assert.isNumber(result[0].confirmed) - assert.isNumber(result[0].unconfirmed) - assert.isNumber(result[0].balance) + assert.hasAnyKeys(result[0], [ + "page", + "totalPages", + "itemsOnPage", + "address", + "balance", + "totalReceived", + "totalSent", + "unconfirmedBalance", + "unconfirmedTxs", + "txs", + "txids" + ]) + assert.isArray(result[0].txids) }) it("should get details for multiple addresses", async () => { @@ -326,7 +336,7 @@ describe("#Blockbook Router", () => { assert.equal(result.length, 2, "2 outputs for 2 inputs") }) }) - + /* describe("#UTXOs Single", () => { // details route handler. const utxosSingle = blockbookRoute.testableComponents.utxosSingle @@ -503,7 +513,7 @@ describe("#Blockbook Router", () => { }) it("should throw 500 when network issues", async () => { - const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl = process.env.BLOCKBOOK_URL try { req.body = { @@ -511,13 +521,13 @@ describe("#Blockbook Router", () => { } // Switch the Insight URL to something that will error out. - process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" const result = await utxosBulk(req, res) //console.log(`network issue result: ${util.inspect(result)}`) // Restore the saved URL. - process.env.BITCOINCOM_BASEURL = savedUrl + process.env.BLOCKBOOK_URL = savedUrl assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") //assert.include(result.error, "ENOTFOUND", "Error message expected") @@ -528,7 +538,7 @@ describe("#Blockbook Router", () => { ) } catch (err) { // Restore the saved URL. - process.env.BITCOINCOM_BASEURL = savedUrl + process.env.BLOCKBOOK_URL = savedUrl } }) From d15e11b75e84b2252634d6ff3ad5d65a9dbba2f8 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Jun 2019 15:04:30 -0700 Subject: [PATCH 4/4] Finished adding unit and integration tests to blockbook API balance and utxo --- src/routes/v3/bitcore.js | 5 +--- src/routes/v3/blockbook.js | 50 ++++++++++++++------------------- test/v3/blockbook.js | 47 ++++++------------------------- test/v3/mocks/blockbook-mock.js | 19 ++++--------- 4 files changed, 35 insertions(+), 86 deletions(-) diff --git a/src/routes/v3/bitcore.js b/src/routes/v3/bitcore.js index 54965f3..70722d6 100644 --- a/src/routes/v3/bitcore.js +++ b/src/routes/v3/bitcore.js @@ -251,10 +251,7 @@ async function utxosSingle(req, res, next) { }) } - wlogger.debug( - `Executing bitcore/balanceSingle with this address: `, - address - ) + wlogger.debug(`Executing bitcore/utxoSingle with this address: `, address) // Ensure the input is a valid BCH address. try { diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index 059685b..00d1542 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -1,5 +1,5 @@ /* - Bitcore Node API route + Blockbook API route */ "use strict" @@ -25,15 +25,15 @@ const BITBOX = new BITBOXJS() router.get("/", root) router.get("/balance/:address", balanceSingle) router.post("/balance", balanceBulk) -//router.get("/utxos/:address", utxosSingle) -//router.post("/utxos", utxosBulk) +router.get("/utxos/:address", utxosSingle) +router.post("/utxos", utxosBulk) // Root API endpoint. Simply acknowledges that it exists. function root(req, res, next) { return res.json({ status: "address" }) } -// Query the Bitcore Node API for a balance on a single BCH address. +// Query the Blockbook Node API for a balance on a single BCH address. // Returns a Promise. async function balanceFromBlockbook(thisAddress) { try { @@ -42,13 +42,9 @@ async function balanceFromBlockbook(thisAddress) { // Convert the address to a cashaddr without a prefix. const addr = BITBOX.Address.toCashAddress(thisAddress) - // Determine if we are working with the testnet or mainnet networks. - let network = "mainnet" - if (process.env.NETWORK === "testnet") network = "testnet" - const path = `${process.env.BLOCKBOOK_URL}api/v2/address/${addr}` - // Query the Bitcore Node API. + // Query the Blockbook Node API. const axiosResponse = await axios.get(path) const retData = axiosResponse.data //console.log(`retData: ${util.inspect(retData)}`) @@ -80,7 +76,7 @@ async function balanceSingle(req, res, next) { } wlogger.debug( - `Executing bitcore/balanceSingle with this address: `, + `Executing blockbook/balanceSingle with this address: `, address ) @@ -103,7 +99,7 @@ async function balanceSingle(req, res, next) { }) } - // Query the Bitcore Node API. + // Query the Blockbook Node API. const retData = await balanceFromBlockbook(address) // Return the retrieved address information. @@ -202,7 +198,7 @@ async function balanceBulk(req, res, next) { return res.json({ error: util.inspect(err) }) } } -/* + // Query the Blockbook API for utxos associated with a BCH address. // Returns a Promise. async function utxosFromBlockbook(thisAddress) { @@ -210,13 +206,9 @@ async function utxosFromBlockbook(thisAddress) { //console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) // Convert the address to a cashaddr without a prefix. - const addr = BITBOX.Address.toCashAddress(thisAddress, false) + const addr = BITBOX.Address.toCashAddress(thisAddress) - // Determine if we are working with the testnet or mainnet networks. - let network = "mainnet" - if (process.env.NETWORK === "testnet") network = "testnet" - - const path = `${process.env.BLOCKBOOK_URL}api/BCH/${network}/address/${addr}/?unspent=true` + const path = `${process.env.BLOCKBOOK_URL}api/v2/utxo/${addr}` // Query the Blockbook API. const axiosResponse = await axios.get(path) @@ -250,7 +242,7 @@ async function utxosSingle(req, res, next) { } wlogger.debug( - `Executing blockbook/balanceSingle with this address: `, + `Executing blockbook/utxosSingle with this address: `, address ) @@ -273,8 +265,8 @@ async function utxosSingle(req, res, next) { }) } - // Query the Bitcore Node API. - const retData = await utxosFromBitcore(address) + // Query the Blockbook API. + const retData = await utxosFromBlockbook(address) // Return the retrieved address information. res.status(200) @@ -288,7 +280,7 @@ async function utxosSingle(req, res, next) { } // Write out error to error log. - wlogger.error(`Error in bitcore.js/utxosSingle().`, err) + wlogger.error(`Error in blockbook.js/utxosSingle().`, err) res.status(500) return res.json({ error: util.inspect(err) }) @@ -318,7 +310,7 @@ async function utxosBulk(req, res, next) { } wlogger.debug( - `Executing bitcore.js/utxosBulk with these addresses: `, + `Executing blockbook.js/utxosBulk with these addresses: `, addresses ) @@ -349,7 +341,7 @@ async function utxosBulk(req, res, next) { // Loops through each address and creates an array of Promises, querying // Insight API in parallel. addresses = addresses.map(async (address, index) => - utxosFromBitcore(address) + utxosFromBlockbook(address) ) // Wait for all parallel Insight requests to return. @@ -366,20 +358,20 @@ async function utxosBulk(req, res, next) { return res.json({ error: msg }) } - wlogger.error(`Error in bitcore.js/utxosBulk().`, err) + wlogger.error(`Error in blockbook.js/utxosBulk().`, err) res.status(500) return res.json({ error: util.inspect(err) }) } } -*/ + module.exports = { router, testableComponents: { root, balanceSingle, - balanceBulk - //utxosSingle, - //utxosBulk + balanceBulk, + utxosSingle, + utxosBulk } } diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index 4cabf5c..b127daa 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -336,7 +336,7 @@ describe("#Blockbook Router", () => { assert.equal(result.length, 2, "2 outputs for 2 inputs") }) }) - /* + describe("#UTXOs Single", () => { // details route handler. const utxosSingle = blockbookRoute.testableComponents.utxosSingle @@ -422,18 +422,10 @@ describe("#Blockbook Router", () => { assert.isArray(result) assert.hasAnyKeys(result[0], [ - "_id", - "chain", - "network", - "coinbase", - "mintIndex", - "spentTxid", - "mintTxid", - "mintHeight", - "spentHeight", - "address", - "script", + "txid", + "vout", "value", + "height", "confirmations" ]) }) @@ -561,18 +553,10 @@ describe("#Blockbook Router", () => { assert.isArray(result) assert.isArray(result[0]) assert.hasAnyKeys(result[0][0], [ - "_id", - "chain", - "network", - "coinbase", - "mintIndex", - "spentTxid", - "mintTxid", - "mintHeight", - "spentHeight", - "address", - "script", + "txid", + "vout", "value", + "height", "confirmations" ]) }) @@ -599,22 +583,7 @@ describe("#Blockbook Router", () => { assert.isArray(result) assert.isArray(result[0]) - assert.hasAnyKeys(result[0][0], [ - "_id", - "chain", - "network", - "coinbase", - "mintIndex", - "spentTxid", - "mintTxid", - "mintHeight", - "spentHeight", - "address", - "script", - "value", - "confirmations" - ]) + assert.equal(result.length, 2, "2 outputs for 2 inputs") }) }) - */ }) diff --git a/test/v3/mocks/blockbook-mock.js b/test/v3/mocks/blockbook-mock.js index 2fe2b09..1a3b89c 100644 --- a/test/v3/mocks/blockbook-mock.js +++ b/test/v3/mocks/blockbook-mock.js @@ -20,20 +20,11 @@ const mockBalance = { const mockUtxos = [ { - _id: "5cf2c31a33bd46a95ec7e730", - chain: "BCH", - network: "testnet", - coinbase: false, - mintIndex: 1, - spentTxid: "", - mintTxid: - "5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392", - mintHeight: 1265275, - spentHeight: -2, - address: "qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4", - script: "76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac", - value: 10000000, - confirmations: -1 + txid: "5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392", + vout: 1, + value: "10000000", + height: 1265275, + confirmations: 42704 } ]