feat(validate3Bulk): Adding bulk SLP tx validtion for whitelist endpoint

This commit is contained in:
Chris Troutner
2020-12-05 09:48:05 -08:00
parent d93d20321d
commit f2c0cd6feb
4 changed files with 268 additions and 2 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
"lint": "standard --env mocha --fix",
"test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/",
"test:temp:integration": "export NETWORK=mainnet && export TEST=integration && mocha --timeout 25000 test/v3/integration/price.js",
"test:temp:unit": "export NETWORK=mainnet && mocha -g '#validateSingle' --exit test/v3/",
"test:temp:unit": "export NETWORK=mainnet && mocha -g '#validate3Bulk' --exit test/v3/",
"test:integration": "mocha test/v3/integration",
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v3/integration/slp*.js",
"coverage": "nyc report --reporter=text-lcov | coveralls",
+124
View File
@@ -75,6 +75,7 @@ class Slp {
_this.router.get('/validateTxid/:txid', _this.validateSingle)
_this.router.get('/validateTxid2/:txid', _this.validate2Single)
_this.router.get('/validateTxid3/:txid', _this.validate3Single)
_this.router.post('/validateTxid3', _this.validate3Bulk)
_this.router.get('/whitelist', _this.getSlpWhitelist)
_this.router.get('/txDetails/:txid', _this.txDetails)
_this.router.get('/tokenStats/:tokenId', _this.tokenStats)
@@ -1340,6 +1341,129 @@ class Slp {
}
}
/**
* @api {post} /slp/validateTxid3/ Validate multiple SLP transactions by txid.
* @apiName Validate multiple SLP transactions by txid.
* @apiGroup SLP
* @apiDescription Alternative validation for tokens on the whitelist
* This endpoint is exactly the same as /slp/validateTxid but it uses
* a different SLPDB. This server only indexes the SLP tokens that are on the
* whitelist. You can see which tokens are on the whitelist by calling the
* /slp/whitelist endpoint.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/validateTxid3" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}'
*
*
*/
async validate3Bulk (req, res, next) {
try {
const txids = req.body.txids
// Reject if txids 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 (!_this.routeUtils.validateArraySize(req, txids)) {
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: 'Array too large.'
})
}
wlogger.debug('Executing slp/validate with these txids: ', txids)
const query = {
v: 3,
q: {
db: ['c', 'u'],
find: {
'tx.h': { $in: txids }
},
limit: 300,
project: { 'slp.valid': 1, 'tx.h': 1, 'slp.invalidReason': 1 }
}
}
const s = JSON.stringify(query)
const b64 = Buffer.from(s).toString('base64')
const url = `${process.env.SLPDB_WHITELIST_URL}q/${b64}`
const options = _this.generateCredentials()
// Get data from SLPDB.
const opt = {
method: 'get',
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
const tokenRes = await _this.axios.request(opt)
// console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
let formattedTokens = []
// Combine the arrays. Why? Generally there is nothing in the u array.
const concatArray = tokenRes.data.c.concat(tokenRes.data.u)
const tokenIds = []
if (concatArray.length > 0) {
concatArray.forEach((token) => {
tokenIds.push(token.tx.h) // txid
const validationResult = {
txid: token.tx.h,
valid: token.slp.valid
}
// If the txid is invalid, add the reason it's invalid.
if (!validationResult.valid) {
validationResult.invalidReason = token.slp.invalidReason
}
formattedTokens.push(validationResult)
})
// If a user-provided txid doesn't exist in the data, add it with
// valid:false property.
txids.forEach((txid) => {
if (!tokenIds.includes(txid)) {
formattedTokens.push({
txid: txid,
valid: false
})
}
})
}
// Catch a corner case of repeated txids. SLPDB will remove redundent TXIDs,
// which will cause the output array to be smaller than the input array.
if (txids.length > formattedTokens.length) {
const newOutput = []
for (let i = 0; i < txids.length; i++) {
const thisTxid = txids[i]
// Find the element that matches the current txid.
const elem = formattedTokens.filter((x) => x.txid === thisTxid)
newOutput.push(elem[0])
}
// Replace the original output object with the new output object.
formattedTokens = newOutput
}
res.status(200)
return res.json(formattedTokens)
} catch (err) {
wlogger.error('Error in slp.ts/validateBulk().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /slp/txDetails/{txid} SLP transaction details.
* @apiName SLP transaction details.
+18 -1
View File
@@ -341,6 +341,22 @@ const mockTwoRedundentTxid = {
u: []
}
const mockPsfToken = {
c: [
{
_id: '5fcaf6152898f9887902986c',
tx: {
h: 'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc'
},
slp: {
valid: true,
invalidReason: null
}
}
],
u: []
}
const mockTxHistory = [
{
tx: {
@@ -541,5 +557,6 @@ module.exports = {
mockSingleValidTxid,
mockTwoValidTxid,
mockTwoRedundentTxid,
mockTxHistory
mockTxHistory,
mockPsfToken
}
+125
View File
@@ -687,6 +687,131 @@ describe('#SLP', () => {
})
})
describe('#validate3Bulk', () => {
const validate3Bulk = slpRoute.validate3Bulk
it('should throw 400 if txid array is empty', async () => {
const result = await validate3Bulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'txids needs to be an array')
assert.equal(res.statusCode, 400)
})
it('should throw 400 error if array is too large', async () => {
const testArray = []
for (var i = 0; i < 25; i++) testArray.push('')
req.body.txids = testArray
const result = await validate3Bulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
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(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' })
req.body.txids = [
'77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
]
const result = await validate3Bulk(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(slpRoute.axios, 'request').throws({ code: 'ECONNREFUSED' })
req.body.txids = [
'77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
]
const result = await validate3Bulk(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 validate array with single element', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute.axios, 'request').resolves({
data: mockData.mockPsfToken
})
}
req.body.txids = [
'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc'
]
const result = await validate3Bulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result)
assert.hasAllKeys(result[0], ['txid', 'valid'])
})
it('should validate array with two elements', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute.axios, 'request').resolves({
data: mockData.mockPsfToken
})
}
req.body.txids = [
'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc',
'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc'
]
const result = await validate3Bulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result)
assert.hasAllKeys(result[0], ['txid', 'valid'])
assert.equal(result.length, 2)
})
// Captures a regression bug that went out to production, captured in this
// GitHub Issue: https://github.com/Bitcoin-com/rest.bitcoin.com/issues/518
it('should return two elements if given two elements', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute.axios, 'request').resolves({
data: mockData.mockPsfToken
})
}
req.body.txids = [
'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc',
'cdfed769ef7b69e09be06d2821b88598d9a5d711b8f9bd369763c78b7a578fbc'
]
const result = await validate3Bulk(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result)
assert.hasAllKeys(result[0], ['txid', 'valid'])
assert.equal(result.length, 2)
})
})
describe('tokenStats()', () => {
it('should throw 400 if tokenID is empty', async () => {
req.params.tokenId = ''