psf-bch-api 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.env-local +9 -0
  2. package/bin/server.js +2 -1
  3. package/package.json +4 -1
  4. package/src/adapters/fulcrum-api.js +124 -0
  5. package/src/adapters/full-node-rpc.js +2 -6
  6. package/src/adapters/index.js +4 -0
  7. package/src/adapters/slp-indexer-api.js +124 -0
  8. package/src/config/env/common.js +21 -24
  9. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  10. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  11. package/src/controllers/rest-api/full-node/blockchain/controller.js +4 -4
  12. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  13. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  14. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  15. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  16. package/src/controllers/rest-api/index.js +19 -3
  17. package/src/controllers/rest-api/slp/controller.js +218 -0
  18. package/src/controllers/rest-api/slp/router.js +55 -0
  19. package/src/controllers/timer-controller.js +1 -1
  20. package/src/use-cases/fulcrum-use-cases.js +155 -0
  21. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  22. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  23. package/src/use-cases/index.js +8 -0
  24. package/src/use-cases/slp-use-cases.js +321 -0
  25. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  26. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  27. package/test/unit/controllers/mining-controller-unit.js +139 -0
  28. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  29. package/test/unit/controllers/rest-api-index-unit.js +59 -3
  30. package/test/unit/controllers/slp-controller-unit.js +312 -0
  31. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  32. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  33. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  34. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  35. package/src/entities/event.js +0 -71
  36. package/test/integration/api/event-integration.js +0 -250
  37. package/test/integration/api/req-integration.js +0 -173
  38. package/test/integration/api/subscription-integration.js +0 -198
  39. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  40. package/test/integration/use-cases/publish-event-integration.js +0 -104
  41. package/test/integration/use-cases/query-events-integration.js +0 -95
  42. package/test/unit/entities/event-unit.js +0 -139
  43. /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
  44. /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
  45. /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
