mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
feat(transactions): Removed transaction library. It used Insight API.
This commit is contained in:
@@ -1,188 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
|
|
||||||
const express = require("express")
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require("axios")
|
|
||||||
|
|
||||||
const routeUtils = require("./route-utils")
|
|
||||||
const logger = require("./logging.js")
|
|
||||||
const wlogger = require("../../util/winston-logging")
|
|
||||||
|
|
||||||
const BITBOXJS = require("@chris.troutner/bitbox-js")
|
|
||||||
const BITBOX = new BITBOXJS()
|
|
||||||
|
|
||||||
// Used to convert error messages to strings, to safely pass to users.
|
|
||||||
const util = require("util")
|
|
||||||
util.inspect.defaultOptions = { depth: 3 }
|
|
||||||
|
|
||||||
// Manipulates and formats the raw data comming from Insight API.
|
|
||||||
const processInputs = tx => {
|
|
||||||
// Add legacy and cashaddr to tx vin
|
|
||||||
if (tx.vin) {
|
|
||||||
tx.vin.forEach(vin => {
|
|
||||||
if (!vin.coinbase) {
|
|
||||||
vin.value = vin.valueSat
|
|
||||||
const address = vin.addr
|
|
||||||
if (address) {
|
|
||||||
vin.legacyAddress = BITBOX.Address.toLegacyAddress(address)
|
|
||||||
vin.cashAddress = BITBOX.Address.toCashAddress(address)
|
|
||||||
delete vin.addr
|
|
||||||
}
|
|
||||||
delete vin.valueSat
|
|
||||||
delete vin.doubleSpentTxID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add legacy and cashaddr to tx vout
|
|
||||||
if (tx.vout) {
|
|
||||||
tx.vout.forEach(vout => {
|
|
||||||
// Overwrite value string with value in satoshis
|
|
||||||
//vout.value = parseFloat(vout.value) * 100000000
|
|
||||||
|
|
||||||
if (vout.scriptPubKey) {
|
|
||||||
if (vout.scriptPubKey.addresses) {
|
|
||||||
const cashAddrs = []
|
|
||||||
vout.scriptPubKey.addresses.forEach(addr => {
|
|
||||||
const cashAddr = BITBOX.Address.toCashAddress(addr)
|
|
||||||
cashAddrs.push(cashAddr)
|
|
||||||
})
|
|
||||||
vout.scriptPubKey.cashAddrs = cashAddrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get("/", root)
|
|
||||||
router.post("/details", detailsBulk)
|
|
||||||
router.get("/details/:txid", detailsSingle)
|
|
||||||
|
|
||||||
function root(req, res, next) {
|
|
||||||
return res.json({ status: "transaction" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve transaction data from the Insight API
|
|
||||||
// This function is also used by the SLP route library.
|
|
||||||
async function transactionsFromInsight(txid) {
|
|
||||||
try {
|
|
||||||
const path = `${process.env.BITCOINCOM_BASEURL}tx/${txid}`
|
|
||||||
|
|
||||||
// Query the Insight server.
|
|
||||||
const response = await axios.get(path)
|
|
||||||
//console.log(`Insight output: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
// Parse the data.
|
|
||||||
const parsed = response.data
|
|
||||||
if (parsed) processInputs(parsed)
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detailsBulk(req, res, next) {
|
|
||||||
try {
|
|
||||||
const txids = req.body.txids
|
|
||||||
|
|
||||||
// Reject if address is not an array.
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: "txids needs to be an array" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: `Array too large.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Executing transaction/details with these txids: `, txids)
|
|
||||||
|
|
||||||
// Collect an array of promises
|
|
||||||
const promises = txids.map(
|
|
||||||
async txid => await transactionsFromInsight(txid)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel promises to return.
|
|
||||||
const result = await Promise.all(promises)
|
|
||||||
|
|
||||||
// Return the array of retrieved transaction 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 transactions.ts/detailsBulk().`, err)
|
|
||||||
|
|
||||||
//console.log(`Error in transaction details: `, err)
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler. Retrieve any unconfirmed TX information for a given address.
|
|
||||||
async function detailsSingle(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."
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Executing transaction.ts/detailsSingle with this txid: `,
|
|
||||||
txid
|
|
||||||
)
|
|
||||||
|
|
||||||
// Query the Insight API.
|
|
||||||
const retData = await transactionsFromInsight(txid)
|
|
||||||
//console.log(`retData: ${JSON.stringify(retData,null,2)}`)
|
|
||||||
|
|
||||||
// Return the array of 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.
|
|
||||||
//logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error(`Error in transactions.ts/detailsSingle().`, err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
router,
|
|
||||||
transactionsFromInsight,
|
|
||||||
testableComponents: {
|
|
||||||
root,
|
|
||||||
detailsBulk,
|
|
||||||
detailsSingle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE TRANSACTION.TS 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.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
-See "should throw an error for an invalid txid" for detailsSingle:
|
|
||||||
--The error handler should be refactored to return an intelligent error message,
|
|
||||||
instead of the 503 error it is returning now.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const chai = require("chai")
|
|
||||||
const assert = chai.assert
|
|
||||||
const transactionRoute = require("../../src/routes/v3/transaction")
|
|
||||||
const nock = require("nock") // HTTP mocking
|
|
||||||
|
|
||||||
let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require("./mocks/express-mocks")
|
|
||||||
const mockData = require("./mocks/transaction-mocks")
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require("util")
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe("#Transactions", () => {
|
|
||||||
let req, res
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
originalEnvVars = {
|
|
||||||
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = "unit"
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/"
|
|
||||||
process.env.RPC_BASEURL = "http://fakeurl/api"
|
|
||||||
process.env.RPC_USERNAME = "fakeusername"
|
|
||||||
process.env.RPC_PASSWORD = "fakepassword"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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(() => {
|
|
||||||
// Restore any pre-existing environment variables.
|
|
||||||
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
|
|
||||||
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
|
|
||||||
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
|
|
||||||
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("#root", async () => {
|
|
||||||
// root route handler.
|
|
||||||
const root = transactionRoute.testableComponents.root
|
|
||||||
|
|
||||||
it("should respond to GET for base route", async () => {
|
|
||||||
const result = root(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, "transaction", "Returns static string")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("#detailsBulk", async () => {
|
|
||||||
const detailsBulk = transactionRoute.testableComponents.detailsBulk
|
|
||||||
|
|
||||||
it("should throw an error for an empty body", async () => {
|
|
||||||
req.body = {}
|
|
||||||
|
|
||||||
const result = await detailsBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, "HTTP status code 400 expected.")
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
"txids needs to be an array",
|
|
||||||
"Proper error message"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should error on non-array single txid", async () => {
|
|
||||||
req.body = {
|
|
||||||
txids: `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40`
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await detailsBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, "HTTP status code 400 expected.")
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
"txids needs to be an array",
|
|
||||||
"Proper error message"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw an error for an invalid txid", async () => {
|
|
||||||
const fakeTXID = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
.get(`/tx/${fakeTXID}`)
|
|
||||||
.reply(400, {
|
|
||||||
result: { error: "parameter 1 must be hexadecimal string" }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
txids: [fakeTXID]
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await detailsBulk(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, "HTTP status code 400 expected.")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should process a single txid", async () => {
|
|
||||||
const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40`
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
.get(`/tx/${txid}`)
|
|
||||||
.reply(200, mockData.mockDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
txids: [txid]
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await detailsBulk(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isArray(result)
|
|
||||||
assert.hasAnyKeys(result[0], [
|
|
||||||
"txid",
|
|
||||||
"version",
|
|
||||||
"locktime",
|
|
||||||
"vin",
|
|
||||||
"vout",
|
|
||||||
"blockhash",
|
|
||||||
"blockheight",
|
|
||||||
"confirmations",
|
|
||||||
"time",
|
|
||||||
"blocktime",
|
|
||||||
"valueOut",
|
|
||||||
"size",
|
|
||||||
"valueIn",
|
|
||||||
"fees"
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should process a multiple txids", async () => {
|
|
||||||
const txid1 = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40`
|
|
||||||
const txid2 = `8d4fd4dcaa9d8051dc7d862dc23d8aa23e20b77b9c928c49380685459caa7043`
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
.get(`/tx/${txid1}`)
|
|
||||||
.reply(200, mockData.mockDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
.get(`/tx/${txid2}`)
|
|
||||||
.reply(200, mockData.mockDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
txids: [txid1, txid2]
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await detailsBulk(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isArray(result)
|
|
||||||
assert.hasAnyKeys(result[0], [
|
|
||||||
"txid",
|
|
||||||
"version",
|
|
||||||
"locktime",
|
|
||||||
"vin",
|
|
||||||
"vout",
|
|
||||||
"blockhash",
|
|
||||||
"blockheight",
|
|
||||||
"confirmations",
|
|
||||||
"time",
|
|
||||||
"blocktime",
|
|
||||||
"valueOut",
|
|
||||||
"size",
|
|
||||||
"valueIn",
|
|
||||||
"fees"
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("#detailsSingle", () => {
|
|
||||||
// details route handler.
|
|
||||||
const detailsSingle = transactionRoute.testableComponents.detailsSingle
|
|
||||||
|
|
||||||
it("should throw 400 if txid is empty", async () => {
|
|
||||||
const result = await detailsSingle(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAllKeys(result, ["error"])
|
|
||||||
assert.include(result.error, "txid can not be empty")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should error on an array", async () => {
|
|
||||||
req.params.txid = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`]
|
|
||||||
|
|
||||||
const result = await detailsSingle(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, "HTTP status code 400 expected.")
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
"txid can not be an array",
|
|
||||||
"Proper error message"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw an error for an invalid txid", async () => {
|
|
||||||
if (process.env.TEST !== "unit") {
|
|
||||||
req.params.txid = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`
|
|
||||||
|
|
||||||
const result = await detailsSingle(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// The error handling code should probably be updated to respond with a better
|
|
||||||
// error message.
|
|
||||||
assert.equal(res.statusCode, 400, "HTTP status code 400 expected.")
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
"parameter 1 must be hexadecimal string",
|
|
||||||
"Proper error message"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw 500 when network issues", async () => {
|
|
||||||
const savedUrl = process.env.BITCOINCOM_BASEURL
|
|
||||||
|
|
||||||
try {
|
|
||||||
req.params.txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40`
|
|
||||||
|
|
||||||
// Switch the Insight URL to something that will error out.
|
|
||||||
process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/"
|
|
||||||
|
|
||||||
const result = await detailsSingle(req, res)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
"HTTP status code 500 or greater expected."
|
|
||||||
)
|
|
||||||
//assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
|
||||||
} catch (err) {
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should get details for a single address", async () => {
|
|
||||||
const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40`
|
|
||||||
req.params.txid = txid
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === "unit") {
|
|
||||||
nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
.get(`/tx/${txid}`)
|
|
||||||
.reply(200, mockData.mockDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the details API.
|
|
||||||
const result = await detailsSingle(req, res)
|
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Assert that required fields exist in the returned object.
|
|
||||||
assert.hasAllKeys(result, [
|
|
||||||
"txid",
|
|
||||||
"version",
|
|
||||||
"locktime",
|
|
||||||
"vin",
|
|
||||||
"vout",
|
|
||||||
"blockhash",
|
|
||||||
"blockheight",
|
|
||||||
"confirmations",
|
|
||||||
"time",
|
|
||||||
"blocktime",
|
|
||||||
"valueOut",
|
|
||||||
"size",
|
|
||||||
"valueIn",
|
|
||||||
"fees"
|
|
||||||
])
|
|
||||||
assert.isArray(result.vin)
|
|
||||||
assert.isArray(result.vout)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user