psf-bch-api 1.1.0 → 1.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/README.md +22 -0
- package/bin/server.js +22 -0
- package/package.json +3 -2
- package/src/config/env/common.js +24 -0
- package/src/config/x402.js +43 -0
- package/src/controllers/index.js +3 -1
- package/src/controllers/rest-api/full-node/blockchain/controller.js +22 -22
- package/src/controllers/rest-api/full-node/blockchain/index.js +5 -1
- package/src/controllers/rest-api/full-node/control/controller.js +68 -0
- package/src/controllers/rest-api/full-node/control/index.js +51 -0
- package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
- package/src/controllers/rest-api/full-node/dsproof/index.js +51 -0
- package/src/controllers/rest-api/index.js +16 -1
- package/src/use-cases/full-node-control-use-cases.js +24 -0
- package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
- package/src/use-cases/index.js +4 -0
- package/test/unit/controllers/control-controller-unit.js +88 -0
- package/test/unit/controllers/dsproof-controller-unit.js +117 -0
- package/test/unit/controllers/rest-api-index-unit.js +19 -5
- package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
- package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
package/README.md
CHANGED
|
@@ -6,3 +6,25 @@ This is a REST API for communicating with Bitcoin Cash infrastructure. It replac
|
|
|
6
6
|
|
|
7
7
|
[MIT](./LICENSE.md)
|
|
8
8
|
|
|
9
|
+
## x402-bch Payments
|
|
10
|
+
|
|
11
|
+
All REST endpoints exposed under the `/v6` prefix are protected by the [`x402-bch-express`](https://www.npmjs.com/package/x402-bch-express) middleware. Each API call requires a BCH payment authorization for **2000 satoshis**. The middleware advertises payment requirements via HTTP 402 responses and validates incoming `X-PAYMENT` headers with a configured Facilitator.
|
|
12
|
+
|
|
13
|
+
### Configuration
|
|
14
|
+
|
|
15
|
+
Environment variables control the payment flow:
|
|
16
|
+
|
|
17
|
+
- `X402_ENABLED` — set to `false` (case-insensitive) to disable the middleware. Defaults to enabled.
|
|
18
|
+
- `SERVER_BCH_ADDRESS` — BCH cash address that receives funding transactions. Defaults to `bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d`.
|
|
19
|
+
- `FACILITATOR_URL` — Root URL of the facilitator service (e.g., `http://localhost:4345/facilitator`).
|
|
20
|
+
- `X402_PRICE_SAT` — Optional; override the satoshi price per call (defaults to `2000`).
|
|
21
|
+
|
|
22
|
+
When `X402_ENABLED=false`, the server continues to operate without payment headers for local development or trusted deployments.
|
|
23
|
+
|
|
24
|
+
### Manual Verification
|
|
25
|
+
|
|
26
|
+
1. Start or point to an `x402-bch` facilitator service (the example facilitator listens at `http://localhost:4345/facilitator`).
|
|
27
|
+
2. Run the API server with the default configuration: `npm start`.
|
|
28
|
+
3. Call a protected endpoint without an `X-PAYMENT` header, e.g. `curl -i http://localhost:5942/v6/full-node/control/getNetworkInfo`. The server will respond with HTTP `402` and include payment requirements.
|
|
29
|
+
4. Restart the server with `X402_ENABLED=false npm start` to confirm that the same request now bypasses the middleware (useful for local development without payments).
|
|
30
|
+
|
package/bin/server.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import express from 'express'
|
|
8
8
|
import cors from 'cors'
|
|
9
9
|
import dotenv from 'dotenv'
|
|
10
|
+
import { paymentMiddleware as x402PaymentMiddleware } from 'x402-bch-express'
|
|
10
11
|
import { fileURLToPath } from 'url'
|
|
11
12
|
import { dirname, join } from 'path'
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ import { dirname, join } from 'path'
|
|
|
14
15
|
import config from '../src/config/index.js'
|
|
15
16
|
import Controllers from '../src/controllers/index.js'
|
|
16
17
|
import wlogger from '../src/adapters/wlogger.js'
|
|
18
|
+
import { buildX402Routes, getX402Settings } from '../src/config/x402.js'
|
|
17
19
|
|
|
18
20
|
// Load environment variables
|
|
19
21
|
dotenv.config()
|
|
@@ -57,6 +59,8 @@ class Server {
|
|
|
57
59
|
// Create an Express instance.
|
|
58
60
|
const app = express()
|
|
59
61
|
|
|
62
|
+
const x402Settings = getX402Settings()
|
|
63
|
+
|
|
60
64
|
// MIDDLEWARE START
|
|
61
65
|
app.use(express.json())
|
|
62
66
|
app.use(express.urlencoded({ extended: true }))
|
|
@@ -68,6 +72,24 @@ class Server {
|
|
|
68
72
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
|
69
73
|
}))
|
|
70
74
|
|
|
75
|
+
if (x402Settings.enabled) {
|
|
76
|
+
const routes = buildX402Routes(this.config.apiPrefix)
|
|
77
|
+
const facilitatorOptions = x402Settings.facilitatorUrl
|
|
78
|
+
? { url: x402Settings.facilitatorUrl }
|
|
79
|
+
: undefined
|
|
80
|
+
|
|
81
|
+
wlogger.info(`x402 middleware enabled; enforcing ${x402Settings.priceSat} satoshis per request`)
|
|
82
|
+
app.use(
|
|
83
|
+
x402PaymentMiddleware(
|
|
84
|
+
x402Settings.serverAddress,
|
|
85
|
+
routes,
|
|
86
|
+
facilitatorOptions
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
} else {
|
|
90
|
+
wlogger.info('x402 middleware disabled via configuration')
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
// Endpoint logging middleware
|
|
72
94
|
app.use((req, res, next) => {
|
|
73
95
|
console.log(`Endpoint called: ${req.method} ${req.path}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "psf-bch-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"dotenv": "16.3.1",
|
|
21
21
|
"express": "5.1.0",
|
|
22
22
|
"winston": "3.11.0",
|
|
23
|
-
"winston-daily-rotate-file": "4.7.1"
|
|
23
|
+
"winston-daily-rotate-file": "4.7.1",
|
|
24
|
+
"x402-bch-express": "1.1.1"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"apidoc": "1.2.0",
|
package/src/config/env/common.js
CHANGED
|
@@ -16,6 +16,25 @@ const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../packag
|
|
|
16
16
|
|
|
17
17
|
const version = pkgInfo.version
|
|
18
18
|
|
|
19
|
+
const normalizeBoolean = (value, defaultValue) => {
|
|
20
|
+
if (value === undefined || value === null || value === '') return defaultValue
|
|
21
|
+
|
|
22
|
+
const normalized = String(value).trim().toLowerCase()
|
|
23
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) return false
|
|
24
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) return true
|
|
25
|
+
return defaultValue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsedPriceSat = Number(process.env.X402_PRICE_SAT)
|
|
29
|
+
const priceSat = Number.isFinite(parsedPriceSat) && parsedPriceSat > 0 ? parsedPriceSat : 2000
|
|
30
|
+
|
|
31
|
+
const x402Defaults = {
|
|
32
|
+
enabled: normalizeBoolean(process.env.X402_ENABLED, true),
|
|
33
|
+
facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
|
|
34
|
+
serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d',
|
|
35
|
+
priceSat
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export default {
|
|
20
39
|
// Server port
|
|
21
40
|
port: process.env.PORT || 5942,
|
|
@@ -23,6 +42,9 @@ export default {
|
|
|
23
42
|
// Environment
|
|
24
43
|
env: process.env.NODE_ENV || 'development',
|
|
25
44
|
|
|
45
|
+
// API prefix for REST controllers
|
|
46
|
+
apiPrefix: process.env.API_PREFIX || '/v6',
|
|
47
|
+
|
|
26
48
|
// Logging level
|
|
27
49
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
28
50
|
|
|
@@ -59,6 +81,8 @@ export default {
|
|
|
59
81
|
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
|
60
82
|
},
|
|
61
83
|
|
|
84
|
+
x402: x402Defaults,
|
|
85
|
+
|
|
62
86
|
// Version
|
|
63
87
|
version
|
|
64
88
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import config from './index.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
|
4
|
+
const DEFAULT_TIMEOUT_SECONDS = 60
|
|
5
|
+
const NETWORK = 'bch'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a route configuration map for x402-bch middleware.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} apiPrefix Express API prefix (e.g., "/v6")
|
|
11
|
+
* @returns {Object} Routes configuration compatible with x402-bch-express
|
|
12
|
+
*/
|
|
13
|
+
export function buildX402Routes (apiPrefix = '/v6') {
|
|
14
|
+
const normalizedPrefix = apiPrefix.endsWith('/')
|
|
15
|
+
? apiPrefix.slice(0, -1)
|
|
16
|
+
: apiPrefix
|
|
17
|
+
const prefixWithSlash = normalizedPrefix.startsWith('/')
|
|
18
|
+
? normalizedPrefix
|
|
19
|
+
: `/${normalizedPrefix}`
|
|
20
|
+
|
|
21
|
+
const routeKey = `${prefixWithSlash}/*`
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
network: NETWORK,
|
|
25
|
+
[routeKey]: {
|
|
26
|
+
price: config.x402.priceSat,
|
|
27
|
+
network: NETWORK,
|
|
28
|
+
config: {
|
|
29
|
+
description: `${DEFAULT_DESCRIPTION} (2000 satoshis)`,
|
|
30
|
+
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getX402Settings () {
|
|
37
|
+
return {
|
|
38
|
+
enabled: Boolean(config.x402?.enabled),
|
|
39
|
+
facilitatorUrl: config.x402?.facilitatorUrl,
|
|
40
|
+
serverAddress: config.x402?.serverAddress,
|
|
41
|
+
priceSat: config.x402?.priceSat
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/controllers/index.js
CHANGED
|
@@ -18,6 +18,7 @@ class Controllers {
|
|
|
18
18
|
this.useCases = new UseCases({ adapters: this.adapters })
|
|
19
19
|
this.config = config
|
|
20
20
|
this.timerController = new TimerController({ adapters: this.adapters, useCases: this.useCases })
|
|
21
|
+
this.apiPrefix = this.config.apiPrefix || '/v6'
|
|
21
22
|
|
|
22
23
|
// Bind 'this' object to all subfunctions
|
|
23
24
|
this.initAdapters = this.initAdapters.bind(this)
|
|
@@ -45,7 +46,8 @@ class Controllers {
|
|
|
45
46
|
attachRESTControllers (app) {
|
|
46
47
|
const restControllers = new RESTControllers({
|
|
47
48
|
adapters: this.adapters,
|
|
48
|
-
useCases: this.useCases
|
|
49
|
+
useCases: this.useCases,
|
|
50
|
+
apiPrefix: this.apiPrefix
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
// Attach the REST API Controllers to the Express app.
|
|
@@ -48,7 +48,7 @@ class BlockchainRESTController {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* @api {get} /full-node/blockchain/ Service status
|
|
51
|
+
* @api {get} /v6/full-node/blockchain/ Service status
|
|
52
52
|
* @apiName BlockchainRoot
|
|
53
53
|
* @apiGroup Blockchain
|
|
54
54
|
*
|
|
@@ -61,13 +61,13 @@ class BlockchainRESTController {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* @api {get} /full-node/blockchain/getBestBlockHash Get best block hash
|
|
64
|
+
* @api {get} /v6/full-node/blockchain/getBestBlockHash Get best block hash
|
|
65
65
|
* @apiName GetBestBlockHash
|
|
66
66
|
* @apiGroup Blockchain
|
|
67
67
|
* @apiDescription Returns the hash of the best (tip) block in the longest block chain.
|
|
68
68
|
*
|
|
69
69
|
* @apiExample Example usage:
|
|
70
|
-
* curl -X GET "https://api.fullstack.cash/
|
|
70
|
+
* curl -X GET "https://api.fullstack.cash/v6/full-node/blockchain/getBestBlockHash" -H "accept: application/json"
|
|
71
71
|
*
|
|
72
72
|
* @apiSuccess {String} bestBlockHash Hash of the best block
|
|
73
73
|
*/
|
|
@@ -81,7 +81,7 @@ class BlockchainRESTController {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* @api {get} /full-node/blockchain/getBlockchainInfo Get blockchain info
|
|
84
|
+
* @api {get} /v6/full-node/blockchain/getBlockchainInfo Get blockchain info
|
|
85
85
|
* @apiName GetBlockchainInfo
|
|
86
86
|
* @apiGroup Blockchain
|
|
87
87
|
* @apiDescription Returns various state info regarding blockchain processing.
|
|
@@ -96,7 +96,7 @@ class BlockchainRESTController {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* @api {get} /full-node/blockchain/getBlockCount Get block count
|
|
99
|
+
* @api {get} /v6/full-node/blockchain/getBlockCount Get block count
|
|
100
100
|
* @apiName GetBlockCount
|
|
101
101
|
* @apiGroup Blockchain
|
|
102
102
|
* @apiDescription Returns the number of blocks in the longest blockchain.
|
|
@@ -111,7 +111,7 @@ class BlockchainRESTController {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* @api {get} /full-node/blockchain/getBlockHeader/:hash Get single block header
|
|
114
|
+
* @api {get} /v6/full-node/blockchain/getBlockHeader/:hash Get single block header
|
|
115
115
|
* @apiName GetSingleBlockHeader
|
|
116
116
|
* @apiGroup Blockchain
|
|
117
117
|
* @apiDescription Returns serialized block header data.
|
|
@@ -136,7 +136,7 @@ class BlockchainRESTController {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* @api {post} /full-node/blockchain/getBlockHeader Get multiple block headers
|
|
139
|
+
* @api {post} /v6/full-node/blockchain/getBlockHeader Get multiple block headers
|
|
140
140
|
* @apiName GetBulkBlockHeader
|
|
141
141
|
* @apiGroup Blockchain
|
|
142
142
|
* @apiDescription Returns serialized block header data for multiple hashes.
|
|
@@ -173,7 +173,7 @@ class BlockchainRESTController {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
* @api {get} /full-node/blockchain/getChainTips Get chain tips
|
|
176
|
+
* @api {get} /v6/full-node/blockchain/getChainTips Get chain tips
|
|
177
177
|
* @apiName GetChainTips
|
|
178
178
|
* @apiGroup Blockchain
|
|
179
179
|
* @apiDescription Returns information about known tips in the block tree.
|
|
@@ -188,7 +188,7 @@ class BlockchainRESTController {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* @api {get} /full-node/blockchain/getDifficulty Get difficulty
|
|
191
|
+
* @api {get} /v6/full-node/blockchain/getDifficulty Get difficulty
|
|
192
192
|
* @apiName GetDifficulty
|
|
193
193
|
* @apiGroup Blockchain
|
|
194
194
|
* @apiDescription Returns the current difficulty value.
|
|
@@ -203,7 +203,7 @@ class BlockchainRESTController {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
* @api {get} /full-node/blockchain/getMempoolEntry/:txid Get single mempool entry
|
|
206
|
+
* @api {get} /v6/full-node/blockchain/getMempoolEntry/:txid Get single mempool entry
|
|
207
207
|
* @apiName GetMempoolEntry
|
|
208
208
|
* @apiGroup Blockchain
|
|
209
209
|
* @apiDescription Returns mempool data for a transaction.
|
|
@@ -223,7 +223,7 @@ class BlockchainRESTController {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* @api {post} /full-node/blockchain/getMempoolEntry Get bulk mempool entry
|
|
226
|
+
* @api {post} /v6/full-node/blockchain/getMempoolEntry Get bulk mempool entry
|
|
227
227
|
* @apiName GetMempoolEntryBulk
|
|
228
228
|
* @apiGroup Blockchain
|
|
229
229
|
* @apiDescription Returns mempool data for multiple transactions.
|
|
@@ -256,7 +256,7 @@ class BlockchainRESTController {
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
|
-
* @api {get} /full-node/blockchain/getMempoolAncestors/:txid Get mempool ancestors
|
|
259
|
+
* @api {get} /v6/full-node/blockchain/getMempoolAncestors/:txid Get mempool ancestors
|
|
260
260
|
* @apiName GetMempoolAncestors
|
|
261
261
|
* @apiGroup Blockchain
|
|
262
262
|
* @apiDescription Returns mempool ancestor data for a transaction.
|
|
@@ -281,7 +281,7 @@ class BlockchainRESTController {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
|
-
* @api {get} /full-node/blockchain/getMempoolInfo Get mempool info
|
|
284
|
+
* @api {get} /v6/full-node/blockchain/getMempoolInfo Get mempool info
|
|
285
285
|
* @apiName GetMempoolInfo
|
|
286
286
|
* @apiGroup Blockchain
|
|
287
287
|
* @apiDescription Returns details on the state of the mempool.
|
|
@@ -296,7 +296,7 @@ class BlockchainRESTController {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
|
-
* @api {get} /full-node/blockchain/getRawMempool Get raw mempool
|
|
299
|
+
* @api {get} /v6/full-node/blockchain/getRawMempool Get raw mempool
|
|
300
300
|
* @apiName GetRawMempool
|
|
301
301
|
* @apiGroup Blockchain
|
|
302
302
|
* @apiDescription Returns all transaction ids in the mempool.
|
|
@@ -314,7 +314,7 @@ class BlockchainRESTController {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/**
|
|
317
|
-
* @api {get} /full-node/blockchain/getTxOut/:txid/:n Get transaction output
|
|
317
|
+
* @api {get} /v6/full-node/blockchain/getTxOut/:txid/:n Get transaction output
|
|
318
318
|
* @apiName GetTxOut
|
|
319
319
|
* @apiGroup Blockchain
|
|
320
320
|
* @apiDescription Returns details about an unspent transaction output.
|
|
@@ -347,7 +347,7 @@ class BlockchainRESTController {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
/**
|
|
350
|
-
* @api {post} /full-node/blockchain/getTxOut Validate a UTXO
|
|
350
|
+
* @api {post} /v6/full-node/blockchain/getTxOut Validate a UTXO
|
|
351
351
|
* @apiName GetTxOutPost
|
|
352
352
|
* @apiGroup Blockchain
|
|
353
353
|
* @apiDescription Returns details about an unspent transaction output.
|
|
@@ -380,7 +380,7 @@ class BlockchainRESTController {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
/**
|
|
383
|
-
* @api {get} /full-node/blockchain/getTxOutProof/:txid Get TxOut proof
|
|
383
|
+
* @api {get} /v6/full-node/blockchain/getTxOutProof/:txid Get TxOut proof
|
|
384
384
|
* @apiName GetTxOutProofSingle
|
|
385
385
|
* @apiGroup Blockchain
|
|
386
386
|
* @apiDescription Returns a hex-encoded proof that the transaction was included in a block.
|
|
@@ -400,7 +400,7 @@ class BlockchainRESTController {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
/**
|
|
403
|
-
* @api {post} /full-node/blockchain/getTxOutProof Get TxOut proofs
|
|
403
|
+
* @api {post} /v6/full-node/blockchain/getTxOutProof Get TxOut proofs
|
|
404
404
|
* @apiName GetTxOutProofBulk
|
|
405
405
|
* @apiGroup Blockchain
|
|
406
406
|
* @apiDescription Returns hex-encoded proofs for transactions.
|
|
@@ -435,7 +435,7 @@ class BlockchainRESTController {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
/**
|
|
438
|
-
* @api {get} /full-node/blockchain/verifyTxOutProof/:proof Verify TxOut proof
|
|
438
|
+
* @api {get} /v6/full-node/blockchain/verifyTxOutProof/:proof Verify TxOut proof
|
|
439
439
|
* @apiName VerifyTxOutProofSingle
|
|
440
440
|
* @apiGroup Blockchain
|
|
441
441
|
* @apiDescription Verifies a hex-encoded proof was included in a block.
|
|
@@ -455,7 +455,7 @@ class BlockchainRESTController {
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
/**
|
|
458
|
-
* @api {post} /full-node/blockchain/verifyTxOutProof Verify TxOut proofs
|
|
458
|
+
* @api {post} /v6/full-node/blockchain/verifyTxOutProof Verify TxOut proofs
|
|
459
459
|
* @apiName VerifyTxOutProofBulk
|
|
460
460
|
* @apiGroup Blockchain
|
|
461
461
|
* @apiDescription Verifies hex-encoded proofs were included in blocks.
|
|
@@ -490,7 +490,7 @@ class BlockchainRESTController {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
|
-
* @api {post} /full-node/blockchain/getBlock Get block details
|
|
493
|
+
* @api {post} /v6/full-node/blockchain/getBlock Get block details
|
|
494
494
|
* @apiName GetBlock
|
|
495
495
|
* @apiGroup Blockchain
|
|
496
496
|
* @apiDescription Returns block details for a hash.
|
|
@@ -519,7 +519,7 @@ class BlockchainRESTController {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/**
|
|
522
|
-
* @api {get} /full-node/blockchain/getBlockHash/:height Get block hash
|
|
522
|
+
* @api {get} /v6/full-node/blockchain/getBlockHash/:height Get block hash
|
|
523
523
|
* @apiName GetBlockHash
|
|
524
524
|
* @apiGroup Blockchain
|
|
525
525
|
* @apiDescription Returns the hash of a block by height.
|
|
@@ -28,7 +28,11 @@ class BlockchainRouter {
|
|
|
28
28
|
|
|
29
29
|
this.blockchainController = new BlockchainRESTController(dependencies)
|
|
30
30
|
|
|
31
|
-
this.
|
|
31
|
+
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
|
32
|
+
this.baseUrl = `${this.apiPrefix}/full-node/blockchain`
|
|
33
|
+
if (!this.baseUrl.startsWith('/')) {
|
|
34
|
+
this.baseUrl = `/${this.baseUrl}`
|
|
35
|
+
}
|
|
32
36
|
this.router = express.Router()
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API Controller for the /full-node/control routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import wlogger from '../../../../adapters/wlogger.js'
|
|
6
|
+
|
|
7
|
+
class ControlRESTController {
|
|
8
|
+
constructor (localConfig = {}) {
|
|
9
|
+
this.adapters = localConfig.adapters
|
|
10
|
+
if (!this.adapters) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Instance of Adapters library required when instantiating Control REST Controller.'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.useCases = localConfig.useCases
|
|
17
|
+
if (!this.useCases || !this.useCases.control) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Instance of Control use cases required when instantiating Control REST Controller.'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.controlUseCases = this.useCases.control
|
|
24
|
+
|
|
25
|
+
this.root = this.root.bind(this)
|
|
26
|
+
this.getNetworkInfo = this.getNetworkInfo.bind(this)
|
|
27
|
+
this.handleError = this.handleError.bind(this)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @api {get} /v6/full-node/control/ Service status
|
|
32
|
+
* @apiName ControlRoot
|
|
33
|
+
* @apiGroup Control
|
|
34
|
+
*
|
|
35
|
+
* @apiDescription Returns the status of the control service.
|
|
36
|
+
*
|
|
37
|
+
* @apiSuccess {String} status Service identifier
|
|
38
|
+
*/
|
|
39
|
+
async root (req, res) {
|
|
40
|
+
return res.status(200).json({ status: 'control' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @api {get} /v6/full-node/control/getNetworkInfo Get Network Info
|
|
45
|
+
* @apiName GetNetworkInfo
|
|
46
|
+
* @apiGroup Control
|
|
47
|
+
* @apiDescription RPC call that gets basic full node information.
|
|
48
|
+
*/
|
|
49
|
+
async getNetworkInfo (req, res) {
|
|
50
|
+
try {
|
|
51
|
+
const result = await this.controlUseCases.getNetworkInfo()
|
|
52
|
+
return res.status(200).json(result)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return this.handleError(err, res)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleError (err, res) {
|
|
59
|
+
wlogger.error('Error in ControlRESTController:', err)
|
|
60
|
+
|
|
61
|
+
const status = err.status || 500
|
|
62
|
+
const message = err.message || 'Internal server error'
|
|
63
|
+
|
|
64
|
+
return res.status(status).json({ error: message })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default ControlRESTController
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API router for /full-node/control routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express'
|
|
6
|
+
import ControlRESTController from './controller.js'
|
|
7
|
+
|
|
8
|
+
class ControlRouter {
|
|
9
|
+
constructor (localConfig = {}) {
|
|
10
|
+
this.adapters = localConfig.adapters
|
|
11
|
+
if (!this.adapters) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Instance of Adapters library required when instantiating Control 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 Control REST Router.'
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dependencies = {
|
|
25
|
+
adapters: this.adapters,
|
|
26
|
+
useCases: this.useCases
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.controlController = new ControlRESTController(dependencies)
|
|
30
|
+
|
|
31
|
+
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
|
32
|
+
this.baseUrl = `${this.apiPrefix}/full-node/control`
|
|
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.controlController.root)
|
|
45
|
+
this.router.get('/getNetworkInfo', this.controlController.getNetworkInfo)
|
|
46
|
+
|
|
47
|
+
app.use(this.baseUrl, this.router)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default ControlRouter
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API Controller for the /full-node/dsproof routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import wlogger from '../../../../adapters/wlogger.js'
|
|
6
|
+
|
|
7
|
+
class DSProofRESTController {
|
|
8
|
+
constructor (localConfig = {}) {
|
|
9
|
+
this.adapters = localConfig.adapters
|
|
10
|
+
if (!this.adapters) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Instance of Adapters library required when instantiating DSProof REST Controller.'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.useCases = localConfig.useCases
|
|
17
|
+
if (!this.useCases || !this.useCases.dsproof) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Instance of DSProof use cases required when instantiating DSProof REST Controller.'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.dsproofUseCases = this.useCases.dsproof
|
|
24
|
+
|
|
25
|
+
this.root = this.root.bind(this)
|
|
26
|
+
this.getDSProof = this.getDSProof.bind(this)
|
|
27
|
+
this.handleError = this.handleError.bind(this)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @api {get} /v6/full-node/dsproof/ Service status
|
|
32
|
+
* @apiName DSProofRoot
|
|
33
|
+
* @apiGroup DSProof
|
|
34
|
+
*
|
|
35
|
+
* @apiDescription Returns the status of the dsproof service.
|
|
36
|
+
*
|
|
37
|
+
* @apiSuccess {String} status Service identifier
|
|
38
|
+
*/
|
|
39
|
+
async root (req, res) {
|
|
40
|
+
return res.status(200).json({ status: 'dsproof' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @api {get} /v6/full-node/dsproof/getDSProof/:txid Get Double-Spend Proof
|
|
45
|
+
* @apiName GetDSProof
|
|
46
|
+
* @apiGroup DSProof
|
|
47
|
+
* @apiDescription Get information for a double-spend proof.
|
|
48
|
+
*
|
|
49
|
+
* @apiParam {String} txid Transaction ID
|
|
50
|
+
* @apiParam {String} verbose Verbose level (`false`, `true`) for compatibility with legacy API
|
|
51
|
+
*/
|
|
52
|
+
async getDSProof (req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const txid = req.params.txid
|
|
55
|
+
|
|
56
|
+
if (!txid) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: 'txid can not be empty'
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (txid.length !== 64) {
|
|
64
|
+
return res.status(400).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: `txid must be of length 64 (not ${txid.length})`
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let verbose = 2
|
|
71
|
+
if (req.query.verbose === 'true') verbose = 3
|
|
72
|
+
|
|
73
|
+
const result = await this.dsproofUseCases.getDSProof({ txid, verbose })
|
|
74
|
+
return res.status(200).json(result)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return this.handleError(err, res)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleError (err, res) {
|
|
81
|
+
wlogger.error('Error in DSProofRESTController:', err)
|
|
82
|
+
|
|
83
|
+
const status = err.status || 500
|
|
84
|
+
const message = err.message || 'Internal server error'
|
|
85
|
+
|
|
86
|
+
return res.status(status).json({ error: message })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default DSProofRESTController
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API router for /full-node/dsproof routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express'
|
|
6
|
+
import DSProofRESTController from './controller.js'
|
|
7
|
+
|
|
8
|
+
class DSProofRouter {
|
|
9
|
+
constructor (localConfig = {}) {
|
|
10
|
+
this.adapters = localConfig.adapters
|
|
11
|
+
if (!this.adapters) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Instance of Adapters library required when instantiating DSProof 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 DSProof REST Router.'
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dependencies = {
|
|
25
|
+
adapters: this.adapters,
|
|
26
|
+
useCases: this.useCases
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.dsproofController = new DSProofRESTController(dependencies)
|
|
30
|
+
|
|
31
|
+
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
|
32
|
+
this.baseUrl = `${this.apiPrefix}/full-node/dsproof`
|
|
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.dsproofController.root)
|
|
45
|
+
this.router.get('/getDSProof/:txid', this.dsproofController.getDSProof)
|
|
46
|
+
|
|
47
|
+
app.use(this.baseUrl, this.router)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default DSProofRouter
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// import EventRouter from './event/index.js'
|
|
9
9
|
// import ReqRouter from './req/index.js'
|
|
10
10
|
import BlockchainRouter from './full-node/blockchain/index.js'
|
|
11
|
+
import ControlRouter from './full-node/control/index.js'
|
|
12
|
+
import DSProofRouter from './full-node/dsproof/index.js'
|
|
11
13
|
import config from '../../config/index.js'
|
|
12
14
|
|
|
13
15
|
class RESTControllers {
|
|
@@ -26,6 +28,12 @@ class RESTControllers {
|
|
|
26
28
|
)
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Allow overriding the API prefix for testing, default to v6.
|
|
32
|
+
this.apiPrefix = localConfig.apiPrefix || '/v6'
|
|
33
|
+
if (this.apiPrefix.length > 1 && this.apiPrefix.endsWith('/')) {
|
|
34
|
+
this.apiPrefix = this.apiPrefix.slice(0, -1)
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// Bind 'this' object to all subfunctions.
|
|
30
38
|
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
|
31
39
|
|
|
@@ -36,7 +44,8 @@ class RESTControllers {
|
|
|
36
44
|
attachRESTControllers (app) {
|
|
37
45
|
const dependencies = {
|
|
38
46
|
adapters: this.adapters,
|
|
39
|
-
useCases: this.useCases
|
|
47
|
+
useCases: this.useCases,
|
|
48
|
+
apiPrefix: this.apiPrefix
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
// Attach the REST API Controllers associated with the /event route
|
|
@@ -49,6 +58,12 @@ class RESTControllers {
|
|
|
49
58
|
|
|
50
59
|
const blockchainRouter = new BlockchainRouter(dependencies)
|
|
51
60
|
blockchainRouter.attach(app)
|
|
61
|
+
|
|
62
|
+
const controlRouter = new ControlRouter(dependencies)
|
|
63
|
+
controlRouter.attach(app)
|
|
64
|
+
|
|
65
|
+
const dsproofRouter = new DSProofRouter(dependencies)
|
|
66
|
+
dsproofRouter.attach(app)
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
|
|
@@ -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
|
package/src/use-cases/index.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
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'
|
|
9
11
|
|
|
10
12
|
class UseCases {
|
|
11
13
|
constructor (localConfig = {}) {
|
|
@@ -17,6 +19,8 @@ class UseCases {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
|
22
|
+
this.control = new ControlUseCases({ adapters: this.adapters })
|
|
23
|
+
this.dsproof = new DSProofUseCases({ adapters: this.adapters })
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
// Run any startup Use Cases at the start of the app.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for ControlRESTController.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import ControlRESTController from '../../../src/controllers/rest-api/full-node/control/controller.js'
|
|
9
|
+
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
|
|
10
|
+
|
|
11
|
+
describe('#control-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
|
+
control: {
|
|
22
|
+
getNetworkInfo: sandbox.stub().resolves({ version: 1 })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
uut = new ControlRESTController({
|
|
27
|
+
adapters: mockAdapters,
|
|
28
|
+
useCases: mockUseCases
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
sandbox.restore()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('#constructor()', () => {
|
|
37
|
+
it('should require adapters', () => {
|
|
38
|
+
assert.throws(() => {
|
|
39
|
+
// eslint-disable-next-line no-new
|
|
40
|
+
new ControlRESTController({ useCases: mockUseCases })
|
|
41
|
+
}, /Adapters library required/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should require control use cases', () => {
|
|
45
|
+
assert.throws(() => {
|
|
46
|
+
// eslint-disable-next-line no-new
|
|
47
|
+
new ControlRESTController({ adapters: mockAdapters, useCases: {} })
|
|
48
|
+
}, /Control use cases required/)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('#root()', () => {
|
|
53
|
+
it('should return control status', async () => {
|
|
54
|
+
const req = createMockRequest()
|
|
55
|
+
const res = createMockResponse()
|
|
56
|
+
|
|
57
|
+
await uut.root(req, res)
|
|
58
|
+
|
|
59
|
+
assert.equal(res.statusValue, 200)
|
|
60
|
+
assert.deepEqual(res.jsonData, { status: 'control' })
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('#getNetworkInfo()', () => {
|
|
65
|
+
it('should return network info on success', async () => {
|
|
66
|
+
const req = createMockRequest()
|
|
67
|
+
const res = createMockResponse()
|
|
68
|
+
|
|
69
|
+
await uut.getNetworkInfo(req, res)
|
|
70
|
+
|
|
71
|
+
assert.equal(res.statusValue, 200)
|
|
72
|
+
assert.deepEqual(res.jsonData, { version: 1 })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle errors via handleError', async () => {
|
|
76
|
+
const error = new Error('failure')
|
|
77
|
+
error.status = 503
|
|
78
|
+
mockUseCases.control.getNetworkInfo.rejects(error)
|
|
79
|
+
const req = createMockRequest()
|
|
80
|
+
const res = createMockResponse()
|
|
81
|
+
|
|
82
|
+
await uut.getNetworkInfo(req, res)
|
|
83
|
+
|
|
84
|
+
assert.equal(res.statusValue, 503)
|
|
85
|
+
assert.deepEqual(res.jsonData, { error: 'failure' })
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for DSProofRESTController.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import DSProofRESTController from '../../../src/controllers/rest-api/full-node/dsproof/controller.js'
|
|
9
|
+
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
|
|
10
|
+
|
|
11
|
+
describe('#dsproof-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
|
+
dsproof: {
|
|
22
|
+
getDSProof: sandbox.stub().resolves({ proof: true })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
uut = new DSProofRESTController({
|
|
27
|
+
adapters: mockAdapters,
|
|
28
|
+
useCases: mockUseCases
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
sandbox.restore()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('#constructor()', () => {
|
|
37
|
+
it('should require adapters', () => {
|
|
38
|
+
assert.throws(() => {
|
|
39
|
+
// eslint-disable-next-line no-new
|
|
40
|
+
new DSProofRESTController({ useCases: mockUseCases })
|
|
41
|
+
}, /Adapters library required/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should require dsproof use cases', () => {
|
|
45
|
+
assert.throws(() => {
|
|
46
|
+
// eslint-disable-next-line no-new
|
|
47
|
+
new DSProofRESTController({ adapters: mockAdapters, useCases: {} })
|
|
48
|
+
}, /DSProof use cases required/)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('#root()', () => {
|
|
53
|
+
it('should return dsproof status', async () => {
|
|
54
|
+
const req = createMockRequest()
|
|
55
|
+
const res = createMockResponse()
|
|
56
|
+
|
|
57
|
+
await uut.root(req, res)
|
|
58
|
+
|
|
59
|
+
assert.equal(res.statusValue, 200)
|
|
60
|
+
assert.deepEqual(res.jsonData, { status: 'dsproof' })
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('#getDSProof()', () => {
|
|
65
|
+
it('should validate txid presence', async () => {
|
|
66
|
+
const req = createMockRequest()
|
|
67
|
+
const res = createMockResponse()
|
|
68
|
+
|
|
69
|
+
await uut.getDSProof(req, res)
|
|
70
|
+
|
|
71
|
+
assert.equal(res.statusValue, 400)
|
|
72
|
+
assert.include(res.jsonData.error, 'txid can not be empty')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should validate txid length', async () => {
|
|
76
|
+
const req = createMockRequest({ params: { txid: 'abc' } })
|
|
77
|
+
const res = createMockResponse()
|
|
78
|
+
|
|
79
|
+
await uut.getDSProof(req, res)
|
|
80
|
+
|
|
81
|
+
assert.equal(res.statusValue, 400)
|
|
82
|
+
assert.include(res.jsonData.error, 'txid must be of length 64')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should call use case with derived verbose when valid', async () => {
|
|
86
|
+
const txid = 'a'.repeat(64)
|
|
87
|
+
const req = createMockRequest({
|
|
88
|
+
params: { txid },
|
|
89
|
+
query: { verbose: 'true' }
|
|
90
|
+
})
|
|
91
|
+
const res = createMockResponse()
|
|
92
|
+
|
|
93
|
+
await uut.getDSProof(req, res)
|
|
94
|
+
|
|
95
|
+
assert.equal(res.statusValue, 200)
|
|
96
|
+
assert.isTrue(mockUseCases.dsproof.getDSProof.calledOnceWithExactly({
|
|
97
|
+
txid,
|
|
98
|
+
verbose: 3
|
|
99
|
+
}))
|
|
100
|
+
assert.deepEqual(res.jsonData, { proof: true })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle errors via handleError', async () => {
|
|
104
|
+
const txid = 'a'.repeat(64)
|
|
105
|
+
const error = new Error('failure')
|
|
106
|
+
error.status = 422
|
|
107
|
+
mockUseCases.dsproof.getDSProof.rejects(error)
|
|
108
|
+
const req = createMockRequest({ params: { txid } })
|
|
109
|
+
const res = createMockResponse()
|
|
110
|
+
|
|
111
|
+
await uut.getDSProof(req, res)
|
|
112
|
+
|
|
113
|
+
assert.equal(res.statusValue, 422)
|
|
114
|
+
assert.deepEqual(res.jsonData, { error: 'failure' })
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -7,6 +7,8 @@ import sinon from 'sinon'
|
|
|
7
7
|
|
|
8
8
|
import RESTControllers from '../../../src/controllers/rest-api/index.js'
|
|
9
9
|
import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/index.js'
|
|
10
|
+
import ControlRouter from '../../../src/controllers/rest-api/full-node/control/index.js'
|
|
11
|
+
import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/index.js'
|
|
10
12
|
|
|
11
13
|
describe('#controllers/rest-api/index.js', () => {
|
|
12
14
|
let sandbox
|
|
@@ -43,7 +45,13 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
mockUseCases = {
|
|
46
|
-
blockchain: createBlockchainUseCaseStubs()
|
|
48
|
+
blockchain: createBlockchainUseCaseStubs(),
|
|
49
|
+
control: {
|
|
50
|
+
getNetworkInfo: () => {}
|
|
51
|
+
},
|
|
52
|
+
dsproof: {
|
|
53
|
+
getDSProof: () => {}
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
})
|
|
49
57
|
|
|
@@ -68,8 +76,10 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
68
76
|
})
|
|
69
77
|
|
|
70
78
|
describe('#attachRESTControllers()', () => {
|
|
71
|
-
it('should instantiate
|
|
72
|
-
const
|
|
79
|
+
it('should instantiate routers and attach to app', () => {
|
|
80
|
+
const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
|
|
81
|
+
const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
|
|
82
|
+
const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
|
|
73
83
|
const restControllers = new RESTControllers({
|
|
74
84
|
adapters: mockAdapters,
|
|
75
85
|
useCases: mockUseCases
|
|
@@ -78,8 +88,12 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
78
88
|
|
|
79
89
|
restControllers.attachRESTControllers(app)
|
|
80
90
|
|
|
81
|
-
assert.isTrue(
|
|
82
|
-
assert.equal(
|
|
91
|
+
assert.isTrue(blockchainAttachStub.calledOnce)
|
|
92
|
+
assert.equal(blockchainAttachStub.getCall(0).args[0], app)
|
|
93
|
+
assert.isTrue(controlAttachStub.calledOnce)
|
|
94
|
+
assert.equal(controlAttachStub.getCall(0).args[0], app)
|
|
95
|
+
assert.isTrue(dsproofAttachStub.calledOnce)
|
|
96
|
+
assert.equal(dsproofAttachStub.getCall(0).args[0], app)
|
|
83
97
|
})
|
|
84
98
|
})
|
|
85
99
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for ControlUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
|
|
7
|
+
import ControlUseCases from '../../../src/use-cases/full-node-control-use-cases.js'
|
|
8
|
+
|
|
9
|
+
describe('#full-node-control-use-cases.js', () => {
|
|
10
|
+
let mockAdapters
|
|
11
|
+
let uut
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockAdapters = {
|
|
15
|
+
fullNode: {
|
|
16
|
+
call: async () => ({})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
uut = new ControlUseCases({ adapters: mockAdapters })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('#constructor()', () => {
|
|
24
|
+
it('should require adapters', () => {
|
|
25
|
+
assert.throws(() => {
|
|
26
|
+
// eslint-disable-next-line no-new
|
|
27
|
+
new ControlUseCases()
|
|
28
|
+
}, /Adapters instance required/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should require full node adapter', () => {
|
|
32
|
+
assert.throws(() => {
|
|
33
|
+
// eslint-disable-next-line no-new
|
|
34
|
+
new ControlUseCases({ adapters: {} })
|
|
35
|
+
}, /Full node adapter required/)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('#getNetworkInfo()', () => {
|
|
40
|
+
it('should call full node adapter with correct method', async () => {
|
|
41
|
+
let capturedMethod = ''
|
|
42
|
+
mockAdapters.fullNode.call = async method => {
|
|
43
|
+
capturedMethod = method
|
|
44
|
+
return { version: 1 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await uut.getNetworkInfo()
|
|
48
|
+
|
|
49
|
+
assert.equal(capturedMethod, 'getnetworkinfo')
|
|
50
|
+
assert.deepEqual(result, { version: 1 })
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for DSProofUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
|
|
7
|
+
import DSProofUseCases from '../../../src/use-cases/full-node-dsproof-use-cases.js'
|
|
8
|
+
|
|
9
|
+
describe('#full-node-dsproof-use-cases.js', () => {
|
|
10
|
+
let mockAdapters
|
|
11
|
+
let uut
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockAdapters = {
|
|
15
|
+
fullNode: {
|
|
16
|
+
call: async () => ({})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
uut = new DSProofUseCases({ adapters: mockAdapters })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('#constructor()', () => {
|
|
24
|
+
it('should require adapters', () => {
|
|
25
|
+
assert.throws(() => {
|
|
26
|
+
// eslint-disable-next-line no-new
|
|
27
|
+
new DSProofUseCases()
|
|
28
|
+
}, /Adapters instance required/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should require full node adapter', () => {
|
|
32
|
+
assert.throws(() => {
|
|
33
|
+
// eslint-disable-next-line no-new
|
|
34
|
+
new DSProofUseCases({ adapters: {} })
|
|
35
|
+
}, /Full node adapter required/)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('#getDSProof()', () => {
|
|
40
|
+
it('should pass txid and verbose parameters to adapter', async () => {
|
|
41
|
+
let capturedArgs = null
|
|
42
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
43
|
+
capturedArgs = { method, params }
|
|
44
|
+
return { success: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await uut.getDSProof({ txid: 'a'.repeat(64), verbose: 2 })
|
|
48
|
+
|
|
49
|
+
assert.equal(capturedArgs.method, 'getdsproof')
|
|
50
|
+
assert.deepEqual(capturedArgs.params, ['a'.repeat(64), 2])
|
|
51
|
+
assert.deepEqual(result, { success: true })
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
})
|