diff --git a/package.json b/package.json index 6a54a98..cd45400 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/slp.js", "test:integration": "mocha test/v3/integration", + "test:integration:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/", "docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs" diff --git a/src/routes/v3/services/slpdb.js b/src/routes/v3/services/slpdb.js index b3e6e7e..9690e4e 100644 --- a/src/routes/v3/services/slpdb.js +++ b/src/routes/v3/services/slpdb.js @@ -75,16 +75,24 @@ class Slpdb { } async getTokenStats (tokenId) { - const [totalMinted, totalBurned, tokenDetails] = await Promise.all([ + const [ + totalMinted, + totalBurned, + tokenDetails, + circulatingSupply + ] = await Promise.all([ this.getTotalMinted(tokenId), this.getTotalBurned(tokenId), - this.getTokenDetails(tokenId) + this.getTokenDetails(tokenId), + this.getTotalCirculating(tokenId) ]) tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted tokenDetails.totalBurned = totalBurned - tokenDetails.circulatingSupply = - tokenDetails.totalMinted - tokenDetails.totalBurned + + // tokenDetails.circulatingSupply = + // tokenDetails.totalMinted - tokenDetails.totalBurned + tokenDetails.circulatingSupply = circulatingSupply return tokenDetails } @@ -164,6 +172,53 @@ class Slpdb { return parseFloat(result.data.g[0].count) } + async getTotalCirculating (tokenId) { + const query = { + v: 3, + q: { + db: ['g'], + aggregate: [ + { + $match: { + 'tokenDetails.tokenIdHex': tokenId, + 'graphTxn.outputs': { + $elemMatch: { + status: 'UNSPENT', + slpAmount: { $gte: 0 } + } + } + } + }, + { $unwind: '$graphTxn.outputs' }, + { + $match: { + 'graphTxn.outputs.status': 'UNSPENT', + 'graphTxn.outputs.slpAmount': { $gte: 0 } + } + }, + { + $group: { + _id: null, + circulating_supply: { + $sum: '$graphTxn.outputs.slpAmount' + } + } + } + ], + limit: 100000 + } + } + + const result = await this.runQuery(query) + // console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`) + + if (!result.data.g.length) { + return 0 + } + + return parseFloat(result.data.g[0].circulating_supply) + } + async getTotalBurned (tokenId) { const query = { v: 3, @@ -255,6 +310,8 @@ class Slpdb { } formatTokenOutput (token) { + // console.log(`token: ${JSON.stringify(token, null, 2)}`) + token.tokenDetails.id = token.tokenDetails.tokenIdHex delete token.tokenDetails.tokenIdHex token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex @@ -286,6 +343,7 @@ class Slpdb { token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix delete token.tokenDetails.timestamp_unix + return token.tokenDetails } } diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 4a4d772..d7844f2 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -1284,7 +1284,9 @@ class Slp { txid: concatArray[0].tx.h, valid: concatArray[0].slp.valid } - if (!result.valid) { result.invalidReason = concatArray[0].slp.invalidReason } + if (!result.valid) { + result.invalidReason = concatArray[0].slp.invalidReason + } } res.status(200) @@ -1405,55 +1407,17 @@ class Slp { * */ async tokenStats (req, res, next) { - const tokenId = req.params.tokenId - if (!tokenId || tokenId === '') { - res.status(400) - return res.json({ error: 'tokenId can not be empty' }) - } - try { - const query = { - v: 3, - q: { - db: ['t'], - find: { - $query: { - 'tokenDetails.tokenIdHex': tokenId - } - }, - project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, - limit: 10 - } + const tokenId = req.params.tokenId + if (!tokenId || tokenId === '') { + res.status(400) + return res.json({ error: 'tokenId can not be empty' }) } - 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, - headers: options.headers, - timeout: options.timeout - } - - // Get data from BitDB. - const tokenRes = await _this.axios.request(opt) - - const formattedTokens = [] - - if (tokenRes.data.t.length) { - tokenRes.data.t.forEach((token) => { - token = _this.formatTokenOutput(token) - formattedTokens.push(token.tokenDetails) - }) - } + const tokenStats = await _this.slpdb.getTokenStats(tokenId) res.status(200) - return res.json(formattedTokens[0]) + return res.json(tokenStats) } catch (err) { wlogger.error('Error in slp.ts/tokenStats().', err) return _this.errorHandler(err, res) @@ -1617,7 +1581,8 @@ class Slp { if (!networkIsValid) { res.status(400) return res.json({ - error: 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + error: + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } diff --git a/test/v3/integration/slp.js b/test/v3/integration/slp.js new file mode 100644 index 0000000..425a22d --- /dev/null +++ b/test/v3/integration/slp.js @@ -0,0 +1,102 @@ +/* + These integration tests need to be run against a live SLPDB. They query + against a live SLPDB and test the results against known token stats. + */ + +'use strict' + +const assert = require('chai').assert +// const axios = require('axios') + +// Used for debugging. +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +// Exit if SLPDB URL is not defined. +if (!process.env.SLPDB_URL) { + throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.') +} + +const SLP = require('../../../src/routes/v3/slp') +const slp = new SLP() + +const { mockReq, mockRes } = require('../mocks/express-mocks') + +describe('#slp', () => { + let req, res + + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + }) + + describe('#tokenStats', () => { + it('should get token stats for token with no mint baton', async () => { + req.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + // 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + + const result = await slp.tokenStats(req, res) + console.log(`result: ${util.inspect(result)}`) + + // Assert that expected properties exist. + assert.property(result, 'decimals') + assert.property(result, 'timestamp') + assert.property(result, 'versionType') + assert.property(result, 'documentUri') + assert.property(result, 'symbol') + assert.property(result, 'name') + assert.property(result, 'containsBaton') + assert.property(result, 'id') + assert.property(result, 'documentHash') + assert.property(result, 'initialTokenQty') + assert.property(result, 'blockCreated') + assert.property(result, 'blockLastActiveSend') + assert.property(result, 'blockLastActiveMint') + assert.property(result, 'txnsSinceGenesis') + assert.property(result, 'validAddresses') + assert.property(result, 'mintingBatonStatus') + assert.property(result, 'timestampUnix') + assert.property(result, 'totalMinted') + assert.property(result, 'totalBurned') + assert.property(result, 'circulatingSupply') + + // baton was never created. + assert.equal(result.containsBaton, false) + }) + + it('should get token stats for token with a mint baton', async () => { + req.params.tokenId = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + + const result = await slp.tokenStats(req, res) + console.log(`result: ${util.inspect(result)}`) + + // Assert that expected properties exist. + assert.property(result, 'decimals') + assert.property(result, 'timestamp') + assert.property(result, 'versionType') + assert.property(result, 'documentUri') + assert.property(result, 'symbol') + assert.property(result, 'name') + assert.property(result, 'containsBaton') + assert.property(result, 'id') + assert.property(result, 'documentHash') + assert.property(result, 'initialTokenQty') + assert.property(result, 'blockCreated') + assert.property(result, 'blockLastActiveSend') + assert.property(result, 'blockLastActiveMint') + assert.property(result, 'txnsSinceGenesis') + assert.property(result, 'validAddresses') + assert.property(result, 'mintingBatonStatus') + assert.property(result, 'timestampUnix') + assert.property(result, 'totalMinted') + assert.property(result, 'totalBurned') + assert.property(result, 'circulatingSupply') + + // baton was created. + assert.equal(result.containsBaton, true) + }) + }) +}) diff --git a/test/v3/integration/slpdb.js b/test/v3/integration/slpdb.js new file mode 100644 index 0000000..940d092 --- /dev/null +++ b/test/v3/integration/slpdb.js @@ -0,0 +1,31 @@ +/* + These integration tests need to be run against a live SLPDB. They query + against a live SLPDB and test the results against known token stats. + */ + +'use strict' + +// const chai = require('chai') +// const assert = chai.assert +// const axios = require('axios') + +// Used for debugging. +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +// Exit if SLPDB URL is not defined. +if (!process.env.SLPDB_URL) { + throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.') +} + +const SLPDB = require('../../../src/routes/v3/services/slpdb') +const slpdb = new SLPDB() + +describe('#slpdb', () => { + describe('#getTotalCirculating', () => { + it('should get circulating supply', async () => { + const result = await slpdb.getTotalCirculating('b10677aef051b73e6b170c1c0824da33a3e0680ab5a01cd8d76aa77840fccfb4') + console.log('result: ', result) + }) + }) +}) diff --git a/test/v3/slp.js b/test/v3/slp.js index ef328f3..de61db6 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -463,11 +463,9 @@ describe('#SLP', () => { }) describe('tokenStats()', () => { - const tokenStats = slpRoute.tokenStats - it('should throw 400 if tokenID is empty', async () => { req.params.tokenId = '' - const result = await tokenStats(req, res) + const result = await slpRoute.tokenStats(req, res) // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ['error']) @@ -476,12 +474,12 @@ describe('#SLP', () => { it('returns proper error when downstream service stalls', async () => { // Mock the timeout error. - sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) + sandbox.stub(slpRoute.slpdb, 'getTokenStats').throws({ code: 'ECONNABORTED' }) req.params.tokenId = '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - const result = await tokenStats(req, res) + const result = await slpRoute.tokenStats(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') @@ -494,12 +492,12 @@ describe('#SLP', () => { it('returns proper error when downstream service is down', async () => { // Mock the timeout error. - sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNREFUSED' }) + sandbox.stub(slpRoute.slpdb, 'getTokenStats').throws({ code: 'ECONNREFUSED' }) req.params.tokenId = '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - const result = await tokenStats(req, res) + const result = await slpRoute.tokenStats(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') @@ -509,50 +507,6 @@ 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') { - sandbox.stub(slpRoute.axios, 'request').resolves({ - data: { - t: [ - { - tokenDetails: mockData.mockTokenDetails, - tokenStats: mockData.mockTokenStats - } - ] - } - }) - } - - req.params.tokenId = - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - - const result = await tokenStats(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAnyKeys(result, [ - 'blockCreated', - 'blockLastActiveMint', - 'blockLastActiveSend', - 'containsBaton', - 'initialTokenQty', - 'mintingBatonStatus', - 'circulatingSupply', - 'decimals', - 'documentHash', - 'versionType', - 'timestamp', - 'documentUri', - 'name', - 'symbol', - 'id', - 'totalBurned', - 'totalMinted', - 'txnsSinceGenesis', - 'validAddresses' - ]) - }) }) describe('balancesForTokenSingle()', () => {