diff --git a/src/routes/v5/electrumx.js b/src/routes/v5/electrumx.js index 5375c3c..24ec61c 100644 --- a/src/routes/v5/electrumx.js +++ b/src/routes/v5/electrumx.js @@ -34,6 +34,7 @@ class Electrum { this.fulcrumApi = process.env.FULCRUM_API if (!this.fulcrumApi) { + // console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.') throw new Error( 'FULCRUM_API env var not set. Can not connect to Fulcrum indexer.' ) @@ -451,11 +452,13 @@ class Electrum { const response = await _this.axios.get( `${_this.fulcrumApi}electrumx/tx/data/${txid}` ) - // console.log(`_transactionDetailsFromElectrum(): ${JSON.stringify(electrumResponse, null, 2)}`) + // console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`) res.status(200) return res.json(response.data) } catch (err) { + console.log('err: ', err) + // Write out error to error log. wlogger.error('Error in elecrumx.js/getTransactionDetails().', err) @@ -1085,6 +1088,9 @@ class Electrum { errorHandler (err, res) { // Attempt to decode the error message. const { msg, status } = _this.routeUtils.decodeError(err) + // console.log('errorHandler msg: ', msg) + // console.log('errorHandler status: ', status) + if (msg) { res.status(status) return res.json({ success: false, error: msg }) diff --git a/src/routes/v5/util.js b/src/routes/v5/util.js index 16e23f2..82b1535 100644 --- a/src/routes/v5/util.js +++ b/src/routes/v5/util.js @@ -14,27 +14,8 @@ util.inspect.defaultOptions = { depth: 1 } const BCHJS = require('@psf/bch-js') const bchjs = new BCHJS() -// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v5/' -// const bchjsHTTP = axios.create({ -// baseURL: process.env.RPC_BASEURL -// }) - -// const username = process.env.RPC_USERNAME -// const password = process.env.RPC_PASSWORD - -// const requestConfig = { -// method: 'post', -// auth: { -// username: username, -// password: password -// }, -// data: { -// jsonrpc: '1.0' -// } -// } - -let _this +// let _this class UtilRoute { constructor (utilConfig) { @@ -58,9 +39,9 @@ class UtilRoute { this.router.get('/', this.root) this.router.get('/validateAddress/:address', this.validateAddressSingle) this.router.post('/validateAddress', this.validateAddressBulk) - this.router.post('/sweep', this.sweepWif) + // this.router.post('/sweep', this.sweepWif) - _this = this + // _this = this } root (req, res, next) { @@ -214,393 +195,6 @@ class UtilRoute { return res.json({ error: util.inspect(err) }) } } - - /** - * @api {post} /util/sweep Sweep BCH and tokens - * @apiName Sweep BCH and tokens from a paper wallet - * @apiGroup Util - * @apiDescription This function can be used to check the BCH balance of a - * paper wallet. It can also be used to sweep BCH and tokens from a paper - * wallet and send them to a destination address. - * - * Note: It does not yet support multiple token classes on the same paper wallet. - * - * - * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v5/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "balanceOnly": true}' - * curl -X POST "https://api.fullstack.cash/v5/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}' - * - * - */ - - async sweepWif (req, res, next) { - try { - // Validate input - const wif = req.body.wif - const toAddr = req.body.toAddr - const balanceOnly = req.body.balanceOnly - - if (typeof wif !== 'string' || wif.length !== 52) { - res.status(400) - return res.json({ - error: 'WIF needs to a proper compressed WIF starting with K or L' - }) - } - - if (!balanceOnly) { - // Only throw error if balanceOnly is false or undefined. - if (!toAddr || toAddr === '') { - res.status(400) - return res.json({ error: 'address can not be empty' }) - } - } - - wlogger.debug('Executing util/sweepWif with this address: ', toAddr) - - // Generate a private and public key pair from the WIF. - const ecPair = bchjs.ECPair.fromWIF(wif) - const fromAddr = bchjs.ECPair.toCashAddress(ecPair) - - // Get a balance on the public address - const balances = await _this.electrumx._balanceFromElectrumx(fromAddr) - // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) - - // Total balance is the sum of the confirmed and unconfirmed balance. - const totalBalance = balances.confirmed + balances.unconfirmed - - // Exit if balance is zero. - if (isNaN(totalBalance) || totalBalance === 0) { - res.status(422) - return res.json({ error: 'No balance found at BCH address.' }) - } - - // Exit if this is a balance-only call. - if (balanceOnly) { - res.status(200) - return res.json(totalBalance) - } - - // Get all UTXOs help by the address. - const utxos = await _this.electrumx._utxosFromElectrumx(fromAddr) - // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - - const tokenUtxos = [] - const bchUtxos = [] - - // Exit if there are no UTXOs. - if (utxos.length === 0) { - res.status(422) - return res.json({ error: 'No utxos found.' }) - } - - // Figure out which UTXOs are associated with SLP tokens. - const isTokenUtxo = await _this.bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`) - - // Separate the bch and token UTXOs. - for (let i = 0; i < utxos.length; i++) { - // Filter based on isTokenUtxo. - if (!isTokenUtxo[i]) bchUtxos.push(utxos[i]) - else tokenUtxos.push(isTokenUtxo[i]) - } - // console.log( - // `bchUtxos.length: ${bchUtxos.length}, tokenUtxos.length: ${tokenUtxos.length}` - // ) - - // Throw error if no BCH to move tokens. - if (bchUtxos.length === 0 && tokenUtxos.length > 0) { - res.status(422) - return res.json({ - error: - 'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens.' - }) - } - - // console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`) - - const options = { - ecPair, - utxos, - fromAddr, - toAddr, - bchUtxos, - tokenUtxos - } - - let hex - - // Choose the sweeping algorithm, based on if there are tokens or not. - if (tokenUtxos.length === 0) hex = await _this._sweepBCH(options) - else hex = await _this._sweepTokens(options, bchUtxos, tokenUtxos) - // console.log(`hex: ${hex}`) - - // Throw error if there is more than one token class. - - // Generate a transaction to move tokens and BCH. - - // Broadcast the transaction. - const txid = _this.bchjs.RawTransactions.sendRawTransaction([hex]) - - res.status(200) - return res.json(txid) - } catch (err) { - // Attempt to decode the error message. - const { msg, status } = routeUtils.decodeError(err) - if (msg) { - res.status(status) - return res.json({ error: msg }) - } - - // Catch the specific case of multiple tokens. - if ( - err.message && - err.message.indexOf('Multiple token classes detected') > -1 - ) { - res.status(422) - return res.json({ error: err.message }) - } - - wlogger.error('Error in util.js/sweepWif().', err) - console.error('Error in util.js/sweepWif().', err) - - res.status(500) - return res.json({ error: err.message }) - } - } - - // Sweep BCH only from a private WIF. - async _sweepBCH (options) { - try { - // const wif = flags.wif - // const toAddr = flags.address - - const ecPair = options.ecPair - const toAddr = options.toAddr - - // const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair) - // - // // Get the UTXOs for that address. - // let utxos = await this.BITBOX.Blockbook.utxo(fromAddr) - // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - - let utxos = options.utxos - - // Ensure all utxos have the satoshis property. - utxos = utxos.map((x) => { - x.satoshis = Number(x.value) - return x - }) - // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - - // instance of transaction builder - let transactionBuilder - if (options.testnet) { - transactionBuilder = new _this.bchjs.TransactionBuilder('testnet') - } else transactionBuilder = new _this.bchjs.TransactionBuilder() - - let originalAmount = 0 - - // Loop through all UTXOs. - for (let i = 0; i < utxos.length; i++) { - const utxo = utxos[i] - - originalAmount = originalAmount + utxo.value - - transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) - } - - if (originalAmount < 546) { - throw new Error( - 'Original amount less than the dust limit. Not enough BCH to send.' - ) - } - - // get byte count to calculate fee. paying 1 sat/byte - const byteCount = _this.bchjs.BitcoinCash.getByteCount( - { P2PKH: utxos.length }, - { P2PKH: 1 } - ) - const fee = Math.ceil(1.1 * byteCount) - - // amount to send to receiver. It's the original amount - 1 sat/byte for tx size - const sendAmount = originalAmount - fee - - // add output w/ address and amount to send - transactionBuilder.addOutput( - _this.bchjs.Address.toLegacyAddress(toAddr), - sendAmount - ) - - // Loop through each input and sign - let redeemScript - for (var i = 0; i < utxos.length; i++) { - const utxo = utxos[i] - - transactionBuilder.sign( - i, - ecPair, - redeemScript, - transactionBuilder.hashTypes.SIGHASH_ALL, - utxo.value - ) - } - - // build tx - const tx = transactionBuilder.build() - - // output rawhex - const hex = tx.toHex() - return hex - } catch (err) { - wlogger.error('Error in util.js/sweepBCH().') - throw err - } - } - - // Sweep BCH and tokens from a WIF. - async _sweepTokens (options) { - try { - // const { ecPair, utxos, fromAddr, toAddr, bchUtxos, tokenUtxos } = options - const { ecPair, utxos, toAddr, bchUtxos, tokenUtxos } = options - - // Input validation - if (!Array.isArray(bchUtxos) || bchUtxos.length === 0) { - throw new Error('bchUtxos need to be an array with one UTXO.') - } - if (!Array.isArray(tokenUtxos) || tokenUtxos.length === 0) { - throw new Error('tokenUtxos need to be an array with one UTXO.') - } - - // if (flags.testnet) - // this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST }) - - // console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`) - - // Ensure there is only one class of token in the wallet. Throw an error if - // there is more than one. - const tokenId = tokenUtxos[0].tokenId - const otherTokens = tokenUtxos.filter((x) => x.tokenId !== tokenId) - if (otherTokens.length > 0) { - throw new Error( - 'Multiple token classes detected. This function only supports a single class of token.' - ) - } - - // instance of transaction builder - let transactionBuilder - if (options.testnet) { - transactionBuilder = new _this.bchjs.TransactionBuilder('testnet') - } else transactionBuilder = new _this.bchjs.TransactionBuilder() - - // Combine all the UTXOs into a single array. - const allUtxos = utxos - // console.log(`allUtxos: ${JSON.stringify(allUtxos, null, 2)}`) - - // Loop through all UTXOs. - let originalAmount = 0 - for (let i = 0; i < allUtxos.length; i++) { - const utxo = allUtxos[i] - - originalAmount = originalAmount + utxo.value - - transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) - } - - if (originalAmount < 300) { - throw new Error( - 'Not enough BCH to send. Send more BCH to the wallet to pay miner fees.' - ) - } - - // get byte count to calculate fee. paying 1 sat - // Note: This may not be totally accurate. Just guessing on the byteCount size. - // const byteCount = this.BITBOX.BitcoinCash.getByteCount( - // { P2PKH: 3 }, - // { P2PKH: 5 } - // ) - // //console.log(`byteCount: ${byteCount}`) - // const satoshisPerByte = 1.1 - // const txFee = Math.floor(satoshisPerByte * byteCount) - // console.log(`txFee: ${txFee} satoshis\n`) - const txFee = 500 - - // amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size - const remainder = originalAmount - txFee - 546 - if (remainder < 1) { - throw new Error('Selected UTXO does not have enough satoshis') - } - // console.log(`remainder: ${remainder}`) - - // Tally up the quantity of tokens - let tokenQty = 0 - for (let i = 0; i < tokenUtxos.length; i++) { - tokenQty += tokenUtxos[i].tokenQty - } - // console.log(`tokenQty: ${tokenQty}`) - - // Generate the OP_RETURN entry for an SLP SEND transaction. - // console.log(`Generating op-return.`) - const { - script, - outputs - } = _this.bchjs.SLP.TokenType1.generateSendOpReturn(tokenUtxos, tokenQty) - // console.log(`token outputs: ${outputs}`) - - // Since we are sweeping all tokens from the WIF, there generateOpReturn() - // function should only compute 1 token output. If it returns 2, then there - // is something unexpected happening. - if (outputs > 1) { - throw new Error( - 'More than one class of token detected. Sweep feature not supported.' - ) - } - - // Add OP_RETURN as first output. - const data = _this.bchjs.Script.encode(script) - transactionBuilder.addOutput(data, 0) - - // Send dust transaction representing tokens being sent. - transactionBuilder.addOutput( - _this.bchjs.Address.toLegacyAddress(toAddr), - 546 - ) - - // Last output: send remaining BCH - transactionBuilder.addOutput( - _this.bchjs.Address.toLegacyAddress(toAddr), - remainder - ) - // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) - - // Sign each UTXO being consumed. - let redeemScript - for (let i = 0; i < allUtxos.length; i++) { - const thisUtxo = allUtxos[i] - // console.log(`thisUtxo: ${JSON.stringify(thisUtxo, null, 2)}`) - - transactionBuilder.sign( - i, - ecPair, - redeemScript, - transactionBuilder.hashTypes.SIGHASH_ALL, - thisUtxo.value - ) - } - - // build tx - const tx = transactionBuilder.build() - - // output rawhex - const hex = tx.toHex() - // console.log(`Transaction raw hex: `) - // console.log(hex) - - return hex - } catch (err) { - wlogger.error('Error in util.js/sweepBCH().') - throw err - } - } } module.exports = UtilRoute diff --git a/test/v5/a01-electrumx.js b/test/v5/a01-electrumx.js index 7714942..11ad05f 100644 --- a/test/v5/a01-electrumx.js +++ b/test/v5/a01-electrumx.js @@ -30,25 +30,18 @@ const mockData = require('./mocks/electrumx-mock') const util = require('util') util.inspect.defaultOptions = { depth: 1 } -// A wrapper for asserting that the correct response is returned when an error -// is expected. -// function expectRouteError (res, result, expectedError, code = 400) { -// assert.equal(res.statusCode, code, `HTTP status code ${code} expected.`) -// -// assert.property(result, 'error') -// assert.include(result.error, expectedError) -// -// assert.property(result, 'success') -// assert.equal(result.success, false) -// } +if (!process.env.FULCRUM_API) process.env.FULCRUM_API = 'http://localhost' describe('#Electrumx', () => { let req, res let sandbox const electrumxRoute = new ElecrumxRoute() + // let electrumxRoute before(async () => { - if (!process.env.TEST) process.env.TEST = 'unit' + if (!process.env.TEST) { + process.env.TEST = 'unit' + } console.log(`Testing type is: ${process.env.TEST}`) if (!process.env.NETWORK) process.env.NETWORK = 'testnet' @@ -641,9 +634,25 @@ describe('#Electrumx', () => { }) it('should pass errors from electrum-cash to user', async () => { + if (process.env.TEST === 'unit') { + sandbox.stub(electrumxRoute.axios, 'get').rejects({ + response: { + data: { + error: { + message: { + success: false, + error: 'Invalid tx hash' + } + } + } + } + }) + } + req.params.txid = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' const result = await electrumxRoute.getTransactionDetails(req, res) + // console.log('result: ', result) assert.property(result, 'error') assert.include(result.error.error, 'Invalid tx hash') @@ -823,11 +832,28 @@ describe('#Electrumx', () => { }) it('should pass errors from electrum-cash to user', async () => { + if (process.env.TEST === 'unit') { + sandbox.stub(electrumxRoute.axios, 'post').rejects({ + response: { + data: { + error: { + message: { + success: false, + error: + 'the transaction was rejected by network rules.\n\nTX decode failed\n' + } + } + } + } + }) + } + req.body.txHex = mockData.txDetails.hex.substring(10) const result = await electrumxRoute.broadcastTransaction(req, res) // console.log(`result: ${util.inspect(result)}`) assert.equal(res.statusCode, 400, 'Expect 400 status code') + // assert.equal(res.statusCode, 503, 'Expect 503 status code') assert.property(result, 'error') assert.include(result.error.error, 'the transaction was rejected') diff --git a/test/v5/util.js b/test/v5/util.js index 24bad9f..eb97a5f 100644 --- a/test/v5/util.js +++ b/test/v5/util.js @@ -19,7 +19,7 @@ const nock = require('nock') // HTTP mocking const sinon = require('sinon') // Local libraries -const Electrumx = require('../../src/routes/v5/electrumx') +// const Electrumx = require('../../src/routes/v5/electrumx') const UtilRoute = require('../../src/routes/v5/util') let originalEnvVars // Used during transition from integration to unit tests. @@ -38,8 +38,8 @@ const utilRouteInst = new UtilRoute({ electrumx: {} }) describe('#Util', () => { let req, res let sandbox - let electrumx - let utilRoute + // let electrumx + // let utilRoute before(async () => { // Save existing environment variables. @@ -59,7 +59,7 @@ describe('#Util', () => { process.env.RPC_PASSWORD = 'fakepassword' } - electrumx = new Electrumx() + // electrumx = new Electrumx() // await electrumx.connect() }) @@ -79,7 +79,7 @@ describe('#Util', () => { sandbox = sinon.createSandbox() - utilRoute = new UtilRoute({ electrumx }) + // utilRoute = new UtilRoute({ electrumx }) }) afterEach(() => { @@ -322,211 +322,4 @@ describe('#Util', () => { ]) }) }) - - describe('#sweepWif', () => { - it('should throw 400 if WIF is not included', async () => { - req.body = {} - - const result = await utilRouteInst.sweepWif(req, res) - - assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') - assert.include( - result.error, - 'WIF needs to a proper compressed WIF starting with K or L', - 'Proper error message' - ) - }) - - it('should throw 400 if WIF is malformed', async () => { - req.body = { - wif: 'abc123' - } - - const result = await utilRouteInst.sweepWif(req, res) - - assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') - assert.include( - result.error, - 'WIF needs to a proper compressed WIF starting with K or L', - 'Proper error message' - ) - }) - - it('should throw 400 if destination address is not included', async () => { - req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt' - } - - const result = await utilRouteInst.sweepWif(req, res) - - assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') - assert.include( - result.error, - 'address can not be empty', - 'Proper error message' - ) - }) - - // Unit test only. - if (process.env.TEST === 'unit') { - it('should generate transaction for valid token sweep', async () => { - // Mock the RPC call for unit tests. - - sandbox - .stub(utilRoute.electrumx, '_balanceFromElectrumx') - .resolves(mockData.mockBalance) - - sandbox - .stub(utilRoute.electrumx, '_utxosFromElectrumx') - .resolves(mockData.mockUtxos) - - sandbox - .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') - .resolves(mockData.mockIsTokenUtxos) - - // Mock sendRawTransaction() so that the hex does not actually get broadcast - // to the network. - sandbox - .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') - .resolves('test-txid') - - req.body = { - wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq', - toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r' - } - - const result = await utilRoute.sweepWif(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.equal(result, 'test-txid') - }) - - it('should return balance if balance-only is true', async () => { - // Mock the RPC call for unit tests. - - sandbox - .stub(utilRoute.electrumx, '_balanceFromElectrumx') - .resolves(mockData.mockBalance) - - // Mock sendRawTransaction() so that the hex does not actually get broadcast - // to the network. - sandbox - .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') - .resolves('test-txid') - - req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', - balanceOnly: true - } - - const result = await utilRoute.sweepWif(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isNumber(result) - }) - - it('should generate transaction for valid BCH-only sweep', async () => { - sandbox - .stub(utilRoute.electrumx, '_balanceFromElectrumx') - .resolves(mockData.mockBalance) - - sandbox - .stub(utilRoute.electrumx, '_utxosFromElectrumx') - .resolves(mockData.mockUtxos) - - // Force token utxo to appear as regular BCH utxo. - sandbox - .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') - .resolves([false, false]) - - // Mock sendRawTransaction() so that the hex does not actually get broadcast - // to the network. - sandbox - .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') - .resolves('test-txid') - - req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', - toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' - } - - const result = await utilRoute.sweepWif(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.equal(result, 'test-txid') - }) - - it('should throw 422 error if no non-token UTXOs', async () => { - sandbox - .stub(utilRoute.electrumx, '_balanceFromElectrumx') - .resolves(mockData.mockBalance) - - sandbox - .stub(utilRoute.electrumx, '_utxosFromElectrumx') - .resolves(mockData.mockUtxos) - - // Force token utxo to appear as regular BCH utxo. - sandbox - .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') - .resolves(mockData.tokensOnly) - - // Mock sendRawTransaction() so that the hex does not actually get broadcast - // to the network. - sandbox - .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') - .resolves('test-txid') - - req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', - toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' - } - - const result = await utilRoute.sweepWif(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.equal(res.statusCode, 422) - assert.property(result, 'error') - assert.include( - result.error, - 'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens' - ) - }) - - it('should detect and throw error for multiple token classes', async () => { - sandbox - .stub(utilRoute.electrumx, '_balanceFromElectrumx') - .resolves(mockData.mockBalance) - - sandbox - .stub(utilRoute.electrumx, '_utxosFromElectrumx') - .resolves(mockData.mockThreeUtxos) - - // Force token utxo to appear as regular BCH utxo. - sandbox - .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') - .resolves(mockData.multipleTokens) - - // Mock sendRawTransaction() so that the hex does not actually get broadcast - // to the network. - sandbox - .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') - .resolves('test-txid') - - req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', - toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' - } - - const result = await utilRoute.sweepWif(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.equal(res.statusCode, 422) - assert.property(result, 'error') - assert.include( - result.error, - 'Multiple token classes detected. This function only supports a single class of token' - ) - }) - } - }) })