mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
feat(rawtransactions): Adding full node raw transaction endpoints
This commit is contained in:
@@ -113,12 +113,8 @@ class FullNodeRPCAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
validateArraySize (length, options = {}) {
|
||||
const { isProUser = false } = options
|
||||
const freemiumLimit = Number(this.config.fullNode?.freemiumArrayLimit || 20)
|
||||
const proLimit = Number(this.config.fullNode?.proArrayLimit || freemiumLimit)
|
||||
|
||||
const limit = isProUser ? proLimit : freemiumLimit
|
||||
validateArraySize (length) {
|
||||
const limit = 20
|
||||
return length <= limit
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class BlockchainRESTController {
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(hashes.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||
if (!this.adapters.fullNode.validateArraySize(hashes.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ class BlockchainRESTController {
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(txids.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||
if (!this.adapters.fullNode.validateArraySize(txids.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ class BlockchainRESTController {
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(txids.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||
if (!this.adapters.fullNode.validateArraySize(txids.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ class BlockchainRESTController {
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(proofs.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||
if (!this.adapters.fullNode.validateArraySize(proofs.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
REST API Controller for the /full-node/rawtransactions routes.
|
||||
*/
|
||||
|
||||
import wlogger from '../../../../adapters/wlogger.js'
|
||||
|
||||
class RawTransactionsRESTController {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating RawTransactions REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases || !this.useCases.rawtransactions) {
|
||||
throw new Error(
|
||||
'Instance of RawTransactions use cases required when instantiating RawTransactions REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
this.rawtransactionsUseCases = this.useCases.rawtransactions
|
||||
|
||||
// Bind functions
|
||||
this.root = this.root.bind(this)
|
||||
this.decodeRawTransactionSingle = this.decodeRawTransactionSingle.bind(this)
|
||||
this.decodeRawTransactionBulk = this.decodeRawTransactionBulk.bind(this)
|
||||
this.decodeScriptSingle = this.decodeScriptSingle.bind(this)
|
||||
this.decodeScriptBulk = this.decodeScriptBulk.bind(this)
|
||||
this.getRawTransactionSingle = this.getRawTransactionSingle.bind(this)
|
||||
this.getRawTransactionBulk = this.getRawTransactionBulk.bind(this)
|
||||
this.sendRawTransactionSingle = this.sendRawTransactionSingle.bind(this)
|
||||
this.sendRawTransactionBulk = this.sendRawTransactionBulk.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/full-node/rawtransactions/ Service status
|
||||
* @apiName RawTransactionsRoot
|
||||
* @apiGroup RawTransactions
|
||||
*
|
||||
* @apiDescription Returns the status of the rawtransactions service.
|
||||
*
|
||||
* @apiSuccess {String} status Service identifier
|
||||
*/
|
||||
async root (req, res) {
|
||||
return res.status(200).json({ status: 'rawtransactions' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/full-node/rawtransactions/decodeRawTransaction/:hex Decode Single Raw Transaction
|
||||
* @apiName DecodeSingleRawTransaction
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Return a JSON object representing the serialized, hex-encoded transaction.
|
||||
*
|
||||
* @apiParam {String} hex Hex-encoded transaction
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v6/full-node/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
|
||||
*/
|
||||
async decodeRawTransactionSingle (req, res) {
|
||||
try {
|
||||
const hex = req.params.hex
|
||||
|
||||
if (!hex || hex === '') {
|
||||
return res.status(400).json({ error: 'hex can not be empty' })
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.decodeRawTransaction({ hex })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /v6/full-node/rawtransactions/decodeRawTransaction Decode Bulk Raw Transactions
|
||||
* @apiName DecodeBulkRawTransactions
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Return bulk hex encoded transaction.
|
||||
*
|
||||
* @apiParam {String[]} hexes Array of hex-encoded transactions
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v6/full-node/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||
*/
|
||||
async decodeRawTransactionBulk (req, res) {
|
||||
try {
|
||||
const hexes = req.body.hexes
|
||||
|
||||
if (!Array.isArray(hexes)) {
|
||||
return res.status(400).json({ error: 'hexes must be an array' })
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(hexes.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
// Validate each element in the array
|
||||
for (const hex of hexes) {
|
||||
if (!hex || hex === '') {
|
||||
return res.status(400).json({ error: 'Encountered empty hex' })
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.decodeRawTransactions({ hexes })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/full-node/rawtransactions/decodeScript/:hex Decode Single Script
|
||||
* @apiName DecodeSingleScript
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Decode a hex-encoded script.
|
||||
*
|
||||
* @apiParam {String} hex Hex-encoded script
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v6/full-node/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
|
||||
*/
|
||||
async decodeScriptSingle (req, res) {
|
||||
try {
|
||||
const hex = req.params.hex
|
||||
|
||||
if (!hex || hex === '') {
|
||||
return res.status(400).json({ error: 'hex can not be empty' })
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.decodeScript({ hex })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /v6/full-node/rawtransactions/decodeScript Bulk Decode Script
|
||||
* @apiName DecodeBulkScript
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Decode multiple hex-encoded scripts.
|
||||
*
|
||||
* @apiParam {String[]} hexes Array of hex-encoded scripts
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v6/full-node/rawtransactions/decodeScript" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||
*/
|
||||
async decodeScriptBulk (req, res) {
|
||||
try {
|
||||
const hexes = req.body.hexes
|
||||
|
||||
if (!Array.isArray(hexes)) {
|
||||
return res.status(400).json({ error: 'hexes must be an array' })
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(hexes.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
// Validate each hex in the array
|
||||
for (const hex of hexes) {
|
||||
if (!hex || hex === '') {
|
||||
return res.status(400).json({ error: 'Encountered empty hex' })
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.decodeScripts({ hexes })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/full-node/rawtransactions/getRawTransaction/:txid Get Raw Transaction
|
||||
* @apiName GetRawTransaction
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Return the raw transaction data. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
|
||||
*
|
||||
* @apiParam {String} txid Transaction ID
|
||||
* @apiParam {Boolean} verbose Return verbose data (default false)
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v6/full-node/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
|
||||
*/
|
||||
async getRawTransactionSingle (req, res) {
|
||||
try {
|
||||
const txid = req.params.txid
|
||||
const verbose = req.query.verbose === 'true'
|
||||
|
||||
if (!txid || txid === '') {
|
||||
return res.status(400).json({ error: 'txid can not be empty' })
|
||||
}
|
||||
|
||||
if (txid.length !== 64) {
|
||||
return res.status(400).json({
|
||||
error: `parameter 1 must be of length 64 (not ${txid.length})`
|
||||
})
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.getRawTransactionWithHeight({ txid, verbose })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /v6/full-node/rawtransactions/getRawTransaction Get Bulk Raw Transactions
|
||||
* @apiName GetBulkRawTransactions
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Return the raw transaction data for multiple transactions. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
|
||||
*
|
||||
* @apiParam {String[]} txids Array of transaction IDs
|
||||
* @apiParam {Boolean} verbose Return verbose data (default false)
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v6/full-node/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
|
||||
*/
|
||||
async getRawTransactionBulk (req, res) {
|
||||
try {
|
||||
const txids = req.body.txids
|
||||
const verbose = !!req.body.verbose
|
||||
|
||||
if (!Array.isArray(txids)) {
|
||||
return res.status(400).json({ error: 'txids must be an array' })
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(txids.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
// Validate each txid in the array
|
||||
for (const txid of txids) {
|
||||
if (!txid || txid === '') {
|
||||
return res.status(400).json({ error: 'Encountered empty TXID' })
|
||||
}
|
||||
|
||||
if (txid.length !== 64) {
|
||||
return res.status(400).json({
|
||||
error: `parameter 1 must be of length 64 (not ${txid.length})`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.getRawTransactions({ txids, verbose })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/full-node/rawtransactions/sendRawTransaction/:hex Send Single Raw Transaction
|
||||
* @apiName SendSingleRawTransaction
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Submits single raw transaction (serialized, hex-encoded) to local node and network.
|
||||
*
|
||||
* @apiParam {String} hex Hex-encoded transaction
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v6/full-node/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: application/json"
|
||||
*/
|
||||
async sendRawTransactionSingle (req, res) {
|
||||
try {
|
||||
const hex = req.params.hex
|
||||
|
||||
if (typeof hex !== 'string') {
|
||||
return res.status(400).json({ error: 'hex must be a string' })
|
||||
}
|
||||
|
||||
if (hex === '') {
|
||||
return res.status(400).json({ error: 'Encountered empty hex' })
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.sendRawTransaction({ hex })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /v6/full-node/rawtransactions/sendRawTransaction Send Bulk Raw Transactions
|
||||
* @apiName SendBulkRawTransactions
|
||||
* @apiGroup RawTransactions
|
||||
* @apiDescription Submits multiple raw transaction (serialized, hex-encoded) to local node and network.
|
||||
*
|
||||
* @apiParam {String[]} hexes Array of hex-encoded transactions
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v6/full-node/rawtransactions/sendRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||
*/
|
||||
async sendRawTransactionBulk (req, res) {
|
||||
try {
|
||||
const hexes = req.body.hexes
|
||||
|
||||
if (!Array.isArray(hexes)) {
|
||||
return res.status(400).json({ error: 'hex must be an array' })
|
||||
}
|
||||
|
||||
if (!this.adapters.fullNode.validateArraySize(hexes.length)) {
|
||||
return res.status(400).json({ error: 'Array too large.' })
|
||||
}
|
||||
|
||||
// Validate each element
|
||||
for (const hex of hexes) {
|
||||
if (hex === '') {
|
||||
return res.status(400).json({ error: 'Encountered empty hex' })
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.rawtransactionsUseCases.sendRawTransactions({ hexes })
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
handleError (err, res) {
|
||||
wlogger.error('Error in RawTransactionsRESTController:', err)
|
||||
|
||||
const status = err.status || 500
|
||||
const message = err.message || 'Internal server error'
|
||||
|
||||
return res.status(status).json({ error: message })
|
||||
}
|
||||
}
|
||||
|
||||
export default RawTransactionsRESTController
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
REST API router for /full-node/rawtransactions routes.
|
||||
*/
|
||||
|
||||
import express from 'express'
|
||||
import RawTransactionsRESTController from './controller.js'
|
||||
|
||||
class RawTransactionsRouter {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating RawTransactions REST Router.'
|
||||
)
|
||||
}
|
||||
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating RawTransactions REST Router.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
this.rawtransactionsController = new RawTransactionsRESTController(dependencies)
|
||||
|
||||
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
||||
this.baseUrl = `${this.apiPrefix}/full-node/rawtransactions`
|
||||
if (!this.baseUrl.startsWith('/')) {
|
||||
this.baseUrl = `/${this.baseUrl}`
|
||||
}
|
||||
this.router = express.Router()
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error('Must pass app object when attaching REST API controllers.')
|
||||
}
|
||||
|
||||
this.router.get('/', this.rawtransactionsController.root)
|
||||
this.router.get('/decodeRawTransaction/:hex', this.rawtransactionsController.decodeRawTransactionSingle)
|
||||
this.router.post('/decodeRawTransaction', this.rawtransactionsController.decodeRawTransactionBulk)
|
||||
this.router.get('/decodeScript/:hex', this.rawtransactionsController.decodeScriptSingle)
|
||||
this.router.post('/decodeScript', this.rawtransactionsController.decodeScriptBulk)
|
||||
this.router.get('/getRawTransaction/:txid', this.rawtransactionsController.getRawTransactionSingle)
|
||||
this.router.post('/getRawTransaction', this.rawtransactionsController.getRawTransactionBulk)
|
||||
this.router.get('/sendRawTransaction/:hex', this.rawtransactionsController.sendRawTransactionSingle)
|
||||
this.router.post('/sendRawTransaction', this.rawtransactionsController.sendRawTransactionBulk)
|
||||
|
||||
app.use(this.baseUrl, this.router)
|
||||
}
|
||||
}
|
||||
|
||||
export default RawTransactionsRouter
|
||||
@@ -11,6 +11,7 @@ import BlockchainRouter from './full-node/blockchain/index.js'
|
||||
import ControlRouter from './full-node/control/index.js'
|
||||
import DSProofRouter from './full-node/dsproof/index.js'
|
||||
import MiningRouter from './full-node/mining/index.js'
|
||||
import RawTransactionsRouter from './full-node/rawtransactions/index.js'
|
||||
import config from '../../config/index.js'
|
||||
|
||||
class RESTControllers {
|
||||
@@ -68,6 +69,9 @@ class RESTControllers {
|
||||
|
||||
const miningRouter = new MiningRouter(dependencies)
|
||||
miningRouter.attach(app)
|
||||
|
||||
const rawtransactionsRouter = new RawTransactionsRouter(dependencies)
|
||||
rawtransactionsRouter.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
Use cases for interacting with the BCH full node raw transactions RPC interface.
|
||||
*/
|
||||
|
||||
import wlogger from '../adapters/wlogger.js'
|
||||
|
||||
class RawTransactionsUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
|
||||
if (!this.adapters) {
|
||||
throw new Error('Adapters instance required when instantiating RawTransactions use cases.')
|
||||
}
|
||||
|
||||
this.fullNode = this.adapters.fullNode
|
||||
if (!this.fullNode) {
|
||||
throw new Error('Full node adapter required when instantiating RawTransactions use cases.')
|
||||
}
|
||||
}
|
||||
|
||||
async decodeRawTransaction ({ hex }) {
|
||||
return this.fullNode.call('decoderawtransaction', [hex])
|
||||
}
|
||||
|
||||
async decodeRawTransactions ({ hexes }) {
|
||||
try {
|
||||
const promises = hexes.map(hex =>
|
||||
this.fullNode.call('decoderawtransaction', [hex], `decoderawtransaction-${hex.slice(0, 16)}`)
|
||||
)
|
||||
|
||||
return await Promise.all(promises)
|
||||
} catch (err) {
|
||||
wlogger.error('Error in RawTransactionsUseCases.decodeRawTransactions()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async decodeScript ({ hex }) {
|
||||
return this.fullNode.call('decodescript', [hex])
|
||||
}
|
||||
|
||||
async decodeScripts ({ hexes }) {
|
||||
try {
|
||||
const promises = hexes.map(hex =>
|
||||
this.fullNode.call('decodescript', [hex], `decodescript-${hex.slice(0, 16)}`)
|
||||
)
|
||||
|
||||
return await Promise.all(promises)
|
||||
} catch (err) {
|
||||
wlogger.error('Error in RawTransactionsUseCases.decodeScripts()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getRawTransaction ({ txid, verbose = false }) {
|
||||
const verboseInt = verbose ? 1 : 0
|
||||
return this.fullNode.call('getrawtransaction', [txid, verboseInt])
|
||||
}
|
||||
|
||||
async getRawTransactions ({ txids, verbose = false }) {
|
||||
try {
|
||||
const verboseInt = verbose ? 1 : 0
|
||||
const promises = txids.map(txid =>
|
||||
this.fullNode.call('getrawtransaction', [txid, verboseInt], `getrawtransaction-${txid}`)
|
||||
)
|
||||
|
||||
return await Promise.all(promises)
|
||||
} catch (err) {
|
||||
wlogger.error('Error in RawTransactionsUseCases.getRawTransactions()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getRawTransactionWithHeight ({ txid, verbose = false }) {
|
||||
const verboseInt = verbose ? 1 : 0
|
||||
const data = await this.fullNode.call('getrawtransaction', [txid, verboseInt])
|
||||
|
||||
if (verbose && data && data.blockhash) {
|
||||
data.height = null
|
||||
try {
|
||||
// Look up the block height and append it to the TX response.
|
||||
const blockHeader = await this.fullNode.call('getblockheader', [data.blockhash, true])
|
||||
data.height = blockHeader.height
|
||||
} catch (err) {
|
||||
// Exit quietly if block header lookup fails
|
||||
wlogger.debug('Could not fetch block header for height lookup', err)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getBlockHeader ({ blockHash, verbose = false }) {
|
||||
return this.fullNode.call('getblockheader', [blockHash, verbose])
|
||||
}
|
||||
|
||||
async sendRawTransaction ({ hex }) {
|
||||
return this.fullNode.call('sendrawtransaction', [hex])
|
||||
}
|
||||
|
||||
async sendRawTransactions ({ hexes }) {
|
||||
// Dev Note: Sending the 'sendrawtransaction' RPC call to a full node in parallel will
|
||||
// not work. Testing showed that the full node will return the same TXID for
|
||||
// different TX hexes. I believe this is by design, to prevent double spends.
|
||||
// In parallel, we are essentially asking the node to broadcast a new TX before
|
||||
// it's finished broadcasting the previous one. Serial execution is required.
|
||||
try {
|
||||
const result = []
|
||||
for (const hex of hexes) {
|
||||
const txid = await this.fullNode.call('sendrawtransaction', [hex], `sendrawtransaction-${hex.slice(0, 16)}`)
|
||||
result.push(txid)
|
||||
}
|
||||
return result
|
||||
} catch (err) {
|
||||
wlogger.error('Error in RawTransactionsUseCases.sendRawTransactions()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RawTransactionsUseCases
|
||||
@@ -9,6 +9,7 @@ import BlockchainUseCases from './full-node-blockchain-use-cases.js'
|
||||
import ControlUseCases from './full-node-control-use-cases.js'
|
||||
import DSProofUseCases from './full-node-dsproof-use-cases.js'
|
||||
import MiningUseCases from './full-node-mining-use-cases.js'
|
||||
import RawTransactionsUseCases from './full-node-rawtransactions-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -23,6 +24,7 @@ class UseCases {
|
||||
this.control = new ControlUseCases({ adapters: this.adapters })
|
||||
this.dsproof = new DSProofUseCases({ adapters: this.adapters })
|
||||
this.mining = new MiningUseCases({ adapters: this.adapters })
|
||||
this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters })
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
|
||||
@@ -161,8 +161,7 @@ describe('#blockchain-controller.js', () => {
|
||||
it('should validate array size and call use case', async () => {
|
||||
const hash = 'a'.repeat(64)
|
||||
const req = createMockRequest({
|
||||
body: { hashes: [hash], verbose: true },
|
||||
locals: { proLimit: false }
|
||||
body: { hashes: [hash], verbose: true }
|
||||
})
|
||||
const res = createMockResponse()
|
||||
mockUseCases.blockchain.getBlockHeaders.resolves(['result'])
|
||||
@@ -172,7 +171,7 @@ describe('#blockchain-controller.js', () => {
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, ['result'])
|
||||
assert.isTrue(
|
||||
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1, { isProUser: false })
|
||||
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1)
|
||||
)
|
||||
assert.isTrue(
|
||||
mockUseCases.blockchain.getBlockHeaders.calledOnceWithExactly({
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
Unit tests for RawTransactionsRESTController.
|
||||
*/
|
||||
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import RawTransactionsRESTController from '../../../src/controllers/rest-api/full-node/rawtransactions/controller.js'
|
||||
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
|
||||
|
||||
describe('#rawtransactions-controller.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let mockUseCases
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
mockAdapters = {
|
||||
fullNode: {
|
||||
validateArraySize: sandbox.stub().returns(true)
|
||||
}
|
||||
}
|
||||
mockUseCases = {
|
||||
rawtransactions: {
|
||||
decodeRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
|
||||
decodeRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
|
||||
decodeScript: sandbox.stub().resolves({ asm: 'OP_DUP' }),
|
||||
decodeScripts: sandbox.stub().resolves([{ asm: 'OP_DUP' }]),
|
||||
getRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
|
||||
getRawTransactionWithHeight: sandbox.stub().resolves({ txid: 'abc123', height: 100 }),
|
||||
getRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
|
||||
sendRawTransaction: sandbox.stub().resolves('txid123'),
|
||||
sendRawTransactions: sandbox.stub().resolves(['txid1', 'txid2'])
|
||||
}
|
||||
}
|
||||
|
||||
uut = new RawTransactionsRESTController({
|
||||
adapters: mockAdapters,
|
||||
useCases: mockUseCases
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters', () => {
|
||||
assert.throws(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new RawTransactionsRESTController({ useCases: mockUseCases })
|
||||
}, /Adapters library required/)
|
||||
})
|
||||
|
||||
it('should require rawtransactions use cases', () => {
|
||||
assert.throws(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new RawTransactionsRESTController({ adapters: mockAdapters, useCases: {} })
|
||||
}, /RawTransactions use cases required/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#root()', () => {
|
||||
it('should return rawtransactions status', async () => {
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.root(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, { status: 'rawtransactions' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeRawTransactionSingle()', () => {
|
||||
it('should return decoded transaction on success', async () => {
|
||||
const req = createMockRequest({ params: { hex: '01000000' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, { txid: 'abc123' })
|
||||
assert.isTrue(mockUseCases.rawtransactions.decodeRawTransaction.calledOnce)
|
||||
assert.deepEqual(mockUseCases.rawtransactions.decodeRawTransaction.firstCall.args[0], { hex: '01000000' })
|
||||
})
|
||||
|
||||
it('should return 400 if hex is empty', async () => {
|
||||
const req = createMockRequest({ params: { hex: '' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
|
||||
})
|
||||
|
||||
it('should handle errors via handleError', async () => {
|
||||
const error = new Error('RPC error')
|
||||
error.status = 500
|
||||
mockUseCases.rawtransactions.decodeRawTransaction.rejects(error)
|
||||
const req = createMockRequest({ params: { hex: '01000000' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 500)
|
||||
assert.deepEqual(res.jsonData, { error: 'RPC error' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeRawTransactionBulk()', () => {
|
||||
it('should return decoded transactions on success', async () => {
|
||||
const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
|
||||
assert.isTrue(mockUseCases.rawtransactions.decodeRawTransactions.calledOnce)
|
||||
})
|
||||
|
||||
it('should return 400 if hexes is not an array', async () => {
|
||||
const req = createMockRequest({ body: { hexes: 'not-array' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'hexes must be an array' })
|
||||
})
|
||||
|
||||
it('should return 400 if array is too large', async () => {
|
||||
mockAdapters.fullNode.validateArraySize.returns(false)
|
||||
const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
|
||||
})
|
||||
|
||||
it('should return 400 if empty hex encountered', async () => {
|
||||
const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeScriptSingle()', () => {
|
||||
it('should return decoded script on success', async () => {
|
||||
const req = createMockRequest({ params: { hex: '76a914' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeScriptSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, { asm: 'OP_DUP' })
|
||||
assert.isTrue(mockUseCases.rawtransactions.decodeScript.calledOnce)
|
||||
})
|
||||
|
||||
it('should return 400 if hex is empty', async () => {
|
||||
const req = createMockRequest({ params: { hex: '' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeScriptSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeScriptBulk()', () => {
|
||||
it('should return decoded scripts on success', async () => {
|
||||
const req = createMockRequest({ body: { hexes: ['script1', 'script2'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeScriptBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, [{ asm: 'OP_DUP' }])
|
||||
})
|
||||
|
||||
it('should return 400 if array is too large', async () => {
|
||||
mockAdapters.fullNode.validateArraySize.returns(false)
|
||||
const req = createMockRequest({ body: { hexes: new Array(25).fill('script') } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.decodeScriptBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRawTransactionSingle()', () => {
|
||||
it('should return raw transaction on success', async () => {
|
||||
const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: {} })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, { txid: 'abc123', height: 100 })
|
||||
assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
|
||||
})
|
||||
|
||||
it('should pass verbose=true when query param is set', async () => {
|
||||
const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: { verbose: 'true' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionSingle(req, res)
|
||||
|
||||
assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
|
||||
assert.deepEqual(mockUseCases.rawtransactions.getRawTransactionWithHeight.firstCall.args[0], {
|
||||
txid: 'a'.repeat(64),
|
||||
verbose: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 400 if txid is empty', async () => {
|
||||
const req = createMockRequest({ params: { txid: '' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'txid can not be empty' })
|
||||
})
|
||||
|
||||
it('should return 400 if txid length is not 64', async () => {
|
||||
const req = createMockRequest({ params: { txid: 'short' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRawTransactionBulk()', () => {
|
||||
it('should return raw transactions on success', async () => {
|
||||
const req = createMockRequest({
|
||||
body: {
|
||||
txids: ['a'.repeat(64), 'b'.repeat(64)],
|
||||
verbose: true
|
||||
}
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
|
||||
assert.isTrue(mockUseCases.rawtransactions.getRawTransactions.calledOnce)
|
||||
assert.deepEqual(mockUseCases.rawtransactions.getRawTransactions.firstCall.args[0], {
|
||||
txids: ['a'.repeat(64), 'b'.repeat(64)],
|
||||
verbose: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 400 if txids is not an array', async () => {
|
||||
const req = createMockRequest({ body: { txids: 'not-array' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'txids must be an array' })
|
||||
})
|
||||
|
||||
it('should return 400 if array is too large', async () => {
|
||||
mockAdapters.fullNode.validateArraySize.returns(false)
|
||||
const req = createMockRequest({ body: { txids: new Array(25).fill('a'.repeat(64)) } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
|
||||
})
|
||||
|
||||
it('should return 400 if empty txid encountered', async () => {
|
||||
const req = createMockRequest({ body: { txids: ['a'.repeat(64), ''] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Encountered empty TXID' })
|
||||
})
|
||||
|
||||
it('should return 400 if txid length is not 64', async () => {
|
||||
const req = createMockRequest({ body: { txids: ['short'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.getRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sendRawTransactionSingle()', () => {
|
||||
it('should return txid on success', async () => {
|
||||
const req = createMockRequest({ params: { hex: '01000000' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.equal(res.jsonData, 'txid123')
|
||||
assert.isTrue(mockUseCases.rawtransactions.sendRawTransaction.calledOnce)
|
||||
})
|
||||
|
||||
it('should return 400 if hex is empty', async () => {
|
||||
const req = createMockRequest({ params: { hex: '' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
|
||||
})
|
||||
|
||||
it('should return 400 if hex is not a string', async () => {
|
||||
const req = createMockRequest({ params: { hex: 123 } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionSingle(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'hex must be a string' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sendRawTransactionBulk()', () => {
|
||||
it('should return txids on success', async () => {
|
||||
const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.deepEqual(res.jsonData, ['txid1', 'txid2'])
|
||||
assert.isTrue(mockUseCases.rawtransactions.sendRawTransactions.calledOnce)
|
||||
})
|
||||
|
||||
it('should return 400 if hexes is not an array', async () => {
|
||||
const req = createMockRequest({ body: { hexes: 'not-array' } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'hex must be an array' })
|
||||
})
|
||||
|
||||
it('should return 400 if array is too large', async () => {
|
||||
mockAdapters.fullNode.validateArraySize.returns(false)
|
||||
const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
|
||||
})
|
||||
|
||||
it('should return 400 if empty hex encountered', async () => {
|
||||
const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.sendRawTransactionBulk(req, res)
|
||||
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,7 @@ import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockc
|
||||
import ControlRouter from '../../../src/controllers/rest-api/full-node/control/index.js'
|
||||
import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/index.js'
|
||||
import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/index.js'
|
||||
import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/index.js'
|
||||
|
||||
describe('#controllers/rest-api/index.js', () => {
|
||||
let sandbox
|
||||
@@ -56,6 +57,17 @@ describe('#controllers/rest-api/index.js', () => {
|
||||
mining: {
|
||||
getMiningInfo: () => {},
|
||||
getNetworkHashPS: () => {}
|
||||
},
|
||||
rawtransactions: {
|
||||
decodeRawTransaction: () => {},
|
||||
decodeRawTransactions: () => {},
|
||||
decodeScript: () => {},
|
||||
decodeScripts: () => {},
|
||||
getRawTransaction: () => {},
|
||||
getRawTransactionWithHeight: () => {},
|
||||
getRawTransactions: () => {},
|
||||
sendRawTransaction: () => {},
|
||||
sendRawTransactions: () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -86,6 +98,7 @@ describe('#controllers/rest-api/index.js', () => {
|
||||
const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
|
||||
const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
|
||||
const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach')
|
||||
const rawtransactionsAttachStub = sandbox.stub(RawTransactionsRouter.prototype, 'attach')
|
||||
const restControllers = new RESTControllers({
|
||||
adapters: mockAdapters,
|
||||
useCases: mockUseCases
|
||||
@@ -102,6 +115,8 @@ describe('#controllers/rest-api/index.js', () => {
|
||||
assert.equal(dsproofAttachStub.getCall(0).args[0], app)
|
||||
assert.isTrue(miningAttachStub.calledOnce)
|
||||
assert.equal(miningAttachStub.getCall(0).args[0], app)
|
||||
assert.isTrue(rawtransactionsAttachStub.calledOnce)
|
||||
assert.equal(rawtransactionsAttachStub.getCall(0).args[0], app)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
Unit tests for RawTransactionsUseCases.
|
||||
*/
|
||||
|
||||
import { assert } from 'chai'
|
||||
|
||||
import RawTransactionsUseCases from '../../../src/use-cases/full-node-rawtransactions-use-cases.js'
|
||||
|
||||
describe('#full-node-rawtransactions-use-cases.js', () => {
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
mockAdapters = {
|
||||
fullNode: {
|
||||
call: async () => ({})
|
||||
}
|
||||
}
|
||||
|
||||
uut = new RawTransactionsUseCases({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters', () => {
|
||||
assert.throws(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new RawTransactionsUseCases()
|
||||
}, /Adapters instance required/)
|
||||
})
|
||||
|
||||
it('should require full node adapter', () => {
|
||||
assert.throws(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new RawTransactionsUseCases({ adapters: {} })
|
||||
}, /Full node adapter required/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeRawTransaction()', () => {
|
||||
it('should call full node adapter with correct method', async () => {
|
||||
let capturedMethod = ''
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedMethod = method
|
||||
capturedParams = params
|
||||
return { txid: 'abc123', version: 2 }
|
||||
}
|
||||
|
||||
const result = await uut.decodeRawTransaction({ hex: '01000000' })
|
||||
|
||||
assert.equal(capturedMethod, 'decoderawtransaction')
|
||||
assert.deepEqual(capturedParams, ['01000000'])
|
||||
assert.deepEqual(result, { txid: 'abc123', version: 2 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeRawTransactions()', () => {
|
||||
it('should call full node adapter for each hex in parallel', async () => {
|
||||
const callCount = { count: 0 }
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callCount.count++
|
||||
return { txid: `tx${callCount.count}`, hex: params[0] }
|
||||
}
|
||||
|
||||
const hexes = ['hex1', 'hex2', 'hex3']
|
||||
const result = await uut.decodeRawTransactions({ hexes })
|
||||
|
||||
assert.equal(callCount.count, 3)
|
||||
assert.equal(result.length, 3)
|
||||
assert.equal(result[0].txid, 'tx1')
|
||||
assert.equal(result[1].txid, 'tx2')
|
||||
assert.equal(result[2].txid, 'tx3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeScript()', () => {
|
||||
it('should call full node adapter with correct method', async () => {
|
||||
let capturedMethod = ''
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedMethod = method
|
||||
capturedParams = params
|
||||
return { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' }
|
||||
}
|
||||
|
||||
const result = await uut.decodeScript({ hex: '76a914' })
|
||||
|
||||
assert.equal(capturedMethod, 'decodescript')
|
||||
assert.deepEqual(capturedParams, ['76a914'])
|
||||
assert.deepEqual(result, { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeScripts()', () => {
|
||||
it('should call full node adapter for each hex in parallel', async () => {
|
||||
const callCount = { count: 0 }
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callCount.count++
|
||||
return { asm: `script${callCount.count}`, hex: params[0] }
|
||||
}
|
||||
|
||||
const hexes = ['script1', 'script2']
|
||||
const result = await uut.decodeScripts({ hexes })
|
||||
|
||||
assert.equal(callCount.count, 2)
|
||||
assert.equal(result.length, 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRawTransaction()', () => {
|
||||
it('should call full node adapter with verbose=false by default', async () => {
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedParams = params
|
||||
return '01000000'
|
||||
}
|
||||
|
||||
await uut.getRawTransaction({ txid: 'abc123' })
|
||||
|
||||
assert.deepEqual(capturedParams, ['abc123', 0])
|
||||
})
|
||||
|
||||
it('should call full node adapter with verbose=true when specified', async () => {
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedParams = params
|
||||
return { txid: 'abc123', version: 2 }
|
||||
}
|
||||
|
||||
await uut.getRawTransaction({ txid: 'abc123', verbose: true })
|
||||
|
||||
assert.deepEqual(capturedParams, ['abc123', 1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRawTransactions()', () => {
|
||||
it('should call full node adapter for each txid in parallel', async () => {
|
||||
const callCount = { count: 0 }
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callCount.count++
|
||||
return { txid: params[0], version: 2 }
|
||||
}
|
||||
|
||||
const txids = ['tx1', 'tx2']
|
||||
const result = await uut.getRawTransactions({ txids, verbose: true })
|
||||
|
||||
assert.equal(callCount.count, 2)
|
||||
assert.equal(result.length, 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRawTransactionWithHeight()', () => {
|
||||
it('should return transaction without height when verbose=false', async () => {
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
if (method === 'getrawtransaction') {
|
||||
return '01000000'
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: false })
|
||||
|
||||
assert.equal(result, '01000000')
|
||||
})
|
||||
|
||||
it('should fetch and append height when verbose=true and blockhash exists', async () => {
|
||||
let callCount = 0
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callCount++
|
||||
if (method === 'getrawtransaction') {
|
||||
return { txid: 'abc123', blockhash: 'block123' }
|
||||
}
|
||||
if (method === 'getblockheader') {
|
||||
return { height: 100, hash: 'block123' }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
|
||||
|
||||
assert.equal(callCount, 2)
|
||||
assert.equal(result.height, 100)
|
||||
assert.equal(result.txid, 'abc123')
|
||||
})
|
||||
|
||||
it('should handle block header lookup failure gracefully', async () => {
|
||||
let callCount = 0
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callCount++
|
||||
if (method === 'getrawtransaction') {
|
||||
return { txid: 'abc123', blockhash: 'block123' }
|
||||
}
|
||||
if (method === 'getblockheader') {
|
||||
throw new Error('Block not found')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
|
||||
|
||||
assert.equal(callCount, 2)
|
||||
assert.isNull(result.height)
|
||||
assert.equal(result.txid, 'abc123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getBlockHeader()', () => {
|
||||
it('should call full node adapter with correct method', async () => {
|
||||
let capturedMethod = ''
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedMethod = method
|
||||
capturedParams = params
|
||||
return { height: 100, hash: 'block123' }
|
||||
}
|
||||
|
||||
const result = await uut.getBlockHeader({ blockHash: 'block123', verbose: true })
|
||||
|
||||
assert.equal(capturedMethod, 'getblockheader')
|
||||
assert.deepEqual(capturedParams, ['block123', true])
|
||||
assert.deepEqual(result, { height: 100, hash: 'block123' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sendRawTransaction()', () => {
|
||||
it('should call full node adapter with correct method', async () => {
|
||||
let capturedMethod = ''
|
||||
let capturedParams = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
capturedMethod = method
|
||||
capturedParams = params
|
||||
return 'txid123'
|
||||
}
|
||||
|
||||
const result = await uut.sendRawTransaction({ hex: '01000000' })
|
||||
|
||||
assert.equal(capturedMethod, 'sendrawtransaction')
|
||||
assert.deepEqual(capturedParams, ['01000000'])
|
||||
assert.equal(result, 'txid123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sendRawTransactions()', () => {
|
||||
it('should send transactions serially, not in parallel', async () => {
|
||||
const callOrder = []
|
||||
mockAdapters.fullNode.call = async (method, params) => {
|
||||
callOrder.push(params[0])
|
||||
// Simulate some async work
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
return `txid-${params[0]}`
|
||||
}
|
||||
|
||||
const hexes = ['hex1', 'hex2', 'hex3']
|
||||
const startTime = Date.now()
|
||||
const result = await uut.sendRawTransactions({ hexes })
|
||||
const endTime = Date.now()
|
||||
|
||||
// Should take at least 30ms if serial (3 * 10ms)
|
||||
assert.isAtLeast(endTime - startTime, 25)
|
||||
assert.deepEqual(callOrder, ['hex1', 'hex2', 'hex3'])
|
||||
assert.equal(result.length, 3)
|
||||
assert.equal(result[0], 'txid-hex1')
|
||||
assert.equal(result[1], 'txid-hex2')
|
||||
assert.equal(result[2], 'txid-hex3')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user