mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api.git
synced 2026-09-21 16:52:00 -07:00
feat(fulcrum): Ported fulcrum endpoints from bch-api
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
Adapter library for interacting with Fulcrum API service over HTTP.
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import wlogger from './wlogger.js'
|
||||
import config from '../config/index.js'
|
||||
|
||||
class FulcrumAPIAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
this.config = localConfig.config || config
|
||||
|
||||
// Allow missing config for testing environments
|
||||
if (!this.config.fulcrumApi || !this.config.fulcrumApi.baseUrl) {
|
||||
if (process.env.NODE_ENV === 'test' || process.env.TEST) {
|
||||
// In test environment, create a mock baseURL
|
||||
this.config.fulcrumApi = {
|
||||
baseUrl: 'http://localhost:50001',
|
||||
timeoutMs: 15000
|
||||
}
|
||||
} else {
|
||||
throw new Error('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
baseUrl,
|
||||
timeoutMs = 15000
|
||||
} = this.config.fulcrumApi
|
||||
|
||||
this.http = axios.create({
|
||||
baseURL: baseUrl,
|
||||
timeout: timeoutMs
|
||||
})
|
||||
}
|
||||
|
||||
async get (path) {
|
||||
try {
|
||||
const response = await this.http.get(path)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
throw this._handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
async post (path, data) {
|
||||
try {
|
||||
const response = await this.http.post(path, data)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
throw this._handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
_handleError (err) {
|
||||
const { status, message } = this.decodeError(err)
|
||||
const error = new Error(message)
|
||||
error.status = status
|
||||
error.originalError = err
|
||||
return error
|
||||
}
|
||||
|
||||
decodeError (err) {
|
||||
try {
|
||||
// Attempt to extract error message from response data
|
||||
if (err.response && err.response.data) {
|
||||
const data = err.response.data
|
||||
// Handle structured error responses
|
||||
if (data.error) {
|
||||
return this._formatError(data.error, err.response.status || 400)
|
||||
}
|
||||
// Handle string error messages
|
||||
if (typeof data === 'string') {
|
||||
return this._formatError(data, err.response.status || 400)
|
||||
}
|
||||
// Handle object responses that might contain error info
|
||||
if (typeof data === 'object' && data.message) {
|
||||
return this._formatError(data.message, err.response.status || 400)
|
||||
}
|
||||
// Fallback to returning the status
|
||||
return this._formatError('Fulcrum API error', err.response.status || 500)
|
||||
}
|
||||
|
||||
// Network errors
|
||||
if (err.message) {
|
||||
if (err.message.includes('ENOTFOUND') || err.message.includes('ENETUNREACH') || err.message.includes('EAI_AGAIN')) {
|
||||
return this._formatError(
|
||||
'Network error: Could not communicate with Fulcrum API service.',
|
||||
503
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (err.code && (err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')) {
|
||||
return this._formatError(
|
||||
'Network error: Could not communicate with Fulcrum API service.',
|
||||
503
|
||||
)
|
||||
}
|
||||
|
||||
if (err.error && typeof err.error === 'string' && err.error.includes('429')) {
|
||||
return this._formatError('429 Too Many Requests', 429)
|
||||
}
|
||||
|
||||
if (err.message) {
|
||||
return this._formatError(err.message, err.status || 422)
|
||||
}
|
||||
|
||||
return this._formatError('Unhandled Fulcrum API error', 500)
|
||||
} catch (decodeError) {
|
||||
wlogger.error('Unhandled error in FulcrumAPIAdapter.decodeError()', decodeError)
|
||||
return this._formatError('Internal server error', 500)
|
||||
}
|
||||
}
|
||||
|
||||
_formatError (message, status = 500) {
|
||||
return {
|
||||
message: message || 'Internal server error',
|
||||
status: status || 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FulcrumAPIAdapter
|
||||
@@ -7,6 +7,7 @@
|
||||
// Load individual adapter libraries.
|
||||
// import NostrRelayAdapter from './nostr-relay.js'
|
||||
import FullNodeRPCAdapter from './full-node-rpc.js'
|
||||
import FulcrumAPIAdapter from './fulcrum-api.js'
|
||||
import config from '../config/index.js'
|
||||
|
||||
class Adapters {
|
||||
@@ -33,6 +34,7 @@ class Adapters {
|
||||
// this.nostrRelay = this.nostrRelays[0]
|
||||
|
||||
this.fullNode = new FullNodeRPCAdapter({ config: this.config })
|
||||
this.fulcrum = new FulcrumAPIAdapter({ config: this.config })
|
||||
}
|
||||
|
||||
async start () {
|
||||
|
||||
Vendored
+6
@@ -60,6 +60,12 @@ export default {
|
||||
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
||||
},
|
||||
|
||||
// Fulcrum API configuration
|
||||
fulcrumApi: {
|
||||
baseUrl: process.env.FULCRUM_API || '',
|
||||
timeoutMs: Number(process.env.FULCRUM_TIMEOUT_MS || 15000)
|
||||
},
|
||||
|
||||
x402: x402Defaults,
|
||||
|
||||
// Version
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
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
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
REST API router for /full-node/fulcrum routes.
|
||||
*/
|
||||
|
||||
import express from 'express'
|
||||
import FulcrumRESTController from './controller.js'
|
||||
|
||||
class FulcrumRouter {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Fulcrum REST Router.'
|
||||
)
|
||||
}
|
||||
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Fulcrum REST Router.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
this.fulcrumController = new FulcrumRESTController(dependencies)
|
||||
|
||||
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
||||
this.baseUrl = `${this.apiPrefix}/full-node/fulcrum`
|
||||
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.fulcrumController.root)
|
||||
this.router.get('/balance/:address', this.fulcrumController.getBalance)
|
||||
this.router.post('/balance', this.fulcrumController.balanceBulk)
|
||||
this.router.get('/utxos/:address', this.fulcrumController.getUtxos)
|
||||
this.router.post('/utxos', this.fulcrumController.utxosBulk)
|
||||
this.router.get('/tx/data/:txid', this.fulcrumController.getTransactionDetails)
|
||||
this.router.post('/tx/data', this.fulcrumController.transactionDetailsBulk)
|
||||
this.router.post('/tx/broadcast', this.fulcrumController.broadcastTransaction)
|
||||
this.router.get('/block/headers/:height', this.fulcrumController.getBlockHeaders)
|
||||
this.router.post('/block/headers', this.fulcrumController.blockHeadersBulk)
|
||||
this.router.get('/transactions/:address', this.fulcrumController.getTransactions)
|
||||
this.router.get('/transactions/:address/:allTxs', this.fulcrumController.getTransactions)
|
||||
this.router.post('/transactions', this.fulcrumController.transactionsBulk)
|
||||
this.router.get('/unconfirmed/:address', this.fulcrumController.getMempool)
|
||||
this.router.post('/unconfirmed', this.fulcrumController.mempoolBulk)
|
||||
|
||||
app.use(this.baseUrl, this.router)
|
||||
}
|
||||
}
|
||||
|
||||
export default FulcrumRouter
|
||||
@@ -10,6 +10,7 @@
|
||||
import BlockchainRouter from './full-node/blockchain/router.js'
|
||||
import ControlRouter from './full-node/control/router.js'
|
||||
import DSProofRouter from './full-node/dsproof/router.js'
|
||||
import FulcrumRouter from './full-node/fulcrum/router.js'
|
||||
import MiningRouter from './full-node/mining/router.js'
|
||||
import RawTransactionsRouter from './full-node/rawtransactions/router.js'
|
||||
import config from '../../config/index.js'
|
||||
@@ -67,6 +68,9 @@ class RESTControllers {
|
||||
const dsproofRouter = new DSProofRouter(dependencies)
|
||||
dsproofRouter.attach(app)
|
||||
|
||||
const fulcrumRouter = new FulcrumRouter(dependencies)
|
||||
fulcrumRouter.attach(app)
|
||||
|
||||
const miningRouter = new MiningRouter(dependencies)
|
||||
miningRouter.attach(app)
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Use cases for interacting with the Fulcrum API service.
|
||||
*/
|
||||
|
||||
import wlogger from '../adapters/wlogger.js'
|
||||
import BCHJS from '@psf/bch-js'
|
||||
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
class FulcrumUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
|
||||
if (!this.adapters) {
|
||||
throw new Error('Adapters instance required when instantiating Fulcrum use cases.')
|
||||
}
|
||||
|
||||
this.fulcrum = this.adapters.fulcrum
|
||||
if (!this.fulcrum) {
|
||||
throw new Error('Fulcrum adapter required when instantiating Fulcrum use cases.')
|
||||
}
|
||||
|
||||
// Allow bchjs to be injected for testing
|
||||
this.bchjs = localConfig.bchjs || bchjs
|
||||
}
|
||||
|
||||
async getBalance ({ address }) {
|
||||
return this.fulcrum.get(`electrumx/balance/${address}`)
|
||||
}
|
||||
|
||||
async getBalances ({ addresses }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/balance/', { addresses })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getBalances()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getUtxos ({ address }) {
|
||||
return this.fulcrum.get(`electrumx/utxos/${address}`)
|
||||
}
|
||||
|
||||
async getUtxosBulk ({ addresses }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/utxos/', { addresses })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getUtxosBulk()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionDetails ({ txid }) {
|
||||
return this.fulcrum.get(`electrumx/tx/data/${txid}`)
|
||||
}
|
||||
|
||||
async getTransactionDetailsBulk ({ txids, verbose }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/tx/data', { txids, verbose })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getTransactionDetailsBulk()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastTransaction ({ txHex }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/tx/broadcast', { txHex })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.broadcastTransaction()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getBlockHeaders ({ height, count }) {
|
||||
return this.fulcrum.get(`electrumx/block/headers/${height}?count=${count}`)
|
||||
}
|
||||
|
||||
async getBlockHeadersBulk ({ heights }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/block/headers', { heights })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getBlockHeadersBulk()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactions ({ address, allTxs }) {
|
||||
try {
|
||||
const response = await this.fulcrum.get(`electrumx/transactions/${address}`)
|
||||
|
||||
// Sort transactions in descending order, so that newest transactions are first.
|
||||
if (response.transactions && Array.isArray(response.transactions)) {
|
||||
response.transactions = await this.bchjs.Electrumx.sortAllTxs(response.transactions, 'DESCENDING')
|
||||
|
||||
if (!allTxs) {
|
||||
// Return only the first 100 transactions of the history.
|
||||
response.transactions = response.transactions.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getTransactions()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionsBulk ({ addresses, allTxs }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/transactions/', { addresses })
|
||||
|
||||
// Sort transactions in descending order for each address entry.
|
||||
if (response.transactions && Array.isArray(response.transactions)) {
|
||||
for (let i = 0; i < response.transactions.length; i++) {
|
||||
const thisEntry = response.transactions[i]
|
||||
if (thisEntry.transactions && Array.isArray(thisEntry.transactions)) {
|
||||
thisEntry.transactions = await this.bchjs.Electrumx.sortAllTxs(thisEntry.transactions, 'DESCENDING')
|
||||
|
||||
if (!allTxs && thisEntry.transactions.length > 100) {
|
||||
// Extract only the first 100 transactions.
|
||||
thisEntry.transactions = thisEntry.transactions.slice(0, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getTransactionsBulk()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getMempool ({ address }) {
|
||||
return this.fulcrum.get(`electrumx/unconfirmed/${address}`)
|
||||
}
|
||||
|
||||
async getMempoolBulk ({ addresses }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/unconfirmed/', { addresses })
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getMempoolBulk()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FulcrumUseCases
|
||||
@@ -8,6 +8,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 FulcrumUseCases from './full-node-fulcrum-use-cases.js'
|
||||
import MiningUseCases from './full-node-mining-use-cases.js'
|
||||
import RawTransactionsUseCases from './full-node-rawtransactions-use-cases.js'
|
||||
|
||||
@@ -23,6 +24,7 @@ class UseCases {
|
||||
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
||||
this.control = new ControlUseCases({ adapters: this.adapters })
|
||||
this.dsproof = new DSProofUseCases({ adapters: this.adapters })
|
||||
this.fulcrum = new FulcrumUseCases({ adapters: this.adapters })
|
||||
this.mining = new MiningUseCases({ adapters: this.adapters })
|
||||
this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user