mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
feat(token-sweep): Added token sweep endpoint
This commit is contained in:
+32
-9
@@ -8,6 +8,9 @@ const routeUtils = require("./route-utils")
|
||||
const wlogger = require("../../util/winston-logging")
|
||||
const blockbook = require("./blockbook")
|
||||
|
||||
const util = require("util")
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require("@chris.troutner/bch-js")
|
||||
const bchjs = new BCHJS()
|
||||
const BCHJS_TESTNET = `https://testnet.bchjs.cash/v3/`
|
||||
@@ -219,7 +222,7 @@ class UtilRoute {
|
||||
const balances = await _this.blockbook.testableComponents.balanceFromBlockbook(
|
||||
fromAddr
|
||||
)
|
||||
console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
||||
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
||||
|
||||
// Total balance is the sum of the confirmed and unconfirmed balance.
|
||||
const totalBalance =
|
||||
@@ -235,6 +238,7 @@ class UtilRoute {
|
||||
const utxos = await _this.blockbook.testableComponents.utxosFromBlockbook(
|
||||
fromAddr
|
||||
)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
const tokenUtxos = []
|
||||
const bchUtxos = []
|
||||
@@ -247,7 +251,7 @@ class UtilRoute {
|
||||
|
||||
// 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)}`)
|
||||
// console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
|
||||
|
||||
// Separate the bch and token UTXOs.
|
||||
for (let i = 0; i < utxos.length; i++) {
|
||||
@@ -255,6 +259,9 @@ class UtilRoute {
|
||||
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) {
|
||||
@@ -264,6 +271,8 @@ class UtilRoute {
|
||||
})
|
||||
}
|
||||
|
||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
||||
|
||||
const options = {
|
||||
ecPair,
|
||||
utxos,
|
||||
@@ -275,19 +284,20 @@ class UtilRoute {
|
||||
|
||||
let hex
|
||||
|
||||
// Choose the sweeping algorithm based if there are tokens or not.
|
||||
// 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}`)
|
||||
// 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(true)
|
||||
return res.json(txid)
|
||||
} catch (err) {
|
||||
// Attempt to decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
@@ -296,6 +306,16 @@ class UtilRoute {
|
||||
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)
|
||||
@@ -310,6 +330,7 @@ class UtilRoute {
|
||||
// const toAddr = flags.address
|
||||
|
||||
const ecPair = options.ecPair
|
||||
const toAddr = options.toAddr
|
||||
|
||||
// const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair)
|
||||
//
|
||||
@@ -329,8 +350,8 @@ class UtilRoute {
|
||||
// instance of transaction builder
|
||||
let transactionBuilder
|
||||
if (options.testnet)
|
||||
transactionBuilder = new this.BITBOX.TransactionBuilder("testnet")
|
||||
else transactionBuilder = new this.BITBOX.TransactionBuilder()
|
||||
transactionBuilder = new _this.bchjs.TransactionBuilder("testnet")
|
||||
else transactionBuilder = new _this.bchjs.TransactionBuilder()
|
||||
|
||||
let originalAmount = 0
|
||||
|
||||
@@ -350,7 +371,7 @@ class UtilRoute {
|
||||
}
|
||||
|
||||
// get byte count to calculate fee. paying 1 sat/byte
|
||||
const byteCount = this.BITBOX.BitcoinCash.getByteCount(
|
||||
const byteCount = _this.bchjs.BitcoinCash.getByteCount(
|
||||
{ P2PKH: utxos.length },
|
||||
{ P2PKH: 1 }
|
||||
)
|
||||
@@ -361,7 +382,7 @@ class UtilRoute {
|
||||
|
||||
// add output w/ address and amount to send
|
||||
transactionBuilder.addOutput(
|
||||
this.BITBOX.Address.toLegacyAddress(toAddr),
|
||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
||||
sendAmount
|
||||
)
|
||||
|
||||
@@ -405,6 +426,8 @@ class UtilRoute {
|
||||
// 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
|
||||
|
||||
+114
-2
@@ -13,6 +13,118 @@ const mockAddress = {
|
||||
isscript: false
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mockAddress
|
||||
const mockBalance = {
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
itemsOnPage: 1000,
|
||||
address: "bitcoincash:qzp7gdl52edm24xlpkyqnza9rv33u3mdxyc77j3u6k",
|
||||
balance: "2546",
|
||||
totalReceived: "2546",
|
||||
totalSent: "0",
|
||||
unconfirmedBalance: "0",
|
||||
unconfirmedTxs: 0,
|
||||
txs: 2,
|
||||
txids: [
|
||||
"e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56",
|
||||
"44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6"
|
||||
]
|
||||
}
|
||||
|
||||
const mockUtxos = [
|
||||
{
|
||||
txid: "e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56",
|
||||
vout: 0,
|
||||
value: "2000",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 2000
|
||||
},
|
||||
{
|
||||
txid: "44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6",
|
||||
vout: 1,
|
||||
value: "546",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
|
||||
const mockThreeUtxos = [
|
||||
{
|
||||
txid: "e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56",
|
||||
vout: 0,
|
||||
value: "2000",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 2000
|
||||
},
|
||||
{
|
||||
txid: "44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6",
|
||||
vout: 1,
|
||||
value: "546",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 546
|
||||
},
|
||||
{
|
||||
txid: "44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6",
|
||||
vout: 1,
|
||||
value: "546",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
|
||||
const mockIsTokenUtxos = [
|
||||
false,
|
||||
{
|
||||
txid: "44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6",
|
||||
vout: 1,
|
||||
value: "546",
|
||||
height: 605873,
|
||||
confirmations: 298,
|
||||
satoshis: 546,
|
||||
utxoType: "token",
|
||||
transactionType: "send",
|
||||
tokenId: "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
|
||||
tokenTicker: "TOK-CH",
|
||||
tokenName: "TokyoCash",
|
||||
tokenDocumentUrl: "",
|
||||
tokenDocumentHash: "",
|
||||
decimals: 8,
|
||||
tokenQty: 2
|
||||
}
|
||||
]
|
||||
|
||||
const tapUtxo = {
|
||||
txid: "2e030df12390186baf817fa2760540b886511e04bc520e88f6b4c2124cc2a7d4",
|
||||
vout: 1,
|
||||
value: "546",
|
||||
height: 606564,
|
||||
confirmations: 3,
|
||||
satoshis: 546,
|
||||
utxoType: "token",
|
||||
transactionType: "send",
|
||||
tokenId: "dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d",
|
||||
tokenTicker: "TAP",
|
||||
tokenName: "Thoughts and Prayers",
|
||||
tokenDocumentUrl: "",
|
||||
tokenDocumentHash: "",
|
||||
decimals: 0,
|
||||
tokenQty: 1
|
||||
}
|
||||
|
||||
const tokensOnly = [mockIsTokenUtxos[1], tapUtxo]
|
||||
|
||||
const multipleTokens = [false, mockIsTokenUtxos[1], tapUtxo]
|
||||
|
||||
module.exports = {
|
||||
mockAddress,
|
||||
mockBalance,
|
||||
mockUtxos,
|
||||
mockThreeUtxos,
|
||||
mockIsTokenUtxos,
|
||||
tokensOnly,
|
||||
multipleTokens
|
||||
}
|
||||
|
||||
+212
-7
@@ -12,6 +12,7 @@ const chai = require("chai")
|
||||
const assert = chai.assert
|
||||
const utilRoute = require("../../src/routes/v3/util")
|
||||
const nock = require("nock") // HTTP mocking
|
||||
const sinon = require("sinon")
|
||||
|
||||
let originalEnvVars // Used during transition from integration to unit tests.
|
||||
|
||||
@@ -19,11 +20,15 @@ let originalEnvVars // Used during transition from integration to unit tests.
|
||||
const { mockReq, mockRes } = require("./mocks/express-mocks")
|
||||
const mockData = require("./mocks/util-mocks")
|
||||
|
||||
const util = require("util")
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const UtilRoute = utilRoute.UtilRoute
|
||||
const utilRouteInst = new utilRoute.UtilRoute()
|
||||
|
||||
describe("#Util", () => {
|
||||
let req, res
|
||||
let sandbox
|
||||
|
||||
before(() => {
|
||||
// Save existing environment variables.
|
||||
@@ -57,12 +62,16 @@ describe("#Util", () => {
|
||||
|
||||
// Activate nock if it's inactive.
|
||||
if (!nock.isActive()) nock.activate()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up HTTP mocks.
|
||||
nock.cleanAll() // clear interceptor list.
|
||||
nock.restore()
|
||||
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
@@ -133,7 +142,7 @@ describe("#Util", () => {
|
||||
req.params.address = `bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd`
|
||||
|
||||
const result = await validateAddress(req, res)
|
||||
//console.log(`result: ${util.inspect(result)}`)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.hasAnyKeys(result, [
|
||||
"isvalid",
|
||||
@@ -298,14 +307,210 @@ describe("#Util", () => {
|
||||
})
|
||||
|
||||
describe("#sweepWif", () => {
|
||||
// const sweepWif = utilRoute.testableComponents.sweepWif
|
||||
|
||||
it("should do something", async () => {
|
||||
req.body.wif = "L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt"
|
||||
req.body.toAddr = "bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p"
|
||||
it("should throw 400 if WIF is not included", async () => {
|
||||
req.body = {}
|
||||
|
||||
const result = await utilRouteInst.sweepWif(req, res)
|
||||
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
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"
|
||||
)
|
||||
})
|
||||
|
||||
it("should generate transaction for valid token sweep", async () => {
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === "unit") {
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"balanceFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"utxosFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.SLP.Utils, "tokenUtxoDetails")
|
||||
.resolves(mockData.mockIsTokenUtxos)
|
||||
}
|
||||
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, "sendRawTransaction")
|
||||
.resolves("test-txid")
|
||||
|
||||
req.body = {
|
||||
wif: "L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt",
|
||||
toAddr: "bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p"
|
||||
}
|
||||
|
||||
const result = await utilRouteInst.sweepWif(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.equal(result, "test-txid")
|
||||
})
|
||||
|
||||
// Unit tests only
|
||||
if (process.env.TEST === "unit") {
|
||||
it("should generate transaction for valid BCH-only sweep", async () => {
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"balanceFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"utxosFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
// Force token utxo to appear as regular BCH utxo.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.SLP.Utils, "tokenUtxoDetails")
|
||||
.resolves([false, false])
|
||||
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, "sendRawTransaction")
|
||||
.resolves("test-txid")
|
||||
|
||||
req.body = {
|
||||
wif: "L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt",
|
||||
toAddr: "bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p"
|
||||
}
|
||||
|
||||
const result = await utilRouteInst.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(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"balanceFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"utxosFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockUtxos)
|
||||
|
||||
// Force token utxo to appear as regular BCH utxo.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.SLP.Utils, "tokenUtxoDetails")
|
||||
.resolves(mockData.tokensOnly)
|
||||
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, "sendRawTransaction")
|
||||
.resolves("test-txid")
|
||||
|
||||
req.body = {
|
||||
wif: "L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt",
|
||||
toAddr: "bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p"
|
||||
}
|
||||
|
||||
const result = await utilRouteInst.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(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"balanceFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockBalance)
|
||||
|
||||
sandbox
|
||||
.stub(
|
||||
utilRouteInst.blockbook.testableComponents,
|
||||
"utxosFromBlockbook"
|
||||
)
|
||||
.resolves(mockData.mockThreeUtxos)
|
||||
|
||||
// Force token utxo to appear as regular BCH utxo.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.SLP.Utils, "tokenUtxoDetails")
|
||||
.resolves(mockData.multipleTokens)
|
||||
|
||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||
// to the network.
|
||||
sandbox
|
||||
.stub(utilRouteInst.bchjs.RawTransactions, "sendRawTransaction")
|
||||
.resolves("test-txid")
|
||||
|
||||
req.body = {
|
||||
wif: "L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt",
|
||||
toAddr: "bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p"
|
||||
}
|
||||
|
||||
const result = await utilRouteInst.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"
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user