@@ -0,0 +1,388 @@
1
+ /*
2
+ Unit tests for RawTransactionsRESTController.
3
+ */
4
+
5
+ import { assert } from 'chai'
6
+ import sinon from 'sinon'
7
+
8
+ import RawTransactionsRESTController from '../../../src/controllers/rest-api/full-node/rawtransactions/controller.js'
9
+ import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
10
+
11
+ describe('#rawtransactions-controller.js', () => {
12
+ let sandbox
13
+ let mockAdapters
14
+ let mockUseCases
15
+ let uut
16
+
17
+ beforeEach(() => {
18
+ sandbox = sinon.createSandbox()
19
+ mockAdapters = {
20
+ fullNode: {
21
+ validateArraySize: sandbox.stub().returns(true)
22
+ }
23
+ }
24
+ mockUseCases = {
25
+ rawtransactions: {
26
+ decodeRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
27
+ decodeRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
28
+ decodeScript: sandbox.stub().resolves({ asm: 'OP_DUP' }),
29
+ decodeScripts: sandbox.stub().resolves([{ asm: 'OP_DUP' }]),
30
+ getRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
31
+ getRawTransactionWithHeight: sandbox.stub().resolves({ txid: 'abc123', height: 100 }),
32
+ getRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
33
+ sendRawTransaction: sandbox.stub().resolves('txid123'),
34
+ sendRawTransactions: sandbox.stub().resolves(['txid1', 'txid2'])
35
+ }
36
+ }
37
+
38
+ uut = new RawTransactionsRESTController({
39
+ adapters: mockAdapters,
40
+ useCases: mockUseCases
41
+ })
42
+ })
43
+
44
+ afterEach(() => {
45
+ sandbox.restore()
46
+ })
47
+
48
+ describe('#constructor()', () => {
49
+ it('should require adapters', () => {
50
+ assert.throws(() => {
51
+ // eslint-disable-next-line no-new
52
+ new RawTransactionsRESTController({ useCases: mockUseCases })
53
+ }, /Adapters library required/)
54
+ })
55
+
56
+ it('should require rawtransactions use cases', () => {
57
+ assert.throws(() => {
58
+ // eslint-disable-next-line no-new
59
+ new RawTransactionsRESTController({ adapters: mockAdapters, useCases: {} })
60
+ }, /RawTransactions use cases required/)
61
+ })
62
+ })
63
+
64
+ describe('#root()', () => {
65
+ it('should return rawtransactions status', async () => {
66
+ const req = createMockRequest()
67
+ const res = createMockResponse()
68
+
69
+ await uut.root(req, res)
70
+
71
+ assert.equal(res.statusValue, 200)
72
+ assert.deepEqual(res.jsonData, { status: 'rawtransactions' })
73
+ })
74
+ })
75
+
76
+ describe('#decodeRawTransactionSingle()', () => {
77
+ it('should return decoded transaction on success', async () => {
78
+ const req = createMockRequest({ params: { hex: '01000000' } })
79
+ const res = createMockResponse()
80
+
81
+ await uut.decodeRawTransactionSingle(req, res)
82
+
83
+ assert.equal(res.statusValue, 200)
84
+ assert.deepEqual(res.jsonData, { txid: 'abc123' })
85
+ assert.isTrue(mockUseCases.rawtransactions.decodeRawTransaction.calledOnce)
86
+ assert.deepEqual(mockUseCases.rawtransactions.decodeRawTransaction.firstCall.args[0], { hex: '01000000' })
87
+ })
88
+
89
+ it('should return 400 if hex is empty', async () => {
90
+ const req = createMockRequest({ params: { hex: '' } })
91
+ const res = createMockResponse()
92
+
93
+ await uut.decodeRawTransactionSingle(req, res)
94
+
95
+ assert.equal(res.statusValue, 400)
96
+ assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
97
+ })
98
+
99
+ it('should handle errors via handleError', async () => {
100
+ const error = new Error('RPC error')
101
+ error.status = 500
102
+ mockUseCases.rawtransactions.decodeRawTransaction.rejects(error)
103
+ const req = createMockRequest({ params: { hex: '01000000' } })
104
+ const res = createMockResponse()
105
+
106
+ await uut.decodeRawTransactionSingle(req, res)
107
+
108
+ assert.equal(res.statusValue, 500)
109
+ assert.deepEqual(res.jsonData, { error: 'RPC error' })
110
+ })
111
+ })
112
+
113
+ describe('#decodeRawTransactionBulk()', () => {
114
+ it('should return decoded transactions on success', async () => {
115
+ const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
116
+ const res = createMockResponse()
117
+
118
+ await uut.decodeRawTransactionBulk(req, res)
119
+
120
+ assert.equal(res.statusValue, 200)
121
+ assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
122
+ assert.isTrue(mockUseCases.rawtransactions.decodeRawTransactions.calledOnce)
123
+ })
124
+
125
+ it('should return 400 if hexes is not an array', async () => {
126
+ const req = createMockRequest({ body: { hexes: 'not-array' } })
127
+ const res = createMockResponse()
128
+
129
+ await uut.decodeRawTransactionBulk(req, res)
130
+
131
+ assert.equal(res.statusValue, 400)
132
+ assert.deepEqual(res.jsonData, { error: 'hexes must be an array' })
133
+ })
134
+
135
+ it('should return 400 if array is too large', async () => {
136
+ mockAdapters.fullNode.validateArraySize.returns(false)
137
+ const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
138
+ const res = createMockResponse()
139
+
140
+ await uut.decodeRawTransactionBulk(req, res)
141
+
142
+ assert.equal(res.statusValue, 400)
143
+ assert.deepEqual(res.jsonData, { error: 'Array too large.' })
144
+ })
145
+
146
+ it('should return 400 if empty hex encountered', async () => {
147
+ const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
148
+ const res = createMockResponse()
149
+
150
+ await uut.decodeRawTransactionBulk(req, res)
151
+
152
+ assert.equal(res.statusValue, 400)
153
+ assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
154
+ })
155
+ })
156
+
157
+ describe('#decodeScriptSingle()', () => {
158
+ it('should return decoded script on success', async () => {
159
+ const req = createMockRequest({ params: { hex: '76a914' } })
160
+ const res = createMockResponse()
161
+
162
+ await uut.decodeScriptSingle(req, res)
163
+
164
+ assert.equal(res.statusValue, 200)
165
+ assert.deepEqual(res.jsonData, { asm: 'OP_DUP' })
166
+ assert.isTrue(mockUseCases.rawtransactions.decodeScript.calledOnce)
167
+ })
168
+
169
+ it('should return 400 if hex is empty', async () => {
170
+ const req = createMockRequest({ params: { hex: '' } })
171
+ const res = createMockResponse()
172
+
173
+ await uut.decodeScriptSingle(req, res)
174
+
175
+ assert.equal(res.statusValue, 400)
176
+ assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
177
+ })
178
+ })
179
+
180
+ describe('#decodeScriptBulk()', () => {
181
+ it('should return decoded scripts on success', async () => {
182
+ const req = createMockRequest({ body: { hexes: ['script1', 'script2'] } })
183
+ const res = createMockResponse()
184
+
185
+ await uut.decodeScriptBulk(req, res)
186
+
187
+ assert.equal(res.statusValue, 200)
188
+ assert.deepEqual(res.jsonData, [{ asm: 'OP_DUP' }])
189
+ })
190
+
191
+ it('should return 400 if array is too large', async () => {
192
+ mockAdapters.fullNode.validateArraySize.returns(false)
193
+ const req = createMockRequest({ body: { hexes: new Array(25).fill('script') } })
194
+ const res = createMockResponse()
195
+
196
+ await uut.decodeScriptBulk(req, res)
197
+
198
+ assert.equal(res.statusValue, 400)
199
+ assert.deepEqual(res.jsonData, { error: 'Array too large.' })
200
+ })
201
+ })
202
+
203
+ describe('#getRawTransactionSingle()', () => {
204
+ it('should return raw transaction on success', async () => {
205
+ const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: {} })
206
+ const res = createMockResponse()
207
+
208
+ await uut.getRawTransactionSingle(req, res)
209
+
210
+ assert.equal(res.statusValue, 200)
211
+ assert.deepEqual(res.jsonData, { txid: 'abc123', height: 100 })
212
+ assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
213
+ })
214
+
215
+ it('should pass verbose=true when query param is set', async () => {
216
+ const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: { verbose: 'true' } })
217
+ const res = createMockResponse()
218
+
219
+ await uut.getRawTransactionSingle(req, res)
220
+
221
+ assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
222
+ assert.deepEqual(mockUseCases.rawtransactions.getRawTransactionWithHeight.firstCall.args[0], {
223
+ txid: 'a'.repeat(64),
224
+ verbose: true
225
+ })
226
+ })
227
+
228
+ it('should return 400 if txid is empty', async () => {
229
+ const req = createMockRequest({ params: { txid: '' } })
230
+ const res = createMockResponse()
231
+
232
+ await uut.getRawTransactionSingle(req, res)
233
+
234
+ assert.equal(res.statusValue, 400)
235
+ assert.deepEqual(res.jsonData, { error: 'txid can not be empty' })
236
+ })
237
+
238
+ it('should return 400 if txid length is not 64', async () => {
239
+ const req = createMockRequest({ params: { txid: 'short' } })
240
+ const res = createMockResponse()
241
+
242
+ await uut.getRawTransactionSingle(req, res)
243
+
244
+ assert.equal(res.statusValue, 400)
245
+ assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
246
+ })
247
+ })
248
+
249
+ describe('#getRawTransactionBulk()', () => {
250
+ it('should return raw transactions on success', async () => {
251
+ const req = createMockRequest({
252
+ body: {
253
+ txids: ['a'.repeat(64), 'b'.repeat(64)],
254
+ verbose: true
255
+ }
256
+ })
257
+ const res = createMockResponse()
258
+
259
+ await uut.getRawTransactionBulk(req, res)
260
+
261
+ assert.equal(res.statusValue, 200)
262
+ assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
263
+ assert.isTrue(mockUseCases.rawtransactions.getRawTransactions.calledOnce)
264
+ assert.deepEqual(mockUseCases.rawtransactions.getRawTransactions.firstCall.args[0], {
265
+ txids: ['a'.repeat(64), 'b'.repeat(64)],
266
+ verbose: true
267
+ })
268
+ })
269
+
270
+ it('should return 400 if txids is not an array', async () => {
271
+ const req = createMockRequest({ body: { txids: 'not-array' } })
272
+ const res = createMockResponse()
273
+
274
+ await uut.getRawTransactionBulk(req, res)
275
+
276
+ assert.equal(res.statusValue, 400)
277
+ assert.deepEqual(res.jsonData, { error: 'txids must be an array' })
278
+ })
279
+
280
+ it('should return 400 if array is too large', async () => {
281
+ mockAdapters.fullNode.validateArraySize.returns(false)
282
+ const req = createMockRequest({ body: { txids: new Array(25).fill('a'.repeat(64)) } })
283
+ const res = createMockResponse()
284
+
285
+ await uut.getRawTransactionBulk(req, res)
286
+
287
+ assert.equal(res.statusValue, 400)
288
+ assert.deepEqual(res.jsonData, { error: 'Array too large.' })
289
+ })
290
+
291
+ it('should return 400 if empty txid encountered', async () => {
292
+ const req = createMockRequest({ body: { txids: ['a'.repeat(64), ''] } })
293
+ const res = createMockResponse()
294
+
295
+ await uut.getRawTransactionBulk(req, res)
296
+
297
+ assert.equal(res.statusValue, 400)
298
+ assert.deepEqual(res.jsonData, { error: 'Encountered empty TXID' })
299
+ })
300
+
301
+ it('should return 400 if txid length is not 64', async () => {
302
+ const req = createMockRequest({ body: { txids: ['short'] } })
303
+ const res = createMockResponse()
304
+
305
+ await uut.getRawTransactionBulk(req, res)
306
+
307
+ assert.equal(res.statusValue, 400)
308
+ assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
309
+ })
310
+ })
311
+
312
+ describe('#sendRawTransactionSingle()', () => {
313
+ it('should return txid on success', async () => {
314
+ const req = createMockRequest({ params: { hex: '01000000' } })
315
+ const res = createMockResponse()
316
+
317
+ await uut.sendRawTransactionSingle(req, res)
318
+
319
+ assert.equal(res.statusValue, 200)
320
+ assert.equal(res.jsonData, 'txid123')
321
+ assert.isTrue(mockUseCases.rawtransactions.sendRawTransaction.calledOnce)
322
+ })
323
+
324
+ it('should return 400 if hex is empty', async () => {
325
+ const req = createMockRequest({ params: { hex: '' } })
326
+ const res = createMockResponse()
327
+
328
+ await uut.sendRawTransactionSingle(req, res)
329
+
330
+ assert.equal(res.statusValue, 400)
331
+ assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
332
+ })
333
+
334
+ it('should return 400 if hex is not a string', async () => {
335
+ const req = createMockRequest({ params: { hex: 123 } })
336
+ const res = createMockResponse()
337
+
338
+ await uut.sendRawTransactionSingle(req, res)
339
+
340
+ assert.equal(res.statusValue, 400)
341
+ assert.deepEqual(res.jsonData, { error: 'hex must be a string' })
342
+ })
343
+ })
344
+
345
+ describe('#sendRawTransactionBulk()', () => {
346
+ it('should return txids on success', async () => {
347
+ const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
348
+ const res = createMockResponse()
349
+
350
+ await uut.sendRawTransactionBulk(req, res)
351
+
352
+ assert.equal(res.statusValue, 200)
353
+ assert.deepEqual(res.jsonData, ['txid1', 'txid2'])
354
+ assert.isTrue(mockUseCases.rawtransactions.sendRawTransactions.calledOnce)
355
+ })
356
+
357
+ it('should return 400 if hexes is not an array', async () => {
358
+ const req = createMockRequest({ body: { hexes: 'not-array' } })
359
+ const res = createMockResponse()
360
+
361
+ await uut.sendRawTransactionBulk(req, res)
362
+
363
+ assert.equal(res.statusValue, 400)
364
+ assert.deepEqual(res.jsonData, { error: 'hex must be an array' })
365
+ })
366
+
367
+ it('should return 400 if array is too large', async () => {
368
+ mockAdapters.fullNode.validateArraySize.returns(false)
369
+ const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
370
+ const res = createMockResponse()
371
+
372
+ await uut.sendRawTransactionBulk(req, res)
373
+
374
+ assert.equal(res.statusValue, 400)
375
+ assert.deepEqual(res.jsonData, { error: 'Array too large.' })
376
+ })
377
+
378
+ it('should return 400 if empty hex encountered', async () => {
379
+ const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
380
+ const res = createMockResponse()
381
+
382
+ await uut.sendRawTransactionBulk(req, res)
383
+
384
+ assert.equal(res.statusValue, 400)
385
+ assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
386
+ })
387
+ })
388
+ })
@@ -6,9 +6,13 @@ import { assert } from 'chai'
6
6
  import sinon from 'sinon'
