psf-bch-api 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env-local +9 -0
- package/README.md +22 -0
- package/bin/server.js +24 -1
- package/package.json +6 -2
- package/src/adapters/fulcrum-api.js +124 -0
- package/src/adapters/full-node-rpc.js +2 -6
- package/src/adapters/index.js +4 -0
- package/src/adapters/slp-indexer-api.js +124 -0
- package/src/config/env/common.js +45 -24
- package/src/config/x402.js +43 -0
- package/src/controllers/index.js +3 -1
- package/src/controllers/rest-api/fulcrum/controller.js +563 -0
- package/src/controllers/rest-api/fulcrum/router.js +64 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +26 -26
- package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +5 -1
- package/src/controllers/rest-api/full-node/control/controller.js +68 -0
- package/src/controllers/rest-api/full-node/control/router.js +51 -0
- package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
- package/src/controllers/rest-api/full-node/dsproof/router.js +51 -0
- package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
- package/src/controllers/rest-api/full-node/mining/router.js +52 -0
- package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
- package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
- package/src/controllers/rest-api/index.js +33 -2
- package/src/controllers/rest-api/slp/controller.js +218 -0
- package/src/controllers/rest-api/slp/router.js +55 -0
- package/src/controllers/timer-controller.js +1 -1
- package/src/use-cases/fulcrum-use-cases.js +155 -0
- package/src/use-cases/full-node-control-use-cases.js +24 -0
- package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
- package/src/use-cases/full-node-mining-use-cases.js +28 -0
- package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
- package/src/use-cases/index.js +12 -0
- package/src/use-cases/slp-use-cases.js +321 -0
- package/test/unit/controllers/blockchain-controller-unit.js +2 -3
- package/test/unit/controllers/control-controller-unit.js +88 -0
- package/test/unit/controllers/dsproof-controller-unit.js +117 -0
- package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +76 -6
- package/test/unit/controllers/slp-controller-unit.js +312 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
- package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
- package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
- package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
- package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
- package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
- package/src/entities/event.js +0 -71
- package/test/integration/api/event-integration.js +0 -250
- package/test/integration/api/req-integration.js +0 -173
- package/test/integration/api/subscription-integration.js +0 -198
- package/test/integration/use-cases/manage-subscription-integration.js +0 -163
- package/test/integration/use-cases/publish-event-integration.js +0 -104
- package/test/integration/use-cases/query-events-integration.js +0 -95
- package/test/unit/entities/event-unit.js +0 -139
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use cases for interacting with the SLP Indexer API service.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import wlogger from '../adapters/wlogger.js'
|
|
6
|
+
import BCHJS from '@psf/bch-js'
|
|
7
|
+
import SlpWallet from 'minimal-slp-wallet'
|
|
8
|
+
import SlpTokenMedia from 'slp-token-media'
|
|
9
|
+
import axios from 'axios'
|
|
10
|
+
import config from '../config/index.js'
|
|
11
|
+
|
|
12
|
+
const bchjs = new BCHJS()
|
|
13
|
+
|
|
14
|
+
class SlpUseCases {
|
|
15
|
+
constructor (localConfig = {}) {
|
|
16
|
+
this.adapters = localConfig.adapters
|
|
17
|
+
|
|
18
|
+
if (!this.adapters) {
|
|
19
|
+
throw new Error('Adapters instance required when instantiating SLP use cases.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.slpIndexer = this.adapters.slpIndexer
|
|
23
|
+
if (!this.slpIndexer) {
|
|
24
|
+
throw new Error('SLP Indexer adapter required when instantiating SLP use cases.')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Allow bchjs to be injected for testing
|
|
28
|
+
this.bchjs = localConfig.bchjs || bchjs
|
|
29
|
+
|
|
30
|
+
// Get config
|
|
31
|
+
this.config = localConfig.config || config
|
|
32
|
+
|
|
33
|
+
// Initialize wallet (lazy initialization)
|
|
34
|
+
this.wallet = null
|
|
35
|
+
this.slpTokenMedia = null
|
|
36
|
+
this.walletInitialized = false
|
|
37
|
+
this.initializationPromise = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Initialize wallet and SlpTokenMedia asynchronously
|
|
41
|
+
async _ensureInitialized () {
|
|
42
|
+
if (this.walletInitialized) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.initializationPromise) {
|
|
47
|
+
return this.initializationPromise
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.initializationPromise = this._initialize()
|
|
51
|
+
return this.initializationPromise
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async _initialize () {
|
|
55
|
+
try {
|
|
56
|
+
// Initialize wallet
|
|
57
|
+
this.wallet = new SlpWallet(undefined, {
|
|
58
|
+
restURL: this.config.restURL,
|
|
59
|
+
interface: 'rest-api'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Wait for wallet to initialize
|
|
63
|
+
await this.wallet.walletInfoPromise
|
|
64
|
+
|
|
65
|
+
// Initialize SlpTokenMedia
|
|
66
|
+
this.slpTokenMedia = new SlpTokenMedia({
|
|
67
|
+
wallet: this.wallet,
|
|
68
|
+
ipfsGatewayUrl: this.config.ipfsGateway
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
this.walletInitialized = true
|
|
72
|
+
wlogger.info('SLP wallet and token media initialized')
|
|
73
|
+
} catch (err) {
|
|
74
|
+
wlogger.error('Error initializing SLP wallet:', err)
|
|
75
|
+
throw err
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getStatus () {
|
|
80
|
+
try {
|
|
81
|
+
return await this.slpIndexer.get('slp/status/')
|
|
82
|
+
} catch (err) {
|
|
83
|
+
wlogger.error('Error in SlpUseCases.getStatus()', err)
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getAddress ({ address }) {
|
|
89
|
+
try {
|
|
90
|
+
return await this.slpIndexer.post('slp/address/', { address })
|
|
91
|
+
} catch (err) {
|
|
92
|
+
wlogger.error('Error in SlpUseCases.getAddress()', err)
|
|
93
|
+
throw err
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getTxid ({ txid }) {
|
|
98
|
+
try {
|
|
99
|
+
return await this.slpIndexer.post('slp/tx/', { txid })
|
|
100
|
+
} catch (err) {
|
|
101
|
+
wlogger.error('Error in SlpUseCases.getTxid()', err)
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getTokenStats ({ tokenId, withTxHistory = false }) {
|
|
107
|
+
try {
|
|
108
|
+
return await this.slpIndexer.post('slp/token/', { tokenId, withTxHistory })
|
|
109
|
+
} catch (err) {
|
|
110
|
+
wlogger.error('Error in SlpUseCases.getTokenStats()', err)
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getTokenData ({ tokenId, withTxHistory = false }) {
|
|
116
|
+
try {
|
|
117
|
+
const tokenData = {}
|
|
118
|
+
|
|
119
|
+
// Get token stats from the Genesis TX of the token
|
|
120
|
+
const response = await this.slpIndexer.post('slp/token/', { tokenId, withTxHistory })
|
|
121
|
+
const tokenStats = response.tokenData
|
|
122
|
+
|
|
123
|
+
tokenData.genesisData = tokenStats
|
|
124
|
+
|
|
125
|
+
// Try to get immutable data
|
|
126
|
+
try {
|
|
127
|
+
const immutableData = tokenStats.documentUri
|
|
128
|
+
tokenData.immutableData = immutableData || ''
|
|
129
|
+
} catch (error) {
|
|
130
|
+
tokenData.immutableData = ''
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try to get mutable data
|
|
134
|
+
try {
|
|
135
|
+
const mutableData = await this.getMutableCid({ tokenStats })
|
|
136
|
+
tokenData.mutableData = mutableData || ''
|
|
137
|
+
} catch (error) {
|
|
138
|
+
wlogger.warn('Error getting mutable data:', error)
|
|
139
|
+
tokenData.mutableData = ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return tokenData
|
|
143
|
+
} catch (err) {
|
|
144
|
+
wlogger.error('Error in SlpUseCases.getTokenData()', err)
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getMutableCid ({ tokenStats }) {
|
|
150
|
+
// Validate input - this should throw, not be caught
|
|
151
|
+
if (!tokenStats || !tokenStats.documentHash) {
|
|
152
|
+
throw new Error('No documentHash property found in tokenStats')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await this._ensureInitialized()
|
|
157
|
+
|
|
158
|
+
// Get the OP_RETURN data and decode it
|
|
159
|
+
const mutableData = await this.decodeOpReturn({ txid: tokenStats.documentHash })
|
|
160
|
+
const jsonData = JSON.parse(mutableData)
|
|
161
|
+
|
|
162
|
+
// mda = mutable data address
|
|
163
|
+
const mda = jsonData.mda
|
|
164
|
+
|
|
165
|
+
// Get the mda transaction history
|
|
166
|
+
const transactions = await this.wallet.getTransactions(mda)
|
|
167
|
+
wlogger.info(`MDA has ${transactions.length} transactions in its history.`)
|
|
168
|
+
|
|
169
|
+
const mdaTxs = transactions
|
|
170
|
+
|
|
171
|
+
let data = false
|
|
172
|
+
|
|
173
|
+
// These are used to filter blockchain data to find the most recent
|
|
174
|
+
// update to the MDA
|
|
175
|
+
let largestBlock = 700000
|
|
176
|
+
let largestTimestamp = 1666107111271
|
|
177
|
+
let bestEntry
|
|
178
|
+
|
|
179
|
+
// Used to track the number of transactions before the best candidate is found
|
|
180
|
+
let txCnt = 0
|
|
181
|
+
|
|
182
|
+
// Map each transaction of the mda
|
|
183
|
+
// If it finds an OP_RETURN, decode it and exit the loop
|
|
184
|
+
for (let i = 0; i < mdaTxs.length; i++) {
|
|
185
|
+
const tx = mdaTxs[i]
|
|
186
|
+
const txid = tx.tx_hash
|
|
187
|
+
txCnt++
|
|
188
|
+
|
|
189
|
+
data = await this.decodeOpReturn({ txid })
|
|
190
|
+
|
|
191
|
+
// Try parse the OP_RETURN data to a JSON object
|
|
192
|
+
if (data) {
|
|
193
|
+
try {
|
|
194
|
+
// Convert the OP_RETURN data to a JSON object
|
|
195
|
+
const obj = JSON.parse(data)
|
|
196
|
+
|
|
197
|
+
// Keep searching if this TX does not have a cid value
|
|
198
|
+
if (!obj.cid) continue
|
|
199
|
+
|
|
200
|
+
// Ensure data was generated by the MDA
|
|
201
|
+
const txData = await this.wallet.getTxData([txid])
|
|
202
|
+
const vinAddress = txData[0].vin[0].address
|
|
203
|
+
|
|
204
|
+
// Skip entry if it was not made by the MDA private key
|
|
205
|
+
if (mda !== vinAddress) {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// First best entry found
|
|
210
|
+
if (!bestEntry) {
|
|
211
|
+
bestEntry = data
|
|
212
|
+
largestBlock = tx.height
|
|
213
|
+
|
|
214
|
+
if (obj.ts) {
|
|
215
|
+
largestTimestamp = obj.ts
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// One candidate already found. Looking for potentially better entry
|
|
219
|
+
|
|
220
|
+
if (tx.height < largestBlock) {
|
|
221
|
+
// Exit loop if next candidate has an older block height
|
|
222
|
+
break
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (obj.ts && obj.ts < largestTimestamp) {
|
|
226
|
+
// Continue looping through entries if the current entry in
|
|
227
|
+
// the same block has a smaller timestamp
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
bestEntry = data
|
|
232
|
+
largestBlock = tx.height
|
|
233
|
+
if (obj.ts) {
|
|
234
|
+
largestTimestamp = obj.ts
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
wlogger.info(`${txCnt} transactions reviewed to find mutable data.`)
|
|
244
|
+
|
|
245
|
+
if (!bestEntry) {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get the CID
|
|
250
|
+
const obj = JSON.parse(bestEntry)
|
|
251
|
+
const cid = obj.cid
|
|
252
|
+
|
|
253
|
+
if (!cid) {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Assuming that CID starts with ipfs://. Cutting out that prefix
|
|
258
|
+
const mutableCid = cid.substring(7)
|
|
259
|
+
|
|
260
|
+
return mutableCid
|
|
261
|
+
} catch (err) {
|
|
262
|
+
wlogger.error('Error in SlpUseCases.getMutableCid()', err)
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async decodeOpReturn ({ txid }) {
|
|
268
|
+
try {
|
|
269
|
+
if (!txid || typeof txid !== 'string') {
|
|
270
|
+
throw new Error('txid must be a string.')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get transaction data
|
|
274
|
+
const txData = await this.bchjs.Electrumx.txData(txid)
|
|
275
|
+
let data = false
|
|
276
|
+
|
|
277
|
+
// Map the vout of the transaction in search of an OP_RETURN
|
|
278
|
+
for (let i = 0; i < txData.details.vout.length; i++) {
|
|
279
|
+
const vout = txData.details.vout[i]
|
|
280
|
+
|
|
281
|
+
const script = this.bchjs.Script.toASM(
|
|
282
|
+
Buffer.from(vout.scriptPubKey.hex, 'hex')
|
|
283
|
+
).split(' ')
|
|
284
|
+
|
|
285
|
+
// Exit on the first OP_RETURN found
|
|
286
|
+
if (script[0] === 'OP_RETURN') {
|
|
287
|
+
data = Buffer.from(script[1], 'hex').toString('ascii')
|
|
288
|
+
break
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return data
|
|
293
|
+
} catch (error) {
|
|
294
|
+
wlogger.error('Error in SlpUseCases.decodeOpReturn()', error)
|
|
295
|
+
throw error
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async getCIDData ({ cid }) {
|
|
300
|
+
try {
|
|
301
|
+
if (!cid || typeof cid !== 'string') {
|
|
302
|
+
throw new Error('cid must be a string.')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Assuming that CID starts with ipfs://. Cutting out that prefix
|
|
306
|
+
const cidWithoutPrefix = cid.substring(7)
|
|
307
|
+
|
|
308
|
+
const dataUrl = `https://${cidWithoutPrefix}.ipfs.dweb.link/data.json`
|
|
309
|
+
wlogger.info(`Fetching IPFS data from: ${dataUrl}`)
|
|
310
|
+
|
|
311
|
+
const response = await axios.get(dataUrl)
|
|
312
|
+
|
|
313
|
+
return response.data
|
|
314
|
+
} catch (error) {
|
|
315
|
+
wlogger.error('Error in SlpUseCases.getCIDData()', error)
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default SlpUseCases
|
|
@@ -161,8 +161,7 @@ describe('#blockchain-controller.js', () => {
|
|
|
161
161
|
it('should validate array size and call use case', async () => {
|
|
162
162
|
const hash = 'a'.repeat(64)
|
|
163
163
|
const req = createMockRequest({
|
|
164
|
-
body: { hashes: [hash], verbose: true }
|
|
165
|
-
locals: { proLimit: false }
|
|
164
|
+
body: { hashes: [hash], verbose: true }
|
|
166
165
|
})
|
|
167
166
|
const res = createMockResponse()
|
|
168
167
|
mockUseCases.blockchain.getBlockHeaders.resolves(['result'])
|
|
@@ -172,7 +171,7 @@ describe('#blockchain-controller.js', () => {
|
|
|
172
171
|
assert.equal(res.statusValue, 200)
|
|
173
172
|
assert.deepEqual(res.jsonData, ['result'])
|
|
174
173
|
assert.isTrue(
|
|
175
|
-
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1
|
|
174
|
+
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1)
|
|
176
175
|
)
|
|
177
176
|
assert.isTrue(
|
|
178
177
|
mockUseCases.blockchain.getBlockHeaders.calledOnceWithExactly({
|
|
@@ -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
|
+
})
|