mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 01:02:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d3dd12a1 | ||
|
|
cb8b4a5586 | ||
|
|
ec0ef24508 | ||
|
|
5a16e69719 | ||
|
|
154dbe8151 | ||
|
|
785d3059c3 | ||
|
|
21b19b59c9 | ||
|
|
588feff5db | ||
|
|
9fafc6fba3 | ||
|
|
8412da15db |
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Chris Troutner
|
||||
Copyright (c) 2021 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Generated
+19639
-85
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -22,14 +22,14 @@
|
||||
"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 --timeout 25000 -g '#generateSendOpReturn()' test/v4/integration/",
|
||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#hydrateUtxosWL' test/v4/integration/",
|
||||
"test:temp2": "mocha test/v4/rate-limits.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.5.4",
|
||||
"@psf/bch-js": "^4.11.1",
|
||||
"apidoc": "^0.26.0",
|
||||
"axios": "^0.21.1",
|
||||
"bitcore-lib-cash": "^8.23.1",
|
||||
|
||||
+92
-1
@@ -17,7 +17,9 @@ const wlogger = require('../../util/winston-logging')
|
||||
const LOCAL_RESTURL = process.env.LOCAL_RESTURL
|
||||
? process.env.LOCAL_RESTURL
|
||||
: 'https://api.fullstack.cash/v4/'
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
// const BCHJS = require('../../../../bch-js')
|
||||
const bchjs = new BCHJS({ restURL: LOCAL_RESTURL })
|
||||
|
||||
// Used to convert error messages to strings, to safely pass to users.
|
||||
@@ -89,6 +91,7 @@ class Slp {
|
||||
)
|
||||
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
|
||||
_this.router.get('/status', _this.getStatus)
|
||||
}
|
||||
|
||||
@@ -1305,6 +1308,7 @@ class Slp {
|
||||
async validate3Single (req, res, next) {
|
||||
try {
|
||||
const txid = req.params.txid
|
||||
// console.log('validate3Single txid: ', txid)
|
||||
|
||||
// Validate input
|
||||
if (!txid || txid === '') {
|
||||
@@ -1386,6 +1390,7 @@ class Slp {
|
||||
async validate3Bulk (req, res, next) {
|
||||
try {
|
||||
const txids = req.body.txids
|
||||
// console.log(`validate3Bulk txids: `, txids)
|
||||
|
||||
// Reject if txids is not an array.
|
||||
if (!Array.isArray(txids)) {
|
||||
@@ -1985,7 +1990,7 @@ class Slp {
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
// console.log('details: ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
@@ -2013,6 +2018,92 @@ class Slp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /slp/hydrateUtxosWL/ hydrateUtxosWL
|
||||
* @apiName SLP hydrateUtxosWL
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Hydrate UTXO data with SLP information, using only the whitelist SLPDB.
|
||||
*
|
||||
* This call is identical to `hydrateUtxos`, except it will only use the
|
||||
* filtered SLPDB with a whitelist. This results in faster performance, more
|
||||
* reliable uptime, but more frequent `isValid: null` values. Some use-cases
|
||||
* prioritize the speed and reliability over acceptance of a wide range of
|
||||
* SLP tokens.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v4/slp/hydrateUtxosWL" -H "accept:application/json" -H "Content-Type: application/json" -d '{"utxos":[{"utxos":[{"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 3, "value": "6816", "height": 606848, "confirmations": 13, "satoshis": 6816}, {"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 2, "value": "546", "height": 606848, "confirmations": 13, "satoshis": 546}]}]}'
|
||||
*
|
||||
*
|
||||
*/
|
||||
async hydrateUtxosWL (req, res, next) {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
// Validate inputs
|
||||
if (!Array.isArray(utxos)) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Input must be an array.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos.length) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array should not be empty'
|
||||
})
|
||||
}
|
||||
|
||||
if (utxos.length > 20) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array too long, max length is 20'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos[0].utxos) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Each element in array should have a utxos property'
|
||||
})
|
||||
}
|
||||
|
||||
// Loop through each address and query the UTXOs for that element.
|
||||
for (let i = 0; i < utxos.length; i++) {
|
||||
const theseUtxos = utxos[i].utxos
|
||||
// console.log(`theseUtxos: ${JSON.stringify(theseUtxos, null, 2)}`)
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetailsWL(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({ slpUtxos: utxos })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in slp.js/hydrateUtxosWL().', err)
|
||||
console.error('Error in slp.js/hydrateUtxosWL().', err)
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
console.log('msg: ', msg)
|
||||
console.log('status: ', status)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg, message: msg, success: false })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
return res.json({
|
||||
error: 'Error in hydrateUtxosWL()',
|
||||
message: 'Error in hydrateUtxosWL()'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /slp/status Get the health status of SLPDB
|
||||
* @apiName Get the health status of SLPDB
|
||||
|
||||
@@ -11,13 +11,19 @@ require('winston-daily-rotate-file')
|
||||
|
||||
var NETWORK = process.env.NETWORK
|
||||
|
||||
// Default 1 Megabyte
|
||||
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
||||
|
||||
// Default 5 days.
|
||||
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
var transport = new winston.transports.DailyRotateFile({
|
||||
filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxSize: '1m',
|
||||
maxFiles: '5d',
|
||||
maxSize: LOG_MAX_SIZE,
|
||||
maxFiles: LOG_MAX_FILES,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
|
||||
@@ -54,4 +54,10 @@ export ANON_RATE_LIMIT=50
|
||||
# 10 = 100 RPM
|
||||
export WHITELIST_RATE_LIMIT=10
|
||||
|
||||
# Set logging parameters
|
||||
#1m means no more than 1 megabyte
|
||||
export LOG_MAX_SIZE=1m
|
||||
#5d means store no more than 5 days
|
||||
export LOG_MAX_FILES=5d
|
||||
|
||||
npm start
|
||||
|
||||
@@ -7,21 +7,22 @@
|
||||
TODO:
|
||||
-/rawtransactions/sendRawTransaction is more appropropriate for an e2e test,
|
||||
so it is omitted here.
|
||||
- Replace request-promise with axios.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
const assert = chai.assert
|
||||
// const chai = require('chai')
|
||||
// const assert = chai.assert
|
||||
// const rawtransactions = require('../../../dist/routes/v2/rawtransactions')
|
||||
const rp = require('request-promise')
|
||||
// const rp = require('request-promise')
|
||||
|
||||
// Used for debugging.
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
// const mockData = require('../mocks/raw-transactions-mocks')
|
||||
|
||||
/*
|
||||
describe('#Raw-Transactions', () => {
|
||||
describe('#root', () => {
|
||||
it('should return root', async () => {
|
||||
@@ -189,3 +190,4 @@ describe('#Raw-Transactions', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
@@ -214,6 +214,39 @@ describe('#slp', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxosWL', () => {
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
tx_hash:
|
||||
'89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a',
|
||||
tx_pos: 1,
|
||||
value: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slp.hydrateUtxosWL(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'tx_hash')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'tx_pos')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validate2Single', () => {
|
||||
it('should invalidate a known invalid TXID', async () => {
|
||||
const txid =
|
||||
|
||||
+175
@@ -1559,6 +1559,181 @@ describe('#SLP', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxosWL', () => {
|
||||
it('should throw error if input is not an array.', async () => {
|
||||
req.body.utxos = 'test'
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
// console.log(`result: `, result)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Input must be an array')
|
||||
})
|
||||
|
||||
it('should throw error if Array is empty', async () => {
|
||||
req.body.utxos = []
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array should not be empty')
|
||||
})
|
||||
|
||||
it('should throw error if Array is too long', async () => {
|
||||
const utxo = {
|
||||
txid:
|
||||
'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90',
|
||||
vout: 3,
|
||||
amount: 0.00002015,
|
||||
satoshis: 2015,
|
||||
height: 594892,
|
||||
confirmations: 5
|
||||
}
|
||||
|
||||
const utxos = []
|
||||
|
||||
// Populate array with 21 utxos
|
||||
for (let i = 0; i < 21; i++) {
|
||||
utxos.push(utxo)
|
||||
}
|
||||
|
||||
req.body.utxos = utxos
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array too long, max length is 20')
|
||||
})
|
||||
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Mock the external network call.
|
||||
sandbox.stub(slpRoute.bchjs.SLP.Utils, 'tokenUtxoDetailsWL').resolves([
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
|
||||
tokenTicker: 'TAP',
|
||||
tokenName: 'Thoughts and Prayers',
|
||||
tokenDocumentUrl: '',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 0,
|
||||
tokenType: 1,
|
||||
tokenQty: 5,
|
||||
isValid: true
|
||||
}
|
||||
])
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
|
||||
it('should throw error for missing properties', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
height: 639443,
|
||||
tx_hash:
|
||||
'30707fffb9b295a06a68d217f49c198e9e1dbe1edc3874a0928ca1905f1709df',
|
||||
tx_pos: 0,
|
||||
value: 6000
|
||||
},
|
||||
{
|
||||
height: 639443,
|
||||
tx_hash:
|
||||
'8962566e413501224d178a02effc89be5ac0d8e4195f617415d443dc4c38fe50',
|
||||
tx_pos: 1,
|
||||
value: 546
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(
|
||||
result.error,
|
||||
'Each element in array should have a utxos property'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getStatus', () => {
|
||||
it('should get the SLPDB status', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
|
||||
Reference in New Issue
Block a user