mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb69a6dee9 | ||
|
|
d67b3aa052 | ||
|
|
7e1c3d6a73 | ||
|
|
6165001790 | ||
|
|
77ba06d68c | ||
|
|
40f6b17d3b | ||
|
|
4d40a7cb01 | ||
|
|
0a14ca08a4 | ||
|
|
198141b75a | ||
|
|
6f25ec4b68 | ||
|
|
cb416d26c0 | ||
|
|
376094c6c2 | ||
|
|
f566d44252 | ||
|
|
0e7b45214f | ||
|
|
58366b5442 | ||
|
|
f97e7016c5 | ||
|
|
f82c85712c | ||
|
|
5f18bc578b | ||
|
|
e72a6b50e4 | ||
|
|
66c99ae442 | ||
|
|
5e04125bc3 | ||
|
|
fd3fd43800 | ||
|
|
5184da7c26 | ||
|
|
595caffb51 |
@@ -19,6 +19,7 @@ start-fullstack-test.sh
|
||||
start-free-main.sh
|
||||
start-local-mainnet.sh.save
|
||||
start-local-testnet.sh.save
|
||||
temp.sh
|
||||
|
||||
coverage
|
||||
start-my-infra
|
||||
|
||||
@@ -18,7 +18,7 @@ Both bch-api and bch-js are part of the
|
||||
- [API Documentation](https://fullstack.cash/documentation)
|
||||
- [Example Code](https://fullstack.cash/examples)
|
||||
|
||||
Have questions? Need help? Join our community support
|
||||
Have questions? Need help? Join our community support:
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## Features
|
||||
@@ -94,4 +94,4 @@ Have questions? Need help? Join our community support
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## License
|
||||
[MIT](LICENSE.md)
|
||||
[MIT](./LICENSE.md)
|
||||
|
||||
Generated
+470
-633
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -28,7 +28,7 @@
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chris.troutner/bch-js": "^3.0.1",
|
||||
"@psf/bch-js": "^3.6.2",
|
||||
"apidoc": "^0.23.0",
|
||||
"axios": "^0.19.0",
|
||||
"bitcore-lib-cash": "^8.20.3",
|
||||
@@ -37,7 +37,7 @@
|
||||
"cors": "^2.8.3",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "^8.0.0",
|
||||
"electrum-cash": "^1.0.1",
|
||||
"electrum-cash": "^2.0.2",
|
||||
"express": "^4.15.5",
|
||||
"express-basic-auth": "^1.1.3",
|
||||
"express-rate-limit": "^5.0.0",
|
||||
|
||||
@@ -135,6 +135,12 @@ class RateLimits {
|
||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
||||
}
|
||||
|
||||
// Apply paid-access rate limits based on key/IP
|
||||
if (key.toString().indexOf('172.17.') > -1) {
|
||||
pointsToConsume = 1
|
||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
||||
}
|
||||
|
||||
wlogger.info(
|
||||
`User ${key} consuming ${pointsToConsume} point for resource ${resource}.`
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ const router = express.Router()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
|
||||
+174
-3
@@ -10,7 +10,7 @@ const axios = require('axios')
|
||||
const util = require('util')
|
||||
const bitcore = require('bitcore-lib-cash')
|
||||
|
||||
const ElectrumCash = require('electrum-cash').Client
|
||||
const ElectrumCash = require('electrum-cash').ElectrumClient
|
||||
// const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line
|
||||
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
@@ -19,7 +19,7 @@ const config = require('../../../config')
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
@@ -50,8 +50,10 @@ class Electrum {
|
||||
_this.router.get('/', _this.root)
|
||||
_this.router.get('/utxos/:address', _this.getUtxos)
|
||||
_this.router.post('/utxos', _this.utxosBulk)
|
||||
_this.router.get('/tx/data/:address', _this.getTransactionDetails)
|
||||
_this.router.get('/tx/data/:txid', _this.getTransactionDetails)
|
||||
_this.router.post('/tx/data', _this.transactionDetailsBulk)
|
||||
_this.router.get('/block/headers/:height', _this.getBlockHeaders)
|
||||
_this.router.post('/block/headers', _this.blockHeadersBulk)
|
||||
_this.router.get('/balance/:address', _this.getBalance)
|
||||
_this.router.post('/balance', _this.balanceBulk)
|
||||
_this.router.get('/transactions/:address', _this.getTransactions)
|
||||
@@ -489,6 +491,175 @@ class Electrum {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to block header data for a block height.
|
||||
// Expects input to be a height number, and input validation to have already
|
||||
// been done by parent, calling function.
|
||||
async _blockHeadersFromElectrum (height, count = 1) {
|
||||
try {
|
||||
if (!_this.isReady) {
|
||||
throw new Error(
|
||||
'ElectrumX server connection is not ready. Call await connectToServer() first.'
|
||||
)
|
||||
}
|
||||
|
||||
// Query the block header from the ElectrumX server.
|
||||
const electrumResponse = await _this.electrumx.request('blockchain.block.headers', height, count)
|
||||
// console.log(
|
||||
// `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`
|
||||
// )
|
||||
|
||||
const HEADER_SIZE = 80 * 2
|
||||
|
||||
if (!(electrumResponse instanceof Error)) {
|
||||
const headers = electrumResponse.hex.match(new RegExp(`.{1,${HEADER_SIZE}}`, 'g'))
|
||||
return headers
|
||||
}
|
||||
|
||||
return electrumResponse
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
|
||||
// Write out error to error log.
|
||||
wlogger.error(
|
||||
'Error in elecrumx.js/_blockHeaderFromElectrum(): ',
|
||||
err
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /electrumx/block/headers/{height} Get `count` block headers starting at a height
|
||||
* @apiName Block header data for a `count` blocks starting at a block height
|
||||
* @apiGroup ElectrumX / Fulcrum
|
||||
* @apiDescription Returns an array with block headers starting at the block height
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v3/electrumx/block/header/42?count=2" -H "accept: application/json"
|
||||
*
|
||||
*/
|
||||
// GET handler for single block headers
|
||||
async getBlockHeaders (req, res, next) {
|
||||
try {
|
||||
const height = Number(req.params.height)
|
||||
const count = req.query.count === undefined ? 1 : Number(req.query.count)
|
||||
|
||||
// Reject if height is not a number
|
||||
if (Number.isNaN(height) || height < 0) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'height must be a positive number'
|
||||
})
|
||||
}
|
||||
|
||||
// Reject if height is not a number
|
||||
if (Number.isNaN(count) || count < 0) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'count must be a positive number'
|
||||
})
|
||||
}
|
||||
|
||||
wlogger.debug(
|
||||
'Executing electrumx/getBlockHeaders with this height: ',
|
||||
height
|
||||
)
|
||||
|
||||
// Get data from ElectrumX server.
|
||||
const electrumResponse = await _this._blockHeadersFromElectrum(height, count)
|
||||
// console.log(`_transactionDetailsFromElectrum(): ${JSON.stringify(electrumResponse, null, 2)}`)
|
||||
|
||||
// Pass the error message if ElectrumX reports an error.
|
||||
if (electrumResponse instanceof Error) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: electrumResponse.message
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({
|
||||
success: true,
|
||||
headers: electrumResponse
|
||||
})
|
||||
} catch (err) {
|
||||
// Write out error to error log.
|
||||
wlogger.error('Error in elecrumx.js/getBlockHeader().', err)
|
||||
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /electrumx/block/headers Get block headers for an array of height + count pairs
|
||||
* @apiName Block headers for an array of height + count pairs
|
||||
* @apiGroup ElectrumX / Fulcrum
|
||||
* @apiDescription Returns an array of objects with blockheaders of an array of TXIDs.
|
||||
* Limited to 20 items per request.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v3/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, count: 2 }, { "height": 100, count: 5 }]}'
|
||||
*
|
||||
*/
|
||||
// POST handler for bulk queries on block headers
|
||||
async blockHeadersBulk (req, res, next) {
|
||||
try {
|
||||
const heights = req.body.heights
|
||||
|
||||
// Reject if heights is not an array.
|
||||
if (!Array.isArray(heights)) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'heights needs to be an array. Use GET for single height.'
|
||||
})
|
||||
}
|
||||
|
||||
// Enforce array size rate limits
|
||||
if (!_this.routeUtils.validateArraySize(req, heights)) {
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'Array too large.'
|
||||
})
|
||||
}
|
||||
|
||||
wlogger.debug(
|
||||
'Executing electrumx.js/blockHeadersBulk with these txids: ',
|
||||
heights
|
||||
)
|
||||
|
||||
// Loops through each address and creates an array of Promises, querying
|
||||
// the Electrum server in parallel.
|
||||
const transactions = heights.map(async (obj) => {
|
||||
const headers = await _this._blockHeadersFromElectrum(
|
||||
obj.height,
|
||||
obj.count
|
||||
)
|
||||
|
||||
return { headers }
|
||||
})
|
||||
|
||||
// Wait for all parallel Electrum requests to return.
|
||||
const result = await Promise.all(transactions)
|
||||
|
||||
// Return the array of retrieved transaction details.
|
||||
res.status(200)
|
||||
return res.json({
|
||||
success: true,
|
||||
headers: result
|
||||
})
|
||||
} catch (err) {
|
||||
wlogger.error('Error in electrumx.js/blockHeadersBulk().', err)
|
||||
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to a balance for an address. Expects input
|
||||
// to be a cash address, and input validation to have already been done by
|
||||
// parent, calling function.
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = require('../../../config')
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Use Blockbook for getting indexer data.
|
||||
|
||||
@@ -17,7 +17,7 @@ const routeUtils = new RouteUtils()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
|
||||
@@ -11,7 +11,7 @@ const express = require('express')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// const BCHJS = require('@chris.troutner/bch-js')
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
// const bchjs = new BCHJS()
|
||||
|
||||
// let _this
|
||||
|
||||
@@ -10,7 +10,7 @@ const wlogger = require('../../util/winston-logging')
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const axios = require('axios')
|
||||
|
||||
const SLPSDK = require('@chris.troutner/bch-js')
|
||||
const SLPSDK = require('@psf/bch-js')
|
||||
const SLP = new SLPSDK()
|
||||
|
||||
class Slpdb {
|
||||
|
||||
+79
-1
@@ -13,7 +13,7 @@ const Slpdb = require('./services/slpdb')
|
||||
// const strftime = require('strftime')
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Used to convert error messages to strings, to safely pass to users.
|
||||
@@ -79,6 +79,7 @@ class Slp {
|
||||
_this.txsByAddressSingle
|
||||
)
|
||||
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
}
|
||||
|
||||
// DRY error handler.
|
||||
@@ -1519,6 +1520,83 @@ class Slp {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /slp/hydrateUtxos/ hydrateUtxos
|
||||
* @apiName SLP hydrateUtxos
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Hydrate UTXO data with SLP information.
|
||||
*
|
||||
* Expects an array of UTXO objects as input. Returns an array of equal size.
|
||||
* Returns UTXO data hydrated with token information. If the UTXO does not
|
||||
* belong to a SLP transaction, it will return an isValid property set to
|
||||
* false. If the UTXO is part of an SLP transaction, it will return the UTXO
|
||||
* object with additional SLP information attached. An isValid property will
|
||||
* be included. If its value is true, the UTXO is a valid SLP UTXO. If the
|
||||
* value is null, then SLPDB has not yet processed that txid and validity has
|
||||
* not been confirmed.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v3/slp/hydrateUtxos" -H "accept:application/json" -H "Content-Type: application/json" -d '{"utxos":[{"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 3, "value": "6816", "height": 606848, "confirmations": 13, "satoshis": 6816}, {"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 2, "value": "546", "height": 606848, "confirmations": 13, "satoshis": 546}]}'
|
||||
*
|
||||
*
|
||||
*/
|
||||
async hydrateUtxos (req, res, next) {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
// Validate inputs
|
||||
if (!Array.isArray(utxos)) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Input must be an array.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos.length) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array should not be empty'
|
||||
})
|
||||
}
|
||||
|
||||
if (utxos.length > 20) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array too long, max length is 20'
|
||||
})
|
||||
}
|
||||
|
||||
// Loop through each address and query the UTXOs for that element.
|
||||
for (let i = 0; i < utxos.length; i++) {
|
||||
const theseUtxos = utxos[i].utxos
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({ slpUtxos: utxos })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
return res.json({
|
||||
error: 'Error in hydrateUtxos()'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Slp
|
||||
|
||||
@@ -12,7 +12,7 @@ const blockbook = new Blockbook()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v3/'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const router = express.Router()
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Connect the route endpoints to their handler functions.
|
||||
|
||||
@@ -10,7 +10,7 @@ const wlogger = require('./winston-logging')
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@chris.troutner/bch-js')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// let _this
|
||||
|
||||
+242
-2
@@ -96,7 +96,7 @@ describe('#ElectrumX Router', () => {
|
||||
|
||||
// A wrapper for stubbing with the Sinon sandbox.
|
||||
function stubMethodForUnitTests (obj, method, value) {
|
||||
if (!process.env.TEST === 'unit') return false
|
||||
if (process.env.TEST !== 'unit') return false
|
||||
|
||||
electrumxRoute.isReady = true // Force flag.
|
||||
|
||||
@@ -495,7 +495,7 @@ describe('#ElectrumX Router', () => {
|
||||
})
|
||||
|
||||
it('should throw 400 on array input', async () => {
|
||||
req.params.address = [
|
||||
req.params.txid = [
|
||||
'4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251'
|
||||
]
|
||||
|
||||
@@ -666,6 +666,246 @@ describe('#ElectrumX Router', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_blockHeadersFromElectrum', () => {
|
||||
it('should return error object for invalid block height', async () => {
|
||||
const height = -10
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute.electrumx,
|
||||
'request',
|
||||
new Error('Invalid height')
|
||||
)
|
||||
|
||||
const result = await electrumxRoute._blockHeadersFromElectrum(height, 2)
|
||||
|
||||
assert.instanceOf(result, Error)
|
||||
assert.include(result.message, 'Invalid height')
|
||||
})
|
||||
|
||||
it('should return error object for invalid count', async () => {
|
||||
const height = 42
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute.electrumx,
|
||||
'request',
|
||||
new Error('Invalid count')
|
||||
)
|
||||
|
||||
const result = await electrumxRoute._blockHeadersFromElectrum(height, -1)
|
||||
|
||||
assert.instanceOf(result, Error)
|
||||
assert.include(result.message, 'Invalid count')
|
||||
})
|
||||
|
||||
it('should get block header for a single block height', async () => {
|
||||
const height = 42
|
||||
|
||||
const mockedResponse = { count: 2, hex: mockData.blockHeaders.join(''), max: 2016 }
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute.electrumx,
|
||||
'request',
|
||||
mockedResponse
|
||||
)
|
||||
|
||||
const result = await electrumxRoute._blockHeadersFromElectrum(height, 2)
|
||||
|
||||
assert.isArray(result)
|
||||
assert.deepEqual(result, mockData.blockHeaders)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getBlockheaders', () => {
|
||||
it('should throw 400 if height is not a number', async () => {
|
||||
req.params.height = 'Hello'
|
||||
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'height must be a positive number')
|
||||
})
|
||||
|
||||
it('should throw 400 if height is negative', async () => {
|
||||
req.params.height = -42
|
||||
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'height must be a positive number')
|
||||
})
|
||||
|
||||
it('should throw 400 if count is not a number', async () => {
|
||||
req.params.height = 42
|
||||
req.query.count = 'Hello'
|
||||
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'count must be a positive number')
|
||||
})
|
||||
|
||||
it('should throw 400 if count is negative', async () => {
|
||||
req.params.height = 42
|
||||
req.query.count = -10
|
||||
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'count must be a positive number')
|
||||
})
|
||||
|
||||
it('should throw 400 on array input', async () => {
|
||||
req.params.height = [42, 42]
|
||||
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'height must be a positive number')
|
||||
})
|
||||
|
||||
it('should return error object for invalid height', async () => {
|
||||
req.params.height = 1000000000
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute.electrumx,
|
||||
'request',
|
||||
new Error('Invalid height')
|
||||
)
|
||||
|
||||
// Call the details API.
|
||||
const result = await electrumxRoute.getBlockHeaders(req, res)
|
||||
|
||||
expectRouteError(res, result, 'Invalid height')
|
||||
})
|
||||
|
||||
it('should get headers for a single block height with count 2', async () => {
|
||||
req.params.height = 42
|
||||
req.query.count = 2
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute,
|
||||
'_blockHeadersFromElectrum',
|
||||
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 an error for an empty body', async () => {
|
||||
req.body = {}
|
||||
|
||||
const result = await electrumxRoute.blockHeadersBulk(req, res)
|
||||
|
||||
expectRouteError(res, result, 'heights needs to be an array')
|
||||
})
|
||||
|
||||
it('should error on non-array single height', async () => {
|
||||
req.body = {
|
||||
heights: 42
|
||||
}
|
||||
|
||||
const result = await electrumxRoute.blockHeadersBulk(req, res)
|
||||
|
||||
expectRouteError(res, result, 'heights needs to be an array')
|
||||
})
|
||||
|
||||
it('should NOT throw 400 error for an invalid height', async () => {
|
||||
req.body = {
|
||||
heights: [
|
||||
{ height: -10, count: 2 }
|
||||
]
|
||||
}
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute,
|
||||
'_blockHeadersFromElectrum',
|
||||
mockData.blockHeaders
|
||||
)
|
||||
|
||||
const result = await electrumxRoute.blockHeadersBulk(req, res)
|
||||
|
||||
// This should probably throw a 400 error, but to be consistent with the other
|
||||
// bulk endpoints it doesn't throw. This will change in the future
|
||||
// expectRouteError(res, result, 'Invalid tx hash')
|
||||
|
||||
assert.property(result, 'success')
|
||||
assert.equal(result.success, true)
|
||||
|
||||
assert.property(result, 'headers')
|
||||
assert.isArray(result.headers)
|
||||
})
|
||||
|
||||
it('should throw 429 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)
|
||||
|
||||
expectRouteError(res, result, 'Array too large', 429)
|
||||
})
|
||||
|
||||
it('should get details for a single height', async () => {
|
||||
req.body = {
|
||||
heights: [
|
||||
{ height: 42, count: 2 }
|
||||
]
|
||||
}
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute,
|
||||
'_blockHeadersFromElectrum',
|
||||
mockData.blockHeaders
|
||||
)
|
||||
|
||||
// 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')
|
||||
assert.isArray(result.headers[0].headers)
|
||||
})
|
||||
|
||||
it('should get details for multiple txids', async () => {
|
||||
req.body = {
|
||||
heights: [
|
||||
{ height: 42, count: 2 },
|
||||
{ height: 42, count: 2 }
|
||||
]
|
||||
}
|
||||
|
||||
stubMethodForUnitTests(
|
||||
electrumxRoute,
|
||||
'_blockHeadersFromElectrum',
|
||||
mockData.blockHeaders
|
||||
)
|
||||
|
||||
// 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.isArray(result.headers)
|
||||
assert.isArray(result.headers[0].headers)
|
||||
assert.equal(result.headers.length, 2, '2 outputs for 2 inputs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_balanceFromElectrumx', () => {
|
||||
it('should throw error for invalid address', async () => {
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,9 @@ if (!process.env.SLPDB_URL) {
|
||||
const SLP = require('../../../src/routes/v3/slp')
|
||||
const slp = new SLP()
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
||||
|
||||
describe('#slp', () => {
|
||||
@@ -102,7 +105,7 @@ describe('#slp', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateSendOpReturn', () => {
|
||||
describe('#generateSendOpReturn', () => {
|
||||
it('should return OP_RETURN script', async () => {
|
||||
req.body.tokenUtxos = [
|
||||
{
|
||||
@@ -121,4 +124,93 @@ describe('#slp', () => {
|
||||
assert.isNumber(result.outputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxos', () => {
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slp.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
|
||||
it('should process data directly from Electrumx', async () => {
|
||||
const addrs = [
|
||||
'bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg',
|
||||
'bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89',
|
||||
'bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk'
|
||||
]
|
||||
|
||||
const utxos = await bchjs.Electrumx.utxo(addrs)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
req.body.utxos = utxos.utxos
|
||||
const result = await slp.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 3)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
||||
assert.equal(result.slpUtxos[1].utxos.length, 1)
|
||||
assert.equal(result.slpUtxos[2].utxos.length, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,10 +107,16 @@ const txDetails = {
|
||||
]
|
||||
}
|
||||
|
||||
const blockHeaders = [
|
||||
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
|
||||
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
utxos,
|
||||
balance,
|
||||
txHistory,
|
||||
mempool,
|
||||
txDetails
|
||||
txDetails,
|
||||
blockHeaders
|
||||
}
|
||||
|
||||
+147
@@ -888,6 +888,153 @@ describe('#SLP', () => {
|
||||
assert.isNumber(result.outputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxos', () => {
|
||||
it('should throw error if input is not an array.', async () => {
|
||||
req.body.utxos = 'test'
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
// console.log(`result: `, result)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Input must be an array')
|
||||
})
|
||||
|
||||
it('should throw error if Array is empty', async () => {
|
||||
req.body.utxos = []
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array should not be empty')
|
||||
})
|
||||
|
||||
it('should throw error if Array is too long', async () => {
|
||||
const utxo = {
|
||||
txid:
|
||||
'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90',
|
||||
vout: 3,
|
||||
amount: 0.00002015,
|
||||
satoshis: 2015,
|
||||
height: 594892,
|
||||
confirmations: 5
|
||||
}
|
||||
|
||||
const utxos = []
|
||||
|
||||
// Populate array with 21 utxos
|
||||
for (let i = 0; i < 21; i++) {
|
||||
utxos.push(utxo)
|
||||
}
|
||||
|
||||
req.body.utxos = utxos
|
||||
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array too long, max length is 20')
|
||||
})
|
||||
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Mock the external network call.
|
||||
sandbox.stub(slpRoute.bchjs.SLP.Utils, 'tokenUtxoDetails').resolves([
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
|
||||
tokenTicker: 'TAP',
|
||||
tokenName: 'Thoughts and Prayers',
|
||||
tokenDocumentUrl: '',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 0,
|
||||
tokenType: 1,
|
||||
tokenQty: 5,
|
||||
isValid: true
|
||||
}
|
||||
])
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.hydrateUtxos(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user