psf-bch-api 1.2.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.env-local +28 -0
  2. package/bin/server.js +61 -9
  3. package/package.json +6 -2
  4. package/src/adapters/fulcrum-api.js +124 -0
  5. package/src/adapters/full-node-rpc.js +2 -6
  6. package/src/adapters/index.js +4 -0
  7. package/src/adapters/slp-indexer-api.js +124 -0
  8. package/src/config/env/common.js +29 -25
  9. package/src/config/x402.js +7 -0
  10. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  11. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  12. package/src/controllers/rest-api/full-node/blockchain/controller.js +4 -4
  13. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  14. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  15. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  16. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  17. package/src/controllers/rest-api/index.js +23 -3
  18. package/src/controllers/rest-api/price/controller.js +96 -0
  19. package/src/controllers/rest-api/price/router.js +52 -0
  20. package/src/controllers/rest-api/slp/controller.js +218 -0
  21. package/src/controllers/rest-api/slp/router.js +55 -0
  22. package/src/controllers/timer-controller.js +1 -1
  23. package/src/middleware/basic-auth.js +61 -0
  24. package/src/use-cases/fulcrum-use-cases.js +155 -0
  25. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  26. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  27. package/src/use-cases/index.js +10 -0
  28. package/src/use-cases/price-use-cases.js +83 -0
  29. package/src/use-cases/slp-use-cases.js +321 -0
  30. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  31. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  32. package/test/unit/controllers/mining-controller-unit.js +139 -0
  33. package/test/unit/controllers/price-controller-unit.js +116 -0
  34. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  35. package/test/unit/controllers/rest-api-index-unit.js +67 -3
  36. package/test/unit/controllers/slp-controller-unit.js +312 -0
  37. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  38. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  39. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  40. package/test/unit/use-cases/price-use-cases-unit.js +103 -0
  41. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  42. package/src/entities/event.js +0 -71
  43. package/test/integration/api/event-integration.js +0 -250
  44. package/test/integration/api/req-integration.js +0 -173
  45. package/test/integration/api/subscription-integration.js +0 -198
  46. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  47. package/test/integration/use-cases/publish-event-integration.js +0 -104
  48. package/test/integration/use-cases/query-events-integration.js +0 -95
  49. package/test/unit/entities/event-unit.js +0 -139
  50. /package/{index.js → psf-bch-api.js} +0 -0
  51. /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
  52. /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
  53. /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
