From e2134e831a646543928a1df8eeb45434ee552bdf Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 15 Aug 2019 14:15:22 -0700 Subject: [PATCH] fix(slp balance bulk): Added POST/bulk call for slp token balance by address --- src/routes/v3/blockbook.js | 8 +- src/routes/v3/slp.js | 180 +++++++++++++++++++++++++++++++++++++ test/v3/slp.js | 86 ++++++++++++++++++ 3 files changed, 270 insertions(+), 4 deletions(-) diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index 4369cfa..747098a 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -341,10 +341,10 @@ async function utxosBulk(req, res, next) { // Loops through each address and creates an array of Promises, querying // Insight API in parallel. - addresses = addresses.map(async (address, index) => { - console.log(`address: ${address}`) - return utxosFromBlockbook(address) - }) + addresses = addresses.map(async (address, index) => + //console.log(`address: ${address}`) + utxosFromBlockbook(address) + ) // Wait for all parallel Insight requests to return. const result = await axios.all(addresses) diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 2b91b9f..481eb76 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -42,6 +42,7 @@ router.get("/list", list) router.get("/list/:tokenId", listSingleToken) router.post("/list", listBulkToken) router.get("/balancesForAddress/:address", balancesForAddress) +router.post("/balancesForAddress", balancesForAddressBulk) router.get("/balancesForToken/:tokenId", balancesForTokenSingle) router.get("/balance/:address/:tokenId", balancesForAddressByTokenID) router.get("/convert/:address", convertAddressSingle) @@ -433,6 +434,7 @@ async function lookupToken(tokenId) { throw err } } + /** * @api {get} /slp/balancesForAddress/{address} List SLP balance for address. * @apiName List SLP balance for address. @@ -567,6 +569,183 @@ async function balancesForAddress(req, res, next) { }) } } + +/** + * @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 "http://localhost:3000/v3/slp/balancesForAddress" -d "{\"addresses\":[\"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57\"]}" -H "accept:application/json" + * + * + */ +async function balancesForAddressBulk(req, res, next) { + try { + const addresses = req.body.addresses + + // Reject if addresses is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ error: "addresses needs to be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + wlogger.debug( + `Executing slp/balancesForAddresss with these addresses: `, + addresses + ) + + // Loop through each address and do error checking. + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] + + // Validate the input data. + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Ensure the input is a valid BCH address. + try { + utils.toCashAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const cashAddr = utils.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.` + }) + } + } + + // Collect an array of promises, one for each request to slpserve. + // This is a nested array of promises. + const balancesPromises = addresses.map(async address => { + try { + const query = { + v: 3, + q: { + db: ["a"], + find: { + address: SLP.Address.toSLPAddress(address), + token_balance: { $gte: 0 } + }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + const tokenRes = await axios.get(url) + + const tokenIds = [] + + if (tokenRes.data.a.length > 0) { + tokenRes.data.a = tokenRes.data.a.map(token => { + token.tokenId = token.tokenDetails.tokenIdHex + tokenIds.push(token.tokenId) + token.balance = parseFloat(token.token_balance) + token.balanceString = token.token_balance + token.slpAddress = token.address + delete token.tokenDetails + delete token.satoshis_balance + delete token.token_balance + delete token._id + delete token.address + return token + }) + } + + // Collect another array of promises. + const promises = tokenIds.map(async tokenId => { + try { + const query2 = { + v: 3, + q: { + db: ["t"], + find: { + $query: { + "tokenDetails.tokenIdHex": tokenId + } + }, + project: { + "tokenDetails.decimals": 1, + "tokenDetails.tokenIdHex": 1, + _id: 0 + }, + limit: 1000 + } + } + + const s2 = JSON.stringify(query2) + const b642 = Buffer.from(s2).toString("base64") + const url2 = `${process.env.SLPDB_URL}q/${b642}` + + const tokenRes2 = await axios.get(url2) + return tokenRes2.data + } catch (err) { + throw err + } + }) + + // Wait for all the promises to resolve. + const details = await axios.all(promises) + + tokenRes.data.a = tokenRes.data.a.map(token => { + details.forEach(detail => { + if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) + token.decimalCount = detail.t[0].tokenDetails.decimals + }) + return token + }) + + return tokenRes.data.a + } catch (err) { + throw err + } + }) + + // Wait for all the promises to resolve. + const axiosResult = await axios.all(balancesPromises) + + return res.json(axiosResult) + } catch (err) { + wlogger.error(`Error in slp.js/balancesForAddressBulk().`, 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 POST balancesForAddress: ${err.message}` + }) + } +} + /** * @api {get} /slp/balancesForToken/{TokenId} List SLP addresses and balances for tokenId. * @apiName List SLP addresses and balances for tokenId. @@ -1495,6 +1674,7 @@ module.exports = { listSingleToken, listBulkToken, balancesForAddress, + balancesForAddressBulk, balancesForAddressByTokenID, convertAddressSingle, convertAddressBulk, diff --git a/test/v3/slp.js b/test/v3/slp.js index a0d2f8a..a344038 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -511,6 +511,92 @@ describe("#SLP", () => { }) }) + describe("balancesForAddressBulk()", () => { + const balancesForAddressBulk = + slpRoute.testableComponents.balancesForAddressBulk + + it("should throw 400 if addresses is empty", async () => { + const result = await balancesForAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "addresses needs to be an array") + }) + + it("should throw 400 if address is invalid", async () => { + req.body.addresses = ["badAddress"] + + const result = await balancesForAddressBulk(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.body.addresses = [ + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + ] + + const result = await balancesForAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid") + }) + + it("should throw 5XX error when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.body.addresses = [ + "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5" + ] + + const result = await balancesForAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + }) + + // Only run as an integration test. Too complex to stub accurately. + if (process.env.TEST !== "unit") { + it("should get token balance for an address", async () => { + req.body.addresses = [ + "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7" + ] + + const result = await balancesForAddressBulk(req, res) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + "tokenId", + "balance", + "balanceString", + "slpAddress", + "decimalCount" + ]) + }) + } + }) + describe("balancesForAddressByTokenID()", () => { const balancesForAddressByTokenID = slpRoute.testableComponents.balancesForAddressByTokenID