Compare commits

...
13 Commits
Author SHA1 Message Date
Chris Troutner e96b9e7a44 Merge pull request #149 from Permissionless-Software-Foundation/ct-unstable
fix(v5 Fulcrum): Requiring FULCRUM_API env var when starting server
2021-06-16 18:48:12 -07:00
Chris Troutner d29f99965f fix(v5 Fulcrum): Requiring FULCRUM_API env var when starting server 2021-06-16 18:33:12 -07:00
Chris Troutner 9825ec1bab Merge pull request #147 from Permissionless-Software-Foundation/dh-v5-dsproof-endpoint
feat(dsproof): Created v5 GET dsproof endpoint
2021-06-09 07:38:37 -07:00
Chris Troutner fb61cfeca3 Merge branch 'master' into dh-v5-dsproof-endpoint 2021-06-09 07:08:52 -07:00
Chris Troutner 5f9a635ab5 Merge pull request #146 from Permissionless-Software-Foundation/dh-v5-fulcrum-unconfirmed
feat(unconfirmed): Created v5 Fulcrum /unconfirmed endpoints
2021-06-09 07:08:16 -07:00
Daniel Gonzalez 41c5504846 feat(dsproof): Created v5 GET dsproof endpoint 2021-06-08 20:45:03 -04:00
Daniel Gonzalez e5f9455fbb feat(unconfirmed): Created v5 Fulcrum /unconfirmed endpoints 2021-06-07 23:39:51 -04:00
Chris Troutner 20cb48caff Merge pull request #145 from Permissionless-Software-Foundation/dh-v5-fulcrum-transactions
feat(txs): Created v5 Fulcrum /transactions endpoints in bch-api
2021-06-05 08:35:26 -07:00
Daniel Gonzalez f241aed5b2 feat(txs): Created v5 Fulcrum /transactions endpoints in bch-api 2021-06-04 21:43:45 -04:00
Chris Troutner 87c2faa6af Merge pull request #144 from Permissionless-Software-Foundation/dh-v5-fulcrum-block
feat(block): Created v5 Fulcrum /block endpoints in bch-api
2021-06-04 08:07:05 -07:00
Daniel Gonzalez 10a840f48d feat(block): Created v5 Fulcrum /block endpoints in bch-api 2021-06-02 23:43:25 -04:00
Chris Troutner 61f9531046 Merge pull request #143 from Permissionless-Software-Foundation/dh-fulcrum-tx-endpoint
feat(tx): Created v5 Fulcrum /tx endpoints in bch-api
2021-06-02 07:10:45 -07:00
Daniel Gonzalez 1ab408a757 feat(tx): Created v5 Fulcrum /tx endpoints in bch-api 2021-06-01 22:20:57 -04:00
7 changed files with 1755 additions and 529 deletions
+3 -1
View File
@@ -46,6 +46,7 @@ const ControlV5 = require('./routes/v5/full-node/control')
const MiningV5 = require('./routes/v5/full-node/mining') const MiningV5 = require('./routes/v5/full-node/mining')
const networkV5 = require('./routes/v5/full-node/network') const networkV5 = require('./routes/v5/full-node/network')
const RawtransactionsV5 = require('./routes/v5/full-node/rawtransactions') const RawtransactionsV5 = require('./routes/v5/full-node/rawtransactions')
const DSProofV5 = require('./routes/v5/full-node/dsproof')
const UtilV5 = require('./routes/v5/util') const UtilV5 = require('./routes/v5/util')
const SlpV5 = require('./routes/v5/slp') const SlpV5 = require('./routes/v5/slp')
const xpubV5 = require('./routes/v5/xpub') const xpubV5 = require('./routes/v5/xpub')
@@ -78,7 +79,7 @@ const electrumxv5 = new ElectrumXV5()
const encryptionv5 = new EncryptionV5() const encryptionv5 = new EncryptionV5()
const pricev5 = new PriceV5() const pricev5 = new PriceV5()
const utilV5 = new UtilV5({ electrumx: electrumxv5 }) const utilV5 = new UtilV5({ electrumx: electrumxv5 })
const dsproofV5 = new DSProofV5()
const app = express() const app = express()
app.locals.env = process.env app.locals.env = process.env
@@ -187,6 +188,7 @@ app.use(`/${v5prefix}/` + 'electrumx', electrumxv5.router)
app.use(`/${v5prefix}/` + 'encryption', encryptionv5.router) app.use(`/${v5prefix}/` + 'encryption', encryptionv5.router)
app.use(`/${v5prefix}/` + 'price', pricev5.router) app.use(`/${v5prefix}/` + 'price', pricev5.router)
app.use(`/${v5prefix}/` + 'util', utilV5.router) app.use(`/${v5prefix}/` + 'util', utilV5.router)
app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
// const ninsight = new Ninsight() // const ninsight = new Ninsight()
app.use(`/${v5prefix}/` + 'ninsight', ninsight.router) app.use(`/${v5prefix}/` + 'ninsight', ninsight.router)
+427 -527
View File
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const wlogger = require('../../../util/winston-logging')
const RouteUtils = require('../../../util/route-utils')
const routeUtils = new RouteUtils()
// Used to convert error messages to strings, to safely pass to users.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
let _this
class DSProof {
constructor () {
_this = this
_this.axios = axios
_this.routeUtils = routeUtils
_this.router = router
_this.router.get('/', this.root)
_this.router.get('/getdsproof/:txid', _this.getDSProof)
}
// 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 (req, res, next) {
return res.json({ status: 'dsproof' })
}
/**
* @api {get} /dsproof/getdsproof/:txid Get Double-Spend Proof.
* @apiName DS Proof.
* @apiGroup DSProof
* @apiDescription Get information for a double-spend proof.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v5/dsproof/getdsproof/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json"
*
*
*/
async getDSProof (req, res, next) {
try {
const txid = req.params.txid
let verbose = 2 // default
if (req.query.verbose === 'true') verbose = 3
if (!txid || txid === '') {
res.status(400)
return res.json({
success: false,
error: 'txid can not be empty'
})
}
if (txid.length !== 64) {
res.status(400)
return res.json({
success: false,
error: `txid must be of length 64 (not ${txid.length})`
})
}
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'getdsproof'
options.data.method = 'getdsproof'
options.data.params = [txid, verbose]
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
wlogger.error('Error in dsproof.js/getDSProof().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = DSProof
+979
View File
@@ -609,6 +609,985 @@ describe('#Electrumx', () => {
}) })
}) })
describe('#getTransactionDetails', () => {
it('should throw 400 if tx is empty', async () => {
const result = await electrumxRoute.getTransactionDetails(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'txid must be a string')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 on array input', async () => {
req.params.txid = [
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d'
]
const result = await electrumxRoute.getTransactionDetails(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'txid must be a string')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
req.params.txid = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
const result = await electrumxRoute.getTransactionDetails(req, res)
assert.property(result, 'error')
assert.include(result.error.error, 'Invalid tx hash')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should get details for a single tx', async () => {
req.params.txid =
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d'
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'get')
.resolves({ data: { success: true, details: mockData.txDetails } })
}
// Call the details API.
const result = await electrumxRoute.getTransactionDetails(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'details')
assert.property(result.details, 'blockhash')
assert.property(result.details, 'blocktime')
assert.property(result.details, 'confirmations')
assert.property(result.details, 'hash')
assert.property(result.details, 'hex')
assert.property(result.details, 'locktime')
assert.property(result.details, 'size')
assert.property(result.details, 'time')
assert.property(result.details, 'txid')
assert.property(result.details, 'version')
assert.property(result.details, 'vin')
assert.property(result.details, 'vout')
})
})
describe('#transactionDetailsBulk', () => {
it('should throw 400 if txids is empty', async () => {
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'txids needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if input provided is not array', async () => {
req.body.txids =
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d'
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'txids needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
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.txids = testArray
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'error')
assert.include(result.error, 'Array too large')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should handle error', async () => {
req.body.txids = [
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d',
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d'
]
// Force error
sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error'))
// Call the details API.
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Test error')
})
it('should get details for an array of tx', async () => {
req.body.txids = [
'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d'
]
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'post')
.resolves({ data: mockData.txDetailsBulk })
}
// Call the details API.
const result = await electrumxRoute.transactionDetailsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'transactions')
const tx = result.transactions[0]
assert.property(tx, 'details')
assert.property(tx.details, 'blockhash')
assert.property(tx.details, 'blocktime')
assert.property(tx.details, 'confirmations')
assert.property(tx.details, 'hash')
assert.property(tx.details, 'hex')
assert.property(tx.details, 'locktime')
assert.property(tx.details, 'size')
assert.property(tx.details, 'time')
assert.property(tx.details, 'txid')
assert.property(tx.details, 'version')
assert.property(tx.details, 'vin')
assert.property(tx.details, 'vout')
})
})
describe('#broadcastTransaction', () => {
it('should throw 400 if txHex is empty', async () => {
const result = await electrumxRoute.broadcastTransaction(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'request body must be a string')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 on invalid input type', async () => {
req.body.txHex = [mockData.txDetails.hex]
const result = await electrumxRoute.broadcastTransaction(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'request body must be a string')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
req.body.txHex = mockData.txDetails.hex.substring(10)
const result = await electrumxRoute.broadcastTransaction(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error.error, 'the transaction was rejected')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should broadcast transaction', async function () {
req.body.txHex = mockData.txDetails.hex
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'post')
.resolves({ data: { success: true, txid: mockData.txDetails.hash } })
} else {
return this.skip()
}
// Call the details API.
const result = await electrumxRoute.broadcastTransaction(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'txid')
assert.isString(result.txid)
})
})
describe('#getBlockHeaders', () => {
it('should throw 400 if height is empty', async () => {
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'height must be a positive number')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if height is not a number', async () => {
req.params.height = 'wrong type'
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'height must be a positive number')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if height is negative', async () => {
req.params.height = -1
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'height must be a positive number')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if count is not a number', async () => {
req.params.height = 2
req.query.count = 'wrong type'
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'count must be a positive number')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if count is negative', async () => {
req.params.height = 2
req.query.count = -1
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'count must be a positive number')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
req.params.height = 99999999999999
req.query.count = 99999999999999
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox.stub(electrumxRoute.axios, 'get').resolves({
data: { success: false, error: { error: 'Invalid height' } }
})
}
// Call the details API.
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error.error, 'Invalid height')
})
it('should handle error', async () => {
req.params.height = 42
req.query.count = 2
// Force error
sandbox.stub(electrumxRoute.axios, 'get').throws(new Error('Test error'))
// Call the details API.
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Test error')
})
it('should get headers for a single block height with count 2', async () => {
req.params.height = 42
req.query.count = 2
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox.stub(electrumxRoute.axios, 'get').resolves({
data: { success: true, headers: mockData.blockHeaders }
})
}
// Call the details API.
const result = await electrumxRoute.getBlockHeaders(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
assert.deepEqual(result.headers, mockData.blockHeaders)
})
})
describe('#blockHeadersBulk', () => {
it('should throw 400 for an empty body', async () => {
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'heights needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should NOT throw 400 error for an invalid height', async () => {
req.body = {
heights: [{ height: -10, count: 2 }]
}
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox.stub(electrumxRoute.axios, 'post').resolves({
data: { success: true, headers: [{ headers: {} }] }
})
}
const result = await electrumxRoute.blockHeadersBulk(req, res)
console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 200, 'Expect 200 status code')
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
assert.property(result.headers[0], 'headers')
})
it('should throw 400 error if heights array is too large', async () => {
const testArray = []
for (var i = 0; i < 25; i++) testArray.push('')
req.body.heights = testArray
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'error')
assert.include(result.error, 'Array too large')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should handle error', async () => {
req.body = {
heights: [
{ height: 42, count: 2 },
{ height: 42, count: 2 }
]
}
// Force error
sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error'))
// Call the details API.
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Test error')
})
it('should get block heights', async () => {
req.body = {
heights: [
{ height: 42, count: 2 },
{ height: 42, count: 2 }
]
}
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'post')
.resolves({ data: mockData.blockHeadersBulk })
}
// Call the details API.
const result = await electrumxRoute.blockHeadersBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'headers')
assert.isArray(result.headers)
assert.property(result.headers[0], 'headers')
})
})
describe('#getTransactions', () => {
it('should throw 400 if address is empty', async () => {
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code')
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 on array input', async () => {
req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c']
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'address can not be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw an error for an invalid address', async () => {
req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code')
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should detect a network mismatch', async () => {
req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid network', 'Proper error message')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
// Address has invalid checksum.
req.params.address =
'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2'
// Call the details API.
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
})
it('should get transaction for a single address', async () => {
req.params.address =
'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox.stub(electrumxRoute.axios, 'get').resolves({
data: { success: true, transactions: mockData.transactions }
})
}
// Call the details API.
const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'transactions')
assert.isArray(result.transactions)
assert.property(result.transactions[0], 'height')
assert.property(result.transactions[0], 'tx_hash')
})
})
describe('#transactionsBulk', () => {
it('should throw 400 if addresses is empty', async () => {
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'addresses needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if input provided is not array', async () => {
req.body.addresses = 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'addresses needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
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 electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'error')
assert.include(result.error, 'Array too large')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw an error for an invalid address', async () => {
req.body.addresses = ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c']
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid BCH address')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should detect a network mismatch', async () => {
req.body.addresses = [
'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
]
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid network', 'Proper error message')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
// Address has invalid checksum.
req.body.addresses = [
'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2'
]
// Call the details API.
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Invalid BCH address')
})
it('should handle error', async () => {
req.body.addresses = [
'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7',
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
]
// Force error
sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error'))
// Call the details API.
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Test error')
})
it('should get transaction for an array of addresses', async () => {
req.body.addresses = [
'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7',
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
]
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'post')
.resolves({ data: mockData.transactionsBulk })
}
// Call the details API.
const result = await electrumxRoute.transactionsBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'transactions')
assert.isArray(result.transactions)
assert.property(result.transactions[0], 'transactions')
assert.isArray(result.transactions[0].transactions)
assert.property(result.transactions[0].transactions[0], 'height')
assert.property(result.transactions[0].transactions[0], 'tx_hash')
})
})
describe('#getMempool', () => {
it('should throw 400 if address is empty', async () => {
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code')
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 on array input', async () => {
req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c']
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'address can not be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw an error for an invalid address', async () => {
req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code')
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should detect a network mismatch', async () => {
req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid network', 'Proper error message')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
// Address has invalid checksum.
req.params.address =
'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2'
// Call the details API.
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format')
})
it('should get mempool for a single address', async () => {
req.params.address =
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'get')
.resolves({ data: { success: true, utxos: mockData.utxos } })
}
// Call the details API.
const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'utxos')
assert.isArray(result.utxos)
})
})
describe('#mempoolBulk', () => {
it('should throw 400 if addresses is empty', async () => {
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'addresses needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw 400 if input provided is not array', async () => {
req.body.addresses = 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'addresses needs to be an array')
assert.property(result, 'success')
assert.equal(result.success, false)
})
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 electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'error')
assert.include(result.error, 'Array too large')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should throw an error for an invalid address', async () => {
req.body.addresses = ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c']
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid BCH address')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should detect a network mismatch', async () => {
req.body.addresses = [
'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
]
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error')
assert.include(result.error, 'Invalid network', 'Proper error message')
assert.property(result, 'success')
assert.equal(result.success, false)
})
it('should pass errors from electrum-cash to user', async () => {
// Address has invalid checksum.
req.body.addresses = [
'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2'
]
// Call the details API.
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Invalid BCH address')
})
it('should handle error', async () => {
req.body.addresses = [
'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7',
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
]
// Force error
sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error'))
// Call the details API.
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'Test error')
})
it('should get mempool for multiple addresses', async () => {
req.body.addresses = [
'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7',
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
]
// Mock unit tests to prevent live network calls.
if (process.env.TEST === 'unit') {
electrumxRoute.isReady = true // Force flag.
sandbox
.stub(electrumxRoute.axios, 'post')
.resolves({ data: mockData.utxosArray })
}
// Call the details API.
const result = await electrumxRoute.mempoolBulk(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'utxos')
assert.property(result.utxos[0], 'utxos')
assert.property(result.utxos[0], 'address')
assert.isArray(result.utxos[0].utxos)
assert.isString(result.utxos[0].address)
})
})
// describe('#_utxosFromElectrumx', () => { // describe('#_utxosFromElectrumx', () => {
// it('should throw error for invalid address', async () => { // it('should throw error for invalid address', async () => {
// try { // try {
+190
View File
@@ -0,0 +1,190 @@
/*
TESTS FOR THE DSPROOF.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.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const DSProofRoute = require('../../src/routes/v5/full-node/dsproof')
const uut = new DSProofRoute()
// const nock = require('nock') // HTTP mocking
const sinon = require('sinon')
let originalEnvVars // Used during transition from integration to unit tests.
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/dsproof-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#DSProof', () => {
let req, res
let sandbox
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 = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
// Restore Sandbox
sandbox.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.
it('should respond to GET for base route', async () => {
const result = uut.root(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(result.status, 'dsproof', 'Returns static string')
})
})
describe('#getDSProof', async () => {
it('should throw 400 error if txid is missing', async () => {
const result = await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'txid can not be empty')
})
it('should throw 400 error if txid is invalid', async () => {
req.params.txid = 'abc123'
const result = await uut.getDSProof(req, res)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.include(result.error, 'txid must be of length 64')
})
it('should throw 503 when network issues', async () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Save the existing RPC URL.
const savedUrl2 = process.env.RPC_BASEURL
// Manipulate the URL to cause a 500 network error.
process.env.RPC_BASEURL = 'http://fakeurl/api/'
await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.RPC_BASEURL = savedUrl2
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")
})
it('returns proper error when downstream service stalls', async () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
const result = await uut.getDSProof(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
const result = await uut.getDSProof(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('should GET double-spend proof', async function () {
req.params.txid =
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: { result: mockData.mockGetDsProof } })
} else {
return this.skip()
}
const result = await uut.getDSProof(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'dspid')
assert.property(result, 'txid')
assert.property(result, 'outpoint')
assert.property(result, 'path')
assert.property(result, 'descendants')
assert.property(result.outpoint, 'txid')
assert.property(result.outpoint, 'vout')
})
})
})
+22
View File
@@ -0,0 +1,22 @@
/*
This library contains mocking data for running unit tests on the dsproof route.
*/
'use strict'
const mockGetDsProof = {
dspid: '6234fff2a9dbcaa50e2c50d74e1d725a2bed1757d7a05a2db7a82df659554397',
txid: 'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e',
outpoint: {
txid: '17ebef34f7e9a0692317dd6fbb43ffccff45b7d688a2a754dbccfc5e6d93ce3e',
vout: 1
},
path: ['ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'],
descendants: [
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
]
}
module.exports = {
mockGetDsProof
}
+39 -1
View File
@@ -166,11 +166,45 @@ const txDetails = {
] ]
} }
const txDetailsBulk = {
success: true,
transactions: [{ details: txDetails }]
}
const blockHeaders = [ const blockHeaders = [
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6', '010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c' '01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
] ]
const blockHeadersBulk = {
success: true,
headers: [{ headers: blockHeaders }, { headers: blockHeaders }]
}
const transactions = [
{
height: 603416,
tx_hash: 'eef683d236d88e978bd406419f144057af3fe1b62ef59162941c1a9f05ded62c'
},
{
height: 646894,
tx_hash: '4c695fae636f3e8e2edc571d11756b880ccaae744390f3950d798ce7b5e25754'
}
]
const transactionsBulk = {
success: true,
transactions: [
{
transactions,
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
},
{
transactions,
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
}
]
}
module.exports = { module.exports = {
utxos, utxos,
utxosArray, utxosArray,
@@ -178,6 +212,10 @@ module.exports = {
txHistory, txHistory,
mempool, mempool,
txDetails, txDetails,
txDetailsBulk,
blockHeaders, blockHeaders,
balances blockHeadersBulk,
balances,
transactions,
transactionsBulk
} }