psf-bch-api 1.3.0 → 7.2.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 (30) hide show
  1. package/.env-local +26 -5
  2. package/bin/server.js +60 -9
  3. package/package.json +5 -4
  4. package/production/docker/.env-local +34 -0
  5. package/production/docker/Dockerfile +8 -25
  6. package/production/docker/docker-compose.yml +4 -3
  7. package/production/docker/temp.js +7 -0
  8. package/src/config/env/common.js +10 -3
  9. package/src/config/x402.js +7 -0
  10. package/src/controllers/rest-api/encryption/controller.js +100 -0
  11. package/src/controllers/rest-api/encryption/router.js +51 -0
  12. package/src/controllers/rest-api/fulcrum/controller.js +2 -1
  13. package/src/controllers/rest-api/index.js +8 -0
  14. package/src/controllers/rest-api/price/controller.js +96 -0
  15. package/src/controllers/rest-api/price/router.js +52 -0
  16. package/src/controllers/rest-api/slp/controller.js +3 -1
  17. package/src/middleware/basic-auth.js +61 -0
  18. package/src/use-cases/encryption-use-cases.js +120 -0
  19. package/src/use-cases/fulcrum-use-cases.js +10 -2
  20. package/src/use-cases/index.js +9 -0
  21. package/src/use-cases/price-use-cases.js +83 -0
  22. package/src/use-cases/slp-use-cases.js +5 -1
  23. package/test/unit/controllers/encryption-controller-unit.js +203 -0
  24. package/test/unit/controllers/price-controller-unit.js +116 -0
  25. package/test/unit/controllers/rest-api-index-unit.js +15 -0
  26. package/test/unit/use-cases/encryption-use-cases-unit.js +247 -0
  27. package/test/unit/use-cases/fulcrum-use-cases-unit.js +1 -1
  28. package/test/unit/use-cases/price-use-cases-unit.js +103 -0
  29. package/test/unit/use-cases/slp-use-cases-unit.js +1 -1
  30. /package/{index.js → psf-bch-api.js} +0 -0
@@ -4,8 +4,9 @@
4
4
 
5
5
  import wlogger from '../../../adapters/wlogger.js'
6
6
  import BCHJS from '@psf/bch-js'
7
+ import config from '../../../config/index.js'
7
8
 
8
- const bchjs = new BCHJS()
9
+ const bchjs = new BCHJS({ restURL: config.restURL })
9
10
 
