From cc17916022ea0cb27680d3afb65ae0d9dc7cd291 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 11 May 2020 21:13:48 -0700 Subject: [PATCH 1/4] 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, [ From dec6aed6389ed7d5238ccd0f79e936614c4946ed Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 12 May 2020 09:33:29 -0700 Subject: [PATCH 2/4] Pausing work on txsByAddressSingle() --- package.json | 2 +- src/routes/v3/services/slpdb.js | 13 +- src/routes/v3/slp.js | 440 ++++++++++++++++++-------------- test/v3/slp.js | 68 ++++- 4 files changed, 332 insertions(+), 191 deletions(-) diff --git a/package.json b/package.json index c111a19..6a54a98 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "npm run lint && npm run test-v3", "lint": "standard --env mocha --fix", "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", - "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/encryption.js", + "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/slp.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/", diff --git a/src/routes/v3/services/slpdb.js b/src/routes/v3/services/slpdb.js index b21596f..b3e6e7e 100644 --- a/src/routes/v3/services/slpdb.js +++ b/src/routes/v3/services/slpdb.js @@ -4,12 +4,14 @@ const SLPSDK = require('@chris.troutner/bch-js') const SLP = new SLPSDK() class Slpdb { - async getHistoricalSlpTransactions (addressList, fromBlock) { + // Gets transaction history for all tokens for an address. Can also specify + // block height, but defaults to 0. + async getHistoricalSlpTransactions (addressList, fromBlock = 0) { // 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 cashAddress = SLP.SLP.Address.toCashAddress(address) + const slpAddress = SLP.SLP.Address.toSLPAddress(address) const cashQuery = { 'in.e.a': cashAddress.slice(12) @@ -55,11 +57,16 @@ class Slpdb { } const result = await this.runQuery(query) + // console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`) let transactions = [] + + // Add confirmed transactions if (result.data && result.data.c) { transactions = transactions.concat(result.data.c) } + + // Add unconfirmed transactions if (result.data && result.data.u) { transactions = transactions.concat(result.data.u) } diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 1e665c5..293566a 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -64,14 +64,24 @@ class Slp { _this.router.get('/balancesForAddress/:address', _this.balancesForAddress) _this.router.post('/balancesForAddress', _this.balancesForAddressBulk) _this.router.get('/balancesForToken/:tokenId', _this.balancesForTokenSingle) - _this.router.get('/balance/:address/:tokenId', _this.balancesForAddressByTokenID) + _this.router.get( + '/balance/:address/:tokenId', + _this.balancesForAddressByTokenID + ) _this.router.get('/convert/:address', _this.convertAddressSingle) _this.router.post('/convert', _this.convertAddressBulk) _this.router.post('/validateTxid', _this.validateBulk) _this.router.get('/validateTxid/:txid', _this.validateSingle) _this.router.get('/txDetails/:txid', _this.txDetails) _this.router.get('/tokenStats/:tokenId', _this.tokenStats) - _this.router.get('/transactions/:tokenId/:address', _this.txsTokenIdAddressSingle) + _this.router.get( + '/transactions/:tokenId/:address', + _this.txsTokenIdAddressSingle + ) + _this.router.get( + '/transactionHistoryAllTokens/:address', + _this.txsByAddressSingle + ) } // DRY error handler. @@ -102,18 +112,24 @@ class Slp { token.tokenDetails.blockCreated = token.tokenStats.block_created token.tokenDetails.blockLastActiveSend = - token.tokenStats.block_last_active_send + token.tokenStats.block_last_active_send token.tokenDetails.blockLastActiveMint = - token.tokenStats.block_last_active_mint + 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.totalMinted = parseFloat(token.tokenStats.qty_token_minted) - token.tokenDetails.totalBurned = parseFloat(token.tokenStats.qty_token_burned) + token.tokenStats.qty_valid_txns_since_genesis + token.tokenDetails.validAddresses = + token.tokenStats.qty_valid_token_addresses + token.tokenDetails.totalMinted = parseFloat( + token.tokenStats.qty_token_minted + ) + token.tokenDetails.totalBurned = parseFloat( + token.tokenStats.qty_token_burned + ) token.tokenDetails.circulatingSupply = parseFloat( token.tokenStats.qty_token_circulating_supply ) - token.tokenDetails.mintingBatonStatus = token.tokenStats.minting_baton_status + token.tokenDetails.mintingBatonStatus = + token.tokenStats.minting_baton_status delete token.tokenStats.block_last_active_send delete token.tokenStats.block_last_active_mint @@ -127,17 +143,17 @@ class Slp { } /** - * @api {get} /slp/list/{tokenId} List single SLP token by id. - * @apiName List single SLP token by id. - * @apiGroup SLP - * @apiDescription Returns the list single SLP token by id. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json" - * - * - */ + * @api {get} /slp/list/{tokenId} List single SLP token by id. + * @apiName List single SLP token by id. + * @apiGroup SLP + * @apiDescription Returns the list single SLP token by id. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json" + * + * + */ async listSingleToken (req, res, next) { try { const tokenId = req.params.tokenId @@ -160,17 +176,17 @@ class Slp { } /** - * @api {post} /slp/list/ List Bulk SLP token . - * @apiName List Bulk SLP token. - * @apiGroup SLP - * @apiDescription Returns the list bulk SLP token by id. - * - * - * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v3/slp/list" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenIds":["7380843cd1089a1a01783f86af37734dc99667a1cdc577391b5f6ea42fc1bfb4","9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0"]}' - * - * - */ + * @api {post} /slp/list/ List Bulk SLP token . + * @apiName List Bulk SLP token. + * @apiGroup SLP + * @apiDescription Returns the list bulk SLP token by id. + * + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/slp/list" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenIds":["7380843cd1089a1a01783f86af37734dc99667a1cdc577391b5f6ea42fc1bfb4","9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0"]}' + * + * + */ async listBulkToken (req, res, next) { try { const tokenIds = req.body.tokenIds @@ -220,14 +236,14 @@ class Slp { const txids = [] if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { + tokenRes.data.t.forEach((token) => { txids.push(token.tokenDetails.tokenIdHex) token = _this.formatTokenOutput(token) formattedTokens.push(token.tokenDetails) }) } - tokenIds.forEach(tokenId => { + tokenIds.forEach((tokenId) => { if (!txids.includes(tokenId)) { formattedTokens.push({ id: tokenId, @@ -281,14 +297,14 @@ class Slp { const formattedTokens = [] if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { + tokenRes.data.t.forEach((token) => { token = _this.formatTokenOutput(token) formattedTokens.push(token.tokenDetails) }) } let t - formattedTokens.forEach(token => { + formattedTokens.forEach((token) => { if (token.id === tokenId) t = token }) @@ -308,17 +324,17 @@ class Slp { } /** - * @api {get} /slp/balancesForAddress/{address} List SLP balance for address. - * @apiName List SLP balance for address. - * @apiGroup SLP - * @apiDescription Returns List SLP balance for address. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/balancesForAddress/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" - * - * - */ + * @api {get} /slp/balancesForAddress/{address} List SLP balance for address. + * @apiName List SLP balance for address. + * @apiGroup SLP + * @apiDescription Returns List SLP balance for address. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/balancesForAddress/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" + * + * + */ // Retrieve token balances for all tokens for a single address. async balancesForAddress (req, res, next) { try { @@ -346,7 +362,7 @@ class Slp { res.status(400) return res.json({ error: - 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } @@ -420,7 +436,7 @@ class Slp { const tokenIds = [] if (tokenRes.data.g.length > 0) { - tokenRes.data.g = tokenRes.data.g.map(token => { + tokenRes.data.g = tokenRes.data.g.map((token) => { token.tokenId = token._id tokenIds.push(token.tokenId) token.balance = parseFloat(token.balanceString) @@ -430,7 +446,7 @@ class Slp { return token }) - const promises = tokenIds.map(async tokenId => { + const promises = tokenIds.map(async (tokenId) => { const query2 = { v: 3, q: { @@ -458,7 +474,6 @@ class Slp { baseURL: url2, headers: options.headers, timeout: options.timeout - } const tokenRes2 = await _this.axios.request(opt) // console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`) @@ -468,8 +483,8 @@ class Slp { const details = await _this.axios.all(promises) - tokenRes.data.g = tokenRes.data.g.map(token => { - details.forEach(detail => { + tokenRes.data.g = tokenRes.data.g.map((token) => { + details.forEach((detail) => { if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) { token.decimalCount = detail.t[0].tokenDetails.decimals } @@ -493,17 +508,17 @@ class Slp { } /** - * @api {post} /slp/balancesForAddress List SLP balances for an array of addresses. - * @apiName List SLP balances for an array of addresses. - * @apiGroup SLP - * @apiDescription Returns SLP balances for an array of addresses. - * - * - * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json" - * - * - */ + * @api {post} /slp/balancesForAddress List SLP balances for an array of addresses. + * @apiName List SLP balances for an array of addresses. + * @apiGroup SLP + * @apiDescription Returns SLP balances for an array of addresses. + * + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json" + * + * + */ async balancesForAddressBulk (req, res, next) { try { const addresses = req.body.addresses @@ -554,7 +569,7 @@ class Slp { res.status(400) return res.json({ error: - 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } } @@ -563,7 +578,7 @@ class Slp { // Collect an array of promises, one for each request to slpserve. // This is a nested array of promises. - const balancesPromises = addresses.map(async address => { + const balancesPromises = addresses.map(async (address) => { const query = { v: 3, q: { @@ -626,7 +641,6 @@ class Slp { 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)}`) @@ -634,7 +648,7 @@ class Slp { const tokenIds = [] if (tokenRes.data.g.length > 0) { - tokenRes.data.g = tokenRes.data.g.map(token => { + tokenRes.data.g = tokenRes.data.g.map((token) => { token.tokenId = token._id tokenIds.push(token.tokenId) token.balance = parseFloat(token.balanceString) @@ -644,7 +658,7 @@ class Slp { } // Collect another array of promises. - const promises = tokenIds.map(async tokenId => { + const promises = tokenIds.map(async (tokenId) => { const query2 = { v: 3, q: { @@ -671,7 +685,6 @@ class Slp { baseURL: url2, headers: options.headers, timeout: options.timeout - } const tokenRes2 = await _this.axios.request(opt) // console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`) @@ -682,8 +695,8 @@ class Slp { // Wait for all the promises to resolve. const details = await Promise.all(promises) - tokenRes.data.g = tokenRes.data.g.map(token => { - details.forEach(detail => { + tokenRes.data.g = tokenRes.data.g.map((token) => { + details.forEach((detail) => { if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) { token.decimalCount = detail.t[0].tokenDetails.decimals } @@ -710,17 +723,17 @@ class Slp { } /** - * @api {get} /slp/balancesForToken/{TokenId} List SLP addresses and balances for tokenId. - * @apiName List SLP addresses and balances for tokenId. - * @apiGroup SLP - * @apiDescription Returns List SLP addresses and balances for tokenId. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" - * - * - */ + * @api {get} /slp/balancesForToken/{TokenId} List SLP addresses and balances for tokenId. + * @apiName List SLP addresses and balances for tokenId. + * @apiGroup SLP + * @apiDescription Returns List SLP addresses and balances for tokenId. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" + * + * + */ // Retrieve token balances for all addresses by single tokenId. async balancesForTokenSingle (req, res, next) { try { @@ -789,7 +802,6 @@ class Slp { baseURL: url, headers: options.headers, timeout: options.timeout - } // Get data from SLPDB. const tokenRes = await _this.axios.request(opt) @@ -818,17 +830,17 @@ class Slp { } /** - * @api {get} /slp/balance/{address}/{TokenId} List single slp token balance for address. - * @apiName List single slp token balance for address. - * @apiGroup SLP - * @apiDescription Returns List single slp token balance for address. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json" - * - * - */ + * @api {get} /slp/balance/{address}/{TokenId} List single slp token balance for address. + * @apiName List single slp token balance for address. + * @apiGroup SLP + * @apiDescription Returns List single slp token balance for address. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json" + * + * + */ // Retrieve token balances for a single token class, for a single address. async balancesForAddressByTokenID (req, res, next) { try { @@ -862,7 +874,7 @@ class Slp { res.status(400) return res.json({ error: - 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } @@ -932,7 +944,6 @@ class Slp { 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)}`) @@ -947,7 +958,7 @@ class Slp { } if (tokenRes.data.g.length > 0) { - tokenRes.data.g.forEach(async token => { + tokenRes.data.g.forEach(async (token) => { if (token._id === tokenId) { resVal = { cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr), @@ -983,17 +994,17 @@ class Slp { } /** - * @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy. - * @apiName Convert address to slpAddr, cashAddr and legacy. - * @apiGroup SLP - * @apiDescription Convert address to slpAddr, cashAddr and legacy. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" - * - * - */ + * @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy. + * @apiName Convert address to slpAddr, cashAddr and legacy. + * @apiGroup SLP + * @apiDescription Convert address to slpAddr, cashAddr and legacy. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" + * + * + */ async convertAddressSingle (req, res, next) { try { const address = req.params.address @@ -1013,7 +1024,9 @@ class Slp { } obj.slpAddress = slpAddr obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr) - obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) + obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress( + obj.cashAddress + ) res.status(200) return res.json(obj) @@ -1027,17 +1040,17 @@ class Slp { } /** - * @api {post} /slp/convert/ Convert multiple addresses to cash, legacy and simpleledger format. - * @apiName Convert multiple addresses to cash, legacy and simpleledger format. - * @apiGroup SLP - * @apiDescription Convert multiple addresses to cash, legacy and simpleledger format. - * - * - * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v3/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}' - * - * - */ + * @api {post} /slp/convert/ Convert multiple addresses to cash, legacy and simpleledger format. + * @apiName Convert multiple addresses to cash, legacy and simpleledger format. + * @apiGroup SLP + * @apiDescription Convert multiple addresses to cash, legacy and simpleledger format. + * + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}' + * + * + */ async convertAddressBulk (req, res, next) { const addresses = req.body.addresses @@ -1077,7 +1090,9 @@ class Slp { } obj.slpAddress = slpAddr obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr) - obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) + obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress( + obj.cashAddress + ) convertedAddresses.push(obj) } @@ -1087,17 +1102,17 @@ class Slp { } /** - * @api {post} /slp/validateTxid/ Validate multiple SLP transactions by txid. - * @apiName Validate multiple SLP transactions by txid. - * @apiGroup SLP - * @apiDescription Validate multiple SLP transactions by txid. - * - * - * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v3/slp/validateTxid" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}' - * - * - */ + * @api {post} /slp/validateTxid/ Validate multiple SLP transactions by txid. + * @apiName Validate multiple SLP transactions by txid. + * @apiGroup SLP + * @apiDescription Validate multiple SLP transactions by txid. + * + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/slp/validateTxid" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}' + * + * + */ async validateBulk (req, res, next) { try { const txids = req.body.txids @@ -1141,7 +1156,6 @@ class Slp { 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)}`) @@ -1153,7 +1167,7 @@ class Slp { const tokenIds = [] if (concatArray.length > 0) { - concatArray.forEach(token => { + concatArray.forEach((token) => { tokenIds.push(token.tx.h) // txid const validationResult = { @@ -1171,7 +1185,7 @@ class Slp { // If a user-provided txid doesn't exist in the data, add it with // valid:false property. - txids.forEach(txid => { + txids.forEach((txid) => { if (!tokenIds.includes(txid)) { formattedTokens.push({ txid: txid, @@ -1189,7 +1203,7 @@ class Slp { const thisTxid = txids[i] // Find the element that matches the current txid. - const elem = formattedTokens.filter(x => x.txid === thisTxid) + const elem = formattedTokens.filter((x) => x.txid === thisTxid) newOutput.push(elem[0]) } @@ -1208,17 +1222,17 @@ class Slp { } /** - * @api {get} /slp/validateTxid/{txid} Validate single SLP transaction by txid. - * @apiName Validate single SLP transaction by txid. - * @apiGroup SLP - * @apiDescription Validate single SLP transaction by txid. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json" - * - * - */ + * @api {get} /slp/validateTxid/{txid} Validate single SLP transaction by txid. + * @apiName Validate single SLP transaction by txid. + * @apiGroup SLP + * @apiDescription Validate single SLP transaction by txid. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json" + * + * + */ async validateSingle (req, res, next) { try { const txid = req.params.txid @@ -1253,7 +1267,6 @@ class Slp { baseURL: url, headers: options.headers, timeout: options.timeout - } // Get data from SLPDB. const tokenRes = await _this.axios.request(opt) @@ -1271,7 +1284,7 @@ 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) @@ -1290,17 +1303,17 @@ class Slp { // } /** - * @api {get} /slp/txDetails/{txid} SLP transaction details. - * @apiName SLP transaction details. - * @apiGroup SLP - * @apiDescription Transaction details on a token transfer. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json" - * - * - */ + * @api {get} /slp/txDetails/{txid} SLP transaction details. + * @apiName SLP transaction details. + * @apiGroup SLP + * @apiDescription Transaction details on a token transfer. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json" + * + * + */ async txDetails (req, res, next) { try { // Validate input parameter @@ -1336,7 +1349,6 @@ class Slp { baseURL: url, headers: options.headers, timeout: options.timeout - } // Get token data from SLPDB const tokenRes = await _this.axios.request(opt) @@ -1353,7 +1365,10 @@ class Slp { // Get information on the transaction from Insight API. // const retData = await transactions.transactionsFromInsight(txid) - const retData = await _this.rawTransactions.getRawTransactionsFromNode(txid, true) + const retData = await _this.rawTransactions.getRawTransactionsFromNode( + txid, + true + ) // console.log(`retData: ${JSON.stringify(retData, null, 2)}`) // Return both the tx data from Insight and the formatted token information. @@ -1378,17 +1393,17 @@ class Slp { } /** - * @api {get} /slp/tokenStats/{tokenId} List stats for a single slp token. - * @apiName List stats for a single slp token. - * @apiGroup SLP - * @apiDescription Return list stats for a single slp token. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" - * - * - */ + * @api {get} /slp/tokenStats/{tokenId} List stats for a single slp token. + * @apiName List stats for a single slp token. + * @apiGroup SLP + * @apiDescription Return list stats for a single slp token. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" + * + * + */ async tokenStats (req, res, next) { const tokenId = req.params.tokenId if (!tokenId || tokenId === '') { @@ -1431,7 +1446,7 @@ class Slp { const formattedTokens = [] if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { + tokenRes.data.t.forEach((token) => { token = _this.formatTokenOutput(token) formattedTokens.push(token.tokenDetails) }) @@ -1447,17 +1462,17 @@ class Slp { } /** - * @api {get} /slp/transactions/{tokenId}/{address} SLP transactions by tokenId and address. - * @apiName SLP transactions by tokenId and address. - * @apiGroup SLP - * @apiDescription Transactions by tokenId and address. - * - * - * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json" - * - * - */ + * @api {get} /slp/transactions/{tokenId}/{address} SLP transactions by tokenId and address. + * @apiName SLP transactions by tokenId and address. + * @apiGroup SLP + * @apiDescription Transactions by tokenId and address. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json" + * + * + */ // Retrieve transactions by tokenId and address. async txsTokenIdAddressSingle (req, res, next) { try { @@ -1558,7 +1573,7 @@ class Slp { const tokenOutputs = transaction.slp.detail.outputs const sendOutputs = ['0'] - tokenOutputs.map(x => { + tokenOutputs.map((x) => { const string = parseFloat(x.amount) * 100000000 sendOutputs.push(string.toString()) }) @@ -1575,6 +1590,59 @@ class Slp { return obj } + + // Retrieve transactions by address. + async txsByAddressSingle (req, res, next) { + try { + // Validate the input data. + const address = req.params.address + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) + } + + // Ensure the input is a valid BCH address. + try { + _this.bchjs.SLP.Address.toCashAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Ensure it is using the correct network. + const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address) + const networkIsValid = routeUtils.validateNetwork(cashAddr) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + }) + } + + const transactions = await _this.slpdb.getHistoricalSlpTransactions([ + address + ]) + + res.status(200) + return res.json(transactions) + } catch (err) { + wlogger.error('Error in slp.ts/txsByAddressSingle().', 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 /transactionHistoryAllTokens/:address: ${err.message}` + }) + } + } } module.exports = Slp diff --git a/test/v3/slp.js b/test/v3/slp.js index ea4d3db..f3fa8e8 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -68,7 +68,7 @@ describe('#SLP', () => { res = mockRes // Explicitly reset the parmas and body. - // req.params = {} + req.params = {} req.body = {} req.query = {} req.locals = {} @@ -774,6 +774,72 @@ describe('#SLP', () => { ) }) }) + + describe('txsByAddressSingle()', () => { + const txsByAddressSingle = slpRoute.txsByAddressSingle + + it('should throw 400 if address is missing', async () => { + const result = await txsByAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'address can not be empty') + }) + + it('should throw 400 if address is empty', async () => { + req.params.address = '' + const result = await txsByAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'address can not be empty') + }) + + it('should throw 400 if address is invalid', async () => { + req.params.address = 'badAddress' + + const result = await txsByAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Invalid BCH address.') + }) + + it('should throw 400 if address network mismatch', async () => { + req.params.address = + 'slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0' + + const result = await txsByAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Invalid') + }) + + it('should get tx history', async () => { + // if (process.env.TEST === "unit") { + // // nock(`${process.env.SLPDB_URL}`) + // // .get(uri => uri.includes("/")) + // // .reply(200, { + // // c: mockData.mockTransactions + // // }) + // slpRoute. + // } + + // req.params.address = 'simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk' + req.params.address = 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza' + + const result = await slpRoute.txsByAddressSingle(req, res) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // for(let i=0; i < result.length; i++) { + // const entry = result[i] + // + // } + + assert.isArray(result) + }) + }) }) /* From 299945df0d9aa4356d4a6b5efcf8bb74655de3b0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 12 May 2020 13:27:02 -0700 Subject: [PATCH 3/4] finished unit tests for tokenStats --- src/routes/v3/slp.js | 1 + test/v3/mocks/slp-mocks.js | 180 ++++++++++++++++++++++++++++++++++++- test/v3/slp.js | 27 +++--- 3 files changed, 190 insertions(+), 18 deletions(-) diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 293566a..4a4d772 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -1624,6 +1624,7 @@ class Slp { const transactions = await _this.slpdb.getHistoricalSlpTransactions([ address ]) + // console.log(`transactions: ${JSON.stringify(transactions, null, 2)}`) res.status(200) return res.json(transactions) diff --git a/test/v3/mocks/slp-mocks.js b/test/v3/mocks/slp-mocks.js index 3081789..c90e2db 100644 --- a/test/v3/mocks/slp-mocks.js +++ b/test/v3/mocks/slp-mocks.js @@ -341,6 +341,183 @@ const mockTwoRedundentTxid = { u: [] } +const mockTxHistory = [ + { + tx: { + h: '3a7646b3976a8745928c7192c8dde989bfa275fa6d7bce950180ab8002cf6cef' + }, + in: [ + { + i: 0, + e: { + h: '7bdd586ebd1e5f3dfd5295ab6e896c48b25855ab77a72c035ce1e7aacc965c2d', + i: 1, + s: 'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==', + a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq' + } + }, + { + i: 1, + e: { + h: 'df49feff24dc34a10a44a0cbd7c908d964801ec7218512c84574fbce698535f0', + i: 3, + s: 'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=', + a: 'simpleledger:qpkpeqfslejw5pptzcy25h2jxhsc9k0vts43n26up0' + } + } + ], + out: [ + { + e: { + v: 0, + i: 0, + s: 'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g==' + } + }, + { + e: { + v: 546, + i: 1, + s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==', + a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza' + } + }, + { + e: { + v: 546, + i: 2, + s: 'dqkUgYSfxf90wVkERCiOMyh16iB4d92IrA==', + a: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl' + } + }, + { + e: { + v: 21849, + i: 3, + s: 'dqkUxjCDbbIQGbn4E5D5IxJviX4qkiWIrA==', + a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq' + } + } + ], + slp: { + detail: { + decimals: 8, + tokenIdHex: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', + transactionType: 'SEND', + versionType: 1, + documentUri: 'psfoundation.cash', + documentSha256Hex: null, + symbol: 'PSF', + name: 'Permissionless Software Foundation', + txnBatonVout: null, + txnContainsBaton: false, + outputs: [ + { + address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza', + amount: '0.1' + }, + { + address: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl', + amount: '6.11833082' + } + ] + } + }, + blk: { + h: '000000000000000001a47bf337ca211c1e7218234d0d70117c64d6f213f5dacb', + i: 634833, + t: 1589300214 + } + }, + { + tx: { + h: '3b11b48cab1e7c8384facf482b3f6bfe659a58245ab4aa4147b42b5cb2a5fac5' + }, + in: [ + { + i: 0, + e: { + h: 'e4e1e1f6b502cbd42f69b919634cea3e37fc8595e6ca800e0a7d8f6dcdfb249e', + i: 3, + s: 'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', + a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' + } + }, + { + i: 1, + e: { + h: '438420345dd8b7fb4ea74aaf2e3090f44899bf3c71662243946a441de6dae720', + i: 2, + s: 'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', + a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' + } + } + ], + out: [ + { + e: { + v: 0, + i: 0, + s: 'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA==' + } + }, + { + e: { + v: 546, + i: 1, + s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==', + a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza' + } + }, + { + e: { + v: 546, + i: 2, + s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==', + a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' + } + }, + { + e: { + v: 43074, + i: 3, + s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==', + a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' + } + } + ], + slp: { + detail: { + decimals: 2, + tokenIdHex: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + transactionType: 'SEND', + versionType: 1, + documentUri: 'troutsblog.com', + documentSha256Hex: null, + symbol: 'TROUT', + name: "Trout's test token", + txnBatonVout: null, + txnContainsBaton: false, + outputs: [ + { + address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza', + amount: '10000' + }, + { + address: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7', + amount: '998990000' + } + ] + } + }, + blk: { + h: '000000000000000002a4ee2ed6fe8a764ffef0b2556f545ca0b3ef39ecd2b7c4', + i: 634832, + t: 1589297145 + } + } +] + module.exports = { mockList, mockSingleToken, @@ -355,5 +532,6 @@ module.exports = { mockFoobar, mockSingleValidTxid, mockTwoValidTxid, - mockTwoRedundentTxid + mockTwoRedundentTxid, + mockTxHistory } diff --git a/test/v3/slp.js b/test/v3/slp.js index f3fa8e8..ef328f3 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -397,6 +397,7 @@ describe('#SLP', () => { 'Error message expected' ) }) + it('should validate array with single element', async () => { // Mock the RPC call for unit tests. if (process.env.TEST === 'unit') { @@ -806,8 +807,7 @@ describe('#SLP', () => { }) it('should throw 400 if address network mismatch', async () => { - req.params.address = - 'slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0' + req.params.address = 'slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0' const result = await txsByAddressSingle(req, res) // console.log(`result: ${util.inspect(result)}`) @@ -817,25 +817,18 @@ describe('#SLP', () => { }) it('should get tx history', async () => { - // if (process.env.TEST === "unit") { - // // nock(`${process.env.SLPDB_URL}`) - // // .get(uri => uri.includes("/")) - // // .reply(200, { - // // c: mockData.mockTransactions - // // }) - // slpRoute. - // } + if (process.env.TEST === 'unit') { + sandbox + .stub(slpRoute.slpdb, 'getHistoricalSlpTransactions') + .resolves(mockData.mockTxHistory) + } // req.params.address = 'simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk' - req.params.address = 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza' + req.params.address = + 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza' const result = await slpRoute.txsByAddressSingle(req, res) - console.log(`result: ${JSON.stringify(result, null, 2)}`) - - // for(let i=0; i < result.length; i++) { - // const entry = result[i] - // - // } + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) }) From 7ce8a0632a3102629aa7d5dd37cb8e4c14d37dc9 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 12 May 2020 17:40:42 -0700 Subject: [PATCH 4/4] Updated tokenstats endpoint --- package.json | 1 + src/routes/v3/services/slpdb.js | 66 +++++++++++++++++++-- src/routes/v3/slp.js | 57 ++++-------------- test/v3/integration/slp.js | 102 ++++++++++++++++++++++++++++++++ test/v3/integration/slpdb.js | 31 ++++++++++ test/v3/slp.js | 56 ++---------------- 6 files changed, 212 insertions(+), 101 deletions(-) create mode 100644 test/v3/integration/slp.js create mode 100644 test/v3/integration/slpdb.js 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()', () => {