Compare commits

...
9 Commits
8 changed files with 581 additions and 11 deletions
+3 -2
View File
@@ -19,11 +19,12 @@
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/",
"test:integration": "mocha test/v4/integration",
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js",
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v4/integration/nft.js",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs",
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#hydrateUtxos-' test/v4/integration/",
"test:temp2": "mocha -g '#rate-routelimit' --timeout 30000 test/v4/"
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#getNftChildren' test/v4/integration/",
"test:temp2": "mocha -g '#getNftGroup' --exit --timeout 30000 test/v4/"
},
"engines": {
"node": ">=10.15.1"
+34 -7
View File
@@ -140,7 +140,7 @@ class RawTransactions {
const options = _this.routeUtils.getAxiosOptions()
// Loop through each height and creates an array of requests to call in parallel
const promises = hexes.map(async hex => {
const promises = hexes.map(async (hex) => {
options.data.id = 'decoderawtransaction'
options.data.method = 'decoderawtransaction'
options.data.params = [hex]
@@ -152,7 +152,7 @@ class RawTransactions {
const axiosResult = await _this.axios.all(promises)
// Retrieve the data part of the result.
const result = axiosResult.map(x => x.data.result)
const result = axiosResult.map((x) => x.data.result)
res.status(200)
return res.json(result)
@@ -254,7 +254,7 @@ class RawTransactions {
const options = _this.routeUtils.getAxiosOptions()
// Loop through each hex and create an array of promises
const promises = hexes.map(async hex => {
const promises = hexes.map(async (hex) => {
options.data.id = 'decodescript'
options.data.method = 'decodescript'
options.data.params = [hex]
@@ -267,7 +267,7 @@ class RawTransactions {
const resolved = await Promise.all(promises)
// Retrieve the data from each resolved promise.
const result = resolved.map(x => x.data.result)
const result = resolved.map((x) => x.data.result)
res.status(200)
return res.json(result)
@@ -348,7 +348,7 @@ class RawTransactions {
}
// Loop through each txid and create an array of promises
const promises = txids.map(async txid =>
const promises = txids.map(async (txid) =>
_this.getRawTransactionsFromNode(txid, verbose)
)
@@ -436,7 +436,8 @@ class RawTransactions {
return res.json({ error: 'hex must be an array' })
}
const options = _this.routeUtils.getAxiosOptions()
let options = _this.routeUtils.getAxiosOptions()
options = _this.sendTxOptions(options)
// Enforce array size rate limits
if (!_this.routeUtils.validateArraySize(req, hexes)) {
@@ -536,7 +537,8 @@ class RawTransactions {
})
}
const options = _this.routeUtils.getAxiosOptions()
let options = _this.routeUtils.getAxiosOptions()
options = _this.sendTxOptions(options)
// RPC call
options.data.id = 'sendrawtransaction'
@@ -557,6 +559,31 @@ class RawTransactions {
return _this.errorHandler(err, res)
}
}
// This method modifies the default axios options. It attempts to inject
// a specific full node to use when broadcasting transactions. This is useful
// because it leverages the built-in protections that a full node has against
// accidental double spends. It mitigates a corner-case when rapidly spending
// TXs on load balanced nodes. By piping all TX sends through a single node,
// accidental double spends can be reduced.
sendTxOptions (options) {
try {
const sendUrl = process.env.RPC_SENDURL
if (sendUrl !== 'undefined' && sendUrl !== undefined) {
// console.log(`original options: ${JSON.stringify(options, null, 2)}`)
options.baseURL = process.env.RPC_SENDURL
// console.log(`modified options: ${JSON.stringify(options, null, 2)}`)
}
return options
} catch (err) {
wlogger.error('Error in rawtransactions.js/sendTxOptions()')
throw err
}
}
}
module.exports = RawTransactions
+122 -1
View File
@@ -100,6 +100,8 @@ class Slp {
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
_this.router.get('/status', _this.getStatus)
_this.router.get('/nftChildren/:tokenId', _this.getNftChildren)
_this.router.get('/nftGroup/:tokenId', _this.getNftGroup)
}
// DRY error handler.
@@ -133,6 +135,10 @@ class Slp {
delete token.tokenDetails.batonVout
delete token.tokenDetails.sendOutputs
if (token.tokenDetails.versionType === 65 && token.nftParentId) {
token.tokenDetails.nftParentId = token.nftParentId
}
token.tokenDetails.blockCreated = token.tokenStats.block_created
token.tokenDetails.blockLastActiveSend =
token.tokenStats.block_last_active_send
@@ -296,7 +302,7 @@ class Slp {
'tokenDetails.tokenIdHex': tokenId
}
},
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
project: { tokenDetails: 1, tokenStats: 1, nftParentId: 1, _id: 0 },
limit: 1000
}
}
@@ -2222,6 +2228,121 @@ class Slp {
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /slp/nftChildren/{tokenId} Get all NFT children for a given NFT group
* @apiName Get all NFT children for a given NFT group
* @apiGroup SLP
* @apiDescription Get all NFT children for a given NFT group
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/slp/nftChildren/68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a" -H "accept:application/json"
*
*/
async getNftChildren (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 token = await _this.lookupToken(tokenId)
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
if (!token || token.id === 'not found' || token.versionType !== 129) {
res.status(400)
return res.json({ error: 'NFT group does not exists' })
}
const query = {
v: 3,
q: {
db: ['t'],
aggregate: [
{ $match: { nftParentId: tokenId } },
{ $skip: 0 }, // TODO: pass start point
{ $limit: 100 } // TODO: pass count limit
]
}
}
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 childrenIds = []
const childrenRes = await _this.axios.request(opt)
// console.log(`childrenRes.data: ${JSON.stringify(childrenRes.data, null, 2)}`)
if (!childrenRes || !childrenRes.data || !childrenRes.data.t) return { error: 'No children data in the group' }
res.status(200)
childrenRes.data.t.forEach(function (token) {
// console.log(`info: ${JSON.stringify(token, null, 2)}`)
if (token.tokenDetails.versionType === 65 && token.tokenDetails.transactionType === 'GENESIS') {
childrenIds.push(token.tokenDetails.tokenIdHex)
}
})
return res.json({ nftChildren: childrenIds })
} catch (err) {
// console.log(err)
wlogger.error('Error in slp.js/getNftChildren().', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {get} /slp/nftGroup/{tokenId} Get the NFT group for a given NFT child token
* @apiName Get the NFT group for a given NFT child token
* @apiGroup SLP
* @apiDescription Get the NFT group for a given NFT child token
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/slp/nftGroup/45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9" -H "accept:application/json"
*
*/
async getNftGroup (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 token = await _this.lookupToken(tokenId)
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
if (!token || token.id === 'not found' || token.versionType !== 65 || !token.nftParentId) {
res.status(400)
return res.json({ error: 'NFT child does not exists' })
}
const parentToken = await _this.lookupToken(token.nftParentId)
// console.log(`parentToken: ${JSON.stringify(token, null, 2)}`)
if (!parentToken || parentToken.id === 'not found' || parentToken.versionType !== 129) {
res.status(400)
return res.json({ error: 'NFT group does not exists' })
}
res.status(200)
return { nftGroup: parentToken }
} catch (err) {
// console.log(err)
wlogger.error('Error in slp.js/getNftGroup().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Slp
+5
View File
@@ -15,6 +15,11 @@ export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password
# (optional) If load-balancing multiple full nodes, it's best to pick a single
# one for broadcasting transactions to the network. Prevents accidental
# double spends.
#export RPC_SENDURL=http://$RPC_IP/
# SLPDB
export SLPDB_PASS_GP=somelongpassword
export SLPDB_URL=http://<SLPDB IP>:12300/
+77
View File
@@ -0,0 +1,77 @@
/*
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 SLP = require('../../../src/routes/v4/slp')
const slp = new SLP()
const { mockReq, mockRes } = require('../mocks/express-mocks')
describe('#nft', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#getNftChildren', () => {
it('should get NFT children list', async () => {
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slp.getNftChildren(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result.nftChildren)
assert.equal(result.nftChildren.length, 2)
assert.equal(
result.nftChildren[0],
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
)
assert.equal(
result.nftChildren[1],
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
)
})
})
describe('#getNftGroup', () => {
it('should get NFT group token info', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
// req.params.tokenId = '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
const result = await slp.getNftGroup(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'nftGroup')
assert.property(result.nftGroup, 'id')
assert.property(result.nftGroup, 'versionType')
assert.property(result.nftGroup, 'symbol')
assert.equal(
result.nftGroup.id,
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
)
assert.equal(result.nftGroup.versionType, 129)
assert.equal(result.nftGroup.symbol, 'PSF.TEST.GROUP')
})
})
})
+66 -1
View File
@@ -66,6 +66,69 @@ const mockSingleToken = {
]
}
const mockNftGroup = {
decimals: 0,
timestamp: '2021-05-03 10:36:01',
timestamp_unix: 1620038161,
versionType: 129,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.GROUP',
name: 'PSF Test NFT Group',
containsBaton: true,
id: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
documentHash: null,
initialTokenQty: 1000000,
blockCreated: 686117,
totalMinted: null,
totalBurned: null,
circulatingSupply: null
}
const mockNftChildren = [
{
decimals: 0,
timestamp: '2021-05-03 11:59:30',
timestamp_unix: 1620043170,
versionType: 65,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.CHILD.1',
name: 'PSF Test NFT Child #1',
containsBaton: false,
id: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9',
documentHash: null,
initialTokenQty: 1,
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
blockCreated: 686130,
totalMinted: null,
totalBurned: null,
circulatingSupply: null,
// only in axios.data.t
transactionType: 'GENESIS',
tokenIdHex: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
},
{
decimals: 0,
timestamp: '2021-05-03 11:59:30',
timestamp_unix: 1620043170,
versionType: 65,
documentUri: 'psfoundation.cash',
symbol: 'PSF.TEST.CHILD.2',
name: 'PSF Test NFT Child #2',
containsBaton: false,
id: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55',
documentHash: null,
initialTokenQty: 1,
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
blockCreated: 686130,
totalMinted: null,
totalBurned: null,
circulatingSupply: null,
// only in axios.data.t
transactionType: 'GENESIS',
tokenIdHex: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
}
]
const mockSingleTokenError = {
t: []
}
@@ -1038,5 +1101,7 @@ module.exports = {
mockPsfToken,
mockValidateBulk,
mockValidate3Bulk,
mockStatus
mockStatus,
mockNftGroup,
mockNftChildren
}
+3
View File
@@ -1047,10 +1047,12 @@ describe('#Raw-Transactions', () => {
// Save the existing RPC URL.
const savedUrl = process.env.BITCOINCOM_BASEURL
const savedUrl2 = process.env.RPC_BASEURL
const savedUrl3 = process.env.RPC_SENDURL
// Manipulate the URL to cause a 500 network error.
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
process.env.RPC_BASEURL = 'http://fakeurl/api/'
process.env.RPC_SENDURL = 'http://fakeurl/api/'
req.params.hex =
'020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900'
@@ -1060,6 +1062,7 @@ describe('#Raw-Transactions', () => {
// Restore the saved URL.
process.env.BITCOINCOM_BASEURL = savedUrl
process.env.RPC_BASEURL = savedUrl2
process.env.RPC_SENDURL = savedUrl3
assert.isAbove(
res.statusCode,
+271
View File
@@ -1760,6 +1760,277 @@ describe('#SLP', () => {
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
})
})
describe('#getNftChildren', () => {
it('should throw 400 if tokenID is empty', async () => {
req.params.tokenId = ''
const result = await slpRoute.getNftChildren(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 =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slpRoute.getNftChildren(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 =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slpRoute.getNftChildren(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 return error on non-existing NFT group token', async () => {
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute, 'lookupToken').resolves({ id: 'not found' })
}
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
const result = await slpRoute.getNftChildren(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT group does not exists',
'Error message expected'
)
})
it('should return error on non-group NFT token', async () => {
if (process.env.TEST === 'unit') {
sandbox
.stub(slpRoute, 'lookupToken')
.resolves(mockData.mockNftChildren[0])
}
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
const result = await slpRoute.getNftChildren(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT group does not exists',
'Error message expected'
)
})
if (process.env.TEST === 'unit') {
it('should return error on invalid NFT group data', async () => {
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
sandbox.stub(slpRoute.axios, 'request').resolves({
data: { u: 'invalid' }
})
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slpRoute.getNftChildren(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'No children data in the group',
'Error message expected'
)
})
}
if (process.env.ISBCHN) {
it('should get NFT children IDs in given NFT group', async () => {
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
sandbox.stub(slpRoute.axios, 'request').resolves({
data: {
t: [
{
tokenDetails: mockData.mockNftChildren[0],
nftParentId: mockData.mockNftGroup.id
},
{
tokenDetails: mockData.mockNftChildren[1],
nftParentId: mockData.mockNftGroup.id
}
]
}
})
}
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slpRoute.getNftChildren(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result.nftChildren)
assert.equal(result.nftChildren.length, 2)
assert.equal(
result.nftChildren[0],
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
)
assert.equal(
result.nftChildren[1],
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
)
})
}
})
describe('#getNftGroup', () => {
it('should throw 400 if tokenID is empty', async () => {
req.params.tokenId = ''
const result = await slpRoute.getNftGroup(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 =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
const result = await slpRoute.getNftGroup(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 =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
const result = await slpRoute.getNftGroup(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 return error on non-existing NFT child token', async () => {
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute, 'lookupToken').resolves({ id: 'not found' })
}
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
const result = await slpRoute.getNftGroup(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT child does not exists',
'Error message expected'
)
})
it('should return error on invalid NFT child token', async () => {
if (process.env.TEST === 'unit') {
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
}
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
const result = await slpRoute.getNftGroup(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT child does not exists',
'Error message expected'
)
})
if (process.env.TEST === 'unit') {
it('should return error on invalid parent', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
const callback = sandbox.stub(slpRoute, 'lookupToken')
callback
.withArgs(req.params.tokenId)
.resolves(mockData.mockNftChildren[0])
// parent is non-valid NFT group (type != 129)
callback
.withArgs(mockData.mockNftGroup.id)
.resolves(mockData.mockNftChildren[0])
const result = await slpRoute.getNftGroup(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.include(
result.error,
'NFT group does not exists',
'Error message expected'
)
})
}
if (process.env.ISBCHN) {
it('should get NFT group information for tokenId', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
if (process.env.TEST === 'unit') {
const callback = sandbox.stub(slpRoute, 'lookupToken')
callback
.withArgs(req.params.tokenId)
.resolves(mockData.mockNftChildren[0])
callback
.withArgs(mockData.mockNftGroup.id)
.resolves(mockData.mockNftGroup)
}
const result = await slpRoute.getNftGroup(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'nftGroup')
assert.property(result.nftGroup, 'id')
assert.equal(result.nftGroup.id, mockData.mockNftGroup.id)
assert.property(result.nftGroup, 'versionType')
assert.equal(result.nftGroup.versionType, 129)
assert.property(result.nftGroup, 'symbol')
assert.property(result.nftGroup, 'initialTokenQty')
})
}
})
})
/*