diff --git a/src/app.js b/src/app.js index b317fc8..3503de3 100644 --- a/src/app.js +++ b/src/app.js @@ -30,7 +30,7 @@ const MiningV3 = require('./routes/v3/full-node/mining') const networkV3 = require('./routes/v3/full-node/network') const RawtransactionsV3 = require('./routes/v3/full-node/rawtransactions') const utilV3 = require('./routes/v3/util') -const slpV3 = require('./routes/v3/slp') +const SlpV3 = require('./routes/v3/slp') const xpubV3 = require('./routes/v3/xpub') const blockbookV3 = require('./routes/v3/blockbook') const Ninsight = require('./routes/v3/ninsight') @@ -42,6 +42,7 @@ const blockchainV3 = new BlockchainV3() const controlV3 = new ControlV3() const miningV3 = new MiningV3() const rawtransactionsV3 = new RawtransactionsV3() +const slpV3 = new SlpV3() const app = express() diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 6bea4d7..e1c4e70 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -5,7 +5,8 @@ const router = express.Router() const axios = require('axios') const BigNumber = require('bignumber.js') -const routeUtils = require('./route-utils') +const RouteUtils = require('../../util/route-utils') +const routeUtils = new RouteUtils() // const strftime = require('strftime') const wlogger = require('../../util/winston-logging') @@ -38,531 +39,345 @@ if (!process.env.TREST_URL) { process.env.TREST_URL = 'https://trest.bitcoin.com/v2/' } -router.get('/', root) -router.get('/list', list) -router.get('/list/:tokenId', listSingleToken) -router.post('/list', listBulkToken) -router.get('/balancesForAddress/:address', balancesForAddress) -router.post('/balancesForAddress', balancesForAddressBulk) -router.get('/balancesForToken/:tokenId', balancesForTokenSingle) -router.get('/balance/:address/:tokenId', balancesForAddressByTokenID) -router.get('/convert/:address', convertAddressSingle) -router.post('/convert', convertAddressBulk) -router.post('/validateTxid', validateBulk) -router.get('/validateTxid/:txid', validateSingle) -router.get('/txDetails/:txid', txDetails) -router.get('/tokenStats/:tokenId', tokenStats) -router.get('/transactions/:tokenId/:address', txsTokenIdAddressSingle) +let _this -// const requestConfig = { -// method: 'post', -// auth: { -// username: username, -// password: password -// }, -// data: { -// jsonrpc: '1.0' -// } -// } +class Slp { + constructor () { + _this = this -function 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 + _this.axios = axios + _this.routeUtils = routeUtils + _this.BigNumber = BigNumber + _this.bchjs = bchjs + _this.rawTransactions = rawTransactions - 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.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 + _this.router = router - 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 - return token -} - -function root (req, res, next) { - return res.json({ status: 'slp' }) -} - -/** - * @api {get} /slp/list List all SLP tokens. - * @apiName List all SLP tokens. - * @apiGroup SLP - * @apiDescription Returns list all SLP tokens. - * - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/slp/list" -H "accept:application/json" - * - * - */ -async function list (req, res, next) { - try { - const query = { - v: 3, - q: { - db: ['t'], - find: { - $query: {} - }, - project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, - sort: { 'tokenStats.block_created': -1 }, - limit: 10000 - } - } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - // Get data from SLPDB. - const tokenRes = await axios.get(url) - - const formattedTokens = [] - - if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { - token = formatTokenOutput(token) - formattedTokens.push(token.tokenDetails) - }) - } - - res.status(200) - return res.json(formattedTokens) - } catch (err) { - wlogger.error('Error in slp.ts/list().', err) - - 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 /list: ${err.message}` }) + _this.router.get('/', _this.root) + _this.router.get('/list', _this.list) + _this.router.get('/list/:tokenId', _this.listSingleToken) + _this.router.post('/list', _this.listBulkToken) + _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('/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) } -} -/** - * @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://mainnet.bchjs.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json" - * - * - */ -async function listSingleToken (req, res, next) { - try { - const tokenId = req.params.tokenId - - if (!tokenId || tokenId === '') { - res.status(400) - return res.json({ error: 'tokenId can not be empty' }) - } - - const t = await lookupToken(tokenId) - - res.status(200) - return res.json(t) - } catch (err) { - wlogger.error('Error in slp.ts/listSingleToken().', err) - - 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 /list/:tokenId: ${err.message}` }) - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/list" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenIds":["7380843cd1089a1a01783f86af37734dc99667a1cdc577391b5f6ea42fc1bfb4","9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0"]}' - * - * - */ -async function listBulkToken (req, res, next) { - try { - const tokenIds = req.body.tokenIds - - // Reject if tokenIds is not an array. - if (!Array.isArray(tokenIds)) { - res.status(400) - return res.json({ - error: 'tokenIds needs to be an array. Use GET for single tokenId.' - }) - } - - // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, tokenIds)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: 'Array too large.' - }) - } - - const query = { - v: 3, - q: { - db: ['t'], - find: { - 'tokenDetails.tokenIdHex': { - $in: tokenIds - } - }, - project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, - sort: { 'tokenStats.block_created': -1 }, - limit: 10000 - } - } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - const tokenRes = await axios.get(url) - - const formattedTokens = [] - const txids = [] - - if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { - txids.push(token.tokenDetails.tokenIdHex) - token = formatTokenOutput(token) - formattedTokens.push(token.tokenDetails) - }) - } - - tokenIds.forEach(tokenId => { - if (!txids.includes(tokenId)) { - formattedTokens.push({ - id: tokenId, - valid: false - }) - } - }) - - res.status(200) - return res.json(formattedTokens) - } catch (err) { - wlogger.error('Error in slp.ts/listBulkToken().', err) - - 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 /list/:tokenId: ${err.message}` }) - } -} - -async function lookupToken (tokenId) { - try { - const query = { - v: 3, - q: { - db: ['t'], - find: { - $query: { - 'tokenDetails.tokenIdHex': tokenId - } - }, - project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, - limit: 1000 - } - } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - // console.log(`url: ${url}`) - - const tokenRes = await axios.get(url) - // console.log(`tokenRes.data: ${util.inspect(tokenRes.data,null,2)}`) - // console.log( - // `tokenRes.data.t[0]: ${util.inspect(tokenRes.data.t[0], null, 2)}` - // ) - - const formattedTokens = [] - - if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { - token = formatTokenOutput(token) - formattedTokens.push(token.tokenDetails) - }) - } - - let t - formattedTokens.forEach(token => { - if (token.id === tokenId) t = token - }) - - // If token could not be found. - if (t === undefined) { - t = { - id: 'not found' - } - } - - return t - } catch (err) { - wlogger.error('Error in slp.ts/lookupToken().', err) - // console.log(`Error in slp.ts/lookupToken()`) - throw err - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/balancesForAddress/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" - * - * - */ -// Retrieve token balances for all tokens for a single address. -async function balancesForAddress (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 { - bchjs.SLP.Address.toCashAddress(address) - } catch (err) { - res.status(400) - return res.json({ - error: `Invalid BCH address. Double check your address is valid: ${address}` - }) - } - - // Prevent a common user error. Ensure they are using the correct network address. - const cashAddr = 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 query = { - v: 3, - q: { - db: ['g'], - aggregate: [ - { - $match: { - 'graphTxn.outputs': { - $elemMatch: { - address: bchjs.SLP.Address.toSLPAddress(address), - status: 'UNSPENT', - slpAmount: { $gte: 0 } - } - } - } - }, - { - $unwind: '$graphTxn.outputs' - }, - { - $match: { - 'graphTxn.outputs.address': bchjs.SLP.Address.toSLPAddress( - address - ), - 'graphTxn.outputs.status': 'UNSPENT', - 'graphTxn.outputs.slpAmount': { $gte: 0 } - } - }, - { - $project: { - amount: '$graphTxn.outputs.slpAmount', - address: '$graphTxn.outputs.address', - txid: '$graphTxn.txid', - vout: '$graphTxn.outputs.vout', - tokenId: '$tokenDetails.tokenIdHex' - } - }, - { - $group: { - _id: '$tokenId', - balanceString: { - $sum: '$amount' - }, - slpAddress: { - $first: '$address' - } - } - } - ], - limit: 10000 - } - } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - const options = generateCredentials() - - const tokenRes = await axios.get(url, options) - // console.log(`tokenRes.data.g: ${JSON.stringify(tokenRes.data.g, null, 2)}`) - - const tokenIds = [] - if (tokenRes.data.g.length > 0) { - tokenRes.data.g = tokenRes.data.g.map(token => { - token.tokenId = token._id - tokenIds.push(token.tokenId) - token.balance = parseFloat(token.balanceString) - - delete token._id - - return token - }) - - const promises = tokenIds.map(async tokenId => { - const query2 = { - v: 3, - q: { - db: ['t'], - find: { - $query: { - 'tokenDetails.tokenIdHex': tokenId - } - }, - project: { - 'tokenDetails.decimals': 1, - 'tokenDetails.tokenIdHex': 1, - _id: 0 - }, - limit: 1000 - } - } - - const s2 = JSON.stringify(query2) - const b642 = Buffer.from(s2).toString('base64') - const url2 = `${process.env.SLPDB_URL}q/${b642}` - - const tokenRes2 = await axios.get(url2, options) - // console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`) - - return tokenRes2.data - }) - - const details = await axios.all(promises) - - 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 - } - }) - return token - }) - - return res.json(tokenRes.data.g) - } - - return res.json('No balance for this address') - } catch (err) { - wlogger.error('Error in slp.ts/balancesForAddress().', err) - - // Decode the error message. - const { msg, status } = routeUtils.decodeError(err) + // DRY error handler. + errorHandler (err, res) { + // Attempt to decode the error message. + const { msg, status } = _this.routeUtils.decodeError(err) if (msg) { res.status(status) return res.json({ error: msg }) } res.status(500) - return res.json({ - error: `Error in /address/:address: ${err.message}` - }) + return res.json({ error: util.inspect(err) }) } -} -/** - * @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://mainnet.bchjs.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json" - * - * - */ -async function balancesForAddressBulk (req, res, next) { - try { - const addresses = req.body.addresses - - // Reject if addresses is not an array. - if (!Array.isArray(addresses)) { - res.status(400) - return res.json({ error: 'addresses needs to be an array' }) - } - - // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, addresses)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: 'Array too large.' - }) - } - - wlogger.debug( - 'Executing slp/balancesForAddresss with these addresses: ', - addresses + 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 - // Loop through each address and do error checking. - for (let i = 0; i < addresses.length; i++) { - const address = addresses[i] + 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.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 + 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 + return token + } + + root (req, res, next) { + return res.json({ status: 'slp' }) + } + + /** + * @api {get} /slp/list List all SLP tokens. + * @apiName List all SLP tokens. + * @apiGroup SLP + * @apiDescription Returns list all SLP tokens. + * + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/slp/list" -H "accept:application/json" + * + * + */ + async list (req, res, next) { + try { + const query = { + v: 3, + q: { + db: ['t'], + find: { + $query: {} + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + sort: { 'tokenStats.block_created': -1 }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Request options + const opt = { + method: 'get', + baseURL: url + } + // Get data from SLPDB. + 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) + }) + } + + res.status(200) + return res.json(formattedTokens) + } catch (err) { + wlogger.error('Error in slp.ts/list().', err) + return _this.errorHandler(err, res) + + // return res.json({ error: `Error in /list: ${err.message}` }) + } + } + + /** + * @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://mainnet.bchjs.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json" + * + * + */ + async listSingleToken (req, res, next) { + try { + const tokenId = req.params.tokenId + + if (!tokenId || tokenId === '') { + res.status(400) + return res.json({ error: 'tokenId can not be empty' }) + } + + const t = await _this.lookupToken(tokenId) + + res.status(200) + return res.json(t) + } catch (err) { + wlogger.error('Error in slp.ts/listSingleToken().', err) + return _this.errorHandler(err, res) + + // return res.json({ error: `Error in /list/:tokenId: ${err.message}` }) + } + } + + /** + * @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://mainnet.bchjs.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 + + // Reject if tokenIds is not an array. + if (!Array.isArray(tokenIds)) { + res.status(400) + return res.json({ + error: 'tokenIds needs to be an array. Use GET for single tokenId.' + }) + } + + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, tokenIds)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) + } + + const query = { + v: 3, + q: { + db: ['t'], + find: { + 'tokenDetails.tokenIdHex': { + $in: tokenIds + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + sort: { 'tokenStats.block_created': -1 }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + // Request options + const opt = { + method: 'get', + baseURL: url + } + const tokenRes = await _this.axios.request(opt) + + const formattedTokens = [] + const txids = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + txids.push(token.tokenDetails.tokenIdHex) + token = _this.formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + tokenIds.forEach(tokenId => { + if (!txids.includes(tokenId)) { + formattedTokens.push({ + id: tokenId, + valid: false + }) + } + }) + + res.status(200) + return res.json(formattedTokens) + } catch (err) { + wlogger.error('Error in slp.ts/listBulkToken().', err) + return _this.errorHandler(err, res) + + // return res.json({ error: `Error in /list/:tokenId: ${err.message}` }) + } + } + + async lookupToken (tokenId) { + try { + const query = { + v: 3, + q: { + db: ['t'], + find: { + $query: { + 'tokenDetails.tokenIdHex': tokenId + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + limit: 1000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + + // console.log(`url: ${url}`) + // Request options + const opt = { + method: 'get', + baseURL: url + } + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${util.inspect(tokenRes.data,null,2)}`) + // console.log( + // `tokenRes.data.t[0]: ${util.inspect(tokenRes.data.t[0], null, 2)}` + // ) + + const formattedTokens = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + token = _this.formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + let t + formattedTokens.forEach(token => { + if (token.id === tokenId) t = token + }) + + // If token could not be found. + if (t === undefined) { + t = { + id: 'not found' + } + } + + return t + } catch (err) { + wlogger.error('Error in slp.ts/lookupToken().', err) + // console.log(`Error in slp.ts/lookupToken()`) + throw err + } + } + + /** + * @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://mainnet.bchjs.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 { // Validate the input data. + const address = req.params.address if (!address || address === '') { res.status(400) return res.json({ error: 'address can not be empty' }) @@ -570,7 +385,7 @@ async function balancesForAddressBulk (req, res, next) { // Ensure the input is a valid BCH address. try { - bchjs.SLP.Address.toCashAddress(address) + _this.bchjs.SLP.Address.toCashAddress(address) } catch (err) { res.status(400) return res.json({ @@ -579,22 +394,16 @@ async function balancesForAddressBulk (req, res, next) { } // Prevent a common user error. Ensure they are using the correct network address. - const cashAddr = bchjs.SLP.Address.toCashAddress(address) - const networkIsValid = routeUtils.validateNetwork(cashAddr) + const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address) + const networkIsValid = _this.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.' + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' }) } - } - const options = generateCredentials() - - // 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 query = { v: 3, q: { @@ -604,7 +413,7 @@ async function balancesForAddressBulk (req, res, next) { $match: { 'graphTxn.outputs': { $elemMatch: { - address: bchjs.SLP.Address.toSLPAddress(address), + address: _this.bchjs.SLP.Address.toSLPAddress(address), status: 'UNSPENT', slpAmount: { $gte: 0 } } @@ -616,7 +425,7 @@ async function balancesForAddressBulk (req, res, next) { }, { $match: { - 'graphTxn.outputs.address': bchjs.SLP.Address.toSLPAddress( + 'graphTxn.outputs.address': _this.bchjs.SLP.Address.toSLPAddress( address ), 'graphTxn.outputs.status': 'UNSPENT', @@ -652,984 +461,1167 @@ async function balancesForAddressBulk (req, res, next) { const b64 = Buffer.from(s).toString('base64') const url = `${process.env.SLPDB_URL}q/${b64}` - const tokenRes = await axios.get(url, options) - // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) + const options = _this.generateCredentials() + // Request options + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout + } + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data.g: ${JSON.stringify(tokenRes.data.g, null, 2)}`) const tokenIds = [] - if (tokenRes.data.g.length > 0) { tokenRes.data.g = tokenRes.data.g.map(token => { token.tokenId = token._id tokenIds.push(token.tokenId) token.balance = parseFloat(token.balanceString) + delete token._id + return token }) + + const promises = tokenIds.map(async tokenId => { + const query2 = { + v: 3, + q: { + db: ['t'], + find: { + $query: { + 'tokenDetails.tokenIdHex': tokenId + } + }, + project: { + 'tokenDetails.decimals': 1, + 'tokenDetails.tokenIdHex': 1, + _id: 0 + }, + limit: 1000 + } + } + + const s2 = JSON.stringify(query2) + const b642 = Buffer.from(s2).toString('base64') + const url2 = `${process.env.SLPDB_URL}q/${b642}` + // Request options + const opt = { + method: 'get', + 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)}`) + + return tokenRes2.data + }) + + const details = await _this.axios.all(promises) + + 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 + } + }) + return token + }) + + return res.json(tokenRes.data.g) + } + + return res.json('No balance for this address') + } catch (err) { + wlogger.error('Error in slp.ts/balancesForAddress().', err) + + return _this.errorHandler(err, res) + + // return res.json({ + // error: `Error in /address/:address: ${err.message}` + // }) + } + } + + /** + * @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://mainnet.bchjs.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json" + * + * + */ + async balancesForAddressBulk (req, res, next) { + try { + const addresses = req.body.addresses + + // Reject if addresses is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ error: 'addresses needs to be an array' }) + } + + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) } - // Collect another array of promises. - const promises = tokenIds.map(async tokenId => { - const query2 = { + wlogger.debug( + 'Executing slp/balancesForAddresss with these addresses: ', + addresses + ) + + // Loop through each address and do error checking. + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] + + // Validate the input data. + 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}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address) + const networkIsValid = _this.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 options = _this.generateCredentials() + + // 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 query = { v: 3, q: { - db: ['t'], - find: { - $query: { - 'tokenDetails.tokenIdHex': tokenId + db: ['g'], + aggregate: [ + { + $match: { + 'graphTxn.outputs': { + $elemMatch: { + address: _this.bchjs.SLP.Address.toSLPAddress(address), + status: 'UNSPENT', + slpAmount: { $gte: 0 } + } + } + } + }, + { + $unwind: '$graphTxn.outputs' + }, + { + $match: { + 'graphTxn.outputs.address': _this.bchjs.SLP.Address.toSLPAddress( + address + ), + 'graphTxn.outputs.status': 'UNSPENT', + 'graphTxn.outputs.slpAmount': { $gte: 0 } + } + }, + { + $project: { + amount: '$graphTxn.outputs.slpAmount', + address: '$graphTxn.outputs.address', + txid: '$graphTxn.txid', + vout: '$graphTxn.outputs.vout', + tokenId: '$tokenDetails.tokenIdHex' + } + }, + { + $group: { + _id: '$tokenId', + balanceString: { + $sum: '$amount' + }, + slpAddress: { + $first: '$address' + } + } } - }, - project: { - 'tokenDetails.decimals': 1, - 'tokenDetails.tokenIdHex': 1, - _id: 0 - }, - limit: 1000 + ], + limit: 10000 } } - const s2 = JSON.stringify(query2) - const b642 = Buffer.from(s2).toString('base64') - const url2 = `${process.env.SLPDB_URL}q/${b642}` + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + // Request options + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout - const tokenRes2 = await axios.get(url2, options) - // console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`) + } + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) - return tokenRes2.data + const tokenIds = [] + + if (tokenRes.data.g.length > 0) { + tokenRes.data.g = tokenRes.data.g.map(token => { + token.tokenId = token._id + tokenIds.push(token.tokenId) + token.balance = parseFloat(token.balanceString) + delete token._id + return token + }) + } + + // Collect another array of promises. + const promises = tokenIds.map(async tokenId => { + const query2 = { + v: 3, + q: { + db: ['t'], + find: { + $query: { + 'tokenDetails.tokenIdHex': tokenId + } + }, + project: { + 'tokenDetails.decimals': 1, + 'tokenDetails.tokenIdHex': 1, + _id: 0 + }, + limit: 1000 + } + } + + const s2 = JSON.stringify(query2) + const b642 = Buffer.from(s2).toString('base64') + const url2 = `${process.env.SLPDB_URL}q/${b642}` + const opt = { + method: 'get', + 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)}`) + + return tokenRes2.data + }) + + // Wait for all the promises to resolve. + const details = await Promise.all(promises) + + 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 + } + }) + + return token + }) + + return tokenRes.data.g }) // Wait for all the promises to resolve. - const details = await Promise.all(promises) + const axiosResult = await _this.axios.all(balancesPromises) - 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 - } - }) - - return token - }) - - return tokenRes.data.g - }) - - // Wait for all the promises to resolve. - const axiosResult = await axios.all(balancesPromises) - - return res.json(axiosResult) - } catch (err) { - wlogger.error('Error in slp.js/balancesForAddressBulk().', 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 POST balancesForAddress: ${err.message}` - }) - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" - * - * - */ -// Retrieve token balances for all addresses by single tokenId. -async function balancesForTokenSingle (req, res, next) { - try { - // Validate the input data. - const tokenId = req.params.tokenId - if (!tokenId || tokenId === '') { - res.status(400) - return res.json({ error: 'tokenId can not be empty' }) - } - - const query = { - v: 3, - q: { - db: ['g'], - aggregate: [ - { - $match: { - 'graphTxn.outputs': { - $elemMatch: { - status: 'UNSPENT', - slpAmount: { $gte: 0 } - } - }, - 'tokenDetails.tokenIdHex': tokenId - } - }, - { - $unwind: '$graphTxn.outputs' - }, - { - $match: { - 'graphTxn.outputs.status': 'UNSPENT', - 'graphTxn.outputs.slpAmount': { $gte: 0 }, - 'tokenDetails.tokenIdHex': tokenId - } - }, - { - $project: { - token_balance: '$graphTxn.outputs.slpAmount', - address: '$graphTxn.outputs.address', - txid: '$graphTxn.txid', - vout: '$graphTxn.outputs.vout', - tokenId: '$tokenDetails.tokenIdHex' - } - }, - { - $group: { - _id: '$address', - token_balance: { - $sum: '$token_balance' - } - } - } - ], - limit: 10000 - } - } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - const options = generateCredentials() - - // Get data from SLPDB. - const tokenRes = await axios.get(url, options) - // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) - - const resBalances = tokenRes.data.g.map((addy, index) => { - delete addy.satoshis_balance - addy.tokenBalanceString = addy.token_balance - addy.slpAddress = addy._id - addy.tokenId = tokenId - delete addy._id - delete addy.token_balance - - return addy - }) - - return res.json(resBalances) - } catch (err) { - wlogger.error('Error in slp.ts/balancesForTokenSingle().', 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 /balancesForToken/:tokenId: ${err.message}` - }) - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json" - * - * - */ -// Retrieve token balances for a single token class, for a single address. -async function balancesForAddressByTokenID (req, res, next) { - try { - // Validate input data. - const address = req.params.address - if (!address || address === '') { - res.status(400) - return res.json({ error: 'address can not be empty' }) - } - - const tokenId = req.params.tokenId - if (!tokenId || tokenId === '') { - res.status(400) - return res.json({ error: 'tokenId can not be empty' }) - } - - // Ensure the input is a valid BCH address. - try { - bchjs.SLP.Address.toCashAddress(address) + return res.json(axiosResult) } catch (err) { - res.status(400) - return res.json({ - error: `Invalid BCH address. Double check your address is valid: ${address}` - }) + wlogger.error('Error in slp.js/balancesForAddressBulk().', err) + + return _this.errorHandler(err, res) + // return res.json({ + // error: `Error in POST balancesForAddress: ${err.message}` + // }) } + } - // Prevent a common user error. Ensure they are using the correct network address. - const cashAddr = 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.' - }) - } + /** + * @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://mainnet.bchjs.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" + * + * + */ + // Retrieve token balances for all addresses by single tokenId. + async balancesForTokenSingle (req, res, next) { + try { + // Validate the input data. + const tokenId = req.params.tokenId + if (!tokenId || tokenId === '') { + res.status(400) + return res.json({ error: 'tokenId can not be empty' }) + } - // Convert input to an simpleledger: address. - const slpAddr = bchjs.SLP.Address.toSlpAddress(req.params.address) - - const query = { - v: 3, - q: { - db: ['g'], - aggregate: [ - { - $match: { - 'graphTxn.outputs': { - $elemMatch: { - address: slpAddr, - status: 'UNSPENT', - slpAmount: { $gte: 0 } + const query = { + v: 3, + q: { + db: ['g'], + aggregate: [ + { + $match: { + 'graphTxn.outputs': { + $elemMatch: { + status: 'UNSPENT', + slpAmount: { $gte: 0 } + } + }, + 'tokenDetails.tokenIdHex': tokenId + } + }, + { + $unwind: '$graphTxn.outputs' + }, + { + $match: { + 'graphTxn.outputs.status': 'UNSPENT', + 'graphTxn.outputs.slpAmount': { $gte: 0 }, + 'tokenDetails.tokenIdHex': tokenId + } + }, + { + $project: { + token_balance: '$graphTxn.outputs.slpAmount', + address: '$graphTxn.outputs.address', + txid: '$graphTxn.txid', + vout: '$graphTxn.outputs.vout', + tokenId: '$tokenDetails.tokenIdHex' + } + }, + { + $group: { + _id: '$address', + token_balance: { + $sum: '$token_balance' } } } - }, - { - $unwind: '$graphTxn.outputs' - }, - { - $match: { - 'graphTxn.outputs.address': slpAddr, - 'graphTxn.outputs.status': 'UNSPENT', - 'graphTxn.outputs.slpAmount': { $gte: 0 } - } - }, - { - $project: { - amount: '$graphTxn.outputs.slpAmount', - address: '$graphTxn.outputs.address', - txid: '$graphTxn.txid', - vout: '$graphTxn.outputs.vout', - tokenId: '$tokenDetails.tokenIdHex' - } - }, - { - $group: { - _id: '$tokenId', - balanceString: { - $sum: '$amount' - }, - slpAddress: { - $first: '$address' + ], + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + + const options = _this.generateCredentials() + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout + + } + // Get data from SLPDB. + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) + + const resBalances = tokenRes.data.g.map((addy, index) => { + delete addy.satoshis_balance + addy.tokenBalanceString = addy.token_balance + addy.slpAddress = addy._id + addy.tokenId = tokenId + delete addy._id + delete addy.token_balance + + return addy + }) + + return res.json(resBalances) + } catch (err) { + wlogger.error('Error in slp.ts/balancesForTokenSingle().', err) + + return _this.errorHandler(err, res) + // return res.json({ + // error: `Error in /balancesForToken/:tokenId: ${err.message}` + // }) + } + } + + /** + * @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://mainnet.bchjs.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 { + // Validate input data. + const address = req.params.address + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === '') { + res.status(400) + return res.json({ error: 'tokenId 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}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address) + const networkIsValid = _this.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.' + }) + } + + // Convert input to an simpleledger: address. + const slpAddr = _this.bchjs.SLP.Address.toSlpAddress(req.params.address) + + const query = { + v: 3, + q: { + db: ['g'], + aggregate: [ + { + $match: { + 'graphTxn.outputs': { + $elemMatch: { + address: slpAddr, + status: 'UNSPENT', + slpAmount: { $gte: 0 } + } + } + } + }, + { + $unwind: '$graphTxn.outputs' + }, + { + $match: { + 'graphTxn.outputs.address': slpAddr, + 'graphTxn.outputs.status': 'UNSPENT', + 'graphTxn.outputs.slpAmount': { $gte: 0 } + } + }, + { + $project: { + amount: '$graphTxn.outputs.slpAmount', + address: '$graphTxn.outputs.address', + txid: '$graphTxn.txid', + vout: '$graphTxn.outputs.vout', + tokenId: '$tokenDetails.tokenIdHex' + } + }, + { + $group: { + _id: '$tokenId', + balanceString: { + $sum: '$amount' + }, + slpAddress: { + $first: '$address' + } } } - } - ], - limit: 10000 - } - } - - const options = generateCredentials() - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - // Get data from SLPDB. - const tokenRes = await axios.get(url, options) - console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) - - let resVal = { - cashAddress: bchjs.SLP.Address.toCashAddress(slpAddr), - legacyAddress: bchjs.SLP.Address.toLegacyAddress(slpAddr), - slpAddress: slpAddr, - tokenId: tokenId, - balance: 0, - balanceString: '0' - } - - if (tokenRes.data.g.length > 0) { - tokenRes.data.g.forEach(async token => { - if (token._id === tokenId) { - resVal = { - cashAddress: bchjs.SLP.Address.toCashAddress(slpAddr), - legacyAddress: bchjs.SLP.Address.toLegacyAddress(slpAddr), - slpAddress: slpAddr, - tokenId: token._id, - balance: parseFloat(token.balanceString), - balanceString: token.balanceString - } + ], + limit: 10000 } - }) - } else { - resVal = { - cashAddress: bchjs.SLP.Address.toCashAddress(slpAddr), - legacyAddress: bchjs.SLP.Address.toLegacyAddress(slpAddr), + } + + const options = _this.generateCredentials() + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + + // 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 resVal = { + cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr), + legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr), slpAddress: slpAddr, tokenId: tokenId, balance: 0, balanceString: '0' } + + if (tokenRes.data.g.length > 0) { + tokenRes.data.g.forEach(async token => { + if (token._id === tokenId) { + resVal = { + cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr), + legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr), + slpAddress: slpAddr, + tokenId: token._id, + balance: parseFloat(token.balanceString), + balanceString: token.balanceString + } + } + }) + } else { + resVal = { + cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr), + legacyAddress: _this.bchjs.SLP.Address.toLegacyAddress(slpAddr), + slpAddress: slpAddr, + tokenId: tokenId, + balance: 0, + balanceString: '0' + } + } + + res.status(200) + return res.json(resVal) + } catch (err) { + wlogger.error('Error in slp.ts/balancesForAddressByTokenID().', err) + + return _this.errorHandler(err, res) + // return res.json({ + // error: `Error in /balance/:address/:tokenId: ${err.message}` + // }) } - - res.status(200) - return res.json(resVal) - } catch (err) { - wlogger.error('Error in slp.ts/balancesForAddressByTokenID().', 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 /balance/:address/:tokenId: ${err.message}` - }) } -} -/** - * @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://mainnet.bchjs.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" - * - * - */ -async function convertAddressSingle (req, res, next) { - try { - const address = req.params.address + /** + * @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://mainnet.bchjs.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json" + * + * + */ + async convertAddressSingle (req, res, next) { + try { + const address = req.params.address - // Validate input - if (!address || address === '') { + // Validate input + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) + } + + const slpAddr = _this.bchjs.SLP.Address.toSLPAddress(address) + + const obj = { + slpAddress: '', + cashAddress: '', + legacyAddress: '' + } + obj.slpAddress = slpAddr + obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr) + obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) + + res.status(200) + return res.json(obj) + } catch (err) { + wlogger.error('Error in slp.ts/convertAddressSingle().', err) + return _this.errorHandler(err, res) + // return res.json({ + // error: `Error in /address/convert/:address: ${err.message}` + // }) + } + } + + /** + * @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://mainnet.bchjs.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 + + // Reject if hashes is not an array. + if (!Array.isArray(addresses)) { res.status(400) - return res.json({ error: 'address can not be empty' }) - } - - const slpAddr = bchjs.SLP.Address.toSLPAddress(address) - - const obj = { - slpAddress: '', - cashAddress: '', - legacyAddress: '' - } - obj.slpAddress = slpAddr - obj.cashAddress = bchjs.SLP.Address.toCashAddress(slpAddr) - obj.legacyAddress = bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) - - res.status(200) - return res.json(obj) - } catch (err) { - wlogger.error('Error in slp.ts/convertAddressSingle().', err) - - 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 /address/convert/:address: ${err.message}` - }) - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}' - * - * - */ -async function convertAddressBulk (req, res, next) { - const addresses = req.body.addresses - - // Reject if hashes is not an array. - if (!Array.isArray(addresses)) { - res.status(400) - return res.json({ - error: 'addresses needs to be an array. Use GET for single address.' - }) - } - - // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, addresses)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: 'Array too large.' - }) - } - - // Convert each address in the array. - const convertedAddresses = [] - for (let i = 0; i < addresses.length; i++) { - const address = addresses[i] - - // Validate input - if (!address || address === '') { - res.status(400) - return res.json({ error: 'address can not be empty' }) - } - - const slpAddr = bchjs.SLP.Address.toSLPAddress(address) - - const obj = { - slpAddress: '', - cashAddress: '', - legacyAddress: '' - } - obj.slpAddress = slpAddr - obj.cashAddress = bchjs.SLP.Address.toCashAddress(slpAddr) - obj.legacyAddress = bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) - - convertedAddresses.push(obj) - } - - res.status(200) - return res.json(convertedAddresses) -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/validateTxid" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}' - * - * - */ -async function validateBulk (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' }) + return res.json({ + error: 'addresses needs to be an array. Use GET for single address.' + }) } // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, txids)) { + if (!_this.routeUtils.validateArraySize(req, addresses)) { 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) + // Convert each address in the array. + const convertedAddresses = [] + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] - 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_URL}q/${b64}` - - const options = generateCredentials() - - // Get data from SLPDB. - const tokenRes = await axios.get(url, options) - // 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]) + // Validate input + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) } - // Replace the original output object with the new output object. - formattedTokens = newOutput + const slpAddr = _this.bchjs.SLP.Address.toSLPAddress(address) + + const obj = { + slpAddress: '', + cashAddress: '', + legacyAddress: '' + } + obj.slpAddress = slpAddr + obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr) + obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress) + + convertedAddresses.push(obj) } res.status(200) - return res.json(formattedTokens) - } catch (err) { - wlogger.error('Error in slp.ts/validateBulk().', err) - - // Attempt to 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: util.inspect(err) }) + return res.json(convertedAddresses) } -} -/** - * @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://mainnet.bchjs.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json" - * - * - */ -async function validateSingle (req, res, next) { - try { - const txid = req.params.txid + /** + * @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://mainnet.bchjs.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 - // Validate input - if (!txid || txid === '') { - res.status(400) - return res.json({ error: 'txid can not be empty' }) - } - - wlogger.debug('Executing slp/validate/:txid with this txid: ', txid) - - const query = { - v: 3, - q: { - db: ['c', 'u'], - find: { - 'tx.h': txid - }, - limit: 300, - project: { 'slp.valid': 1, 'tx.h': 1, 'slp.invalidReason': 1 } + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ error: 'txids needs to be an array' }) } - } - const options = generateCredentials() - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - // Get data from SLPDB. - const tokenRes = await axios.get(url, options) - - // Default return value. - let result = { - txid: txid, - valid: false - } - - // Build result. - const concatArray = tokenRes.data.c.concat(tokenRes.data.u) - if (concatArray.length > 0) { - result = { - txid: concatArray[0].tx.h, - valid: concatArray[0].slp.valid + // 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.' + }) } - if (!result.valid) result.invalidReason = concatArray[0].slp.invalidReason - } - res.status(200) - return res.json(result) - } catch (err) { - wlogger.error('Error in slp.ts/validateSingle().', err) + wlogger.debug('Executing slp/validate with these txids: ', txids) - // Attempt to 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: util.inspect(err) }) - } -} - -// Returns a Boolean if the input TXID is a valid SLP TXID. -// async function isValidSlpTxid (txid) { -// const isValid = await slpValidator.isValidSlpTxid(txid) -// return isValid -// } - -/** - * @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://mainnet.bchjs.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json" - * - * - */ -async function txDetails (req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === '') { - res.status(400) - return res.json({ error: 'txid can not be empty' }) - } - - if (txid.length !== 64) { - res.status(400) - return res.json({ error: 'This is not a txid' }) - } - - const query = { - v: 3, - db: ['g'], - q: { - find: { - 'tx.h': txid - }, - limit: 300 + 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_URL}q/${b64}` - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` + const options = _this.generateCredentials() - const options = generateCredentials() + // Get data from SLPDB. + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout - // Get token data from SLPDB - const tokenRes = await axios.get(url, options) - // console.log(`tokenRes: ${util.inspect(tokenRes)}`) + } + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) - if (tokenRes.data.c.length === 0) { - res.status(404) - return res.json({ error: 'TXID not found' }) - } + let formattedTokens = [] - // Format the returned data to an object. - const formatted = await formatToRestObject(tokenRes) - // console.log(`formatted: ${JSON.stringify(formatted,null,2)}`) + // Combine the arrays. Why? Generally there is nothing in the u array. + const concatArray = tokenRes.data.c.concat(tokenRes.data.u) - // Get information on the transaction from Insight API. - // const retData = await transactions.transactionsFromInsight(txid) - const retData = await rawTransactions.getRawTransactionsFromNode(txid, true) - // console.log(`retData: ${JSON.stringify(retData, null, 2)}`) + const tokenIds = [] + if (concatArray.length > 0) { + concatArray.forEach(token => { + tokenIds.push(token.tx.h) // txid - // Return both the tx data from Insight and the formatted token information. - const response = { - retData, - ...formatted - } - - res.status(200) - return res.json(response) - } catch (err) { - wlogger.error('Error in slp.ts/txDetails().', err) - - // Attempt to decode the error message. - const { msg, status } = routeUtils.decodeError(err) - if (msg) { - res.status(status) - return res.json({ error: msg }) - } - - // Handle corner case of mis-typted txid - if (err.error && err.error.indexOf('Not found') > -1) { - res.status(400) - return res.json({ error: 'TXID not found' }) - } - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @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://mainnet.bchjs.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" - * - * - */ -async function 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 + const validationResult = { + txid: token.tx.h, + valid: token.slp.valid } - }, - project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, - limit: 10 + + // 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) } - - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` - - // Get data from BitDB. - const tokenRes = await axios.get(url) - - const formattedTokens = [] - - if (tokenRes.data.t.length) { - tokenRes.data.t.forEach(token => { - token = formatTokenOutput(token) - formattedTokens.push(token.tokenDetails) - }) - } - - res.status(200) - return res.json(formattedTokens[0]) - } catch (err) { - wlogger.error('Error in slp.ts/tokenStats().', err) - - 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 /tokenStats: ${err.message}` }) } -} -/** - * @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://mainnet.bchjs.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json" - * - * - */ -// Retrieve transactions by tokenId and address. -async function txsTokenIdAddressSingle (req, res, next) { - try { - // Validate the input data. + /** + * @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://mainnet.bchjs.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json" + * + * + */ + async validateSingle (req, res, next) { + try { + const txid = req.params.txid + + // Validate input + if (!txid || txid === '') { + res.status(400) + return res.json({ error: 'txid can not be empty' }) + } + + wlogger.debug('Executing slp/validate/:txid with this txid: ', txid) + + const query = { + v: 3, + q: { + db: ['c', 'u'], + find: { + 'tx.h': txid + }, + limit: 300, + project: { 'slp.valid': 1, 'tx.h': 1, 'slp.invalidReason': 1 } + } + } + + const options = _this.generateCredentials() + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout + + } + // Get data from SLPDB. + const tokenRes = await _this.axios.request(opt) + + // Default return value. + let result = { + txid: txid, + valid: false + } + + // Build result. + const concatArray = tokenRes.data.c.concat(tokenRes.data.u) + if (concatArray.length > 0) { + result = { + txid: concatArray[0].tx.h, + valid: concatArray[0].slp.valid + } + if (!result.valid) result.invalidReason = concatArray[0].slp.invalidReason + } + + res.status(200) + return res.json(result) + } catch (err) { + wlogger.error('Error in slp.ts/validateSingle().', err) + + return _this.errorHandler(err, res) + } + } + + // Returns a Boolean if the input TXID is a valid SLP TXID. + // async function isValidSlpTxid (txid) { + // const isValid = await slpValidator.isValidSlpTxid(txid) + // return isValid + // } + + /** + * @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://mainnet.bchjs.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json" + * + * + */ + async txDetails (req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === '') { + res.status(400) + return res.json({ error: 'txid can not be empty' }) + } + + if (txid.length !== 64) { + res.status(400) + return res.json({ error: 'This is not a txid' }) + } + + const query = { + v: 3, + db: ['g'], + q: { + find: { + 'tx.h': txid + }, + limit: 300 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + + const options = _this.generateCredentials() + const opt = { + method: 'get', + baseURL: url, + headers: options.headers, + timeout: options.timeout + + } + // Get token data from SLPDB + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes: ${util.inspect(tokenRes)}`) + + if (tokenRes.data.c.length === 0) { + res.status(404) + return res.json({ error: 'TXID not found' }) + } + + // Format the returned data to an object. + const formatted = await _this.formatToRestObject(tokenRes) + // console.log(`formatted: ${JSON.stringify(formatted,null,2)}`) + + // Get information on the transaction from Insight API. + // const retData = await transactions.transactionsFromInsight(txid) + 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. + const response = { + retData, + ...formatted + } + + res.status(200) + return res.json(response) + } catch (err) { + wlogger.error('Error in slp.ts/txDetails().', err) + + // Handle corner case of mis-typted txid + // if (err.error && err.error.indexOf('Not found') > -1) { + // res.status(400) + // return res.json({ error: 'TXID not found' }) + // } + + return _this.errorHandler(err, res) + } + } + + /** + * @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://mainnet.bchjs.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json" + * + * + */ + 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' }) } - const address = req.params.address - if (!address || address === '') { - res.status(400) - return res.json({ error: 'address can not be empty' }) - } - - const query = { - v: 3, - q: { - find: { - db: ['c', 'u'], - $query: { - $or: [ - { - 'in.e.a': address - }, - { - 'out.e.a': address - } - ], - 'slp.detail.tokenIdHex': tokenId + try { + const query = { + v: 3, + q: { + db: ['t'], + find: { + $query: { + 'tokenDetails.tokenIdHex': tokenId + } }, - $orderby: { - 'blk.i': -1 - } - }, - limit: 100 - }, - r: { - f: '[.[] | { txid: .tx.h, tokenDetails: .slp } ]' + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + limit: 10 + } } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + const opt = { + method: 'get', + baseURL: url + } + // 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) + }) + } + + res.status(200) + return res.json(formattedTokens[0]) + } catch (err) { + wlogger.error('Error in slp.ts/tokenStats().', err) + return _this.errorHandler(err, res) + // return res.json({ error: `Error in /tokenStats: ${err.message}` }) + } + } + + /** + * @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://mainnet.bchjs.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json" + * + * + */ + // Retrieve transactions by tokenId and address. + async txsTokenIdAddressSingle (req, res, next) { + try { + // Validate the input data. + const tokenId = req.params.tokenId + if (!tokenId || tokenId === '') { + res.status(400) + return res.json({ error: 'tokenId can not be empty' }) + } + + const address = req.params.address + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) + } + + const query = { + v: 3, + q: { + find: { + db: ['c', 'u'], + $query: { + $or: [ + { + 'in.e.a': address + }, + { + 'out.e.a': address + } + ], + 'slp.detail.tokenIdHex': tokenId + }, + $orderby: { + 'blk.i': -1 + } + }, + limit: 100 + }, + r: { + f: '[.[] | { txid: .tx.h, tokenDetails: .slp } ]' + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString('base64') + const url = `${process.env.SLPDB_URL}q/${b64}` + const opt = { + method: 'get', + baseURL: url + } + // Get data from SLPDB. + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) + + return res.json(tokenRes.data.c) + } catch (err) { + wlogger.error('Error in slp.ts/txsTokenIdAddressSingle().', err) + + return _this.errorHandler(err, res) + // return res.json({ + // error: `Error in /transactions/:tokenId/:address: ${err.message}` + // }) + } + } + + // Generates a Basic Authorization header for slpserve. + generateCredentials () { + // Generate the Basic Authentication header for a private instance of SLPDB. + 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: 15000 } - const s = JSON.stringify(query) - const b64 = Buffer.from(s).toString('base64') - const url = `${process.env.SLPDB_URL}q/${b64}` + return options + } - // Get data from SLPDB. - const tokenRes = await axios.get(url) - // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data,null,2)}`) + // Format the response from SLPDB into an object. + async formatToRestObject (slpDBFormat) { + _this.BigNumber.set({ DECIMAL_PLACES: 8 }) - return res.json(tokenRes.data.c) - } catch (err) { - wlogger.error('Error in slp.ts/txsTokenIdAddressSingle().', err) + // console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`) - // Decode the error message. - const { msg, status } = routeUtils.decodeError(err) - if (msg) { - res.status(status) - return res.json({ error: msg }) - } + const transaction = slpDBFormat.data.u.length + ? slpDBFormat.data.u[0] + : slpDBFormat.data.c[0] - res.status(500) - return res.json({ - error: `Error in /transactions/:tokenId/:address: ${err.message}` + // const inputs = transaction.in + + // const outputs = transaction.out + const tokenOutputs = transaction.slp.detail.outputs + + const sendOutputs = ['0'] + tokenOutputs.map(x => { + const string = parseFloat(x.amount) * 100000000 + sendOutputs.push(string.toString()) }) + + const obj = { + tokenInfo: { + versionType: transaction.slp.detail.versionType, + transactionType: transaction.slp.detail.transactionType, + tokenIdHex: transaction.slp.detail.tokenIdHex, + sendOutputs: sendOutputs + }, + tokenIsValid: transaction.slp.valid + } + + return obj } } -// Generates a Basic Authorization header for slpserve. -function generateCredentials () { - // Generate the Basic Authentication header for a private instance of SLPDB. - 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: 15000 - } - - return options -} - -// Format the response from SLPDB into an object. -async function formatToRestObject (slpDBFormat) { - BigNumber.set({ DECIMAL_PLACES: 8 }) - - // console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`) - - const transaction = slpDBFormat.data.u.length - ? slpDBFormat.data.u[0] - : slpDBFormat.data.c[0] - - // const inputs = transaction.in - - // const outputs = transaction.out - const tokenOutputs = transaction.slp.detail.outputs - - const sendOutputs = ['0'] - tokenOutputs.map(x => { - const string = parseFloat(x.amount) * 100000000 - sendOutputs.push(string.toString()) - }) - - const obj = { - tokenInfo: { - versionType: transaction.slp.detail.versionType, - transactionType: transaction.slp.detail.transactionType, - tokenIdHex: transaction.slp.detail.tokenIdHex, - sendOutputs: sendOutputs - }, - tokenIsValid: transaction.slp.valid - } - - return obj -} - -module.exports = { - router, - testableComponents: { - root, - list, - listSingleToken, - listBulkToken, - balancesForAddress, - balancesForAddressBulk, - balancesForAddressByTokenID, - convertAddressSingle, - convertAddressBulk, - validateBulk, - // isValidSlpTxid, - txDetails, - tokenStats, - balancesForTokenSingle, - txsTokenIdAddressSingle - } -} +module.exports = Slp diff --git a/test/v3/slp.js b/test/v3/slp.js index df9b601..61eec0f 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -13,13 +13,14 @@ const chai = require('chai') const assert = chai.assert -const nock = require('nock') // HTTP mocking const sinon = require('sinon') // const proxyquire = require('proxyquire').noPreserveCache() // const axios = require('axios') // Save existing environment variables. // Used during transition from integration to unit tests. + +// eslint-disable-next-line no-unused-vars let mockServerUrl const originalEnvVars = { BITDB_URL: process.env.BITDB_URL, @@ -39,7 +40,9 @@ if (process.env.TEST === 'unit') { } // Prepare the slpRoute for stubbing dependcies on slpjs. -const slpRoute = require('../../src/routes/v3/slp') +const SlpRoute = require('../../src/routes/v3/slp') +const slpRoute = new SlpRoute() + // const pathStub = {} // Used to stub methods within slpjs. // const slpRouteStub = proxyquire('../../src/routes/v3/slp', { slpjs: pathStub }) @@ -70,17 +73,10 @@ describe('#SLP', () => { req.query = {} req.locals = {} - // Activate nock if it's inactive. - if (!nock.isActive()) nock.activate() - sandbox = sinon.createSandbox() }) afterEach(() => { - // Clean up HTTP mocks. - nock.cleanAll() // clear interceptor list. - nock.restore() - sandbox.restore() }) @@ -93,7 +89,7 @@ describe('#SLP', () => { describe('#root', async () => { // root route handler. - const root = slpRoute.testableComponents.root + const root = slpRoute.root it('should respond to GET for base route', async () => { const result = root(req, res) @@ -102,7 +98,686 @@ describe('#SLP', () => { assert.equal(result.status, 'slp', 'Returns static string') }) }) - /* + + describe('balancesForAddress()', () => { + const balancesForAddress = slpRoute.balancesForAddress + + it('should throw 400 if address is empty', async () => { + const result = await balancesForAddress(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 balancesForAddress(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:qzcvpw3ah7r880d49wsqzrsl90pg0rqjjurmj3g4nk' + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Invalid') + }) + + it('should throw 5XX error when network issues', async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = 'http://fakeurl/api/' + + req.params.address = + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 500 or greater expected.' + ) + assert.include( + result.error, + 'Network error: Could not communicate', + 'Error message expected' + ) + }) + it('returns proper error when downstream service stalls', async () => { + // Mock the timeout error. + sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) + + req.params.address = + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + + const result = await balancesForAddress(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.params.address = + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + + const result = await balancesForAddress(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 get token balance for an address', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox.stub(slpRoute.axios, 'request').resolves({ + data: mockData.mockSingleAddress + }) + } + + req.params.address = + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + + assert.property(result[0], 'tokenId') + assert.property(result[0], 'balanceString') + assert.property(result[0], 'slpAddress') + assert.property(result[0], 'balance') + assert.property(result[0], 'decimalCount') + + assert.isNumber(result[0].balance) + assert.isNumber(result[0].decimalCount) + }) + }) + + describe('balancesForAddressBulk()', () => { + const balancesForAddressBulk = + slpRoute.balancesForAddressBulk + + it('should throw 400 if addresses is empty', async () => { + const result = await balancesForAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'addresses needs to be an array') + }) + + it('should throw 400 if address is invalid', async () => { + req.body.addresses = ['badAddress'] + + const result = await balancesForAddressBulk(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.body.addresses = [ + 'slptest:qzcvpw3ah7r880d49wsqzrsl90pg0rqjjurmj3g4nk' + ] + + const result = await balancesForAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Invalid') + }) + + it('should throw 5XX error when network issues', async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = 'http://fakeurl/api/' + + req.body.addresses = [ + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + ] + + const result = await balancesForAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 500 or greater expected.' + ) + assert.include( + result.error, + 'Network error: Could not communicate', + 'Error message expected' + ) + }) + it('returns proper error when downstream service stalls', async () => { + // Mock the timeout error. + sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) + + req.body.addresses = [ + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + ] + const result = await balancesForAddressBulk(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.addresses = [ + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + ] + const result = await balancesForAddressBulk(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' + ) + }) + // Only run as an integration test. Too complex to stub accurately. + if (process.env.TEST !== 'unit') { + it('should get token balance for an address', async () => { + req.body.addresses = [ + 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' + ] + + const result = await balancesForAddressBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + 'tokenId', + 'balance', + 'balanceString', + 'slpAddress', + 'decimalCount' + ]) + }) + } + }) + + describe('validateBulk()', () => { + const validateBulk = slpRoute.validateBulk + + it('should throw 400 if txid array is empty', async () => { + const result = await validateBulk(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 validateBulk(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 validateBulk(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 validateBulk(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.mockSingleValidTxid + }) + } + + req.body.txids = [ + '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d' + ] + + const result = await validateBulk(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.mockTwoValidTxid + }) + } + + req.body.txids = [ + '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d', + '552112f9e458dc7d1d8b328b0a6685e8af74a64b60b6846e7c86407f27f47e42' + ] + + const result = await validateBulk(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.mockTwoRedundentTxid + }) + } + + req.body.txids = [ + 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56', + 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56' + ] + + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ['txid', 'valid']) + assert.equal(result.length, 2) + }) + }) + + describe('tokenStatsSingle()', () => { + const tokenStatsSingle = slpRoute.tokenStats + + it('should throw 400 if tokenID is empty', async () => { + req.params.tokenId = '' + const result = await tokenStatsSingle(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' }) + + req.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await tokenStatsSingle(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.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await tokenStatsSingle(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 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 tokenStatsSingle(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()', () => { + const balancesForTokenSingle = + slpRoute.balancesForTokenSingle + + it('should throw 400 if tokenID is empty', async () => { + req.params.tokenId = '' + const result = await balancesForTokenSingle(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' }) + + req.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await balancesForTokenSingle(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.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await balancesForTokenSingle(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 get balances for tokenId', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox.stub(slpRoute.axios, 'request').resolves({ + data: { + g: [mockData.mockBalance] + } + }) + } + + req.params.tokenId = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await balancesForTokenSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.property(result[0], 'tokenId') + assert.property(result[0], 'slpAddress') + assert.property(result[0], 'tokenBalanceString') + }) + }) + + describe('txDetails()', () => { + const txDetails = slpRoute.txDetails + + it('should throw 400 if txid is empty', async () => { + const result = await txDetails(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'txid can not be empty') + }) + + it('should throw 400 for malformed txid', async () => { + req.params.txid = + '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457' + + const result = await txDetails(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'This is not a txid') + }) + + it('should throw 400 for non-existant txid', async () => { + // Integration test + if (process.env.TEST !== 'unit') { + req.params.txid = + '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333' + + const result = await txDetails(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'TXID not found') + } + }) + it('returns proper error when downstream service stalls', async () => { + // Mock the timeout error. + sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) + + req.params.txid = + '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333' + + const result = await txDetails(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.params.txid = + '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333' + + const result = await txDetails(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' + ) + }) + if (process.env.TEST === 'integration') { + it('should get tx details with token info', async () => { + // TODO: add mocking for unit testing. How do I mock reponse form SLPDB + // since it's not an object? + + // if (process.env.TEST === "unit") { + // // Mock the slpjs library for unit tests. + // pathStub.BitboxNetwork = slpjsMock.BitboxNetwork + // txDetails = slpRouteStub.testableComponents.txDetails + // } + + req.params.txid = + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' + + const result = await txDetails(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result, ['tokenIsValid', 'tokenInfo']) + }) + } + }) + + describe('txsTokenIdAddressSingle()', () => { + const txsTokenIdAddressSingle = + slpRoute.txsTokenIdAddressSingle + + it('should throw 400 if tokenId is empty', async () => { + req.params.tokenId = '' + const result = await txsTokenIdAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'tokenId can not be empty') + }) + + it('should throw 400 if address is empty', async () => { + req.params.tokenId = + '495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a' + req.params.address = '' + const result = await txsTokenIdAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'address 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' }) + + req.params.tokenId = + '7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796' + req.params.address = 'slptest:qpwa35xq0q0cnmdu0rwzkct369hddzsqpsqdzw6h9h' + + const result = await txsTokenIdAddressSingle(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.params.tokenId = + '7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796' + req.params.address = 'slptest:qpwa35xq0q0cnmdu0rwzkct369hddzsqpsqdzw6h9h' + + const result = await txsTokenIdAddressSingle(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' + ) + }) + }) +}) + +/* describe("list()", () => { // list route handler const list = slpRoute.testableComponents.list @@ -429,180 +1104,7 @@ describe('#SLP', () => { ]) }) }) -*/ - describe('balancesForAddress()', () => { - const balancesForAddress = slpRoute.testableComponents.balancesForAddress - it('should throw 400 if address is empty', async () => { - const result = await balancesForAddress(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 balancesForAddress(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:qzcvpw3ah7r880d49wsqzrsl90pg0rqjjurmj3g4nk' - - const result = await balancesForAddress(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'Invalid') - }) - - it('should throw 5XX error when network issues', async () => { - // Save the existing SLPDB_URL. - const savedUrl2 = process.env.SLPDB_URL - - // Manipulate the URL to cause a 500 network error. - process.env.SLPDB_URL = 'http://fakeurl/api/' - - req.params.address = - 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' - - const result = await balancesForAddress(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.SLPDB_URL = savedUrl2 - - assert.isAbove( - res.statusCode, - 499, - 'HTTP status code 500 or greater expected.' - ) - assert.include( - result.error, - 'Network error: Could not communicate', - 'Error message expected' - ) - }) - - it('should get token balance for an address', async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .times(2) - .reply(200, mockData.mockSingleAddress) - } - - req.params.address = - 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' - - const result = await balancesForAddress(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - - assert.property(result[0], 'tokenId') - assert.property(result[0], 'balanceString') - assert.property(result[0], 'slpAddress') - assert.property(result[0], 'balance') - assert.property(result[0], 'decimalCount') - - assert.isNumber(result[0].balance) - assert.isNumber(result[0].decimalCount) - }) - }) - - describe('balancesForAddressBulk()', () => { - const balancesForAddressBulk = - slpRoute.testableComponents.balancesForAddressBulk - - it('should throw 400 if addresses is empty', async () => { - const result = await balancesForAddressBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'addresses needs to be an array') - }) - - it('should throw 400 if address is invalid', async () => { - req.body.addresses = ['badAddress'] - - const result = await balancesForAddressBulk(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.body.addresses = [ - 'slptest:qzcvpw3ah7r880d49wsqzrsl90pg0rqjjurmj3g4nk' - ] - - const result = await balancesForAddressBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'Invalid') - }) - - it('should throw 5XX error when network issues', async () => { - // Save the existing SLPDB_URL. - const savedUrl2 = process.env.SLPDB_URL - - // Manipulate the URL to cause a 500 network error. - process.env.SLPDB_URL = 'http://fakeurl/api/' - - req.body.addresses = [ - 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' - ] - - const result = await balancesForAddressBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.SLPDB_URL = savedUrl2 - - assert.isAbove( - res.statusCode, - 499, - 'HTTP status code 500 or greater expected.' - ) - assert.include( - result.error, - 'Network error: Could not communicate', - 'Error message expected' - ) - }) - - // Only run as an integration test. Too complex to stub accurately. - if (process.env.TEST !== 'unit') { - it('should get token balance for an address', async () => { - req.body.addresses = [ - 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn' - ] - - const result = await balancesForAddressBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isArray(result[0]) - assert.hasAnyKeys(result[0][0], [ - 'tokenId', - 'balance', - 'balanceString', - 'slpAddress', - 'decimalCount' - ]) - }) - } - }) - /* describe("balancesForAddressByTokenID()", () => { const balancesForAddressByTokenID = slpRoute.testableComponents.balancesForAddressByTokenID @@ -809,271 +1311,11 @@ describe('#SLP', () => { ]) }) }) -*/ - describe('validateBulk()', () => { - const validateBulk = slpRoute.testableComponents.validateBulk - it('should throw 400 if txid array is empty', async () => { - const result = await validateBulk(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 validateBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'Array too large') - }) - - it('should validate array with single element', async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === 'unit') { - nock(`${process.env.SLPDB_URL}`) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockSingleValidTxid) - } - - req.body.txids = [ - '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d' - ] - - const result = await validateBulk(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') { - nock(`${process.env.SLPDB_URL}`) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockTwoValidTxid) - } - - req.body.txids = [ - '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d', - '552112f9e458dc7d1d8b328b0a6685e8af74a64b60b6846e7c86407f27f47e42' - ] - - const result = await validateBulk(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') { - nock(`${process.env.SLPDB_URL}`) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockTwoRedundentTxid) - } - - req.body.txids = [ - 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56', - 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56' - ] - - const result = await validateBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - assert.hasAllKeys(result[0], ['txid', 'valid']) - assert.equal(result.length, 2) - }) - }) - - describe('tokenStatsSingle()', () => { - const tokenStatsSingle = slpRoute.testableComponents.tokenStats - - it('should throw 400 if tokenID is empty', async () => { - req.params.tokenId = '' - const result = await tokenStatsSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'tokenId can not be empty') - }) - // - it('should get token stats for tokenId', async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === 'unit') { - nock(`${process.env.SLPDB_URL}`) - .get(uri => uri.includes('/')) - .reply(200, { - t: [ - { - tokenDetails: mockData.mockTokenDetails, - tokenStats: mockData.mockTokenStats - } - ] - }) - } - - req.params.tokenId = - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - - const result = await tokenStatsSingle(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()', () => { - const balancesForTokenSingle = - slpRoute.testableComponents.balancesForTokenSingle - - it('should throw 400 if tokenID is empty', async () => { - req.params.tokenId = '' - const result = await balancesForTokenSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'tokenId can not be empty') - }) - - it('should get balances for tokenId', async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === 'unit') { - nock(`${process.env.SLPDB_URL}`) - .get(uri => uri.includes('/')) - .reply(200, { - g: [mockData.mockBalance] - }) - } - - req.params.tokenId = - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - - const result = await balancesForTokenSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - assert.property(result[0], 'tokenId') - assert.property(result[0], 'slpAddress') - assert.property(result[0], 'tokenBalanceString') - }) - }) - - describe('txDetails()', () => { - const txDetails = slpRoute.testableComponents.txDetails - - it('should throw 400 if txid is empty', async () => { - const result = await txDetails(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'txid can not be empty') - }) - - it('should throw 400 for malformed txid', async () => { - req.params.txid = - '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457' - - const result = await txDetails(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'This is not a txid') - }) - - it('should throw 400 for non-existant txid', async () => { - // Integration test - if (process.env.TEST !== 'unit') { - req.params.txid = - '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333' - - const result = await txDetails(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'TXID not found') - } - }) - - if (process.env.TEST === 'integration') { - it('should get tx details with token info', async () => { - // TODO: add mocking for unit testing. How do I mock reponse form SLPDB - // since it's not an object? - - // if (process.env.TEST === "unit") { - // // Mock the slpjs library for unit tests. - // pathStub.BitboxNetwork = slpjsMock.BitboxNetwork - // txDetails = slpRouteStub.testableComponents.txDetails - // } - - req.params.txid = - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7' - - const result = await txDetails(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.hasAnyKeys(result, ['tokenIsValid', 'tokenInfo']) - }) - } - }) - - describe('txsTokenIdAddressSingle()', () => { + describe('txsTokenIdAddressSingle()', () => { const txsTokenIdAddressSingle = - slpRoute.testableComponents.txsTokenIdAddressSingle + slpRoute.txsTokenIdAddressSingle - it('should throw 400 if tokenId is empty', async () => { - req.params.tokenId = '' - const result = await txsTokenIdAddressSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'tokenId can not be empty') - }) - - it('should throw 400 if address is empty', async () => { - req.params.tokenId = - '495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a' - req.params.address = '' - const result = await txsTokenIdAddressSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ['error']) - assert.include(result.error, 'address can not be empty') - }) - /* it("should get tx details with tokenId and address", async () => { if (process.env.TEST === "unit") { nock(`${process.env.SLPDB_URL}`) @@ -1096,6 +1338,7 @@ describe('#SLP', () => { assert.hasAnyKeys(result[0], ["txid", "tokenDetails"]) }) -*/ - }) + }) + +*/