Adding getTxOut as a POST call

This commit is contained in:
Chris Troutner
2020-04-23 21:49:31 -07:00
parent 6a7fe05570
commit 75a3cee7bd
2 changed files with 194 additions and 13 deletions
+68 -13
View File
@@ -48,6 +48,7 @@ class Blockchain {
this.router.get('/getMempoolInfo', this.getMempoolInfo)
this.router.get('/getRawMempool', this.getRawMempool)
this.router.get('/getTxOut/:txid/:n', this.getTxOut)
this.router.post('/getTxOut', this.getTxOutPost)
this.router.get('/getTxOutProof/:txid', this.getTxOutProofSingle)
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
@@ -248,7 +249,7 @@ class Blockchain {
* returns an Object with information about blockheader hash.
*
* @apiExample Example usage:
* curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
* curl -X POST "https://api.fullstack.cash/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
*
* @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data
@@ -283,7 +284,7 @@ class Blockchain {
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, hashes)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
@@ -308,7 +309,7 @@ class Blockchain {
const options = _this.routeUtils.getAxiosOptions()
// Loop through each hash and creates an array of requests to call in parallel
const promises = hashes.map(async hash => {
const promises = hashes.map(async (hash) => {
options.data.id = 'getblockheader'
options.data.method = 'getblockheader'
options.data.params = [hash, verbose]
@@ -319,7 +320,7 @@ class Blockchain {
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map(x => x.data.result)
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
@@ -456,7 +457,7 @@ class Blockchain {
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
@@ -481,7 +482,7 @@ class Blockchain {
const options = _this.routeUtils.getAxiosOptions()
// Loop through each txid and creates an array of requests to call in parallel
const promises = txids.map(async txid => {
const promises = txids.map(async (txid) => {
options.data.id = 'getmempoolentry'
options.data.method = 'getmempoolentry'
options.data.params = [txid]
@@ -492,7 +493,7 @@ class Blockchain {
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map(x => x.data.result)
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
@@ -684,6 +685,60 @@ class Blockchain {
}
}
/**
* @api {post} /blockchain/getTxOut Validate a UTXO
* @apiName getTxOut
* @apiGroup Blockchain
* @apiDescription Returns details about an unspent transaction output (UTXO).
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockchain/getTxOut" -H "Content-Type: application/json" -d '{"txid":"a402836c7ced7d7ad2df26d7ee5235f2605d59d65c5c567a750f9eaf186ebb47","vout": 0, "mempool": true}'
*
* @apiParam {String} txid Transaction id (required)
* @apiParam {Number} vout of transaction (required)
* @apiParam {Boolean} mempool Check mempool or not (optional)
*
*/
// Returns details about an unspent transaction output.
async getTxOutPost (req, res, next) {
try {
const txid = req.body.txid
let n = req.body.vout
const mempool = req.body.mempool ? req.body.mempool : true
// Validate input parameter
if (!txid || txid === '') {
res.status(400)
return res.json({ error: 'txid can not be empty' })
}
if (n === undefined || n === '') {
res.status(400)
return res.json({ error: 'vout can not be empty' })
}
n = parseInt(n)
// Axios options
const options = _this.routeUtils.getAxiosOptions()
options.data.id = 'gettxout'
options.data.method = 'gettxout'
options.data.params = [txid, n, mempool]
// console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`)
const response = await _this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
wlogger.error('Error in blockchain.ts/getTxOutPost().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof
* @apiName getTxOutProofSingle
@@ -739,7 +794,7 @@ class Blockchain {
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
@@ -766,7 +821,7 @@ class Blockchain {
)
// Loop through each txid and creates an array of requests to call in parallel
const promises = txids.map(async txid => {
const promises = txids.map(async (txid) => {
options.data.id = 'gettxoutproof'
options.data.method = 'gettxoutproof'
options.data.params = [[txid]]
@@ -778,7 +833,7 @@ class Blockchain {
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map(x => x.data.result)
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
@@ -833,7 +888,7 @@ class Blockchain {
// Enforce array size rate limits
if (!routeUtils.validateArraySize(req, proofs)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({
error: 'Array too large.'
})
@@ -858,7 +913,7 @@ class Blockchain {
)
// Loop through each proof and creates an array of requests to call in parallel
const promises = proofs.map(async proof => {
const promises = proofs.map(async (proof) => {
options.data.id = 'verifytxoutproof'
options.data.method = 'verifytxoutproof'
options.data.params = [proof]
@@ -870,7 +925,7 @@ class Blockchain {
const axiosResult = await _this.axios.all(promises)
// Extract the data component from the axios response.
const result = axiosResult.map(x => x.data.result[0])
const result = axiosResult.map((x) => x.data.result[0])
res.status(200)
return res.json(result)
+126
View File
@@ -1164,6 +1164,7 @@ describe('#BlockchainRouter', () => {
assert.isArray(result)
})
})
describe('getTxOut()', () => {
it('should throw 400 if txid is empty', async () => {
const result = await uut.getTxOut(req, res)
@@ -1226,6 +1227,7 @@ describe('#BlockchainRouter', () => {
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
@@ -1245,6 +1247,7 @@ describe('#BlockchainRouter', () => {
'Error message expected'
)
})
// This test can only run for unit tests. See TODO at the top of this file.
it('should GET /getTxOut', async () => {
// Mock the RPC call for unit tests.
@@ -1279,6 +1282,124 @@ describe('#BlockchainRouter', () => {
assert.isArray(result.scriptPubKey.addresses)
})
})
describe('getTxOutPost()', () => {
it('should throw 400 if txid is empty', async () => {
const result = await uut.getTxOutPost(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'txid can not be empty')
})
it('should throw 400 if n is empty', async () => {
req.body.txid = 'sometxid'
const result = await uut.getTxOutPost(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'vout can not be empty')
})
it('should throw 503 when network issues', async () => {
// 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/'
req.body.txid =
'd65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde'
req.body.vout = 0
const result = await uut.getTxOutPost(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 503 expected.')
assert.include(
result.error,
'Could not communicate with full node',
'Error message expected'
)
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
req.body.txid =
'197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d'
req.body.vout = 0
req.body.mempool = true
const result = await uut.getTxOutPost(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 () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
req.body.txid =
'197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d'
req.body.vout = 0
req.body.mempool = true
const result = await uut.getTxOutPost(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 POST /getTxOut', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(uut.axios, 'request')
.resolves({ data: { result: mockData.mockTxOut } })
}
req.body.txid =
'197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d'
req.body.vout = 0
req.body.mempool = true
const result = await uut.getTxOutPost(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.hasAllKeys(result, [
'bestblock',
'confirmations',
'value',
'scriptPubKey',
'coinbase'
])
assert.hasAllKeys(result.scriptPubKey, [
'asm',
'hex',
'reqSigs',
'type',
'addresses'
])
assert.isArray(result.scriptPubKey.addresses)
})
})
describe('getTxOutProofSingle()', () => {
it('should throw 400 if txid is empty', async () => {
const result = await uut.getTxOutProofSingle(req, res)
@@ -1311,6 +1432,7 @@ describe('#BlockchainRouter', () => {
'Error message expected'
)
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
@@ -1330,6 +1452,7 @@ describe('#BlockchainRouter', () => {
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
@@ -1367,6 +1490,7 @@ describe('#BlockchainRouter', () => {
assert.isString(result)
})
})
describe('#getTxOutProofBulk', () => {
it('should throw an error for an empty body', async () => {
req.body = {}
@@ -1407,6 +1531,7 @@ describe('#BlockchainRouter', () => {
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'Array too large')
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
@@ -1425,6 +1550,7 @@ describe('#BlockchainRouter', () => {
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })