mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api.git
synced 2026-09-21 16:52:00 -07:00
564 lines
17 KiB
JavaScript
564 lines
17 KiB
JavaScript
/*
|
|
REST API Controller for the /full-node/fulcrum routes.
|
|
*/
|
|
|
|
import wlogger from '../../../../adapters/wlogger.js'
|
|
import BCHJS from '@psf/bch-js'
|
|
|
|
const bchjs = new BCHJS()
|
|
|
|
class FulcrumRESTController {
|
|
constructor (localConfig = {}) {
|
|
this.adapters = localConfig.adapters
|
|
if (!this.adapters) {
|
|
throw new Error(
|
|
'Instance of Adapters library required when instantiating Fulcrum REST Controller.'
|
|
)
|
|
}
|
|
|
|
this.useCases = localConfig.useCases
|
|
if (!this.useCases || !this.useCases.fulcrum) {
|
|
throw new Error(
|
|
'Instance of Fulcrum use cases required when instantiating Fulcrum REST Controller.'
|
|
)
|
|
}
|
|
|
|
this.fulcrumUseCases = this.useCases.fulcrum
|
|
|
|
// Bind functions
|
|
this.root = this.root.bind(this)
|
|
this.getBalance = this.getBalance.bind(this)
|
|
this.balanceBulk = this.balanceBulk.bind(this)
|
|
this.getUtxos = this.getUtxos.bind(this)
|
|
this.utxosBulk = this.utxosBulk.bind(this)
|
|
this.getTransactionDetails = this.getTransactionDetails.bind(this)
|
|
this.transactionDetailsBulk = this.transactionDetailsBulk.bind(this)
|
|
this.broadcastTransaction = this.broadcastTransaction.bind(this)
|
|
this.getBlockHeaders = this.getBlockHeaders.bind(this)
|
|
this.blockHeadersBulk = this.blockHeadersBulk.bind(this)
|
|
this.getTransactions = this.getTransactions.bind(this)
|
|
this.transactionsBulk = this.transactionsBulk.bind(this)
|
|
this.getMempool = this.getMempool.bind(this)
|
|
this.mempoolBulk = this.mempoolBulk.bind(this)
|
|
this.handleError = this.handleError.bind(this)
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/ Service status
|
|
* @apiName FulcrumRoot
|
|
* @apiGroup Fulcrum
|
|
*
|
|
* @apiDescription Returns the status of the fulcrum service.
|
|
*
|
|
* @apiSuccess {String} status Service identifier
|
|
*/
|
|
async root (req, res) {
|
|
return res.status(200).json({ status: 'fulcrum' })
|
|
}
|
|
|
|
/**
|
|
* Validates and converts an address to cash address format
|
|
* @param {string} address - Address to validate and convert
|
|
* @returns {string} Cash address
|
|
* @throws {Error} If address is invalid or not mainnet
|
|
*/
|
|
_validateAndConvertAddress (address) {
|
|
if (!address) {
|
|
throw new Error('address is empty')
|
|
}
|
|
|
|
// Convert legacy to cash address
|
|
const cashAddr = bchjs.Address.toCashAddress(address)
|
|
|
|
// Ensure it's a valid BCH address
|
|
try {
|
|
bchjs.Address.toLegacyAddress(cashAddr)
|
|
} catch (err) {
|
|
throw new Error(`Invalid BCH address. Double check your address is valid: ${address}`)
|
|
}
|
|
|
|
// Ensure it's mainnet (no testnet support)
|
|
const isMainnet = bchjs.Address.isMainnetAddress(cashAddr)
|
|
if (!isMainnet) {
|
|
throw new Error('Invalid network. Only mainnet addresses are supported.')
|
|
}
|
|
|
|
return cashAddr
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/balance/:address Get balance for a single address
|
|
* @apiName GetBalance
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an object with confirmed and unconfirmed balance associated with an address.
|
|
*/
|
|
async getBalance (req, res) {
|
|
try {
|
|
const address = req.params.address
|
|
|
|
if (Array.isArray(address)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'address can not be an array. Use POST for bulk upload.'
|
|
})
|
|
}
|
|
|
|
const cashAddr = this._validateAndConvertAddress(address)
|
|
|
|
const result = await this.fulcrumUseCases.getBalance({ address: cashAddr })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/balance Get balances for an array of addresses
|
|
* @apiName GetBalances
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of balances associated with an array of addresses. Limited to 20 items per request.
|
|
*/
|
|
async balanceBulk (req, res) {
|
|
try {
|
|
const addresses = req.body.addresses
|
|
|
|
if (!Array.isArray(addresses)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'addresses needs to be an array. Use GET for single address.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(addresses.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
// Validate and convert all addresses
|
|
const validatedAddresses = []
|
|
for (let i = 0; i < addresses.length; i++) {
|
|
try {
|
|
const cashAddr = this._validateAndConvertAddress(addresses[i])
|
|
validatedAddresses.push(cashAddr)
|
|
} catch (err) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: err.message
|
|
})
|
|
}
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getBalances({ addresses: validatedAddresses })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/utxos/:address Get utxos for a single address
|
|
* @apiName GetUtxos
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an object with UTXOs associated with an address.
|
|
*/
|
|
async getUtxos (req, res) {
|
|
try {
|
|
const address = req.params.address
|
|
|
|
if (Array.isArray(address)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'address can not be an array. Use POST for bulk upload.'
|
|
})
|
|
}
|
|
|
|
const cashAddr = this._validateAndConvertAddress(address)
|
|
|
|
const result = await this.fulcrumUseCases.getUtxos({ address: cashAddr })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/utxos Get utxos for an array of addresses
|
|
* @apiName GetUtxosBulk
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of objects with UTXOs associated with an address. Limited to 20 items per request.
|
|
*/
|
|
async utxosBulk (req, res) {
|
|
try {
|
|
const addresses = req.body.addresses
|
|
|
|
if (!Array.isArray(addresses)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'addresses needs to be an array. Use GET for single address.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(addresses.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
// Validate and convert all addresses
|
|
const validatedAddresses = []
|
|
for (let i = 0; i < addresses.length; i++) {
|
|
try {
|
|
const cashAddr = this._validateAndConvertAddress(addresses[i])
|
|
validatedAddresses.push(cashAddr)
|
|
} catch (err) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: err.message
|
|
})
|
|
}
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getUtxosBulk({ addresses: validatedAddresses })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/tx/data/:txid Get transaction details for a TXID
|
|
* @apiName GetTransactionDetails
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an object with transaction details of the TXID
|
|
*/
|
|
async getTransactionDetails (req, res) {
|
|
try {
|
|
const txid = req.params.txid
|
|
|
|
if (typeof txid !== 'string') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'txid must be a string'
|
|
})
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getTransactionDetails({ txid })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/tx/data Get transaction details for an array of TXIDs
|
|
* @apiName GetTransactionDetailsBulk
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of objects with transaction details of an array of TXIDs. Limited to 20 items per request.
|
|
*/
|
|
async transactionDetailsBulk (req, res) {
|
|
try {
|
|
const txids = req.body.txids
|
|
const verbose = req.body.verbose !== undefined ? req.body.verbose : true
|
|
|
|
if (!Array.isArray(txids)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'txids needs to be an array. Use GET for single txid.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(txids.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getTransactionDetailsBulk({ txids, verbose })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/tx/broadcast Broadcast a raw transaction
|
|
* @apiName BroadcastTransaction
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Broadcast a raw transaction and return the transaction ID on success or error on failure.
|
|
*/
|
|
async broadcastTransaction (req, res) {
|
|
try {
|
|
const txHex = req.body.txHex
|
|
|
|
if (typeof txHex !== 'string') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'txHex must be a string'
|
|
})
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.broadcastTransaction({ txHex })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/block/headers/:height Get block headers
|
|
* @apiName GetBlockHeaders
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array with block headers starting at the block height
|
|
*
|
|
* @apiParam {Number} height Block height
|
|
* @apiParam {Number} count Number of block headers to return (query parameter, default: 1)
|
|
*/
|
|
async getBlockHeaders (req, res) {
|
|
try {
|
|
const heightRaw = req.params.height
|
|
const countRaw = req.query.count
|
|
|
|
const height = Number(heightRaw)
|
|
const count = countRaw === undefined ? 1 : Number(countRaw)
|
|
|
|
if (Number.isNaN(height) || height < 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'height must be a positive number'
|
|
})
|
|
}
|
|
|
|
if (Number.isNaN(count) || count < 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'count must be a positive number'
|
|
})
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getBlockHeaders({ height, count })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/block/headers Get block headers for an array of height + count pairs
|
|
* @apiName GetBlockHeadersBulk
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of objects with block headers. Limited to 20 items per request.
|
|
*/
|
|
async blockHeadersBulk (req, res) {
|
|
try {
|
|
const heights = req.body.heights
|
|
|
|
if (!Array.isArray(heights)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'heights needs to be an array. Use GET for single height.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(heights.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
// Validate each height object
|
|
for (const item of heights) {
|
|
if (!item || typeof item.height !== 'number' || typeof item.count !== 'number') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Each height object must have numeric height and count properties'
|
|
})
|
|
}
|
|
if (item.height < 0 || item.count < 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'height and count must be positive numbers'
|
|
})
|
|
}
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getBlockHeadersBulk({ heights })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/transactions/:address Get transaction history for a single address
|
|
* @apiName GetTransactions
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of historical transactions associated with an address. Results are returned in descending order (most recent TX first). Passing allTxs=true will return the entire transaction history, otherwise, only the last 100 TXIDs will be returned.
|
|
*
|
|
* @apiParam {String} address Address
|
|
* @apiParam {Boolean} allTxs Optional: return all transactions (default: false, limited to 100)
|
|
*/
|
|
async getTransactions (req, res) {
|
|
try {
|
|
const address = req.params.address
|
|
let allTxs = false
|
|
|
|
// Check if allTxs is in params or query
|
|
if (req.params.allTxs) {
|
|
allTxs = req.params.allTxs === 'true'
|
|
} else if (req.query.allTxs) {
|
|
allTxs = req.query.allTxs === 'true'
|
|
}
|
|
|
|
if (Array.isArray(address)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'address can not be an array. Use POST for bulk upload.'
|
|
})
|
|
}
|
|
|
|
const cashAddr = this._validateAndConvertAddress(address)
|
|
|
|
const result = await this.fulcrumUseCases.getTransactions({ address: cashAddr, allTxs })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/transactions Get the transaction history for an array of addresses
|
|
* @apiName GetTransactionsBulk
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of transactions associated with an array of addresses. Limited to 20 items per request. Passing allTxs=true will return the entire transaction history, otherwise, only the last 100 TXIDs will be returned.
|
|
*/
|
|
async transactionsBulk (req, res) {
|
|
try {
|
|
const addresses = req.body.addresses
|
|
const allTxs = req.body.allTxs === true
|
|
|
|
if (!Array.isArray(addresses)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'addresses needs to be an array. Use GET for single address.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(addresses.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
// Validate and convert all addresses
|
|
const validatedAddresses = []
|
|
for (let i = 0; i < addresses.length; i++) {
|
|
try {
|
|
const cashAddr = this._validateAndConvertAddress(addresses[i])
|
|
validatedAddresses.push(cashAddr)
|
|
} catch (err) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: err.message
|
|
})
|
|
}
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getTransactionsBulk({
|
|
addresses: validatedAddresses,
|
|
allTxs
|
|
})
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {get} /v6/full-node/fulcrum/unconfirmed/:address Get unconfirmed utxos for a single address
|
|
* @apiName GetMempool
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an object with unconfirmed UTXOs associated with an address.
|
|
*/
|
|
async getMempool (req, res) {
|
|
try {
|
|
const address = req.params.address
|
|
|
|
if (Array.isArray(address)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'address can not be an array. Use POST for bulk upload.'
|
|
})
|
|
}
|
|
|
|
const cashAddr = this._validateAndConvertAddress(address)
|
|
|
|
const result = await this.fulcrumUseCases.getMempool({ address: cashAddr })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @api {post} /v6/full-node/fulcrum/unconfirmed Get unconfirmed utxos for an array of addresses
|
|
* @apiName GetMempoolBulk
|
|
* @apiGroup Fulcrum
|
|
* @apiDescription Returns an array of objects with unconfirmed UTXOs associated with an address. Limited to 20 items per request.
|
|
*/
|
|
async mempoolBulk (req, res) {
|
|
try {
|
|
const addresses = req.body.addresses
|
|
|
|
if (!Array.isArray(addresses)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'addresses needs to be an array. Use GET for single address.'
|
|
})
|
|
}
|
|
|
|
if (!this.adapters.fullNode.validateArraySize(addresses.length)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Array too large.'
|
|
})
|
|
}
|
|
|
|
// Validate and convert all addresses
|
|
const validatedAddresses = []
|
|
for (let i = 0; i < addresses.length; i++) {
|
|
try {
|
|
const cashAddr = this._validateAndConvertAddress(addresses[i])
|
|
validatedAddresses.push(cashAddr)
|
|
} catch (err) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: err.message
|
|
})
|
|
}
|
|
}
|
|
|
|
const result = await this.fulcrumUseCases.getMempoolBulk({ addresses: validatedAddresses })
|
|
return res.status(200).json(result)
|
|
} catch (err) {
|
|
return this.handleError(err, res)
|
|
}
|
|
}
|
|
|
|
handleError (err, res) {
|
|
wlogger.error('Error in FulcrumRESTController:', err)
|
|
|
|
const status = err.status || 500
|
|
const message = err.message || 'Internal server error'
|
|
|
|
return res.status(status).json({ error: message })
|
|
}
|
|
}
|
|
|
|
export default FulcrumRESTController
|