2025-11-14 07:25:23 -08:00
/*
2025-11-15 17:55:56 -08:00
REST API Controller for the /fulcrum routes.
2025-11-14 07:25:23 -08:00
*/
2025-11-14 08:46:34 -08:00
import wlogger from '../../../adapters/wlogger.js'
2025-11-14 07:25:23 -08:00
import BCHJS from '@psf/bch-js'
2025-11-26 11:00:08 -08:00
import config from '../../../config/index.js'
2025-11-14 07:25:23 -08:00
2025-11-26 11:00:08 -08:00
const bchjs = new BCHJS ({ restURL : config . restURL })
2025-11-14 07:25:23 -08:00
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 )
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/ Service status
2025-11-14 07:25:23 -08:00
* @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
}
2026-03-17 16:19:36 -07:00
_isValidTxid ( txid ) {
return typeof txid === 'string' && /^[a-fA-F0-9]{64}$/ . test ( txid )
}
_isCommonMissingTxError ( err ) {
return err ? . status === 404 &&
typeof err ? . message === 'string' &&
err . message . includes ( 'Transaction not found' )
}
2025-11-14 07:25:23 -08:00
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/balance/:address Get balance for a single address
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/balance Get balances for an array of addresses
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/utxos/:address Get utxos for a single address
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/utxos Get utxos for an array of addresses
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/tx/data/:txid Get transaction details for a TXID
2025-11-14 07:25:23 -08:00
* @apiName GetTransactionDetails
* @apiGroup Fulcrum
* @apiDescription Returns an object with transaction details of the TXID
*/
async getTransactionDetails ( req , res ) {
try {
const txid = req . params . txid
2026-03-17 16:19:36 -07:00
if ( ! this . _isValidTxid ( txid )) {
2025-11-14 07:25:23 -08:00
return res . status ( 400 ). json ({
success : false ,
2026-03-17 16:19:36 -07:00
error : 'txid must be a 64-character hex string'
2025-11-14 07:25:23 -08:00
})
}
const result = await this . fulcrumUseCases . getTransactionDetails ({ txid })
return res . status ( 200 ). json ( result )
} catch ( err ) {
return this . handleError ( err , res )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/tx/data Get transaction details for an array of TXIDs
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/tx/broadcast Broadcast a raw transaction
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/block/headers/:height Get block headers
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/block/headers Get block headers for an array of height + count pairs
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/transactions/:address Get transaction history for a single address
2025-11-14 07:25:23 -08:00
* @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 )
2026-02-04 14:31:06 -07:00
// Extract bearer token from request header if present
let bearerToken = null
if ( req . headers && req . headers . authorization ) {
const parts = req . headers . authorization . split ( ' ' )
if ( parts . length === 2 && parts [ 0 ] === 'Bearer' ) {
bearerToken = parts [ 1 ]
}
}
const result = await this . fulcrumUseCases . getTransactions ({
address : cashAddr ,
allTxs ,
bearerToken
})
2025-11-14 07:25:23 -08:00
return res . status ( 200 ). json ( result )
} catch ( err ) {
return this . handleError ( err , res )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/transactions Get the transaction history for an array of addresses
2025-11-14 07:25:23 -08:00
* @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
})
}
}
2026-02-04 14:31:06 -07:00
// Extract bearer token from request header if present
let bearerToken = null
if ( req . headers && req . headers . authorization ) {
const parts = req . headers . authorization . split ( ' ' )
if ( parts . length === 2 && parts [ 0 ] === 'Bearer' ) {
bearerToken = parts [ 1 ]
}
}
2025-11-14 07:25:23 -08:00
const result = await this . fulcrumUseCases . getTransactionsBulk ({
addresses : validatedAddresses ,
2026-02-04 14:31:06 -07:00
allTxs ,
bearerToken
2025-11-14 07:25:23 -08:00
})
return res . status ( 200 ). json ( result )
} catch ( err ) {
return this . handleError ( err , res )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {get} /v6/fulcrum/unconfirmed/:address Get unconfirmed utxos for a single address
2025-11-14 07:25:23 -08:00
* @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 )
}
}
/**
2025-11-15 17:55:56 -08:00
* @api {post} /v6/fulcrum/unconfirmed Get unconfirmed utxos for an array of addresses
2025-11-14 07:25:23 -08:00
* @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 ) {
const status = err . status || 500
2026-03-17 16:19:36 -07:00
const isCommonMissingTxError = this . _isCommonMissingTxError ( err )
if ( isCommonMissingTxError ) {
wlogger . info ( `Fulcrum transaction not found: ${ err . message } ` )
} else if ( status >= 500 ) {
wlogger . error ( 'Error in FulcrumRESTController:' , err )
} else {
wlogger . warn ( `Fulcrum client error ( ${ status } ): ${ err . message } ` )
}
2025-11-14 07:25:23 -08:00
const message = err . message || 'Internal server error'
return res . status ( status ). json ({ error : message })
}
}
export default FulcrumRESTController