10
11
  class SlpRESTController {
11
12
  constructor (localConfig = {}) {
@@ -201,6 +202,7 @@ class SlpRESTController {
201
202
  const result = await this.slpUseCases.getTokenData({ tokenId, withTxHistory })
202
203
  return res.status(200).json(result)
203
204
  } catch (err) {
205
+ console.log('Error in /v6/slp/token/data getTokenData(): ', err)
204
206
  return this.handleError(err, res)
205
207
  }
206
208
  }
@@ -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,120 @@
1
+ /*
2
+ Use cases for encryption-related operations.
3
+ Retrieves public keys from the blockchain for BCH addresses.
4
+ */
5
+
6
+ // Global npm libraries
7
+ import BCHJS from '@psf/bch-js'
8
+
9
+ // Local libraries
10
+ import wlogger from '../adapters/wlogger.js'
11
+ import config from '../config/index.js'
12
+
13
+ const bchjs = new BCHJS({ restURL: config.restURL })
14
+
15
+ class EncryptionUseCases {
16
+ constructor (localConfig = {}) {
17
+ this.adapters = localConfig.adapters
18
+ if (!this.adapters) {
19
+ throw new Error('Adapters instance required when instantiating Encryption use cases.')
20
+ }
21
+
22
+ this.useCases = localConfig.useCases
23
+ if (!this.useCases) {
24
+ throw new Error('UseCases instance required when instantiating Encryption use cases.')
25
+ }
26
+
27
+ // Allow bchjs to be injected for testing
28
+ this.bchjs = localConfig.bchjs || bchjs
29
+ }
30
+
31
+ /**
32
+ * Get the public key for a BCH address by searching the blockchain.
33
+ * Searches the transaction history of the address for a transaction input
34
+ * that contains the public key.
35
+ *
36
+ * @param {Object} params - Parameters object
37
+ * @param {string} params.address - BCH address (cash address or legacy format)
38
+ * @returns {Promise<Object>} Object with success status and publicKey if found
39
+ */
40
+ async getPublicKey ({ address }) {
41
+ try {
42
+ // Convert to cash address format
43
+ const cashAddr = this.bchjs.Address.toCashAddress(address)
44
+
45
+ // Get transaction history for the address
46
+ const txHistory = await this.useCases.fulcrum.getTransactions({ address: cashAddr })
47
+
48
+ // Extract just the TXIDs
49
+ const txids = txHistory.transactions.map((elem) => elem.tx_hash)
50
+
51
+ // Throw error if there is no transaction history
52
+ if (!txids || txids.length === 0) {
53
+ throw new Error('No transaction history.')
54
+ }
55
+
56
+ // Loop through the transaction history and search for the public key
57
+ for (let i = 0; i < txids.length; i++) {
58
+ const thisTx = txids[i]
59
+
60
+ // Get verbose transaction details
61
+ const txDetails = await this.useCases.rawtransactions.getRawTransaction({
62
+ txid: thisTx,
63
+ verbose: true
64
+ })
65
+
66
+ const vin = txDetails.vin
67
+
68
+ // Loop through each input
69
+ for (let j = 0; j < vin.length; j++) {
70
+ const thisVin = vin[j]
71
+
72
+ // Skip if no scriptSig (e.g., coinbase transactions)
73
+ if (!thisVin.scriptSig || !thisVin.scriptSig.asm) {
74
+ continue
75
+ }
76
+
77
+ // Extract the script signature
78
+ const scriptSig = thisVin.scriptSig.asm.split(' ')
79
+
80
+ // Extract the public key from the script signature (last element)
81
+ const pubKey = scriptSig[scriptSig.length - 1]
82
+
83
+ // Skip if pubKey is not a valid hex string (basic validation)
84
+ if (!pubKey || !/^[0-9a-fA-F]+$/.test(pubKey)) {
85
+ continue
86
+ }
87
+
88
+ try {
89
+ // Generate cash address from public key
90
+ const keyBuf = Buffer.from(pubKey, 'hex')
91
+ const ec = this.bchjs.ECPair.fromPublicKey(keyBuf)
92
+ const cashAddr2 = this.bchjs.ECPair.toCashAddress(ec)
93
+
94
+ // If public keys match, this is the correct public key
95
+ if (cashAddr === cashAddr2) {
96
+ return {
97
+ success: true,
98
+ publicKey: pubKey
99
+ }
100
+ }
101
+ } catch (err) {
102
+ // Skip invalid public keys - continue searching
103
+ continue
104
+ }
105
+ }
106
+ }
107
+
108
+ // Public key not found in any transaction
109
+ return {
110
+ success: false,
111
+ publicKey: 'not found'
112
+ }
113
+ } catch (err) {
114
+ wlogger.error('Error in EncryptionUseCases.getPublicKey()', err)
115
+ throw err
116
+ }
117
+ }
118
+ }
119
+
120
+ export default EncryptionUseCases
@@ -4,8 +4,9 @@
4
4
 
5
5
  import wlogger from '../adapters/wlogger.js'
6
6
  import BCHJS from '@psf/bch-js'
7
+ import config from '../config/index.js'
7
8
 
8
- const bchjs = new BCHJS()
9
+ const bchjs = new BCHJS({ restURL: config.restURL })
9
10
 
10
11
  class FulcrumUseCases {
11
12
  constructor (localConfig = {}) {
@@ -53,7 +54,14 @@ class FulcrumUseCases {
53
54
  }
54
55
 
55
56
  async getTransactionDetails ({ txid }) {
56
- return this.fulcrum.get(`electrumx/tx/data/${txid}`)
57
+ try {
58
+ const response = await this.fulcrum.get(`electrumx/tx/data/${txid}`)
59
+ console.log(`getTransactionDetails() TXID ${txid}: ${JSON.stringify(response, null, 2)}`)
60
+ return response
61
+ } catch (err) {
62
+ wlogger.error('Error in FulcrumUseCases.getTransactionDetails()', err)
63
+ throw err
64
+ }
57
65
  }
58
66
 
59
67
  async getTransactionDetailsBulk ({ txids, verbose }) {
@@ -8,8 +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 EncryptionUseCases from './encryption-use-cases.js'
11
12
  import FulcrumUseCases from './fulcrum-use-cases.js'
12
13
  import MiningUseCases from './full-node-mining-use-cases.js'
14
+ import PriceUseCases from './price-use-cases.js'
13
15
  import RawTransactionsUseCases from './full-node-rawtransactions-use-cases.js'
14
16
  import SlpUseCases from './slp-use-cases.js'
15
17
 
@@ -27,8 +29,15 @@ class UseCases {
27
29
  this.dsproof = new DSProofUseCases({ adapters: this.adapters })
28
30
  this.fulcrum = new FulcrumUseCases({ adapters: this.adapters })
29
31
  this.mining = new MiningUseCases({ adapters: this.adapters })
32
+ this.price = new PriceUseCases({ adapters: this.adapters })
30
33
  this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters })
31
34
  this.slp = new SlpUseCases({ adapters: this.adapters })
35
+
36
+ // Encryption use cases require access to other use cases (fulcrum, rawtransactions)
37
+ this.encryption = new EncryptionUseCases({
38
+ adapters: this.adapters,
39
+ useCases: this
40
+ })
32
41
  }
33
42
 
34
43
  // Run any startup Use Cases at the start of the app.
@@ -0,0 +1,83 @@
1
+ /*
2
+ Use cases for price-related operations.
3
+ */
4
+
5
+ // Global npm libraries
6
+ import axios from 'axios'
7
+ import SlpWallet from 'minimal-slp-wallet'
8
+ import PSFFPP from 'psffpp'
9
+
10
+ // Local libraries
11
+ import wlogger from '../adapters/wlogger.js'
12
+ import config from '../config/index.js'
13
+
14
+ class PriceUseCases {
15
+ constructor (localConfig = {}) {
16
+ this.adapters = localConfig.adapters
17
+
18
+ if (!this.adapters) {
19
+ throw new Error('Adapters instance required when instantiating Price use cases.')
20
+ }
21
+
22
+ // Get config
23
+ this.config = localConfig.config || config
24
+
25
+ // Coinex API URL for BCH/USDT
26
+ this.bchCoinexPriceUrl =
27
+ 'https://api.coinex.com/v1/market/ticker?market=bchusdt'
28
+
29
+ // Allow axios to be injected for testing
30
+ this.axios = localConfig.axios || axios
31
+ }
32
+
33
+ /**
34
+ * Get the USD price of BCH from Coinex.
35
+ * @returns {Promise<number>} The USD price of BCH
36
+ */
37
+ async getBCHUSD () {
38
+ try {
39
+ // Request options
40
+ const opt = {
41
+ method: 'get',
42
+ baseURL: this.bchCoinexPriceUrl,
43
+ timeout: 15000
44
+ }
45
+
46
+ const response = await this.axios.request(opt)
47
+
48
+ const price = Number(response.data.data.ticker.last)
49
+
50
+ return price
51
+ } catch (err) {
52
+ wlogger.error('Error in PriceUseCases.getBCHUSD()', err)
53
+ throw err
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get the PSF price for writing to the PSFFPP.
59
+ * Returns the price to pin 1MB of content to the PSFFPP pinning
60
+ * network on IPFS. The price is denominated in PSF tokens.
61
+ * @returns {Promise<number>} The write price in PSF tokens
62
+ */
63
+ async getPsffppWritePrice () {
64
+ try {
65
+ const wallet = new SlpWallet(undefined, {
66
+ interface: 'rest-api',
67
+ restURL: this.config.restURL
68
+ })
69
+ await wallet.walletInfoPromise
70
+
71
+ const psffpp = new PSFFPP({ wallet })
72
+
73
+ const writePrice = await psffpp.getMcWritePrice()
74
+
75
+ return writePrice
76
+ } catch (err) {
77
+ wlogger.error('Error in PriceUseCases.getPsffppWritePrice()', err)
78
+ throw err
79
+ }
80
+ }
81
+ }
82
+
83
+ export default PriceUseCases
@@ -9,7 +9,7 @@ import SlpTokenMedia from 'slp-token-media'
9
9
  import axios from 'axios'
10
10
  import config from '../config/index.js'
11
11
 
12
- const bchjs = new BCHJS()
12
+ const bchjs = new BCHJS({ restURL: config.restURL })
13
13
 
14
14
  class SlpUseCases {
15
15
  constructor (localConfig = {}) {
@@ -259,6 +259,7 @@ class SlpUseCases {
259
259
 
260
260
  return mutableCid
261
261
  } catch (err) {
262
+ console.log('Error in SlpUseCases.getMutableCid()', err)
262
263
  wlogger.error('Error in SlpUseCases.getMutableCid()', err)
263
264
  return false
264
265
  }
@@ -271,7 +272,9 @@ class SlpUseCases {
271
272
  }
272
273
 
273
274
  // Get transaction data
275
+ console.log('Decoding OP_RETURN for TXID: ', txid)
274
276
  const txData = await this.bchjs.Electrumx.txData(txid)
277
+ console.log(`TXID ${txid}: ${JSON.stringify(txData, null, 2)}`)
275
278
  let data = false
276
279
 
277
280
  // Map the vout of the transaction in search of an OP_RETURN
@@ -291,6 +294,7 @@ class SlpUseCases {
291
294
 
292
295
  return data
293
296
  } catch (error) {
297
+ console.log('Error in SlpUseCases.decodeOpReturn()', error)
294
298
  wlogger.error('Error in SlpUseCases.decodeOpReturn()', error)
295
299
  throw error
296
300
  }
@@ -0,0 +1,203 @@
1
+ /*
2
+ Unit tests for EncryptionRESTController.
3
+ */
4
+
5
+ import { assert } from 'chai'
6
+ import sinon from 'sinon'
7
+
8
+ import EncryptionRESTController from '../../../src/controllers/rest-api/encryption/controller.js'
9
+ import { createMockRequest, createMockResponse, createMockRequestWithParams } from '../mocks/controller-mocks.js'
10
+
11
+ describe('#encryption-controller.js', () => {
12
+ let sandbox
13
+ let mockAdapters
14
+ let mockUseCases
15
+ let uut
16
+
17
+ beforeEach(() => {
18
+ sandbox = sinon.createSandbox()
19
+ mockAdapters = {}
20
+ mockUseCases = {
21
+ encryption: {
22
+ getPublicKey: sandbox.stub().resolves({
23
+ success: true,
24
+ publicKey: '02abc123def456789'
25
+ })
26
+ }
27
+ }
28
+
29
+ uut = new EncryptionRESTController({
30
+ adapters: mockAdapters,
31
+ useCases: mockUseCases
32
+ })
33
+ })
34
+
35
+ afterEach(() => {
36
+ sandbox.restore()
37
+ })
38
+
39
+ describe('#constructor()', () => {
40
+ it('should require adapters', () => {
41
+ assert.throws(() => {
42
+ // eslint-disable-next-line no-new
43
+ new EncryptionRESTController({ useCases: mockUseCases })
44
+ }, /Adapters library required/)
45
+ })
46
+
47
+ it('should require encryption use cases', () => {
48
+ assert.throws(() => {
49
+ // eslint-disable-next-line no-new
50
+ new EncryptionRESTController({ adapters: mockAdapters, useCases: {} })
51
+ }, /Encryption use cases required/)
52
+ })
53
+ })
54
+
55
+ describe('#root()', () => {
56
+ it('should return encryption status', async () => {
57
+ const req = createMockRequest()
58
+ const res = createMockResponse()
59
+
60
+ await uut.root(req, res)
61
+
62
+ assert.equal(res.statusValue, 200)
63
+ assert.deepEqual(res.jsonData, { status: 'encryption' })
64
+ })
65
+ })
66
+
67
+ describe('#getPublicKey()', () => {
68
+ it('should return public key on success', async () => {
69
+ const req = createMockRequestWithParams({
70
+ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
71
+ })
72
+ const res = createMockResponse()
73
+
74
+ await uut.getPublicKey(req, res)
75
+
76
+ assert.equal(res.statusValue, 200)
77
+ assert.deepEqual(res.jsonData, {
78
+ success: true,
79
+ publicKey: '02abc123def456789'
80
+ })
81
+ assert.isTrue(mockUseCases.encryption.getPublicKey.calledOnce)
82
+ assert.isTrue(mockUseCases.encryption.getPublicKey.calledWith({
83
+ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
84
+ }))
85
+ })
86
+
87
+ it('should return not found when public key is not found', async () => {
88
+ mockUseCases.encryption.getPublicKey.resolves({
89
+ success: false,
90
+ publicKey: 'not found'
91
+ })
92
+
93
+ const req = createMockRequestWithParams({
94
+ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
95
+ })
96
+ const res = createMockResponse()
97
+
98
+ await uut.getPublicKey(req, res)
99
+
100
+ assert.equal(res.statusValue, 200)
101
+ assert.deepEqual(res.jsonData, {
102
+ success: false,
103
+ publicKey: 'not found'
104
+ })
105
+ })
106
+
107
+ it('should reject array addresses', async () => {
108
+ const req = createMockRequestWithParams({
109
+ address: ['addr1', 'addr2']
110
+ })
111
+ const res = createMockResponse()
112
+
113
+ await uut.getPublicKey(req, res)
114
+
115
+ assert.equal(res.statusValue, 400)
116
+ assert.deepEqual(res.jsonData, {
117
+ success: false,
118
+ error: 'address can not be an array.'
119
+ })
120
+ assert.isFalse(mockUseCases.encryption.getPublicKey.called)
121
+ })
122
+
123
+ it('should reject missing address', async () => {
124
+ const req = createMockRequestWithParams({})
125
+ const res = createMockResponse()
126
+
127
+ await uut.getPublicKey(req, res)
128
+
129
+ assert.equal(res.statusValue, 400)
130
+ assert.deepEqual(res.jsonData, {
131
+ success: false,
132
+ error: 'address is required.'
133
+ })
134
+ assert.isFalse(mockUseCases.encryption.getPublicKey.called)
135
+ })
136
+
137
+ it('should handle errors via handleError', async () => {
138
+ const error = new Error('No transaction history.')
139
+ error.status = 400
140
+ mockUseCases.encryption.getPublicKey.rejects(error)
141
+
142
+ const req = createMockRequestWithParams({
143
+ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
144
+ })
145
+ const res = createMockResponse()
146
+
147
+ await uut.getPublicKey(req, res)
148
+
149
+ assert.equal(res.statusValue, 400)
150
+ assert.deepEqual(res.jsonData, {
151
+ success: false,
152
+ error: 'No transaction history.'
153
+ })
154
+ })
155
+
156
+ it('should default to 500 status for errors without status', async () => {
157
+ const error = new Error('Internal error')
158
+ mockUseCases.encryption.getPublicKey.rejects(error)
159
+
160
+ const req = createMockRequestWithParams({
161
+ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
162
+ })
163
+ const res = createMockResponse()
164
+
165
+ await uut.getPublicKey(req, res)
166
+
167
+ assert.equal(res.statusValue, 500)
168
+ assert.deepEqual(res.jsonData, {
169
+ success: false,
170
+ error: 'Internal error'
171
+ })
172
+ })
173
+ })
174
+
175
+ describe('#handleError()', () => {
176
+ it('should use error status and message when provided', () => {
177
+ const error = new Error('Custom error')
178
+ error.status = 422
179
+ const res = createMockResponse()
180
+
181
+ uut.handleError(error, res)
182
+
183
+ assert.equal(res.statusValue, 422)
184
+ assert.deepEqual(res.jsonData, {
185
+ success: false,
186
+ error: 'Custom error'
187
+ })
188
+ })
189
+
190
+ it('should default to 500 and Internal server error', () => {
191
+ const error = {}
192
+ const res = createMockResponse()
193
+
194
+ uut.handleError(error, res)
195
+
196
+ assert.equal(res.statusValue, 500)
197
+ assert.deepEqual(res.jsonData, {
198
+ success: false,
199
+ error: 'Internal server error'
200
+ })
201
+ })
202
+ })
203
+ })
@@ -0,0 +1,116 @@
1
+ /*
2
+ Unit tests for PriceRESTController.
3
+ */
4
+
5
+ import { assert } from 'chai'
6
+ import sinon from 'sinon'
7
+
8
+ import PriceRESTController from '../../../src/controllers/rest-api/price/controller.js'
9
+ import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
10
+
11
+ describe('#price-controller.js', () => {
12
+ let sandbox
13
+ let mockAdapters
14
+ let mockUseCases
15
+ let uut
16
+
17
+ beforeEach(() => {
18
+ sandbox = sinon.createSandbox()
19
+ mockAdapters = {}
20
+ mockUseCases = {
21
+ price: {
22
+ getBCHUSD: sandbox.stub().resolves(250.5),
23
+ getPsffppWritePrice: sandbox.stub().resolves(0.08335233)
24
+ }
25
+ }
26
+
27
+ uut = new PriceRESTController({
28
+ adapters: mockAdapters,
29
+ useCases: mockUseCases
30
+ })
31
+ })
32
+
33
+ afterEach(() => {
34
+ sandbox.restore()
35
+ })
36
+
37
+ describe('#constructor()', () => {
38
+ it('should require adapters', () => {
39
+ assert.throws(() => {
40
+ // eslint-disable-next-line no-new
41
+ new PriceRESTController({ useCases: mockUseCases })
42
+ }, /Adapters library required/)
43
+ })
44
+
45
+ it('should require price use cases', () => {
46
+ assert.throws(() => {
47
+ // eslint-disable-next-line no-new
48
+ new PriceRESTController({ adapters: mockAdapters, useCases: {} })
49
+ }, /Price use cases required/)
50
+ })
51
+ })
52
+
53
+ describe('#root()', () => {
54
+ it('should return price status', async () => {
55
+ const req = createMockRequest()
56
+ const res = createMockResponse()
57
+
58
+ await uut.root(req, res)
59
+
60
+ assert.equal(res.statusValue, 200)
61
+ assert.deepEqual(res.jsonData, { status: 'price' })
62
+ })
63
+ })
64
+
65
+ describe('#getBCHUSD()', () => {
66
+ it('should return BCH USD price on success', async () => {
67
+ const req = createMockRequest()
68
+ const res = createMockResponse()
69
+
70
+ await uut.getBCHUSD(req, res)
71
+
72
+ assert.equal(res.statusValue, 200)
73
+ assert.deepEqual(res.jsonData, { usd: 250.5 })
74
+ assert.isTrue(mockUseCases.price.getBCHUSD.calledOnce)
75
+ })
76
+
77
+ it('should handle errors via handleError', async () => {
78
+ const error = new Error('API failure')
79
+ error.status = 503
80
+ mockUseCases.price.getBCHUSD.rejects(error)
81
+ const req = createMockRequest()
82
+ const res = createMockResponse()
83
+
84
+ await uut.getBCHUSD(req, res)
85
+
86
+ assert.equal(res.statusValue, 503)
87
+ assert.deepEqual(res.jsonData, { error: 'API failure' })
88
+ })
89
+ })
90
+
91
+ describe('#getPsffppWritePrice()', () => {
92
+ it('should return PSFFPP write price on success', async () => {
93
+ const req = createMockRequest()
94
+ const res = createMockResponse()
95
+
96
+ await uut.getPsffppWritePrice(req, res)
97
+
98
+ assert.equal(res.statusValue, 200)
99
+ assert.deepEqual(res.jsonData, { writePrice: 0.08335233 })
100
+ assert.isTrue(mockUseCases.price.getPsffppWritePrice.calledOnce)
101
+ })
102
+
103
+ it('should handle errors via handleError', async () => {
104
+ const error = new Error('PSFFPP failure')
105
+ error.status = 500
106
+ mockUseCases.price.getPsffppWritePrice.rejects(error)
107
+ const req = createMockRequest()
108
+ const res = createMockResponse()
109
+
110
+ await uut.getPsffppWritePrice(req, res)
111
+
112
+ assert.equal(res.statusValue, 500)
113
+ assert.deepEqual(res.jsonData, { error: 'PSFFPP failure' })
114
+ })
115
+ })
116
+ })