psf-bch-api 1.1.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.
Files changed (56) hide show
  1. package/.env-local +9 -0
  2. package/README.md +22 -0
  3. package/bin/server.js +24 -1
  4. package/package.json +6 -2
  5. package/src/adapters/fulcrum-api.js +124 -0
  6. package/src/adapters/full-node-rpc.js +2 -6
  7. package/src/adapters/index.js +4 -0
  8. package/src/adapters/slp-indexer-api.js +124 -0
  9. package/src/config/env/common.js +45 -24
  10. package/src/config/x402.js +43 -0
  11. package/src/controllers/index.js +3 -1
  12. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  13. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  14. package/src/controllers/rest-api/full-node/blockchain/controller.js +26 -26
  15. package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +5 -1
  16. package/src/controllers/rest-api/full-node/control/controller.js +68 -0
  17. package/src/controllers/rest-api/full-node/control/router.js +51 -0
  18. package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
  19. package/src/controllers/rest-api/full-node/dsproof/router.js +51 -0
  20. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  21. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  22. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  23. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  24. package/src/controllers/rest-api/index.js +33 -2
  25. package/src/controllers/rest-api/slp/controller.js +218 -0
  26. package/src/controllers/rest-api/slp/router.js +55 -0
  27. package/src/controllers/timer-controller.js +1 -1
  28. package/src/use-cases/fulcrum-use-cases.js +155 -0
  29. package/src/use-cases/full-node-control-use-cases.js +24 -0
  30. package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
  31. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  32. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  33. package/src/use-cases/index.js +12 -0
  34. package/src/use-cases/slp-use-cases.js +321 -0
  35. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  36. package/test/unit/controllers/control-controller-unit.js +88 -0
  37. package/test/unit/controllers/dsproof-controller-unit.js +117 -0
  38. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  39. package/test/unit/controllers/mining-controller-unit.js +139 -0
  40. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  41. package/test/unit/controllers/rest-api-index-unit.js +76 -6
  42. package/test/unit/controllers/slp-controller-unit.js +312 -0
  43. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  44. package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
  45. package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
  46. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  47. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  48. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  49. package/src/entities/event.js +0 -71
  50. package/test/integration/api/event-integration.js +0 -250
  51. package/test/integration/api/req-integration.js +0 -173
  52. package/test/integration/api/subscription-integration.js +0 -198
  53. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  54. package/test/integration/use-cases/publish-event-integration.js +0 -104
  55. package/test/integration/use-cases/query-events-integration.js +0 -95
  56. package/test/unit/entities/event-unit.js +0 -139
@@ -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,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,24 @@
1
+ /*
2
+ Use cases for interacting with the BCH full node control RPC interface.
3
+ */
4
+
5
+ class ControlUseCases {
6
+ constructor (localConfig = {}) {
7
+ this.adapters = localConfig.adapters
8
+
9
+ if (!this.adapters) {
10
+ throw new Error('Adapters instance required when instantiating Control use cases.')
11
+ }
12
+
13
+ this.fullNode = this.adapters.fullNode
14
+ if (!this.fullNode) {
15
+ throw new Error('Full node adapter required when instantiating Control use cases.')
16
+ }
17
+ }
18
+
19
+ async getNetworkInfo () {
20
+ return this.fullNode.call('getnetworkinfo')
21
+ }
22
+ }
23
+
24
+ export default ControlUseCases
@@ -0,0 +1,24 @@
1
+ /*
2
+ Use cases for interacting with the BCH full node double-spend proof RPC interface.
3
+ */
4
+
5
+ class DSProofUseCases {
6
+ constructor (localConfig = {}) {
7
+ this.adapters = localConfig.adapters
8
+
9
+ if (!this.adapters) {
10
+ throw new Error('Adapters instance required when instantiating DSProof use cases.')
11
+ }
12
+
13
+ this.fullNode = this.adapters.fullNode
14
+ if (!this.fullNode) {
15
+ throw new Error('Full node adapter required when instantiating DSProof use cases.')
16
+ }
17
+ }
18
+
19
+ async getDSProof ({ txid, verbose }) {
20
+ return this.fullNode.call('getdsproof', [txid, verbose])
21
+ }
22
+ }
23
+
24
+ export default DSProofUseCases
@@ -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
@@ -6,6 +6,12 @@
6
6
 
7
7
  // Local libraries
8
8
  import BlockchainUseCases from './full-node-blockchain-use-cases.js'
9
+ import ControlUseCases from './full-node-control-use-cases.js'
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'
9
15
 
10
16
  class UseCases {
11
17
  constructor (localConfig = {}) {
@@ -17,6 +23,12 @@ class UseCases {
17
23
  }
18
24
 
19
25
  this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
26
+ this.control = new ControlUseCases({ adapters: this.adapters })
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 })
20
32
  }
21
33
 
22
34
  // Run any startup Use Cases at the start of the app.