psf-bch-api 1.2.0 → 7.1.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 +28 -0
- package/bin/server.js +61 -9
- 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 +29 -25
- package/src/config/x402.js +7 -0
- 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 +4 -4
- 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 +23 -3
- 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 +218 -0
- package/src/controllers/rest-api/slp/router.js +55 -0
- package/src/controllers/timer-controller.js +1 -1
- package/src/middleware/basic-auth.js +61 -0
- package/src/use-cases/fulcrum-use-cases.js +155 -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 +10 -0
- package/src/use-cases/price-use-cases.js +83 -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/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/price-controller-unit.js +116 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +67 -3
- 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-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/price-use-cases-unit.js +103 -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
- /package/{index.js → psf-bch-api.js} +0 -0
- /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for RawTransactionsUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
|
|
7
|
+
import RawTransactionsUseCases from '../../../src/use-cases/full-node-rawtransactions-use-cases.js'
|
|
8
|
+
|
|
9
|
+
describe('#full-node-rawtransactions-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 RawTransactionsUseCases({ adapters: mockAdapters })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('#constructor()', () => {
|
|
24
|
+
it('should require adapters', () => {
|
|
25
|
+
assert.throws(() => {
|
|
26
|
+
// eslint-disable-next-line no-new
|
|
27
|
+
new RawTransactionsUseCases()
|
|
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 RawTransactionsUseCases({ adapters: {} })
|
|
35
|
+
}, /Full node adapter required/)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('#decodeRawTransaction()', () => {
|
|
40
|
+
it('should call full node adapter with correct method', async () => {
|
|
41
|
+
let capturedMethod = ''
|
|
42
|
+
let capturedParams = []
|
|
43
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
44
|
+
capturedMethod = method
|
|
45
|
+
capturedParams = params
|
|
46
|
+
return { txid: 'abc123', version: 2 }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await uut.decodeRawTransaction({ hex: '01000000' })
|
|
50
|
+
|
|
51
|
+
assert.equal(capturedMethod, 'decoderawtransaction')
|
|
52
|
+
assert.deepEqual(capturedParams, ['01000000'])
|
|
53
|
+
assert.deepEqual(result, { txid: 'abc123', version: 2 })
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('#decodeRawTransactions()', () => {
|
|
58
|
+
it('should call full node adapter for each hex in parallel', async () => {
|
|
59
|
+
const callCount = { count: 0 }
|
|
60
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
61
|
+
callCount.count++
|
|
62
|
+
return { txid: `tx${callCount.count}`, hex: params[0] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hexes = ['hex1', 'hex2', 'hex3']
|
|
66
|
+
const result = await uut.decodeRawTransactions({ hexes })
|
|
67
|
+
|
|
68
|
+
assert.equal(callCount.count, 3)
|
|
69
|
+
assert.equal(result.length, 3)
|
|
70
|
+
assert.equal(result[0].txid, 'tx1')
|
|
71
|
+
assert.equal(result[1].txid, 'tx2')
|
|
72
|
+
assert.equal(result[2].txid, 'tx3')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('#decodeScript()', () => {
|
|
77
|
+
it('should call full node adapter with correct method', async () => {
|
|
78
|
+
let capturedMethod = ''
|
|
79
|
+
let capturedParams = []
|
|
80
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
81
|
+
capturedMethod = method
|
|
82
|
+
capturedParams = params
|
|
83
|
+
return { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await uut.decodeScript({ hex: '76a914' })
|
|
87
|
+
|
|
88
|
+
assert.equal(capturedMethod, 'decodescript')
|
|
89
|
+
assert.deepEqual(capturedParams, ['76a914'])
|
|
90
|
+
assert.deepEqual(result, { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' })
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('#decodeScripts()', () => {
|
|
95
|
+
it('should call full node adapter for each hex in parallel', async () => {
|
|
96
|
+
const callCount = { count: 0 }
|
|
97
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
98
|
+
callCount.count++
|
|
99
|
+
return { asm: `script${callCount.count}`, hex: params[0] }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hexes = ['script1', 'script2']
|
|
103
|
+
const result = await uut.decodeScripts({ hexes })
|
|
104
|
+
|
|
105
|
+
assert.equal(callCount.count, 2)
|
|
106
|
+
assert.equal(result.length, 2)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('#getRawTransaction()', () => {
|
|
111
|
+
it('should call full node adapter with verbose=false by default', async () => {
|
|
112
|
+
let capturedParams = []
|
|
113
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
114
|
+
capturedParams = params
|
|
115
|
+
return '01000000'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await uut.getRawTransaction({ txid: 'abc123' })
|
|
119
|
+
|
|
120
|
+
assert.deepEqual(capturedParams, ['abc123', 0])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should call full node adapter with verbose=true when specified', async () => {
|
|
124
|
+
let capturedParams = []
|
|
125
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
126
|
+
capturedParams = params
|
|
127
|
+
return { txid: 'abc123', version: 2 }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await uut.getRawTransaction({ txid: 'abc123', verbose: true })
|
|
131
|
+
|
|
132
|
+
assert.deepEqual(capturedParams, ['abc123', 1])
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('#getRawTransactions()', () => {
|
|
137
|
+
it('should call full node adapter for each txid in parallel', async () => {
|
|
138
|
+
const callCount = { count: 0 }
|
|
139
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
140
|
+
callCount.count++
|
|
141
|
+
return { txid: params[0], version: 2 }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const txids = ['tx1', 'tx2']
|
|
145
|
+
const result = await uut.getRawTransactions({ txids, verbose: true })
|
|
146
|
+
|
|
147
|
+
assert.equal(callCount.count, 2)
|
|
148
|
+
assert.equal(result.length, 2)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('#getRawTransactionWithHeight()', () => {
|
|
153
|
+
it('should return transaction without height when verbose=false', async () => {
|
|
154
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
155
|
+
if (method === 'getrawtransaction') {
|
|
156
|
+
return '01000000'
|
|
157
|
+
}
|
|
158
|
+
return {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: false })
|
|
162
|
+
|
|
163
|
+
assert.equal(result, '01000000')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should fetch and append height when verbose=true and blockhash exists', async () => {
|
|
167
|
+
let callCount = 0
|
|
168
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
169
|
+
callCount++
|
|
170
|
+
if (method === 'getrawtransaction') {
|
|
171
|
+
return { txid: 'abc123', blockhash: 'block123' }
|
|
172
|
+
}
|
|
173
|
+
if (method === 'getblockheader') {
|
|
174
|
+
return { height: 100, hash: 'block123' }
|
|
175
|
+
}
|
|
176
|
+
return {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
|
|
180
|
+
|
|
181
|
+
assert.equal(callCount, 2)
|
|
182
|
+
assert.equal(result.height, 100)
|
|
183
|
+
assert.equal(result.txid, 'abc123')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should handle block header lookup failure gracefully', async () => {
|
|
187
|
+
let callCount = 0
|
|
188
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
189
|
+
callCount++
|
|
190
|
+
if (method === 'getrawtransaction') {
|
|
191
|
+
return { txid: 'abc123', blockhash: 'block123' }
|
|
192
|
+
}
|
|
193
|
+
if (method === 'getblockheader') {
|
|
194
|
+
throw new Error('Block not found')
|
|
195
|
+
}
|
|
196
|
+
return {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
|
|
200
|
+
|
|
201
|
+
assert.equal(callCount, 2)
|
|
202
|
+
assert.isNull(result.height)
|
|
203
|
+
assert.equal(result.txid, 'abc123')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('#getBlockHeader()', () => {
|
|
208
|
+
it('should call full node adapter with correct method', async () => {
|
|
209
|
+
let capturedMethod = ''
|
|
210
|
+
let capturedParams = []
|
|
211
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
212
|
+
capturedMethod = method
|
|
213
|
+
capturedParams = params
|
|
214
|
+
return { height: 100, hash: 'block123' }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await uut.getBlockHeader({ blockHash: 'block123', verbose: true })
|
|
218
|
+
|
|
219
|
+
assert.equal(capturedMethod, 'getblockheader')
|
|
220
|
+
assert.deepEqual(capturedParams, ['block123', true])
|
|
221
|
+
assert.deepEqual(result, { height: 100, hash: 'block123' })
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('#sendRawTransaction()', () => {
|
|
226
|
+
it('should call full node adapter with correct method', async () => {
|
|
227
|
+
let capturedMethod = ''
|
|
228
|
+
let capturedParams = []
|
|
229
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
230
|
+
capturedMethod = method
|
|
231
|
+
capturedParams = params
|
|
232
|
+
return 'txid123'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = await uut.sendRawTransaction({ hex: '01000000' })
|
|
236
|
+
|
|
237
|
+
assert.equal(capturedMethod, 'sendrawtransaction')
|
|
238
|
+
assert.deepEqual(capturedParams, ['01000000'])
|
|
239
|
+
assert.equal(result, 'txid123')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('#sendRawTransactions()', () => {
|
|
244
|
+
it('should send transactions serially, not in parallel', async () => {
|
|
245
|
+
const callOrder = []
|
|
246
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
247
|
+
callOrder.push(params[0])
|
|
248
|
+
// Simulate some async work
|
|
249
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
250
|
+
return `txid-${params[0]}`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const hexes = ['hex1', 'hex2', 'hex3']
|
|
254
|
+
const startTime = Date.now()
|
|
255
|
+
const result = await uut.sendRawTransactions({ hexes })
|
|
256
|
+
const endTime = Date.now()
|
|
257
|
+
|
|
258
|
+
// Should take at least 30ms if serial (3 * 10ms)
|
|
259
|
+
assert.isAtLeast(endTime - startTime, 25)
|
|
260
|
+
assert.deepEqual(callOrder, ['hex1', 'hex2', 'hex3'])
|
|
261
|
+
assert.equal(result.length, 3)
|
|
262
|
+
assert.equal(result[0], 'txid-hex1')
|
|
263
|
+
assert.equal(result[1], 'txid-hex2')
|
|
264
|
+
assert.equal(result[2], 'txid-hex3')
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for PriceUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import PriceUseCases from '../../../src/use-cases/price-use-cases.js'
|
|
9
|
+
|
|
10
|
+
describe('#price-use-cases.js', () => {
|
|
11
|
+
let sandbox
|
|
12
|
+
let mockAdapters
|
|
13
|
+
let mockAxios
|
|
14
|
+
let mockConfig
|
|
15
|
+
let uut
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
sandbox = sinon.createSandbox()
|
|
19
|
+
mockAdapters = {}
|
|
20
|
+
|
|
21
|
+
mockConfig = {
|
|
22
|
+
restURL: 'http://localhost:3000/v5/'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Mock axios
|
|
26
|
+
mockAxios = {
|
|
27
|
+
request: sandbox.stub()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
uut = new PriceUseCases({
|
|
31
|
+
adapters: mockAdapters,
|
|
32
|
+
axios: mockAxios,
|
|
33
|
+
config: mockConfig
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
sandbox.restore()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('#constructor()', () => {
|
|
42
|
+
it('should require adapters', () => {
|
|
43
|
+
assert.throws(() => {
|
|
44
|
+
// eslint-disable-next-line no-new
|
|
45
|
+
new PriceUseCases()
|
|
46
|
+
}, /Adapters instance required/)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('#getBCHUSD()', () => {
|
|
51
|
+
it('should return BCH price from Coinex API', async () => {
|
|
52
|
+
const mockPrice = 250.5
|
|
53
|
+
mockAxios.request.resolves({
|
|
54
|
+
data: {
|
|
55
|
+
data: {
|
|
56
|
+
ticker: {
|
|
57
|
+
last: mockPrice.toString()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const result = await uut.getBCHUSD()
|
|
64
|
+
|
|
65
|
+
assert.equal(result, mockPrice)
|
|
66
|
+
assert.isTrue(mockAxios.request.calledOnce)
|
|
67
|
+
const callArgs = mockAxios.request.getCall(0).args[0]
|
|
68
|
+
assert.equal(callArgs.method, 'get')
|
|
69
|
+
assert.equal(callArgs.baseURL, 'https://api.coinex.com/v1/market/ticker?market=bchusdt')
|
|
70
|
+
assert.equal(callArgs.timeout, 15000)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should handle errors', async () => {
|
|
74
|
+
const error = new Error('API error')
|
|
75
|
+
mockAxios.request.rejects(error)
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await uut.getBCHUSD()
|
|
79
|
+
assert.fail('Should have thrown an error')
|
|
80
|
+
} catch (err) {
|
|
81
|
+
assert.equal(err.message, 'API error')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('#getPsffppWritePrice()', () => {
|
|
87
|
+
it('should handle errors properly', async () => {
|
|
88
|
+
// Note: Full unit testing of getPsffppWritePrice is difficult due to dynamic imports
|
|
89
|
+
// of SlpWallet and PSFFPP. Integration tests should verify the full flow.
|
|
90
|
+
// This test verifies that errors are properly handled and propagated.
|
|
91
|
+
try {
|
|
92
|
+
// This will likely fail in unit test environment without proper setup
|
|
93
|
+
// but we verify error handling works correctly
|
|
94
|
+
await uut.getPsffppWritePrice()
|
|
95
|
+
// If it succeeds, that's also acceptable
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Verify error is properly formatted
|
|
98
|
+
assert.isTrue(err instanceof Error)
|
|
99
|
+
// Verify error was logged (indirectly through wlogger)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for SlpUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
import BCHJS from '@psf/bch-js'
|
|
8
|
+
|
|
9
|
+
import SlpUseCases from '../../../src/use-cases/slp-use-cases.js'
|
|
10
|
+
|
|
11
|
+
describe('#slp-use-cases.js', () => {
|
|
12
|
+
let sandbox
|
|
13
|
+
let mockAdapters
|
|
14
|
+
let mockConfig
|
|
15
|
+
let uut
|
|
16
|
+
let mockBchjs
|
|
17
|
+
let mockWallet
|
|
18
|
+
let mockSlpTokenMedia
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
sandbox = sinon.createSandbox()
|
|
22
|
+
mockConfig = {
|
|
23
|
+
restURL: 'http://localhost:3000/v5/',
|
|
24
|
+
ipfsGateway: 'p2wdb-gateway-678.fullstack.cash'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
mockAdapters = {
|
|
28
|
+
slpIndexer: {
|
|
29
|
+
get: sandbox.stub().resolves({}),
|
|
30
|
+
post: sandbox.stub().resolves({})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create mock BCHJS
|
|
35
|
+
mockBchjs = new BCHJS()
|
|
36
|
+
mockBchjs.Electrumx = {
|
|
37
|
+
txData: sandbox.stub().resolves({
|
|
38
|
+
details: {
|
|
39
|
+
vout: [
|
|
40
|
+
{
|
|
41
|
+
scriptPubKey: {
|
|
42
|
+
hex: '6a0c48656c6c6f20576f726c6421'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
mockBchjs.Script = {
|
|
50
|
+
toASM: sandbox.stub().returns('OP_RETURN 48656c6c6f20576f726c6421')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create mock wallet
|
|
54
|
+
mockWallet = {
|
|
55
|
+
walletInfoPromise: Promise.resolve(),
|
|
56
|
+
getTransactions: sandbox.stub().resolves([]),
|
|
57
|
+
getTxData: sandbox.stub().resolves([{
|
|
58
|
+
vin: [{
|
|
59
|
+
address: 'bitcoincash:test123'
|
|
60
|
+
}]
|
|
61
|
+
}])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create mock SlpTokenMedia
|
|
65
|
+
mockSlpTokenMedia = {
|
|
66
|
+
getIcon: sandbox.stub().resolves({ tokenIcon: 'test-icon.png' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Mock the imports
|
|
70
|
+
uut = new SlpUseCases({
|
|
71
|
+
adapters: mockAdapters,
|
|
72
|
+
bchjs: mockBchjs,
|
|
73
|
+
config: mockConfig
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Replace the wallet initialization with our mocks
|
|
77
|
+
uut.wallet = mockWallet
|
|
78
|
+
uut.slpTokenMedia = mockSlpTokenMedia
|
|
79
|
+
uut.walletInitialized = true
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
sandbox.restore()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('#constructor()', () => {
|
|
87
|
+
it('should require adapters', () => {
|
|
88
|
+
assert.throws(() => {
|
|
89
|
+
// eslint-disable-next-line no-new
|
|
90
|
+
new SlpUseCases()
|
|
91
|
+
}, /Adapters instance required/)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should require slpIndexer adapter', () => {
|
|
95
|
+
assert.throws(() => {
|
|
96
|
+
// eslint-disable-next-line no-new
|
|
97
|
+
new SlpUseCases({ adapters: {} })
|
|
98
|
+
}, /SLP Indexer adapter required/)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('#getStatus()', () => {
|
|
103
|
+
it('should call slpIndexer adapter get method', async () => {
|
|
104
|
+
mockAdapters.slpIndexer.get.resolves({ status: 'ok' })
|
|
105
|
+
|
|
106
|
+
const result = await uut.getStatus()
|
|
107
|
+
|
|
108
|
+
assert.isTrue(mockAdapters.slpIndexer.get.calledOnceWith('slp/status/'))
|
|
109
|
+
assert.deepEqual(result, { status: 'ok' })
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('#getAddress()', () => {
|
|
114
|
+
it('should call slpIndexer adapter post method', async () => {
|
|
115
|
+
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
116
|
+
mockAdapters.slpIndexer.post.resolves({ balance: 1000 })
|
|
117
|
+
|
|
118
|
+
const result = await uut.getAddress({ address })
|
|
119
|
+
|
|
120
|
+
assert.isTrue(
|
|
121
|
+
mockAdapters.slpIndexer.post.calledOnceWith('slp/address/', { address })
|
|
122
|
+
)
|
|
123
|
+
assert.deepEqual(result, { balance: 1000 })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('#getTxid()', () => {
|
|
128
|
+
it('should call slpIndexer adapter post method', async () => {
|
|
129
|
+
const txid = 'a'.repeat(64)
|
|
130
|
+
mockAdapters.slpIndexer.post.resolves({ txid })
|
|
131
|
+
|
|
132
|
+
const result = await uut.getTxid({ txid })
|
|
133
|
+
|
|
134
|
+
assert.isTrue(
|
|
135
|
+
mockAdapters.slpIndexer.post.calledOnceWith('slp/tx/', { txid })
|
|
136
|
+
)
|
|
137
|
+
assert.deepEqual(result, { txid })
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('#getTokenStats()', () => {
|
|
142
|
+
it('should call slpIndexer adapter post method', async () => {
|
|
143
|
+
const tokenId = 'a'.repeat(64)
|
|
144
|
+
const withTxHistory = false
|
|
145
|
+
mockAdapters.slpIndexer.post.resolves({ tokenData: {} })
|
|
146
|
+
|
|
147
|
+
const result = await uut.getTokenStats({ tokenId, withTxHistory })
|
|
148
|
+
|
|
149
|
+
assert.isTrue(
|
|
150
|
+
mockAdapters.slpIndexer.post.calledOnceWith('slp/token/', { tokenId, withTxHistory })
|
|
151
|
+
)
|
|
152
|
+
assert.deepEqual(result, { tokenData: {} })
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('#getTokenData()', () => {
|
|
157
|
+
it('should get token data with mutable and immutable data', async () => {
|
|
158
|
+
const tokenId = 'a'.repeat(64)
|
|
159
|
+
const tokenStats = {
|
|
160
|
+
tokenData: {
|
|
161
|
+
documentUri: 'ipfs://test123',
|
|
162
|
+
documentHash: 'b'.repeat(64)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
mockAdapters.slpIndexer.post.resolves(tokenStats)
|
|
166
|
+
|
|
167
|
+
// Mock decodeOpReturn to return JSON with mda
|
|
168
|
+
sandbox.stub(uut, 'decodeOpReturn').resolves(JSON.stringify({ mda: 'bitcoincash:test123' }))
|
|
169
|
+
sandbox.stub(uut, 'getMutableCid').resolves('mutable-cid-123')
|
|
170
|
+
|
|
171
|
+
const result = await uut.getTokenData({ tokenId })
|
|
172
|
+
|
|
173
|
+
assert.property(result, 'genesisData')
|
|
174
|
+
assert.property(result, 'immutableData')
|
|
175
|
+
assert.property(result, 'mutableData')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle errors when getting mutable data', async () => {
|
|
179
|
+
const tokenId = 'a'.repeat(64)
|
|
180
|
+
const tokenStats = {
|
|
181
|
+
tokenData: {
|
|
182
|
+
documentUri: 'ipfs://test123',
|
|
183
|
+
documentHash: 'b'.repeat(64)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
mockAdapters.slpIndexer.post.resolves(tokenStats)
|
|
187
|
+
|
|
188
|
+
sandbox.stub(uut, 'getMutableCid').rejects(new Error('Test error'))
|
|
189
|
+
|
|
190
|
+
const result = await uut.getTokenData({ tokenId })
|
|
191
|
+
|
|
192
|
+
assert.property(result, 'genesisData')
|
|
193
|
+
assert.property(result, 'immutableData')
|
|
194
|
+
assert.equal(result.mutableData, '')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('#decodeOpReturn()', () => {
|
|
199
|
+
it('should decode OP_RETURN data from transaction', async () => {
|
|
200
|
+
const txid = 'a'.repeat(64)
|
|
201
|
+
const mockTxData = {
|
|
202
|
+
details: {
|
|
203
|
+
vout: [
|
|
204
|
+
{
|
|
205
|
+
scriptPubKey: {
|
|
206
|
+
hex: '6a0c48656c6c6f20576f726c6421'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
mockBchjs.Electrumx.txData.resolves(mockTxData)
|
|
213
|
+
mockBchjs.Script.toASM.returns('OP_RETURN 48656c6c6f20576f726c6421')
|
|
214
|
+
|
|
215
|
+
const result = await uut.decodeOpReturn({ txid })
|
|
216
|
+
|
|
217
|
+
assert.isTrue(mockBchjs.Electrumx.txData.calledOnceWith(txid))
|
|
218
|
+
assert.isString(result)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should throw error if txid is not a string', async () => {
|
|
222
|
+
try {
|
|
223
|
+
await uut.decodeOpReturn({ txid: null })
|
|
224
|
+
assert.fail('Should have thrown an error')
|
|
225
|
+
} catch (err) {
|
|
226
|
+
assert.include(err.message, 'txid must be a string')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('#getCIDData()', () => {
|
|
232
|
+
it('should fetch IPFS data from CID', async () => {
|
|
233
|
+
const cid = 'ipfs://test123'
|
|
234
|
+
const mockData = { name: 'Test Token' }
|
|
235
|
+
|
|
236
|
+
// Mock axios
|
|
237
|
+
const axios = await import('axios')
|
|
238
|
+
sandbox.stub(axios.default, 'get').resolves({ data: mockData })
|
|
239
|
+
|
|
240
|
+
const result = await uut.getCIDData({ cid })
|
|
241
|
+
|
|
242
|
+
assert.deepEqual(result, mockData)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should throw error if cid is not a string', async () => {
|
|
246
|
+
try {
|
|
247
|
+
await uut.getCIDData({ cid: null })
|
|
248
|
+
assert.fail('Should have thrown an error')
|
|
249
|
+
} catch (err) {
|
|
250
|
+
assert.include(err.message, 'cid must be a string')
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('#getMutableCid()', () => {
|
|
256
|
+
it('should extract mutable CID from token stats', async () => {
|
|
257
|
+
const tokenStats = {
|
|
258
|
+
documentHash: 'a'.repeat(64)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const mockOpReturn = JSON.stringify({ mda: 'bitcoincash:test123' })
|
|
262
|
+
sandbox.stub(uut, 'decodeOpReturn').resolves(mockOpReturn)
|
|
263
|
+
|
|
264
|
+
mockWallet.getTransactions.resolves([
|
|
265
|
+
{
|
|
266
|
+
tx_hash: 'b'.repeat(64),
|
|
267
|
+
height: 100
|
|
268
|
+
}
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
mockWallet.getTxData.resolves([{
|
|
272
|
+
vin: [{
|
|
273
|
+
address: 'bitcoincash:test123'
|
|
274
|
+
}]
|
|
275
|
+
}])
|
|
276
|
+
|
|
277
|
+
// Mock decodeOpReturn for the transaction
|
|
278
|
+
uut.decodeOpReturn.onSecondCall().resolves(JSON.stringify({ cid: 'ipfs://mutable-cid-123', ts: 1234567890 }))
|
|
279
|
+
|
|
280
|
+
const result = await uut.getMutableCid({ tokenStats })
|
|
281
|
+
|
|
282
|
+
assert.isString(result)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should return false if no documentHash in tokenStats', async () => {
|
|
286
|
+
const tokenStats = {}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await uut.getMutableCid({ tokenStats })
|
|
290
|
+
assert.fail('Should have thrown an error')
|
|
291
|
+
} catch (err) {
|
|
292
|
+
assert.include(err.message, 'No documentHash property found')
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
})
|