7
7
 
8
8
  import RESTControllers from '../../../src/controllers/rest-api/index.js'
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'
9
+ import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/router.js'
10
+ import ControlRouter from '../../../src/controllers/rest-api/full-node/control/router.js'
11
+ import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/router.js'
12
+ import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/router.js'
13
+ import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/router.js'
14
+ import FulcrumRouter from '../../../src/controllers/rest-api/fulcrum/router.js'
15
+ import SlpRouter from '../../../src/controllers/rest-api/slp/router.js'
12
16
 
13
17
  describe('#controllers/rest-api/index.js', () => {
14
18
  let sandbox
@@ -51,6 +55,46 @@ describe('#controllers/rest-api/index.js', () => {
51
55
  },
52
56
  dsproof: {
53
57
  getDSProof: () => {}
58
+ },
59
+ fulcrum: {
60
+ getBalance: () => {},
61
+ getBalances: () => {},
62
+ getUtxos: () => {},
63
+ getUtxosBulk: () => {},
64
+ getTransactionDetails: () => {},
65
+ getTransactionDetailsBulk: () => {},
66
+ broadcastTransaction: () => {},
67
+ getBlockHeaders: () => {},
68
+ getBlockHeadersBulk: () => {},
69
+ getTransactions: () => {},
70
+ getTransactionsBulk: () => {},
71
+ getMempool: () => {},
72
+ getMempoolBulk: () => {}
73
+ },
74
+ mining: {
75
+ getMiningInfo: () => {},
76
+ getNetworkHashPS: () => {}
77
+ },
78
+ rawtransactions: {
79
+ decodeRawTransaction: () => {},
80
+ decodeRawTransactions: () => {},
81
+ decodeScript: () => {},
82
+ decodeScripts: () => {},
83
+ getRawTransaction: () => {},
84
+ getRawTransactionWithHeight: () => {},
85
+ getRawTransactions: () => {},
86
+ sendRawTransaction: () => {},
87
+ sendRawTransactions: () => {}
88
+ },
89
+ slp: {
90
+ getStatus: () => {},
91
+ getAddress: () => {},
92
+ getTxid: () => {},
93
+ getTokenStats: () => {},
94
+ getTokenData: () => {},
95
+ getMutableCid: () => {},
96
+ decodeOpReturn: () => {},
97
+ getCIDData: () => {}
54
98
  }
55
99
  }
56
100
  })