@@ -0,0 +1,52 @@
1
+ /*
2
+ REST API router for /price routes.
3
+ */
4
+
5
+ import express from 'express'
6
+ import PriceRESTController from './controller.js'
7
+
8
+ class PriceRouter {
9
+ constructor (localConfig = {}) {
10
+ this.adapters = localConfig.adapters
11
+ if (!this.adapters) {
12
+ throw new Error(
13
+ 'Instance of Adapters library required when instantiating Price REST Router.'
14
+ )
15
+ }
16
+
17
+ this.useCases = localConfig.useCases
18
+ if (!this.useCases) {
19
+ throw new Error(
20
+ 'Instance of Use Cases library required when instantiating Price REST Router.'
21
+ )
22
+ }
23
+
24
+ const dependencies = {
25
+ adapters: this.adapters,
26
+ useCases: this.useCases
27
+ }
28
+
29
+ this.priceController = new PriceRESTController(dependencies)
30
+
31
+ this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
32
+ this.baseUrl = `${this.apiPrefix}/price`
33
+ if (!this.baseUrl.startsWith('/')) {
34
+ this.baseUrl = `/${this.baseUrl}`
35
+ }
36
+ this.router = express.Router()
37
+ }
38
+
39
+ attach (app) {
40
+ if (!app) {
41
+ throw new Error('Must pass app object when attaching REST API controllers.')
42
+ }
43
+
44
+ this.router.get('/', this.priceController.root)
45
+ this.router.get('/bchusd', this.priceController.getBCHUSD)
46
+ this.router.get('/psffpp', this.priceController.getPsffppWritePrice)
47
+
48
+ app.use(this.baseUrl, this.router)
49
+ }
50
+ }
51
+
52
+ export default PriceRouter
@@ -0,0 +1,218 @@
1
+ /*
2
+ REST API Controller for the /slp routes.
3
+ */
4
+
5
+ import wlogger from '../../../adapters/wlogger.js'
6
+ import BCHJS from '@psf/bch-js'
7
+
8
+ const bchjs = new BCHJS()
9
+
10
+ class SlpRESTController {
11
+ constructor (localConfig = {}) {
12
+ this.adapters = localConfig.adapters
13
+ if (!this.adapters) {
14
+ throw new Error(
15
+ 'Instance of Adapters library required when instantiating SLP REST Controller.'
16
+ )
17
+ }
18
+
19
+ this.useCases = localConfig.useCases
20
+ if (!this.useCases || !this.useCases.slp) {
21
+ throw new Error(
22
+ 'Instance of SLP use cases required when instantiating SLP REST Controller.'
23
+ )
24
+ }
25
+
26
+ this.slpUseCases = this.useCases.slp
27
+
28
+ // Bind functions
29
+ this.root = this.root.bind(this)
30
+ this.getStatus = this.getStatus.bind(this)
31
+ this.getAddress = this.getAddress.bind(this)
32
+ this.getTxid = this.getTxid.bind(this)
33
+ this.getTokenStats = this.getTokenStats.bind(this)
34
+ this.getTokenData = this.getTokenData.bind(this)
35
+ this.handleError = this.handleError.bind(this)
36
+ }
37
+
38
+ /**
39
+ * @api {get} /v6/slp/ Service status
40
+ * @apiName SlpRoot
41
+ * @apiGroup SLP
42
+ *
43
+ * @apiDescription Returns the status of the SLP service.
44
+ *
45
+ * @apiSuccess {String} status Service identifier
46
+ */
47
+ async root (req, res) {
48
+ return res.status(200).json({ status: 'psf-slp-indexer' })
49
+ }
50
+
51
+ /**
52
+ * Validates and converts an address to cash address format
53
+ * @param {string} address - Address to validate and convert
54
+ * @returns {string} Cash address
55
+ * @throws {Error} If address is invalid or not mainnet
56
+ */
57
+ _validateAndConvertAddress (address) {
58
+ if (!address) {
59
+ throw new Error('address is empty')
60
+ }
61
+
62
+ // Convert legacy to cash address
63
+ const cashAddr = bchjs.SLP.Address.toCashAddress(address)
64
+
65
+ // Ensure it's a valid BCH address
66
+ try {
67
+ bchjs.SLP.Address.toLegacyAddress(cashAddr)
68
+ } catch (err) {
69
+ throw new Error(`Invalid BCH address. Double check your address is valid: ${address}`)
70
+ }
71
+
72
+ // Ensure it's mainnet (no testnet support)
73
+ const isMainnet = bchjs.Address.isMainnetAddress(cashAddr)
74
+ if (!isMainnet) {
75
+ throw new Error('Invalid network. Only mainnet addresses are supported.')
76
+ }
77
+
78
+ return cashAddr
79
+ }
80
+
81
+ /**
82
+ * @api {get} /v6/slp/status Get indexer status
83
+ * @apiName GetStatus
84
+ * @apiGroup SLP
85
+ * @apiDescription Returns the status of the SLP indexer.
86
+ */
87
+ async getStatus (req, res) {
88
+ try {
89
+ const result = await this.slpUseCases.getStatus()
90
+ return res.status(200).json(result)
91
+ } catch (err) {
92
+ return this.handleError(err, res)
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @api {post} /v6/slp/address Get SLP balance for address
98
+ * @apiName GetAddress
99
+ * @apiGroup SLP
100
+ * @apiDescription Returns SLP balance for an address.
101
+ */
102
+ async getAddress (req, res) {
103
+ try {
104
+ const address = req.body.address
105
+
106
+ if (!address || address === '') {
107
+ return res.status(400).json({
108
+ success: false,
109
+ error: 'address can not be empty'
110
+ })
111
+ }
112
+
113
+ // Validate and convert address
114
+ const cashAddr = this._validateAndConvertAddress(address)
115
+
116
+ const result = await this.slpUseCases.getAddress({ address: cashAddr })
117
+ return res.status(200).json(result)
118
+ } catch (err) {
119
+ return this.handleError(err, res)
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @api {post} /v6/slp/txid Get SLP transaction data
125
+ * @apiName GetTxid
126
+ * @apiGroup SLP
127
+ * @apiDescription Returns SLP transaction data for a TXID.
128
+ */
129
+ async getTxid (req, res) {
130
+ try {
131
+ const txid = req.body.txid
132
+
133
+ if (!txid || txid === '') {
134
+ return res.status(400).json({
135
+ success: false,
136
+ error: 'txid can not be empty'
137
+ })
138
+ }
139
+
140
+ if (txid.length !== 64) {
141
+ return res.status(400).json({
142
+ success: false,
143
+ error: 'This is not a txid'
144
+ })
145
+ }
146
+
147
+ const result = await this.slpUseCases.getTxid({ txid })
148
+ return res.status(200).json(result)
149
+ } catch (err) {
150
+ return this.handleError(err, res)
151
+ }
152
+ }
153
+
154
+ /**
155
+ * @api {post} /v6/slp/token Get token statistics
156
+ * @apiName GetTokenStats
157
+ * @apiGroup SLP
158
+ * @apiDescription Returns statistics for a single SLP token.
159
+ */
160
+ async getTokenStats (req, res) {
161
+ try {
162
+ const tokenId = req.body.tokenId
163
+
164
+ if (!tokenId || tokenId === '') {
165
+ return res.status(400).json({
166
+ success: false,
167
+ error: 'tokenId can not be empty'
168
+ })
169
+ }
170
+
171
+ // Flag to toggle tx history of the token
172
+ const withTxHistory = req.body.withTxHistory === true
173
+
174
+ const result = await this.slpUseCases.getTokenStats({ tokenId, withTxHistory })
175
+ return res.status(200).json(result)
176
+ } catch (err) {
177
+ return this.handleError(err, res)
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @api {post} /v6/slp/token/data Get token data
183
+ * @apiName GetTokenData
184
+ * @apiGroup SLP
185
+ * @apiDescription Get mutable and immutable data if the token contains them.
186
+ */
187
+ async getTokenData (req, res) {
188
+ try {
189
+ const tokenId = req.body.tokenId
190
+
191
+ if (!tokenId || tokenId === '') {
192
+ return res.status(400).json({
193
+ success: false,
194
+ error: 'tokenId can not be empty'
195
+ })
196
+ }
197
+
198
+ // Flag to toggle tx history of the token
199
+ const withTxHistory = req.body.withTxHistory === true
200
+
201
+ const result = await this.slpUseCases.getTokenData({ tokenId, withTxHistory })
202
+ return res.status(200).json(result)
203
+ } catch (err) {
204
+ return this.handleError(err, res)
205
+ }
206
+ }
207
+
208
+ handleError (err, res) {
209
+ wlogger.error('Error in SlpRESTController:', err)
210
+
211
+ const status = err.status || 500
212
+ const message = err.message || 'Internal server error'
213
+
214
+ return res.status(status).json({ error: message })
215
+ }
216
+ }
217
+
218
+ export default SlpRESTController
@@ -0,0 +1,55 @@
1
+ /*
2
+ REST API router for /slp routes.
3
+ */
4
+
5
+ import express from 'express'
6
+ import SlpRESTController from './controller.js'
7
+
8
+ class SlpRouter {
9
+ constructor (localConfig = {}) {
10
+ this.adapters = localConfig.adapters
11
+ if (!this.adapters) {
12
+ throw new Error(
13
+ 'Instance of Adapters library required when instantiating SLP REST Router.'
14
+ )
15
+ }
16
+
17
+ this.useCases = localConfig.useCases
18
+ if (!this.useCases) {
19
+ throw new Error(
20
+ 'Instance of Use Cases library required when instantiating SLP REST Router.'
21
+ )
22
+ }
23
+
24
+ const dependencies = {
25
+ adapters: this.adapters,
26
+ useCases: this.useCases
27
+ }
28
+
29
+ this.slpController = new SlpRESTController(dependencies)
30
+
31
+ this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
32
+ this.baseUrl = `${this.apiPrefix}/slp`
33
+ if (!this.baseUrl.startsWith('/')) {
34
+ this.baseUrl = `/${this.baseUrl}`
35
+ }
36
+ this.router = express.Router()
37
+ }
38
+
39
+ attach (app) {
40
+ if (!app) {
41
+ throw new Error('Must pass app object when attaching REST API controllers.')
42
+ }
43
+
44
+ this.router.get('/', this.slpController.root)
45
+ this.router.get('/status', this.slpController.getStatus)
46
+ this.router.post('/address', this.slpController.getAddress)
47
+ this.router.post('/txid', this.slpController.getTxid)
48
+ this.router.post('/token', this.slpController.getTokenStats)
49
+ this.router.post('/token/data', this.slpController.getTokenData)
50
+
51
+ app.use(this.baseUrl, this.router)
52
+ }
53
+ }
54
+
55
+ export default SlpRouter
@@ -23,7 +23,7 @@ class TimerController {
23
23
  }
24
24
 
25
25
  // Constants
26
- this.SHUTDOWN_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes in milliseconds
26
+ this.SHUTDOWN_INTERVAL_MS = 10 * 60 * 60 * 1000 // 10 hours in milliseconds
27
27
  this.LIVENESS_CHECK_INTERVAL_MS = 1 * 60 * 1000 // 1 minute in milliseconds
28
28
 
29
29
  // Handlers
@@ -0,0 +1,61 @@
1
+ /*
2
+ Basic Authentication Middleware
3
+
4
+ This middleware validates Bearer tokens from the Authorization header.
5
+ When a valid token is provided, it sets req.locals.basicAuthValid = true
6
+ to allow bypassing x402 middleware.
7
+ */
8
+
9
+ import config from '../config/index.js'
10
+ import wlogger from '../adapters/wlogger.js'
11
+
12
+ /**
13
+ * Middleware function that validates Bearer token authentication
14
+ * @param {Object} req - Express request object
15
+ * @param {Object} res - Express response object
16
+ * @param {Function} next - Express next middleware function
17
+ */
18
+ export function basicAuthMiddleware (req, res, next) {
19
+ // Initialize req.locals if it doesn't exist
20
+ if (!req.locals) {
21
+ req.locals = {}
22
+ }
23
+
24
+ // Default to false
25
+ req.locals.basicAuthValid = false
26
+
27
+ // Get the configured token
28
+ const configuredToken = config.basicAuth?.token
29
+
30
+ // If no token is configured, skip validation
31
+ if (!configuredToken) {
32
+ wlogger.warn('Basic auth enabled but no BASIC_AUTH_TOKEN configured')
33
+ return next()
34
+ }
35
+
36
+ // Get the Authorization header
37
+ const authHeader = req.headers.authorization
38
+
39
+ // If no Authorization header, continue (x402 will handle unauthorized requests)
40
+ if (!authHeader) {
41
+ return next()
42
+ }
43
+
44
+ // Check if it's a Bearer token
45
+ const parts = authHeader.split(' ')
46
+ if (parts.length !== 2 || parts[0] !== 'Bearer') {
47
+ return next()
48
+ }
49
+
50
+ const providedToken = parts[1]
51
+
52
+ // Compare tokens
53
+ if (providedToken === configuredToken) {
54
+ req.locals.basicAuthValid = true
55
+ wlogger.verbose(`Basic auth validated for request to ${req.path}`)
56
+ }
57
+
58
+ // Always continue to next middleware
59
+ // If auth failed, x402 middleware will handle the request
60
+ next()
61
+ }
@@ -0,0 +1,155 @@
1
+ /*
2
+ Use cases for interacting with the Fulcrum API service.
3
+ */
4
+
5
+ import wlogger from '../adapters/wlogger.js'
6
+ import BCHJS from '@psf/bch-js'
7
+
8
+ const bchjs = new BCHJS()
9
+
10
+ class FulcrumUseCases {
11
+ constructor (localConfig = {}) {
12
+ this.adapters = localConfig.adapters
13
+
14
+ if (!this.adapters) {
15
+ throw new Error('Adapters instance required when instantiating Fulcrum use cases.')
16
+ }
17
+
18
+ this.fulcrum = this.adapters.fulcrum
19
+ if (!this.fulcrum) {
20
+ throw new Error('Fulcrum adapter required when instantiating Fulcrum use cases.')
21
+ }
22
+
23
+ // Allow bchjs to be injected for testing
24
+ this.bchjs = localConfig.bchjs || bchjs
25
+ }
26
+
27
+ async getBalance ({ address }) {
28
+ return this.fulcrum.get(`electrumx/balance/${address}`)
29
+ }
30
+
31
+ async getBalances ({ addresses }) {
32
+ try {
33
+ const response = await this.fulcrum.post('electrumx/balance/', { addresses })
34
+ return response
35
+ } catch (err) {
36
+ wlogger.error('Error in FulcrumUseCases.getBalances()', err)
37
+ throw err
38
+ }
39
+ }
40
+
41
+ async getUtxos ({ address }) {
42
+ return this.fulcrum.get(`electrumx/utxos/${address}`)
43
+ }
44
+
45
+ async getUtxosBulk ({ addresses }) {
46
+ try {
47
+ const response = await this.fulcrum.post('electrumx/utxos/', { addresses })
48
+ return response
49
+ } catch (err) {
50
+ wlogger.error('Error in FulcrumUseCases.getUtxosBulk()', err)
51
+ throw err
52
+ }
53
+ }
54
+
55
+ async getTransactionDetails ({ txid }) {
56
+ return this.fulcrum.get(`electrumx/tx/data/${txid}`)
57
+ }
58
+
59
+ async getTransactionDetailsBulk ({ txids, verbose }) {
60
+ try {
61
+ const response = await this.fulcrum.post('electrumx/tx/data', { txids, verbose })
62
+ return response
63
+ } catch (err) {
64
+ wlogger.error('Error in FulcrumUseCases.getTransactionDetailsBulk()', err)
65
+ throw err
66
+ }
67
+ }
68
+
69
+ async broadcastTransaction ({ txHex }) {
70
+ try {
71
+ const response = await this.fulcrum.post('electrumx/tx/broadcast', { txHex })
72
+ return response
73
+ } catch (err) {
74
+ wlogger.error('Error in FulcrumUseCases.broadcastTransaction()', err)
75
+ throw err
76
+ }
77
+ }
78
+
79
+ async getBlockHeaders ({ height, count }) {
80
+ return this.fulcrum.get(`electrumx/block/headers/${height}?count=${count}`)
81
+ }
82
+
83
+ async getBlockHeadersBulk ({ heights }) {
84
+ try {
85
+ const response = await this.fulcrum.post('electrumx/block/headers', { heights })
86
+ return response
87
+ } catch (err) {
88
+ wlogger.error('Error in FulcrumUseCases.getBlockHeadersBulk()', err)
89
+ throw err
90
+ }
91
+ }
92
+
93
+ async getTransactions ({ address, allTxs }) {
94
+ try {
95
+ const response = await this.fulcrum.get(`electrumx/transactions/${address}`)
96
+
97
+ // Sort transactions in descending order, so that newest transactions are first.
98
+ if (response.transactions && Array.isArray(response.transactions)) {
99
+ response.transactions = await this.bchjs.Electrumx.sortAllTxs(response.transactions, 'DESCENDING')
100
+
101
+ if (!allTxs) {
102
+ // Return only the first 100 transactions of the history.
103
+ response.transactions = response.transactions.slice(0, 100)
104
+ }
105
+ }
106
+
107
+ return response
108
+ } catch (err) {
109
+ wlogger.error('Error in FulcrumUseCases.getTransactions()', err)
110
+ throw err
111
+ }
112
+ }
113
+
114
+ async getTransactionsBulk ({ addresses, allTxs }) {
115
+ try {
116
+ const response = await this.fulcrum.post('electrumx/transactions/', { addresses })
117
+
118
+ // Sort transactions in descending order for each address entry.
119
+ if (response.transactions && Array.isArray(response.transactions)) {
120
+ for (let i = 0; i < response.transactions.length; i++) {
121
+ const thisEntry = response.transactions[i]
122
+ if (thisEntry.transactions && Array.isArray(thisEntry.transactions)) {
123
+ thisEntry.transactions = await this.bchjs.Electrumx.sortAllTxs(thisEntry.transactions, 'DESCENDING')
124
+
125
+ if (!allTxs && thisEntry.transactions.length > 100) {
126
+ // Extract only the first 100 transactions.
127
+ thisEntry.transactions = thisEntry.transactions.slice(0, 100)
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ return response
134
+ } catch (err) {
135
+ wlogger.error('Error in FulcrumUseCases.getTransactionsBulk()', err)
136
+ throw err
137
+ }
138
+ }
139
+
140
+ async getMempool ({ address }) {
141
+ return this.fulcrum.get(`electrumx/unconfirmed/${address}`)
142
+ }
143
+
144
+ async getMempoolBulk ({ addresses }) {
145
+ try {
146
+ const response = await this.fulcrum.post('electrumx/unconfirmed/', { addresses })
147
+ return response
148
+ } catch (err) {
149
+ wlogger.error('Error in FulcrumUseCases.getMempoolBulk()', err)
150
+ throw err
151
+ }
152
+ }
153
+ }
154
+
155
+ export default FulcrumUseCases
@@ -0,0 +1,28 @@
1
+ /*
2
+ Use cases for interacting with the BCH full node mining RPC interface.
3
+ */
4
+
5
+ class MiningUseCases {
6
+ constructor (localConfig = {}) {
7
+ this.adapters = localConfig.adapters
8
+
9
+ if (!this.adapters) {
10
+ throw new Error('Adapters instance required when instantiating Mining use cases.')
11
+ }
12
+
13
+ this.fullNode = this.adapters.fullNode
14
+ if (!this.fullNode) {
15
+ throw new Error('Full node adapter required when instantiating Mining use cases.')
16
+ }
17
+ }
18
+
19
+ async getMiningInfo () {
20
+ return this.fullNode.call('getmininginfo')
21
+ }
22
+
23
+ async getNetworkHashPS ({ nblocks, height }) {
24
+ return this.fullNode.call('getnetworkhashps', [nblocks, height])
25
+ }
26
+ }
27
+
28
+ export default MiningUseCases
@@ -0,0 +1,121 @@
1
+ /*
2
+ Use cases for interacting with the BCH full node raw transactions RPC interface.
3
+ */
4
+
5
+ import wlogger from '../adapters/wlogger.js'
6
+
7
+ class RawTransactionsUseCases {
8
+ constructor (localConfig = {}) {
9
+ this.adapters = localConfig.adapters
10
+
11
+ if (!this.adapters) {
12
+ throw new Error('Adapters instance required when instantiating RawTransactions use cases.')
13
+ }
14
+
15
+ this.fullNode = this.adapters.fullNode
16
+ if (!this.fullNode) {
17
+ throw new Error('Full node adapter required when instantiating RawTransactions use cases.')
18
+ }
19
+ }
20
+
21
+ async decodeRawTransaction ({ hex }) {
22
+ return this.fullNode.call('decoderawtransaction', [hex])
23
+ }
24
+
25
+ async decodeRawTransactions ({ hexes }) {
26
+ try {
27
+ const promises = hexes.map(hex =>
28
+ this.fullNode.call('decoderawtransaction', [hex], `decoderawtransaction-${hex.slice(0, 16)}`)
29
+ )
30
+
31
+ return await Promise.all(promises)
32
+ } catch (err) {
33
+ wlogger.error('Error in RawTransactionsUseCases.decodeRawTransactions()', err)
34
+ throw err
35
+ }
36
+ }
37
+
38
+ async decodeScript ({ hex }) {
39
+ return this.fullNode.call('decodescript', [hex])
40
+ }
41
+
42
+ async decodeScripts ({ hexes }) {
43
+ try {
44
+ const promises = hexes.map(hex =>
45
+ this.fullNode.call('decodescript', [hex], `decodescript-${hex.slice(0, 16)}`)
46
+ )
47
+
48
+ return await Promise.all(promises)
49
+ } catch (err) {
50
+ wlogger.error('Error in RawTransactionsUseCases.decodeScripts()', err)
51
+ throw err
52
+ }
53
+ }
54
+
55
+ async getRawTransaction ({ txid, verbose = false }) {
56
+ const verboseInt = verbose ? 1 : 0
57
+ return this.fullNode.call('getrawtransaction', [txid, verboseInt])
58
+ }
59
+
60
+ async getRawTransactions ({ txids, verbose = false }) {
61
+ try {
62
+ const verboseInt = verbose ? 1 : 0
63
+ const promises = txids.map(txid =>
64
+ this.fullNode.call('getrawtransaction', [txid, verboseInt], `getrawtransaction-${txid}`)
65
+ )
66
+
67
+ return await Promise.all(promises)
68
+ } catch (err) {
69
+ wlogger.error('Error in RawTransactionsUseCases.getRawTransactions()', err)
70
+ throw err
71
+ }
72
+ }
73
+
74
+ async getRawTransactionWithHeight ({ txid, verbose = false }) {
75
+ const verboseInt = verbose ? 1 : 0
76
+ const data = await this.fullNode.call('getrawtransaction', [txid, verboseInt])
77
+
78
+ if (verbose && data && data.blockhash) {
79
+ data.height = null
80
+ try {
81
+ // Look up the block height and append it to the TX response.
82
+ const blockHeader = await this.fullNode.call('getblockheader', [data.blockhash, true])
83
+ data.height = blockHeader.height
84
+ } catch (err) {
85
+ // Exit quietly if block header lookup fails
86
+ wlogger.debug('Could not fetch block header for height lookup', err)
87
+ }
88
+ }
89
+
90
+ return data
91
+ }
92
+
93
+ async getBlockHeader ({ blockHash, verbose = false }) {
94
+ return this.fullNode.call('getblockheader', [blockHash, verbose])
95
+ }
96
+
97
+ async sendRawTransaction ({ hex }) {
98
+ return this.fullNode.call('sendrawtransaction', [hex])
99
+ }
100
+
101
+ async sendRawTransactions ({ hexes }) {
102
+ // Dev Note: Sending the 'sendrawtransaction' RPC call to a full node in parallel will
103
+ // not work. Testing showed that the full node will return the same TXID for
104
+ // different TX hexes. I believe this is by design, to prevent double spends.
105
+ // In parallel, we are essentially asking the node to broadcast a new TX before
106
+ // it's finished broadcasting the previous one. Serial execution is required.
107
+ try {
108
+ const result = []
109
+ for (const hex of hexes) {
110
+ const txid = await this.fullNode.call('sendrawtransaction', [hex], `sendrawtransaction-${hex.slice(0, 16)}`)
111
+ result.push(txid)
112
+ }
113
+ return result
114
+ } catch (err) {
115
+ wlogger.error('Error in RawTransactionsUseCases.sendRawTransactions()', err)
116
+ throw err
117
+ }
118
+ }
119
+ }
120
+
121
+ export default RawTransactionsUseCases