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.
- package/.env-local +26 -5
- package/bin/server.js +60 -9
- package/package.json +5 -4
- package/production/docker/.env-local +34 -0
- package/production/docker/Dockerfile +8 -25
- package/production/docker/docker-compose.yml +4 -3
- package/production/docker/temp.js +7 -0
- package/src/config/env/common.js +10 -3
- package/src/config/x402.js +7 -0
- package/src/controllers/rest-api/encryption/controller.js +100 -0
- package/src/controllers/rest-api/encryption/router.js +51 -0
- package/src/controllers/rest-api/fulcrum/controller.js +2 -1
- package/src/controllers/rest-api/index.js +8 -0
- package/src/controllers/rest-api/price/controller.js +96 -0
- package/src/controllers/rest-api/price/router.js +52 -0
- package/src/controllers/rest-api/slp/controller.js +3 -1
- package/src/middleware/basic-auth.js +61 -0
- package/src/use-cases/encryption-use-cases.js +120 -0
- package/src/use-cases/fulcrum-use-cases.js +10 -2
- package/src/use-cases/index.js +9 -0
- package/src/use-cases/price-use-cases.js +83 -0
- package/src/use-cases/slp-use-cases.js +5 -1
- package/test/unit/controllers/encryption-controller-unit.js +203 -0
- package/test/unit/controllers/price-controller-unit.js +116 -0
- package/test/unit/controllers/rest-api-index-unit.js +15 -0
- package/test/unit/use-cases/encryption-use-cases-unit.js +247 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +1 -1
- package/test/unit/use-cases/price-use-cases-unit.js +103 -0
- package/test/unit/use-cases/slp-use-cases-unit.js +1 -1
- /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
|
-
|
|
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 }) {
|
package/src/use-cases/index.js
CHANGED
|
@@ -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
|
+
})
|