From cc17916022ea0cb27680d3afb65ae0d9dc7cd291 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 11 May 2020 21:13:48 -0700 Subject: [PATCH] fix(tokenStats): Added generateCredentials() to tokenStats SLP method --- src/routes/v3/services/slpdb.js | 286 ++++++++++++++++++++++++++++++++ src/routes/v3/slp.js | 13 +- test/v3/slp.js | 15 +- 3 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 src/routes/v3/services/slpdb.js diff --git a/src/routes/v3/services/slpdb.js b/src/routes/v3/services/slpdb.js new file mode 100644 index 0000000..b21596f --- /dev/null +++ b/src/routes/v3/services/slpdb.js @@ -0,0 +1,286 @@ +const axios = require('axios') + +const SLPSDK = require('@chris.troutner/bch-js') +const SLP = new SLPSDK() + +class Slpdb { + async getHistoricalSlpTransactions (addressList, fromBlock) { + // Build SLPDB or query from addressList + const orQueryArray = [] + for (const address of addressList) { + const cashAddress = SLP.Address.toCashAddress(address) + const slpAddress = SLP.Address.toSLPAddress(address) + + const cashQuery = { + 'in.e.a': cashAddress.slice(12) + } + const slpQuery = { + 'slp.detail.outputs.address': slpAddress + } + + orQueryArray.push(cashQuery) + orQueryArray.push(slpQuery) + } + + const query = { + v: 3, + q: { + find: { + db: ['c', 'u'], + $query: { + $or: orQueryArray, + 'slp.valid': true, + 'blk.i': { + $not: { + $lte: fromBlock + } + } + }, + $orderby: { + 'blk.i': -1 + } + }, + project: { + _id: 0, + 'tx.h': 1, + 'in.i': 1, + 'in.e': 1, + 'out.e': 1, + 'out.a': 1, + 'slp.detail': 1, + blk: 1 + }, + limit: 500 + } + } + + const result = await this.runQuery(query) + + let transactions = [] + if (result.data && result.data.c) { + transactions = transactions.concat(result.data.c) + } + if (result.data && result.data.u) { + transactions = transactions.concat(result.data.u) + } + + return transactions + } + + async getTokenStats (tokenId) { + const [totalMinted, totalBurned, tokenDetails] = await Promise.all([ + this.getTotalMinted(tokenId), + this.getTotalBurned(tokenId), + this.getTokenDetails(tokenId) + ]) + + tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted + tokenDetails.totalBurned = totalBurned + tokenDetails.circulatingSupply = + tokenDetails.totalMinted - tokenDetails.totalBurned + + return tokenDetails + } + + generateCredentials () { + // Generate the Basic Authentication header for a private instance of SLPDB. + const SLPDB_PASS = process.env.SLPDB_PASS + ? process.env.SLPDB_PASS + : 'BITBOX' + const username = 'BITBOX' + const password = SLPDB_PASS + const combined = `${username}:${password}` + var base64Credential = Buffer.from(combined).toString('base64') + var readyCredential = `Basic ${base64Credential}` + + const options = { + headers: { + authorization: readyCredential, + timeout: 30000 + } + } + + return options + } + + async runQuery (query) { + const queryString = JSON.stringify(query) + const queryBase64 = Buffer.from(queryString).toString('base64') + const url = `${process.env.SLPDB_URL}q/${queryBase64}` + + const options = this.generateCredentials() + + const response = await axios.get(url, options) + return response + } + + async getTotalMinted (tokenId) { + const query = { + v: 3, + q: { + db: ['g'], + aggregate: [ + { + $match: { + 'tokenDetails.tokenIdHex': tokenId, + 'graphTxn.outputs.status': { + $in: [ + 'BATON_SPENT_IN_MINT', + 'BATON_UNSPENT', + 'BATON_SPENT_NOT_IN_MINT' + ] + } + } + }, + { + $unwind: '$graphTxn.outputs' + }, + { + $group: { + _id: null, + count: { + $sum: '$graphTxn.outputs.slpAmount' + } + } + } + ], + limit: 1 + } + } + + const result = await this.runQuery(query) + + if (!result.data.g.length) { + return 0 + } + + return parseFloat(result.data.g[0].count) + } + + async getTotalBurned (tokenId) { + const query = { + v: 3, + q: { + db: ['g'], + aggregate: [ + { + $match: { + 'tokenDetails.tokenIdHex': tokenId, + 'graphTxn.outputs.status': { + $in: [ + 'SPENT_NON_SLP', + 'BATON_SPENT_INVALID_SLP', + 'SPENT_INVALID_SLP', + 'BATON_SPENT_NON_SLP', + 'MISSING_BCH_VOUT', + 'BATON_MISSING_BCH_VOUT', + 'BATON_SPENT_NOT_IN_MINT', + 'EXCESS_INPUT_BURNED' + ] + } + } + }, + { + $unwind: '$graphTxn.outputs' + }, + { + $match: { + 'graphTxn.outputs.status': { + $in: [ + 'SPENT_NON_SLP', + 'BATON_SPENT_INVALID_SLP', + 'SPENT_INVALID_SLP', + 'BATON_SPENT_NON_SLP', + 'MISSING_BCH_VOUT', + 'BATON_MISSING_BCH_VOUT', + 'BATON_SPENT_NOT_IN_MINT', + 'EXCESS_INPUT_BURNED' + ] + } + } + }, + { + $group: { + _id: null, + count: { + $sum: '$graphTxn.outputs.slpAmount' + } + } + } + ], + limit: 1 + } + } + + const result = await this.runQuery(query) + + if (!result.data.g.length) { + return 0 + } + + return parseFloat(result.data.g[0].count) + } + + async getTokenDetails (tokenId) { + const query = { + v: 3, + q: { + db: ['t'], + find: { + $query: { + 'tokenDetails.tokenIdHex': tokenId + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + limit: 1 + } + } + + const result = await this.runQuery(query) + + if (!result.data.t.length) { + throw new Error('Token could not be found') + } + + const token = this.formatTokenOutput(result.data.t[0]) + + return token + } + + formatTokenOutput (token) { + token.tokenDetails.id = token.tokenDetails.tokenIdHex + delete token.tokenDetails.tokenIdHex + token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex + delete token.tokenDetails.documentSha256Hex + token.tokenDetails.initialTokenQty = parseFloat( + token.tokenDetails.genesisOrMintQuantity + ) + delete token.tokenDetails.genesisOrMintQuantity + delete token.tokenDetails.transactionType + delete token.tokenDetails.batonVout + delete token.tokenDetails.sendOutputs + + token.tokenDetails.blockCreated = token.tokenStats.block_created + token.tokenDetails.blockLastActiveSend = + token.tokenStats.block_last_active_send + token.tokenDetails.blockLastActiveMint = + token.tokenStats.block_last_active_mint + token.tokenDetails.txnsSinceGenesis = + token.tokenStats.qty_valid_txns_since_genesis + token.tokenDetails.validAddresses = + token.tokenStats.qty_valid_token_addresses + token.tokenDetails.mintingBatonStatus = + token.tokenStats.minting_baton_status + + delete token.tokenStats.block_last_active_send + delete token.tokenStats.block_last_active_mint + delete token.tokenStats.qty_valid_txns_since_genesis + delete token.tokenStats.qty_valid_token_addresses + + token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix + delete token.tokenDetails.timestamp_unix + return token.tokenDetails + } +} + +module.exports = Slpdb diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 3388274..1e665c5 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -7,6 +7,9 @@ const BigNumber = require('bignumber.js') const RouteUtils = require('../../util/route-utils') const routeUtils = new RouteUtils() + +const Slpdb = require('./services/slpdb') + // const strftime = require('strftime') const wlogger = require('../../util/winston-logging') @@ -50,6 +53,7 @@ class Slp { _this.BigNumber = BigNumber _this.bchjs = bchjs _this.rawTransactions = rawTransactions + _this.slpdb = new Slpdb() _this.router = router @@ -1410,10 +1414,17 @@ class Slp { const s = JSON.stringify(query) const b64 = Buffer.from(s).toString('base64') const url = `${process.env.SLPDB_URL}q/${b64}` + + const options = _this.generateCredentials() + + // Request options const opt = { method: 'get', - baseURL: url + baseURL: url, + headers: options.headers, + timeout: options.timeout } + // Get data from BitDB. const tokenRes = await _this.axios.request(opt) diff --git a/test/v3/slp.js b/test/v3/slp.js index 4109954..ea4d3db 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -461,17 +461,18 @@ describe('#SLP', () => { }) }) - describe('tokenStatsSingle()', () => { - const tokenStatsSingle = slpRoute.tokenStats + describe('tokenStats()', () => { + const tokenStats = slpRoute.tokenStats it('should throw 400 if tokenID is empty', async () => { req.params.tokenId = '' - const result = await tokenStatsSingle(req, res) + const result = await tokenStats(req, res) // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ['error']) assert.include(result.error, 'tokenId can not be empty') }) + it('returns proper error when downstream service stalls', async () => { // Mock the timeout error. sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) @@ -479,7 +480,7 @@ describe('#SLP', () => { req.params.tokenId = '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - const result = await tokenStatsSingle(req, res) + const result = await tokenStats(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') @@ -497,7 +498,7 @@ describe('#SLP', () => { req.params.tokenId = '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - const result = await tokenStatsSingle(req, res) + const result = await tokenStats(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') @@ -507,7 +508,7 @@ describe('#SLP', () => { 'Error message expected' ) }) - // + it('should get token stats for tokenId', async () => { // Mock the RPC call for unit tests. if (process.env.TEST === 'unit') { @@ -526,7 +527,7 @@ describe('#SLP', () => { req.params.tokenId = '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - const result = await tokenStatsSingle(req, res) + const result = await tokenStats(req, res) // console.log(`result: ${util.inspect(result)}`) assert.hasAnyKeys(result, [