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.
Files changed (56) hide show
  1. package/.env-local +9 -0
  2. package/README.md +22 -0
  3. package/bin/server.js +24 -1
  4. package/package.json +6 -2
  5. package/src/adapters/fulcrum-api.js +124 -0
  6. package/src/adapters/full-node-rpc.js +2 -6
  7. package/src/adapters/index.js +4 -0
  8. package/src/adapters/slp-indexer-api.js +124 -0
  9. package/src/config/env/common.js +45 -24
  10. package/src/config/x402.js +43 -0
  11. package/src/controllers/index.js +3 -1
  12. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  13. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  14. package/src/controllers/rest-api/full-node/blockchain/controller.js +26 -26
  15. package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +5 -1
  16. package/src/controllers/rest-api/full-node/control/controller.js +68 -0
  17. package/src/controllers/rest-api/full-node/control/router.js +51 -0
  18. package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
  19. package/src/controllers/rest-api/full-node/dsproof/router.js +51 -0
  20. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  21. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  22. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  23. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  24. package/src/controllers/rest-api/index.js +33 -2
  25. package/src/controllers/rest-api/slp/controller.js +218 -0
  26. package/src/controllers/rest-api/slp/router.js +55 -0
  27. package/src/controllers/timer-controller.js +1 -1
  28. package/src/use-cases/fulcrum-use-cases.js +155 -0
  29. package/src/use-cases/full-node-control-use-cases.js +24 -0
  30. package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
  31. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  32. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  33. package/src/use-cases/index.js +12 -0
  34. package/src/use-cases/slp-use-cases.js +321 -0
  35. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  36. package/test/unit/controllers/control-controller-unit.js +88 -0
  37. package/test/unit/controllers/dsproof-controller-unit.js +117 -0
  38. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  39. package/test/unit/controllers/mining-controller-unit.js +139 -0
  40. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  41. package/test/unit/controllers/rest-api-index-unit.js +76 -6
  42. package/test/unit/controllers/slp-controller-unit.js +312 -0
  43. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  44. package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
  45. package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
  46. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  47. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  48. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  49. package/src/entities/event.js +0 -71
  50. package/test/integration/api/event-integration.js +0 -250
  51. package/test/integration/api/req-integration.js +0 -173
  52. package/test/integration/api/subscription-integration.js +0 -198
  53. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  54. package/test/integration/use-cases/publish-event-integration.js +0 -104
  55. package/test/integration/use-cases/query-events-integration.js +0 -95
  56. 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
+ })