@@ -80,6 +124,10 @@ describe('#controllers/rest-api/index.js', () => {
80
124
  const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
81
125
  const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
82
126
  const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
127
+ const fulcrumAttachStub = sandbox.stub(FulcrumRouter.prototype, 'attach')
128
+ const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach')
129
+ const rawtransactionsAttachStub = sandbox.stub(RawTransactionsRouter.prototype, 'attach')
130
+ const slpAttachStub = sandbox.stub(SlpRouter.prototype, 'attach')
83
131
  const restControllers = new RESTControllers({
84
132
  adapters: mockAdapters,
85
133
  useCases: mockUseCases
@@ -94,6 +142,14 @@ describe('#controllers/rest-api/index.js', () => {
94
142
  assert.equal(controlAttachStub.getCall(0).args[0], app)
95
143
  assert.isTrue(dsproofAttachStub.calledOnce)
96
144
  assert.equal(dsproofAttachStub.getCall(0).args[0], app)
145
+ assert.isTrue(fulcrumAttachStub.calledOnce)
146
+ assert.equal(fulcrumAttachStub.getCall(0).args[0], app)
147
+ assert.isTrue(miningAttachStub.calledOnce)
148
+ assert.equal(miningAttachStub.getCall(0).args[0], app)
149
+ assert.isTrue(rawtransactionsAttachStub.calledOnce)
150
+ assert.equal(rawtransactionsAttachStub.getCall(0).args[0], app)
151
+ assert.isTrue(slpAttachStub.calledOnce)
152
+ assert.equal(slpAttachStub.getCall(0).args[0], app)
97
153
  })
98
154
  })
99
155
  })