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,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
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for MiningUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
|
|
7
|
+
import MiningUseCases from '../../../src/use-cases/full-node-mining-use-cases.js'
|
|
8
|
+
|
|
9
|
+
describe('#full-node-mining-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 MiningUseCases({ adapters: mockAdapters })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('#constructor()', () => {
|
|
24
|
+
it('should require adapters', () => {
|
|
25
|
+
assert.throws(() => {
|
|
26
|
+
// eslint-disable-next-line no-new
|
|
27
|
+
new MiningUseCases()
|
|
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 MiningUseCases({ adapters: {} })
|
|
35
|
+
}, /Full node adapter required/)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('#getMiningInfo()', () => {
|
|
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 { blocks: 100, difficulty: 1.5 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await uut.getMiningInfo()
|
|
48
|
+
|
|
49
|
+
assert.equal(capturedMethod, 'getmininginfo')
|
|
50
|
+
assert.deepEqual(result, { blocks: 100, difficulty: 1.5 })
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('#getNetworkHashPS()', () => {
|
|
55
|
+
it('should call full node adapter with correct method and default params', async () => {
|
|
56
|
+
let capturedMethod = ''
|
|
57
|
+
let capturedParams = []
|
|
58
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
59
|
+
capturedMethod = method
|
|
60
|
+
capturedParams = params
|
|
61
|
+
return 1234567890
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await uut.getNetworkHashPS({ nblocks: 120, height: -1 })
|
|
65
|
+
|
|
66
|
+
assert.equal(capturedMethod, 'getnetworkhashps')
|
|
67
|
+
assert.deepEqual(capturedParams, [120, -1])
|
|
68
|
+
assert.equal(result, 1234567890)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should call full node adapter with custom params', async () => {
|
|
72
|
+
let capturedParams = []
|
|
73
|
+
mockAdapters.fullNode.call = async (method, params) => {
|
|
74
|
+
capturedParams = params
|
|
75
|
+
return 9876543210
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await uut.getNetworkHashPS({ nblocks: 240, height: 1000 })
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(capturedParams, [240, 1000])
|
|
81
|
+
assert.equal(result, 9876543210)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -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,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
|
+
})
|