psf-bch-api 1.2.0 → 1.3.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.
- package/.env-local +9 -0
- package/bin/server.js +2 -1
- package/package.json +4 -1
- package/src/adapters/fulcrum-api.js +124 -0
- package/src/adapters/full-node-rpc.js +2 -6
- package/src/adapters/index.js +4 -0
- package/src/adapters/slp-indexer-api.js +124 -0
- package/src/config/env/common.js +21 -24
- package/src/controllers/rest-api/fulcrum/controller.js +563 -0
- package/src/controllers/rest-api/fulcrum/router.js +64 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +4 -4
- package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
- package/src/controllers/rest-api/full-node/mining/router.js +52 -0
- package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
- package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
- package/src/controllers/rest-api/index.js +19 -3
- package/src/controllers/rest-api/slp/controller.js +218 -0
- package/src/controllers/rest-api/slp/router.js +55 -0
- package/src/controllers/timer-controller.js +1 -1
- package/src/use-cases/fulcrum-use-cases.js +155 -0
- package/src/use-cases/full-node-mining-use-cases.js +28 -0
- package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
- package/src/use-cases/index.js +8 -0
- package/src/use-cases/slp-use-cases.js +321 -0
- package/test/unit/controllers/blockchain-controller-unit.js +2 -3
- package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +59 -3
- package/test/unit/controllers/slp-controller-unit.js +312 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
- package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
- package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
- package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
- package/src/entities/event.js +0 -71
- package/test/integration/api/event-integration.js +0 -250
- package/test/integration/api/req-integration.js +0 -173
- package/test/integration/api/subscription-integration.js +0 -198
- package/test/integration/use-cases/manage-subscription-integration.js +0 -163
- package/test/integration/use-cases/publish-event-integration.js +0 -104
- package/test/integration/use-cases/query-events-integration.js +0 -95
- package/test/unit/entities/event-unit.js +0 -139
- /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
|
@@ -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
|
|
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,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
|
package/src/use-cases/index.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import BlockchainUseCases from './full-node-blockchain-use-cases.js'
|
|
9
9
|
import ControlUseCases from './full-node-control-use-cases.js'
|
|
10
10
|
import DSProofUseCases from './full-node-dsproof-use-cases.js'
|
|
11
|
+
import FulcrumUseCases from './fulcrum-use-cases.js'
|
|
12
|
+
import MiningUseCases from './full-node-mining-use-cases.js'
|
|
13
|
+
import RawTransactionsUseCases from './full-node-rawtransactions-use-cases.js'
|
|
14
|
+
import SlpUseCases from './slp-use-cases.js'
|
|
11
15
|
|
|
12
16
|
class UseCases {
|
|
13
17
|
constructor (localConfig = {}) {
|
|
@@ -21,6 +25,10 @@ class UseCases {
|
|
|
21
25
|
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
|
22
26
|
this.control = new ControlUseCases({ adapters: this.adapters })
|
|
23
27
|
this.dsproof = new DSProofUseCases({ adapters: this.adapters })
|
|
28
|
+
this.fulcrum = new FulcrumUseCases({ adapters: this.adapters })
|
|
29
|
+
this.mining = new MiningUseCases({ adapters: this.adapters })
|
|
30
|
+
this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters })
|
|
31
|
+
this.slp = new SlpUseCases({ adapters: this.adapters })
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
// Run any startup Use Cases at the start of the app.
|