From 96b5e0e408ff76caf4fce0e901a1434ecc6766db Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 22 Oct 2021 18:53:22 -0400 Subject: [PATCH] feat(bcash): Added getUtxos for bcash to bch-api --- src/app.js | 4 + src/routes/v5/bcash/slp.js | 66 ++++++++++++-- test/v5/bcash-slp.js | 155 +++++++++++++++++++++++++++++++- test/v5/mocks/bcash-slp-mock.js | 71 +++++++++++++++ 4 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 test/v5/mocks/bcash-slp-mock.js diff --git a/src/app.js b/src/app.js index 2ca0dda..b60231a 100644 --- a/src/app.js +++ b/src/app.js @@ -54,6 +54,7 @@ const ElectrumXV5 = require('./routes/v5/electrumx') const EncryptionV5 = require('./routes/v5/encryption') const PriceV5 = require('./routes/v5/price') const JWTV5 = require('./routes/v5/jwt') +const BcashSLP = require('./routes/v5/bcash/slp') // const Ninsight = require('./routes/v5/ninsight') require('dotenv').config() @@ -82,6 +83,7 @@ const pricev5 = new PriceV5() const utilV5 = new UtilV5({ electrumx: electrumxv5 }) const dsproofV5 = new DSProofV5() const jwtV5 = new JWTV5() +const bcashSLP = new BcashSLP() const app = express() app.locals.env = process.env @@ -196,6 +198,8 @@ app.use(`/${v5prefix}/` + 'jwt', jwtV5.router) // const ninsight = new Ninsight() app.use(`/${v5prefix}/` + 'ninsight', ninsight.router) +app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router) + // catch 404 and forward to error handler app.use((req, res, next) => { const err = { diff --git a/src/routes/v5/bcash/slp.js b/src/routes/v5/bcash/slp.js index 62e4848..5611a79 100644 --- a/src/routes/v5/bcash/slp.js +++ b/src/routes/v5/bcash/slp.js @@ -19,17 +19,18 @@ const config = require('../../../../config') const RouteUtils = require('../../../util/route-utils') const routeUtils = new RouteUtils() -// const BCHJS = require("@psf/bch-js"); -// const bchjs = new BCHJS(); +const BCHJS = require('@psf/bch-js') +const bchjs = new BCHJS() let _this class BcashSlp { constructor () { + _this = this this.config = config this.axios = axios this.routeUtils = routeUtils - // this.bchjs = bchjs; + this.bchjs = bchjs // _this.bitcore = bitcore this.bcashServer = process.env.BCASH_SERVER @@ -88,26 +89,73 @@ class BcashSlp { // https://gist.github.com/christroutner/dcb25a895900e557d7b43341ee85fa79 wlogger.debug( - 'Executing electrumx/getUtxos with this address: ', + 'Executing bcash/slp.js/getUtxos(). with this address: ', cashAddr ) // Get data from ElectrumX server. const response = await _this.axios.get( - `${_this.fulcrumApi}electrumx/utxos/${address}` + `${_this.bcashServer}coin/address/${cashAddr}?slp=true` ) - // console.log('response', response, _this.fulcrumApi) - + // Get address UTXOs + const utxos = response.data + // Hydrate UTXOs + const hydratedUtxos = await _this.hydrateUTXOS(utxos) res.status(200) - return res.json(response.data) + return res.json(hydratedUtxos) } catch (err) { // Write out error to error log. - wlogger.error('Error in elecrumx.js/getUtxos().', err) + wlogger.error('Error in bcash/slp.js/getUtxos().', err) return _this.errorHandler(err, res) } } + // Maps and filter the SLP UTXOs of an UTXOs array + // get information about the SLP UTXOs + async hydrateUTXOS (UTXOS) { + try { + if (!Array.isArray(UTXOS)) { + throw new Error('UTXOs must be an array of slp utxos') + } + const slpUtxos = UTXOS.filter((val) => val.slp) + const hydrated = [] + + // Find information of each token + for (let i = 0; i < slpUtxos.length; i++) { + const slp = slpUtxos[i].slp + const info = await _this.getTokenInfo(slp.tokenId) + const obj = Object.assign(slp, info) + + slpUtxos[i].slp = obj + hydrated.push(slpUtxos[i]) + } + return hydrated + } catch (error) { + console.log(error) + throw error + } + } + + // Get information of a token + async getTokenInfo (tokenId) { + try { + if (!tokenId || typeof tokenId !== 'string') { + throw new Error('tokenId must be string') + } + + const result = await _this.axios.get( + `${_this.bcashServer}token/${tokenId}` + ) + const tokenInfo = result.data + // console.log('token info', tokenInfo) + return tokenInfo + } catch (error) { + console.log(error) + throw error + } + } + // DRY error handler. errorHandler (err, res) { // Attempt to decode the error message. diff --git a/test/v5/bcash-slp.js b/test/v5/bcash-slp.js index 7ff4d21..fe9122a 100644 --- a/test/v5/bcash-slp.js +++ b/test/v5/bcash-slp.js @@ -13,7 +13,7 @@ const BcashSlp = require('../../src/routes/v5/bcash/slp') // Mocking data. const { mockReq, mockRes } = require('./mocks/express-mocks') -// const mockData = require('./mocks/electrumx-mock') +const mockData = require('./mocks/bcash-slp-mock') // Used for debugging. const util = require('util') @@ -88,8 +88,157 @@ describe('#bcash-slp', () => { describe('#getUtxos', () => { it('should return UTXOs for an address, hydrated with SLP info', async () => { - // TODO: Add unit tests. - assert.isOk(true) + sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.utxos }) + sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo) + req.params.address = + 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk' + + const utxos = await bcashSlp.getUtxos(req, res) + assert.isArray(utxos) + const utxo = utxos[0] + + assert.property(utxo, 'version') + assert.property(utxo, 'height') + assert.property(utxo, 'value') + assert.property(utxo, 'script') + assert.property(utxo, 'address') + assert.property(utxo, 'coinbase') + assert.property(utxo, 'hash') + assert.property(utxo, 'index') + assert.property(utxo, 'slp') + + assert.property(utxo.slp, 'tokenId') + assert.property(utxo.slp, 'ticker') + assert.property(utxo.slp, 'name') + assert.property(utxo.slp, 'uri') + assert.property(utxo.slp, 'hash') + assert.property(utxo.slp, 'decimals') + assert.property(utxo.slp, 'vout') + assert.property(utxo.slp, 'value') + assert.property(utxo.slp, 'type') + }) + it('should handle error if address is array', async () => { + req.params.address = [ + 'bchtest:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk' + ] + + const result = await bcashSlp.getUtxos(req, res) + assert.include( + result.error, + 'address can not be an array. Use POST for bulk upload.' + ) + }) + it('should throw error if address has invalid format', async () => { + sandbox.stub(bcashSlp.routeUtils, 'validateNetwork').returns(false) + + req.params.address = 'qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk' + + const result = await bcashSlp.getUtxos(req, res) + assert.include( + result.error, + 'Invalid network. Trying to use a testnet address on mainnet' + ) + }) + it('should catch axios error', async () => { + sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error')) + req.params.address = + 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk' + const result = await bcashSlp.getUtxos(req, res) + assert.include(result.error, 'test error') + }) + }) + + describe('#hydrateUTXOS', () => { + it('should hydrate slp utxos', async () => { + sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo) + + const hydratedUtxos = await bcashSlp.hydrateUTXOS(mockData.utxos) + assert.isArray(hydratedUtxos) + const hydratedUtxo = hydratedUtxos[0] + assert.property(hydratedUtxo, 'version') + assert.property(hydratedUtxo, 'height') + assert.property(hydratedUtxo, 'value') + assert.property(hydratedUtxo, 'script') + assert.property(hydratedUtxo, 'address') + assert.property(hydratedUtxo, 'coinbase') + assert.property(hydratedUtxo, 'hash') + assert.property(hydratedUtxo, 'index') + assert.property(hydratedUtxo, 'slp') + + assert.property(hydratedUtxo.slp, 'tokenId') + assert.property(hydratedUtxo.slp, 'ticker') + assert.property(hydratedUtxo.slp, 'name') + assert.property(hydratedUtxo.slp, 'uri') + assert.property(hydratedUtxo.slp, 'hash') + assert.property(hydratedUtxo.slp, 'decimals') + assert.property(hydratedUtxo.slp, 'vout') + assert.property(hydratedUtxo.slp, 'value') + assert.property(hydratedUtxo.slp, 'type') + }) + + it('should throw error if utxos is not provided', async () => { + try { + await bcashSlp.hydrateUTXOS() + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'UTXOs must be an array of slp utxos') + } + }) + it('should handle error', async () => { + try { + sandbox.stub(bcashSlp, 'getTokenInfo').throws(new Error('test error')) + + await bcashSlp.getTokenInfo(mockData.utxos) + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'test error') + } + }) + }) + + describe('#getTokenInfo', () => { + it('should return SLP info for an token id', async () => { + sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.tokenInfo }) + + const tokenId = + 'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900' + const slpInfo = await bcashSlp.getTokenInfo(tokenId) + assert.isObject(slpInfo) + assert.property(slpInfo, 'tokenId') + assert.property(slpInfo, 'ticker') + assert.property(slpInfo, 'name') + assert.property(slpInfo, 'uri') + assert.property(slpInfo, 'hash') + assert.property(slpInfo, 'decimals') + }) + it('should throw error if token id is not provided', async () => { + try { + await bcashSlp.getTokenInfo() + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'tokenId must be string') + } + }) + it('should catch axios error', async () => { + try { + sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error')) + const tokenId = + 'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900' + await bcashSlp.getTokenInfo(tokenId) + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'test error') + } + }) + }) + describe('#errorHandler', () => { + it('should handle unexpected errors', () => { + sandbox.stub(bcashSlp.routeUtils, 'decodeError').returns({ msg: false }) + + const result = bcashSlp.errorHandler(new Error('test error'), res) + // console.log('result: ', result) + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.property(result, 'error') }) }) }) diff --git a/test/v5/mocks/bcash-slp-mock.js b/test/v5/mocks/bcash-slp-mock.js new file mode 100644 index 0000000..8ef8576 --- /dev/null +++ b/test/v5/mocks/bcash-slp-mock.js @@ -0,0 +1,71 @@ +/* + This library contains mocking data for running unit tests on the bcash route. +*/ + +const tokenInfo = { + tokenId: 'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84', + ticker: 'PSI', + name: 'Psidium', + uri: '', + hash: '', + decimals: 8 +} + +const utxos = [ + { + version: 2, + height: 707135, + value: 546, + script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac', + address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk', + coinbase: false, + hash: '77e514d59c0c866e791d08ddab73a1f7827be5f1ca15c1e05515dac9b271b596', + index: 2, + slp: { + vout: 2, + tokenId: + '5f2914840d0fd82689bdf0a0f8194ea363b9d0e2bd69b01608e18e1c6a4b9861', + value: '9999777700000', + type: 'SEND' + } + }, + { + version: 2, + height: 707135, + value: 546, + script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac', + address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk', + coinbase: false, + hash: '350aab06bf9523c062692f62622cb28b18cff9e4aa92be5476143336da217e98', + index: 1, + slp: { + vout: 1, + tokenId: + '2d860662801067828be3c4f504ce31b2a1a36c2cdbc6d92f6160920f66b0a9ee', + value: '1', + type: 'SEND' + } + }, + { + version: 2, + height: 687335, + value: 546, + script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac', + address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk', + coinbase: false, + hash: 'b7a0f52a344b1169292c757a36c62dbdaaa4ad684e96837eec732a9a2033dfd1', + index: 1, + slp: { + vout: 1, + tokenId: + 'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84', + value: '100000000', + type: 'SEND' + } + } +] + +module.exports = { + tokenInfo, + utxos +}