Merge pull request #138 from christroutner/slp-refactor

Slp refactor
This commit is contained in:
Chris Troutner
2020-05-12 17:43:44 -07:00
committed by GitHub
7 changed files with 999 additions and 277 deletions
+2 -1
View File
@@ -17,8 +17,9 @@
"test": "npm run lint && npm run test-v3",
"lint": "standard --env mocha --fix",
"test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/",
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/encryption.js",
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/slp.js",
"test:integration": "mocha test/v3/integration",
"test:integration:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs"
+351
View File
@@ -0,0 +1,351 @@
const axios = require('axios')
const SLPSDK = require('@chris.troutner/bch-js')
const SLP = new SLPSDK()
class Slpdb {
// Gets transaction history for all tokens for an address. Can also specify
// block height, but defaults to 0.
async getHistoricalSlpTransactions (addressList, fromBlock = 0) {
// Build SLPDB or query from addressList
const orQueryArray = []
for (const address of addressList) {
const cashAddress = SLP.SLP.Address.toCashAddress(address)
const slpAddress = SLP.SLP.Address.toSLPAddress(address)
const cashQuery = {
'in.e.a': cashAddress.slice(12)
}
const slpQuery = {
'slp.detail.outputs.address': slpAddress
}
orQueryArray.push(cashQuery)
orQueryArray.push(slpQuery)
}
const query = {
v: 3,
q: {
find: {
db: ['c', 'u'],
$query: {
$or: orQueryArray,
'slp.valid': true,
'blk.i': {
$not: {
$lte: fromBlock
}
}
},
$orderby: {
'blk.i': -1
}
},
project: {
_id: 0,
'tx.h': 1,
'in.i': 1,
'in.e': 1,
'out.e': 1,
'out.a': 1,
'slp.detail': 1,
blk: 1
},
limit: 500
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
let transactions = []
// Add confirmed transactions
if (result.data && result.data.c) {
transactions = transactions.concat(result.data.c)
}
// Add unconfirmed transactions
if (result.data && result.data.u) {
transactions = transactions.concat(result.data.u)
}
return transactions
}
async getTokenStats (tokenId) {
const [
totalMinted,
totalBurned,
tokenDetails,
circulatingSupply
] = await Promise.all([
this.getTotalMinted(tokenId),
this.getTotalBurned(tokenId),
this.getTokenDetails(tokenId),
this.getTotalCirculating(tokenId)
])
tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted
tokenDetails.totalBurned = totalBurned
// tokenDetails.circulatingSupply =
// tokenDetails.totalMinted - tokenDetails.totalBurned
tokenDetails.circulatingSupply = circulatingSupply
return tokenDetails
}
generateCredentials () {
// Generate the Basic Authentication header for a private instance of SLPDB.
const SLPDB_PASS = process.env.SLPDB_PASS
? process.env.SLPDB_PASS
: 'BITBOX'
const username = 'BITBOX'
const password = SLPDB_PASS
const combined = `${username}:${password}`
var base64Credential = Buffer.from(combined).toString('base64')
var readyCredential = `Basic ${base64Credential}`
const options = {
headers: {
authorization: readyCredential,
timeout: 30000
}
}
return options
}
async runQuery (query) {
const queryString = JSON.stringify(query)
const queryBase64 = Buffer.from(queryString).toString('base64')
const url = `${process.env.SLPDB_URL}q/${queryBase64}`
const options = this.generateCredentials()
const response = await axios.get(url, options)
return response
}
async getTotalMinted (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'BATON_SPENT_IN_MINT',
'BATON_UNSPENT',
'BATON_SPENT_NOT_IN_MINT'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTotalCirculating (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs': {
$elemMatch: {
status: 'UNSPENT',
slpAmount: { $gte: 0 }
}
}
}
},
{ $unwind: '$graphTxn.outputs' },
{
$match: {
'graphTxn.outputs.status': 'UNSPENT',
'graphTxn.outputs.slpAmount': { $gte: 0 }
}
},
{
$group: {
_id: null,
circulating_supply: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 100000
}
}
const result = await this.runQuery(query)
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].circulating_supply)
}
async getTotalBurned (tokenId) {
const query = {
v: 3,
q: {
db: ['g'],
aggregate: [
{
$match: {
'tokenDetails.tokenIdHex': tokenId,
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$unwind: '$graphTxn.outputs'
},
{
$match: {
'graphTxn.outputs.status': {
$in: [
'SPENT_NON_SLP',
'BATON_SPENT_INVALID_SLP',
'SPENT_INVALID_SLP',
'BATON_SPENT_NON_SLP',
'MISSING_BCH_VOUT',
'BATON_MISSING_BCH_VOUT',
'BATON_SPENT_NOT_IN_MINT',
'EXCESS_INPUT_BURNED'
]
}
}
},
{
$group: {
_id: null,
count: {
$sum: '$graphTxn.outputs.slpAmount'
}
}
}
],
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.g.length) {
return 0
}
return parseFloat(result.data.g[0].count)
}
async getTokenDetails (tokenId) {
const query = {
v: 3,
q: {
db: ['t'],
find: {
$query: {
'tokenDetails.tokenIdHex': tokenId
}
},
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
limit: 1
}
}
const result = await this.runQuery(query)
if (!result.data.t.length) {
throw new Error('Token could not be found')
}
const token = this.formatTokenOutput(result.data.t[0])
return token
}
formatTokenOutput (token) {
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
token.tokenDetails.id = token.tokenDetails.tokenIdHex
delete token.tokenDetails.tokenIdHex
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
delete token.tokenDetails.documentSha256Hex
token.tokenDetails.initialTokenQty = parseFloat(
token.tokenDetails.genesisOrMintQuantity
)
delete token.tokenDetails.genesisOrMintQuantity
delete token.tokenDetails.transactionType
delete token.tokenDetails.batonVout
delete token.tokenDetails.sendOutputs
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
token.tokenDetails.blockLastActiveMint =
token.tokenStats.block_last_active_mint
token.tokenDetails.txnsSinceGenesis =
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses =
token.tokenStats.qty_valid_token_addresses
token.tokenDetails.mintingBatonStatus =
token.tokenStats.minting_baton_status
delete token.tokenStats.block_last_active_send
delete token.tokenStats.block_last_active_mint
delete token.tokenStats.qty_valid_txns_since_genesis
delete token.tokenStats.qty_valid_token_addresses
token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix
delete token.tokenDetails.timestamp_unix
return token.tokenDetails
}
}
module.exports = Slpdb
+267 -222
View File
@@ -7,6 +7,9 @@ const BigNumber = require('bignumber.js')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const Slpdb = require('./services/slpdb')
// const strftime = require('strftime')
const wlogger = require('../../util/winston-logging')
@@ -50,6 +53,7 @@ class Slp {
_this.BigNumber = BigNumber
_this.bchjs = bchjs
_this.rawTransactions = rawTransactions
_this.slpdb = new Slpdb()
_this.router = router
@@ -60,14 +64,24 @@ class Slp {
_this.router.get('/balancesForAddress/:address', _this.balancesForAddress)
_this.router.post('/balancesForAddress', _this.balancesForAddressBulk)
_this.router.get('/balancesForToken/:tokenId', _this.balancesForTokenSingle)
_this.router.get('/balance/:address/:tokenId', _this.balancesForAddressByTokenID)
_this.router.get(
'/balance/:address/:tokenId',
_this.balancesForAddressByTokenID
)
_this.router.get('/convert/:address', _this.convertAddressSingle)
_this.router.post('/convert', _this.convertAddressBulk)
_this.router.post('/validateTxid', _this.validateBulk)
_this.router.get('/validateTxid/:txid', _this.validateSingle)
_this.router.get('/txDetails/:txid', _this.txDetails)
_this.router.get('/tokenStats/:tokenId', _this.tokenStats)
_this.router.get('/transactions/:tokenId/:address', _this.txsTokenIdAddressSingle)
_this.router.get(
'/transactions/:tokenId/:address',
_this.txsTokenIdAddressSingle
)
_this.router.get(
'/transactionHistoryAllTokens/:address',
_this.txsByAddressSingle
)
}
// DRY error handler.
@@ -98,18 +112,24 @@ class Slp {
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
token.tokenStats.block_last_active_send
token.tokenDetails.blockLastActiveMint =
token.tokenStats.block_last_active_mint
token.tokenStats.block_last_active_mint
token.tokenDetails.txnsSinceGenesis =
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses = token.tokenStats.qty_valid_token_addresses
token.tokenDetails.totalMinted = parseFloat(token.tokenStats.qty_token_minted)
token.tokenDetails.totalBurned = parseFloat(token.tokenStats.qty_token_burned)
token.tokenStats.qty_valid_txns_since_genesis
token.tokenDetails.validAddresses =
token.tokenStats.qty_valid_token_addresses
token.tokenDetails.totalMinted = parseFloat(
token.tokenStats.qty_token_minted
)
token.tokenDetails.totalBurned = parseFloat(
token.tokenStats.qty_token_burned
)
token.tokenDetails.circulatingSupply = parseFloat(
token.tokenStats.qty_token_circulating_supply
)
token.tokenDetails.mintingBatonStatus = token.tokenStats.minting_baton_status
token.tokenDetails.mintingBatonStatus =
token.tokenStats.minting_baton_status
delete token.tokenStats.block_last_active_send
delete token.tokenStats.block_last_active_mint
@@ -123,17 +143,17 @@ class Slp {
}
/**
* @api {get} /slp/list/{tokenId} List single SLP token by id.
* @apiName List single SLP token by id.
* @apiGroup SLP
* @apiDescription Returns the list single SLP token by id.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json"
*
*
*/
* @api {get} /slp/list/{tokenId} List single SLP token by id.
* @apiName List single SLP token by id.
* @apiGroup SLP
* @apiDescription Returns the list single SLP token by id.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/list/259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" -H "accept:application/json"
*
*
*/
async listSingleToken (req, res, next) {
try {
const tokenId = req.params.tokenId
@@ -156,17 +176,17 @@ class Slp {
}
/**
* @api {post} /slp/list/ List Bulk SLP token .
* @apiName List Bulk SLP token.
* @apiGroup SLP
* @apiDescription Returns the list bulk SLP token by id.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/list" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenIds":["7380843cd1089a1a01783f86af37734dc99667a1cdc577391b5f6ea42fc1bfb4","9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0"]}'
*
*
*/
* @api {post} /slp/list/ List Bulk SLP token .
* @apiName List Bulk SLP token.
* @apiGroup SLP
* @apiDescription Returns the list bulk SLP token by id.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/list" -H "accept:application/json" -H "Content-Type: application/json" -d '{"tokenIds":["7380843cd1089a1a01783f86af37734dc99667a1cdc577391b5f6ea42fc1bfb4","9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0"]}'
*
*
*/
async listBulkToken (req, res, next) {
try {
const tokenIds = req.body.tokenIds
@@ -216,14 +236,14 @@ class Slp {
const txids = []
if (tokenRes.data.t.length) {
tokenRes.data.t.forEach(token => {
tokenRes.data.t.forEach((token) => {
txids.push(token.tokenDetails.tokenIdHex)
token = _this.formatTokenOutput(token)
formattedTokens.push(token.tokenDetails)
})
}
tokenIds.forEach(tokenId => {
tokenIds.forEach((tokenId) => {
if (!txids.includes(tokenId)) {
formattedTokens.push({
id: tokenId,
@@ -277,14 +297,14 @@ class Slp {
const formattedTokens = []
if (tokenRes.data.t.length) {
tokenRes.data.t.forEach(token => {
tokenRes.data.t.forEach((token) => {
token = _this.formatTokenOutput(token)
formattedTokens.push(token.tokenDetails)
})
}
let t
formattedTokens.forEach(token => {
formattedTokens.forEach((token) => {
if (token.id === tokenId) t = token
})
@@ -304,17 +324,17 @@ class Slp {
}
/**
* @api {get} /slp/balancesForAddress/{address} List SLP balance for address.
* @apiName List SLP balance for address.
* @apiGroup SLP
* @apiDescription Returns List SLP balance for address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balancesForAddress/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json"
*
*
*/
* @api {get} /slp/balancesForAddress/{address} List SLP balance for address.
* @apiName List SLP balance for address.
* @apiGroup SLP
* @apiDescription Returns List SLP balance for address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balancesForAddress/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json"
*
*
*/
// Retrieve token balances for all tokens for a single address.
async balancesForAddress (req, res, next) {
try {
@@ -342,7 +362,7 @@ class Slp {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
@@ -416,7 +436,7 @@ class Slp {
const tokenIds = []
if (tokenRes.data.g.length > 0) {
tokenRes.data.g = tokenRes.data.g.map(token => {
tokenRes.data.g = tokenRes.data.g.map((token) => {
token.tokenId = token._id
tokenIds.push(token.tokenId)
token.balance = parseFloat(token.balanceString)
@@ -426,7 +446,7 @@ class Slp {
return token
})
const promises = tokenIds.map(async tokenId => {
const promises = tokenIds.map(async (tokenId) => {
const query2 = {
v: 3,
q: {
@@ -454,7 +474,6 @@ class Slp {
baseURL: url2,
headers: options.headers,
timeout: options.timeout
}
const tokenRes2 = await _this.axios.request(opt)
// console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`)
@@ -464,8 +483,8 @@ class Slp {
const details = await _this.axios.all(promises)
tokenRes.data.g = tokenRes.data.g.map(token => {
details.forEach(detail => {
tokenRes.data.g = tokenRes.data.g.map((token) => {
details.forEach((detail) => {
if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) {
token.decimalCount = detail.t[0].tokenDetails.decimals
}
@@ -489,17 +508,17 @@ class Slp {
}
/**
* @api {post} /slp/balancesForAddress List SLP balances for an array of addresses.
* @apiName List SLP balances for an array of addresses.
* @apiGroup SLP
* @apiDescription Returns SLP balances for an array of addresses.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json"
*
*
*/
* @api {post} /slp/balancesForAddress List SLP balances for an array of addresses.
* @apiName List SLP balances for an array of addresses.
* @apiGroup SLP
* @apiDescription Returns SLP balances for an array of addresses.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json"
*
*
*/
async balancesForAddressBulk (req, res, next) {
try {
const addresses = req.body.addresses
@@ -550,7 +569,7 @@ class Slp {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
}
@@ -559,7 +578,7 @@ class Slp {
// Collect an array of promises, one for each request to slpserve.
// This is a nested array of promises.
const balancesPromises = addresses.map(async address => {
const balancesPromises = addresses.map(async (address) => {
const query = {
v: 3,
q: {
@@ -622,7 +641,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
const tokenRes = await _this.axios.request(opt)
// console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
@@ -630,7 +648,7 @@ class Slp {
const tokenIds = []
if (tokenRes.data.g.length > 0) {
tokenRes.data.g = tokenRes.data.g.map(token => {
tokenRes.data.g = tokenRes.data.g.map((token) => {
token.tokenId = token._id
tokenIds.push(token.tokenId)
token.balance = parseFloat(token.balanceString)
@@ -640,7 +658,7 @@ class Slp {
}
// Collect another array of promises.
const promises = tokenIds.map(async tokenId => {
const promises = tokenIds.map(async (tokenId) => {
const query2 = {
v: 3,
q: {
@@ -667,7 +685,6 @@ class Slp {
baseURL: url2,
headers: options.headers,
timeout: options.timeout
}
const tokenRes2 = await _this.axios.request(opt)
// console.log(`tokenRes2.data: ${JSON.stringify(tokenRes2.data, null, 2)}`)
@@ -678,8 +695,8 @@ class Slp {
// Wait for all the promises to resolve.
const details = await Promise.all(promises)
tokenRes.data.g = tokenRes.data.g.map(token => {
details.forEach(detail => {
tokenRes.data.g = tokenRes.data.g.map((token) => {
details.forEach((detail) => {
if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) {
token.decimalCount = detail.t[0].tokenDetails.decimals
}
@@ -706,17 +723,17 @@ class Slp {
}
/**
* @api {get} /slp/balancesForToken/{TokenId} List SLP addresses and balances for tokenId.
* @apiName List SLP addresses and balances for tokenId.
* @apiGroup SLP
* @apiDescription Returns List SLP addresses and balances for tokenId.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json"
*
*
*/
* @api {get} /slp/balancesForToken/{TokenId} List SLP addresses and balances for tokenId.
* @apiName List SLP addresses and balances for tokenId.
* @apiGroup SLP
* @apiDescription Returns List SLP addresses and balances for tokenId.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balancesForToken/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json"
*
*
*/
// Retrieve token balances for all addresses by single tokenId.
async balancesForTokenSingle (req, res, next) {
try {
@@ -785,7 +802,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
// Get data from SLPDB.
const tokenRes = await _this.axios.request(opt)
@@ -814,17 +830,17 @@ class Slp {
}
/**
* @api {get} /slp/balance/{address}/{TokenId} List single slp token balance for address.
* @apiName List single slp token balance for address.
* @apiGroup SLP
* @apiDescription Returns List single slp token balance for address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json"
*
*
*/
* @api {get} /slp/balance/{address}/{TokenId} List single slp token balance for address.
* @apiName List single slp token balance for address.
* @apiGroup SLP
* @apiDescription Returns List single slp token balance for address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/balance/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m/1cda254d0a995c713b7955298ed246822bee487458cd9747a91d9e81d9d28125" -H "accept:application/json"
*
*
*/
// Retrieve token balances for a single token class, for a single address.
async balancesForAddressByTokenID (req, res, next) {
try {
@@ -858,7 +874,7 @@ class Slp {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
@@ -928,7 +944,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
const tokenRes = await _this.axios.request(opt)
console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
@@ -943,7 +958,7 @@ class Slp {
}
if (tokenRes.data.g.length > 0) {
tokenRes.data.g.forEach(async token => {
tokenRes.data.g.forEach(async (token) => {
if (token._id === tokenId) {
resVal = {
cashAddress: _this.bchjs.SLP.Address.toCashAddress(slpAddr),
@@ -979,17 +994,17 @@ class Slp {
}
/**
* @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy.
* @apiName Convert address to slpAddr, cashAddr and legacy.
* @apiGroup SLP
* @apiDescription Convert address to slpAddr, cashAddr and legacy.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json"
*
*
*/
* @api {get} /slp/convert/{address} Convert address to slpAddr, cashAddr and legacy.
* @apiName Convert address to slpAddr, cashAddr and legacy.
* @apiGroup SLP
* @apiDescription Convert address to slpAddr, cashAddr and legacy.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/convert/simpleledger:qz9tzs6d5097ejpg279rg0rnlhz546q4fsnck9wh5m" -H "accept:application/json"
*
*
*/
async convertAddressSingle (req, res, next) {
try {
const address = req.params.address
@@ -1009,7 +1024,9 @@ class Slp {
}
obj.slpAddress = slpAddr
obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(
obj.cashAddress
)
res.status(200)
return res.json(obj)
@@ -1023,17 +1040,17 @@ class Slp {
}
/**
* @api {post} /slp/convert/ Convert multiple addresses to cash, legacy and simpleledger format.
* @apiName Convert multiple addresses to cash, legacy and simpleledger format.
* @apiGroup SLP
* @apiDescription Convert multiple addresses to cash, legacy and simpleledger format.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}'
*
*
*/
* @api {post} /slp/convert/ Convert multiple addresses to cash, legacy and simpleledger format.
* @apiName Convert multiple addresses to cash, legacy and simpleledger format.
* @apiGroup SLP
* @apiDescription Convert multiple addresses to cash, legacy and simpleledger format.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/convert" -H "accept:application/json" -H "Content-Type: application/json" -d '{"addresses":["simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l"]}'
*
*
*/
async convertAddressBulk (req, res, next) {
const addresses = req.body.addresses
@@ -1073,7 +1090,9 @@ class Slp {
}
obj.slpAddress = slpAddr
obj.cashAddress = _this.bchjs.SLP.Address.toCashAddress(slpAddr)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(obj.cashAddress)
obj.legacyAddress = _this.bchjs.SLP.Address.toLegacyAddress(
obj.cashAddress
)
convertedAddresses.push(obj)
}
@@ -1083,17 +1102,17 @@ class Slp {
}
/**
* @api {post} /slp/validateTxid/ Validate multiple SLP transactions by txid.
* @apiName Validate multiple SLP transactions by txid.
* @apiGroup SLP
* @apiDescription Validate multiple SLP transactions by txid.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/validateTxid" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}'
*
*
*/
* @api {post} /slp/validateTxid/ Validate multiple SLP transactions by txid.
* @apiName Validate multiple SLP transactions by txid.
* @apiGroup SLP
* @apiDescription Validate multiple SLP transactions by txid.
*
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/slp/validateTxid" -H "accept:application/json" -H "Content-Type: application/json" -d '{"txids":["f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a","fb0eeaa501a6e1acb721669c62a3f70741f48ae0fd7f4b8e1d72088785c51952"]}'
*
*
*/
async validateBulk (req, res, next) {
try {
const txids = req.body.txids
@@ -1137,7 +1156,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
const tokenRes = await _this.axios.request(opt)
// console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`)
@@ -1149,7 +1167,7 @@ class Slp {
const tokenIds = []
if (concatArray.length > 0) {
concatArray.forEach(token => {
concatArray.forEach((token) => {
tokenIds.push(token.tx.h) // txid
const validationResult = {
@@ -1167,7 +1185,7 @@ class Slp {
// If a user-provided txid doesn't exist in the data, add it with
// valid:false property.
txids.forEach(txid => {
txids.forEach((txid) => {
if (!tokenIds.includes(txid)) {
formattedTokens.push({
txid: txid,
@@ -1185,7 +1203,7 @@ class Slp {
const thisTxid = txids[i]
// Find the element that matches the current txid.
const elem = formattedTokens.filter(x => x.txid === thisTxid)
const elem = formattedTokens.filter((x) => x.txid === thisTxid)
newOutput.push(elem[0])
}
@@ -1204,17 +1222,17 @@ class Slp {
}
/**
* @api {get} /slp/validateTxid/{txid} Validate single SLP transaction by txid.
* @apiName Validate single SLP transaction by txid.
* @apiGroup SLP
* @apiDescription Validate single SLP transaction by txid.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json"
*
*
*/
* @api {get} /slp/validateTxid/{txid} Validate single SLP transaction by txid.
* @apiName Validate single SLP transaction by txid.
* @apiGroup SLP
* @apiDescription Validate single SLP transaction by txid.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/validateTxid/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json"
*
*
*/
async validateSingle (req, res, next) {
try {
const txid = req.params.txid
@@ -1249,7 +1267,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
// Get data from SLPDB.
const tokenRes = await _this.axios.request(opt)
@@ -1267,7 +1284,9 @@ class Slp {
txid: concatArray[0].tx.h,
valid: concatArray[0].slp.valid
}
if (!result.valid) result.invalidReason = concatArray[0].slp.invalidReason
if (!result.valid) {
result.invalidReason = concatArray[0].slp.invalidReason
}
}
res.status(200)
@@ -1286,17 +1305,17 @@ class Slp {
// }
/**
* @api {get} /slp/txDetails/{txid} SLP transaction details.
* @apiName SLP transaction details.
* @apiGroup SLP
* @apiDescription Transaction details on a token transfer.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json"
*
*
*/
* @api {get} /slp/txDetails/{txid} SLP transaction details.
* @apiName SLP transaction details.
* @apiGroup SLP
* @apiDescription Transaction details on a token transfer.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/txDetails/8ab4ac5dea3f9024e3954ee5b61452955d659a34561f79ef62ac44e133d0980e" -H "accept:application/json"
*
*
*/
async txDetails (req, res, next) {
try {
// Validate input parameter
@@ -1332,7 +1351,6 @@ class Slp {
baseURL: url,
headers: options.headers,
timeout: options.timeout
}
// Get token data from SLPDB
const tokenRes = await _this.axios.request(opt)
@@ -1349,7 +1367,10 @@ class Slp {
// Get information on the transaction from Insight API.
// const retData = await transactions.transactionsFromInsight(txid)
const retData = await _this.rawTransactions.getRawTransactionsFromNode(txid, true)
const retData = await _this.rawTransactions.getRawTransactionsFromNode(
txid,
true
)
// console.log(`retData: ${JSON.stringify(retData, null, 2)}`)
// Return both the tx data from Insight and the formatted token information.
@@ -1374,60 +1395,29 @@ class Slp {
}
/**
* @api {get} /slp/tokenStats/{tokenId} List stats for a single slp token.
* @apiName List stats for a single slp token.
* @apiGroup SLP
* @apiDescription Return list stats for a single slp token.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json"
*
*
*/
* @api {get} /slp/tokenStats/{tokenId} List stats for a single slp token.
* @apiName List stats for a single slp token.
* @apiGroup SLP
* @apiDescription Return list stats for a single slp token.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/tokenStats/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0" -H "accept:application/json"
*
*
*/
async tokenStats (req, res, next) {
const tokenId = req.params.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({ error: 'tokenId can not be empty' })
}
try {
const query = {
v: 3,
q: {
db: ['t'],
find: {
$query: {
'tokenDetails.tokenIdHex': tokenId
}
},
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
limit: 10
}
const tokenId = req.params.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({ error: 'tokenId can not be empty' })
}
const s = JSON.stringify(query)
const b64 = Buffer.from(s).toString('base64')
const url = `${process.env.SLPDB_URL}q/${b64}`
const 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)
})
}
const tokenStats = await _this.slpdb.getTokenStats(tokenId)
res.status(200)
return res.json(formattedTokens[0])
return res.json(tokenStats)
} catch (err) {
wlogger.error('Error in slp.ts/tokenStats().', err)
return _this.errorHandler(err, res)
@@ -1436,17 +1426,17 @@ class Slp {
}
/**
* @api {get} /slp/transactions/{tokenId}/{address} SLP transactions by tokenId and address.
* @apiName SLP transactions by tokenId and address.
* @apiGroup SLP
* @apiDescription Transactions by tokenId and address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json"
*
*
*/
* @api {get} /slp/transactions/{tokenId}/{address} SLP transactions by tokenId and address.
* @apiName SLP transactions by tokenId and address.
* @apiGroup SLP
* @apiDescription Transactions by tokenId and address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/slp/transactions/9ba379fe8171176d4e7e6771d9a24cd0e044c7b788d5f86a3fdf80904832b2c0/simpleledger:qrxa0unrn67rtn85v7asfddhhth43ecnxua0antk2l" -H "accept:application/json"
*
*
*/
// Retrieve transactions by tokenId and address.
async txsTokenIdAddressSingle (req, res, next) {
try {
@@ -1547,7 +1537,7 @@ class Slp {
const tokenOutputs = transaction.slp.detail.outputs
const sendOutputs = ['0']
tokenOutputs.map(x => {
tokenOutputs.map((x) => {
const string = parseFloat(x.amount) * 100000000
sendOutputs.push(string.toString())
})
@@ -1564,6 +1554,61 @@ class Slp {
return obj
}
// Retrieve transactions by address.
async txsByAddressSingle (req, res, next) {
try {
// Validate the input data.
const address = req.params.address
if (!address || address === '') {
res.status(400)
return res.json({ error: 'address can not be empty' })
}
// Ensure the input is a valid BCH address.
try {
_this.bchjs.SLP.Address.toCashAddress(address)
} catch (err) {
res.status(400)
return res.json({
error: `Invalid BCH address. Double check your address is valid: ${address}`
})
}
// Ensure it is using the correct network.
const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address)
const networkIsValid = routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
const transactions = await _this.slpdb.getHistoricalSlpTransactions([
address
])
// console.log(`transactions: ${JSON.stringify(transactions, null, 2)}`)
res.status(200)
return res.json(transactions)
} catch (err) {
wlogger.error('Error in slp.ts/txsByAddressSingle().', err)
// Decode the error message.
const { msg, status } = routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({
error: `Error in /transactionHistoryAllTokens/:address: ${err.message}`
})
}
}
}
module.exports = Slp
+102
View File
@@ -0,0 +1,102 @@
/*
These integration tests need to be run against a live SLPDB. They query
against a live SLPDB and test the results against known token stats.
*/
'use strict'
const assert = require('chai').assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Exit if SLPDB URL is not defined.
if (!process.env.SLPDB_URL) {
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
}
const SLP = require('../../../src/routes/v3/slp')
const slp = new SLP()
const { mockReq, mockRes } = require('../mocks/express-mocks')
describe('#slp', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#tokenStats', () => {
it('should get token stats for token with no mint baton', async () => {
req.params.tokenId =
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
// 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
const result = await slp.tokenStats(req, res)
console.log(`result: ${util.inspect(result)}`)
// Assert that expected properties exist.
assert.property(result, 'decimals')
assert.property(result, 'timestamp')
assert.property(result, 'versionType')
assert.property(result, 'documentUri')
assert.property(result, 'symbol')
assert.property(result, 'name')
assert.property(result, 'containsBaton')
assert.property(result, 'id')
assert.property(result, 'documentHash')
assert.property(result, 'initialTokenQty')
assert.property(result, 'blockCreated')
assert.property(result, 'blockLastActiveSend')
assert.property(result, 'blockLastActiveMint')
assert.property(result, 'txnsSinceGenesis')
assert.property(result, 'validAddresses')
assert.property(result, 'mintingBatonStatus')
assert.property(result, 'timestampUnix')
assert.property(result, 'totalMinted')
assert.property(result, 'totalBurned')
assert.property(result, 'circulatingSupply')
// baton was never created.
assert.equal(result.containsBaton, false)
})
it('should get token stats for token with a mint baton', async () => {
req.params.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
const result = await slp.tokenStats(req, res)
console.log(`result: ${util.inspect(result)}`)
// Assert that expected properties exist.
assert.property(result, 'decimals')
assert.property(result, 'timestamp')
assert.property(result, 'versionType')
assert.property(result, 'documentUri')
assert.property(result, 'symbol')
assert.property(result, 'name')
assert.property(result, 'containsBaton')
assert.property(result, 'id')
assert.property(result, 'documentHash')
assert.property(result, 'initialTokenQty')
assert.property(result, 'blockCreated')
assert.property(result, 'blockLastActiveSend')
assert.property(result, 'blockLastActiveMint')
assert.property(result, 'txnsSinceGenesis')
assert.property(result, 'validAddresses')
assert.property(result, 'mintingBatonStatus')
assert.property(result, 'timestampUnix')
assert.property(result, 'totalMinted')
assert.property(result, 'totalBurned')
assert.property(result, 'circulatingSupply')
// baton was created.
assert.equal(result.containsBaton, true)
})
})
})
+31
View File
@@ -0,0 +1,31 @@
/*
These integration tests need to be run against a live SLPDB. They query
against a live SLPDB and test the results against known token stats.
*/
'use strict'
// const chai = require('chai')
// const assert = chai.assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Exit if SLPDB URL is not defined.
if (!process.env.SLPDB_URL) {
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
}
const SLPDB = require('../../../src/routes/v3/services/slpdb')
const slpdb = new SLPDB()
describe('#slpdb', () => {
describe('#getTotalCirculating', () => {
it('should get circulating supply', async () => {
const result = await slpdb.getTotalCirculating('b10677aef051b73e6b170c1c0824da33a3e0680ab5a01cd8d76aa77840fccfb4')
console.log('result: ', result)
})
})
})
+179 -1
View File
@@ -341,6 +341,183 @@ const mockTwoRedundentTxid = {
u: []
}
const mockTxHistory = [
{
tx: {
h: '3a7646b3976a8745928c7192c8dde989bfa275fa6d7bce950180ab8002cf6cef'
},
in: [
{
i: 0,
e: {
h: '7bdd586ebd1e5f3dfd5295ab6e896c48b25855ab77a72c035ce1e7aacc965c2d',
i: 1,
s: 'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==',
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
}
},
{
i: 1,
e: {
h: 'df49feff24dc34a10a44a0cbd7c908d964801ec7218512c84574fbce698535f0',
i: 3,
s: 'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=',
a: 'simpleledger:qpkpeqfslejw5pptzcy25h2jxhsc9k0vts43n26up0'
}
}
],
out: [
{
e: {
v: 0,
i: 0,
s: 'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g=='
}
},
{
e: {
v: 546,
i: 1,
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
}
},
{
e: {
v: 546,
i: 2,
s: 'dqkUgYSfxf90wVkERCiOMyh16iB4d92IrA==',
a: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl'
}
},
{
e: {
v: 21849,
i: 3,
s: 'dqkUxjCDbbIQGbn4E5D5IxJviX4qkiWIrA==',
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
}
}
],
slp: {
detail: {
decimals: 8,
tokenIdHex: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
transactionType: 'SEND',
versionType: 1,
documentUri: 'psfoundation.cash',
documentSha256Hex: null,
symbol: 'PSF',
name: 'Permissionless Software Foundation',
txnBatonVout: null,
txnContainsBaton: false,
outputs: [
{
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
amount: '0.1'
},
{
address: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl',
amount: '6.11833082'
}
]
}
},
blk: {
h: '000000000000000001a47bf337ca211c1e7218234d0d70117c64d6f213f5dacb',
i: 634833,
t: 1589300214
}
},
{
tx: {
h: '3b11b48cab1e7c8384facf482b3f6bfe659a58245ab4aa4147b42b5cb2a5fac5'
},
in: [
{
i: 0,
e: {
h: 'e4e1e1f6b502cbd42f69b919634cea3e37fc8595e6ca800e0a7d8f6dcdfb249e',
i: 3,
s: 'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
},
{
i: 1,
e: {
h: '438420345dd8b7fb4ea74aaf2e3090f44899bf3c71662243946a441de6dae720',
i: 2,
s: 'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
}
],
out: [
{
e: {
v: 0,
i: 0,
s: 'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA=='
}
},
{
e: {
v: 546,
i: 1,
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
}
},
{
e: {
v: 546,
i: 2,
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
},
{
e: {
v: 43074,
i: 3,
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
}
}
],
slp: {
detail: {
decimals: 2,
tokenIdHex: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
transactionType: 'SEND',
versionType: 1,
documentUri: 'troutsblog.com',
documentSha256Hex: null,
symbol: 'TROUT',
name: "Trout's test token",
txnBatonVout: null,
txnContainsBaton: false,
outputs: [
{
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
amount: '10000'
},
{
address: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7',
amount: '998990000'
}
]
}
},
blk: {
h: '000000000000000002a4ee2ed6fe8a764ffef0b2556f545ca0b3ef39ecd2b7c4',
i: 634832,
t: 1589297145
}
}
]
module.exports = {
mockList,
mockSingleToken,
@@ -355,5 +532,6 @@ module.exports = {
mockFoobar,
mockSingleValidTxid,
mockTwoValidTxid,
mockTwoRedundentTxid
mockTwoRedundentTxid,
mockTxHistory
}
+67 -53
View File
@@ -68,7 +68,7 @@ describe('#SLP', () => {
res = mockRes
// Explicitly reset the parmas and body.
// req.params = {}
req.params = {}
req.body = {}
req.query = {}
req.locals = {}
@@ -397,6 +397,7 @@ describe('#SLP', () => {
'Error message expected'
)
})
it('should validate array with single element', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
@@ -461,25 +462,24 @@ describe('#SLP', () => {
})
})
describe('tokenStatsSingle()', () => {
const tokenStatsSingle = slpRoute.tokenStats
describe('tokenStats()', () => {
it('should throw 400 if tokenID is empty', async () => {
req.params.tokenId = ''
const result = await tokenStatsSingle(req, res)
const result = await slpRoute.tokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'tokenId can not be empty')
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' })
sandbox.stub(slpRoute.slpdb, 'getTokenStats').throws({ code: 'ECONNABORTED' })
req.params.tokenId =
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
const result = await tokenStatsSingle(req, res)
const result = await slpRoute.tokenStats(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
@@ -492,12 +492,12 @@ describe('#SLP', () => {
it('returns proper error when downstream service is down', async () => {
// Mock the timeout error.
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNREFUSED' })
sandbox.stub(slpRoute.slpdb, 'getTokenStats').throws({ code: 'ECONNREFUSED' })
req.params.tokenId =
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
const result = await tokenStatsSingle(req, res)
const result = await slpRoute.tokenStats(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
@@ -507,50 +507,6 @@ describe('#SLP', () => {
'Error message expected'
)
})
//
it('should get token stats for tokenId', async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute.axios, 'request').resolves({
data: {
t: [
{
tokenDetails: mockData.mockTokenDetails,
tokenStats: mockData.mockTokenStats
}
]
}
})
}
req.params.tokenId =
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
const result = await 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()', () => {
@@ -773,6 +729,64 @@ describe('#SLP', () => {
)
})
})
describe('txsByAddressSingle()', () => {
const txsByAddressSingle = slpRoute.txsByAddressSingle
it('should throw 400 if address is missing', async () => {
const result = await txsByAddressSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'address can not be empty')
})
it('should throw 400 if address is empty', async () => {
req.params.address = ''
const result = await txsByAddressSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'address can not be empty')
})
it('should throw 400 if address is invalid', async () => {
req.params.address = 'badAddress'
const result = await txsByAddressSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'Invalid BCH address.')
})
it('should throw 400 if address network mismatch', async () => {
req.params.address = 'slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0'
const result = await txsByAddressSingle(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'Invalid')
})
it('should get tx history', async () => {
if (process.env.TEST === 'unit') {
sandbox
.stub(slpRoute.slpdb, 'getHistoricalSlpTransactions')
.resolves(mockData.mockTxHistory)
}
// req.params.address = 'simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk'
req.params.address =
'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
const result = await slpRoute.txsByAddressSingle(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
})
})
})
/*