psf-bch-api 1.3.0 → 7.2.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 (30) hide show
  1. package/.env-local +26 -5
  2. package/bin/server.js +60 -9
  3. package/package.json +5 -4
  4. package/production/docker/.env-local +34 -0
  5. package/production/docker/Dockerfile +8 -25
  6. package/production/docker/docker-compose.yml +4 -3
  7. package/production/docker/temp.js +7 -0
  8. package/src/config/env/common.js +10 -3
  9. package/src/config/x402.js +7 -0
  10. package/src/controllers/rest-api/encryption/controller.js +100 -0
  11. package/src/controllers/rest-api/encryption/router.js +51 -0
  12. package/src/controllers/rest-api/fulcrum/controller.js +2 -1
  13. package/src/controllers/rest-api/index.js +8 -0
  14. package/src/controllers/rest-api/price/controller.js +96 -0
  15. package/src/controllers/rest-api/price/router.js +52 -0
  16. package/src/controllers/rest-api/slp/controller.js +3 -1
  17. package/src/middleware/basic-auth.js +61 -0
  18. package/src/use-cases/encryption-use-cases.js +120 -0
  19. package/src/use-cases/fulcrum-use-cases.js +10 -2
  20. package/src/use-cases/index.js +9 -0
  21. package/src/use-cases/price-use-cases.js +83 -0
  22. package/src/use-cases/slp-use-cases.js +5 -1
  23. package/test/unit/controllers/encryption-controller-unit.js +203 -0
  24. package/test/unit/controllers/price-controller-unit.js +116 -0
  25. package/test/unit/controllers/rest-api-index-unit.js +15 -0
  26. package/test/unit/use-cases/encryption-use-cases-unit.js +247 -0
  27. package/test/unit/use-cases/fulcrum-use-cases-unit.js +1 -1
  28. package/test/unit/use-cases/price-use-cases-unit.js +103 -0
  29. package/test/unit/use-cases/slp-use-cases-unit.js +1 -1
  30. /package/{index.js → psf-bch-api.js} +0 -0
@@ -9,7 +9,9 @@ import RESTControllers from '../../../src/controllers/rest-api/index.js'
9
9
  import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/router.js'
10
10
  import ControlRouter from '../../../src/controllers/rest-api/full-node/control/router.js'
11
11
  import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/router.js'
12
+ import EncryptionRouter from '../../../src/controllers/rest-api/encryption/router.js'
12
13
  import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/router.js'
14
+ import PriceRouter from '../../../src/controllers/rest-api/price/router.js'
13
15
  import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/router.js'
14
16
  import FulcrumRouter from '../../../src/controllers/rest-api/fulcrum/router.js'
15
17
  import SlpRouter from '../../../src/controllers/rest-api/slp/router.js'
@@ -75,6 +77,10 @@ describe('#controllers/rest-api/index.js', () => {
75
77
  getMiningInfo: () => {},
76
78
  getNetworkHashPS: () => {}
77
79
  },
80
+ price: {
81
+ getBCHUSD: () => {},
82
+ getPsffppWritePrice: () => {}
83
+ },
78
84
  rawtransactions: {
79
85
  decodeRawTransaction: () => {},
80
86
  decodeRawTransactions: () => {},
@@ -95,6 +101,9 @@ describe('#controllers/rest-api/index.js', () => {
95
101
  getMutableCid: () => {},
96
102
  decodeOpReturn: () => {},
97
103
  getCIDData: () => {}
104
+ },
105
+ encryption: {
106
+ getPublicKey: () => {}
98
107
  }
99
108
  }
100
109
  })
@@ -124,8 +133,10 @@ describe('#controllers/rest-api/index.js', () => {
124
133
  const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
125
134
  const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
126
135
  const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
136
+ const encryptionAttachStub = sandbox.stub(EncryptionRouter.prototype, 'attach')
127
137
  const fulcrumAttachStub = sandbox.stub(FulcrumRouter.prototype, 'attach')
128
138
  const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach')
139
+ const priceAttachStub = sandbox.stub(PriceRouter.prototype, 'attach')
129
140
  const rawtransactionsAttachStub = sandbox.stub(RawTransactionsRouter.prototype, 'attach')
130
141
  const slpAttachStub = sandbox.stub(SlpRouter.prototype, 'attach')
131
142
  const restControllers = new RESTControllers({
@@ -142,10 +153,14 @@ describe('#controllers/rest-api/index.js', () => {
142
153
  assert.equal(controlAttachStub.getCall(0).args[0], app)
143
154
  assert.isTrue(dsproofAttachStub.calledOnce)
144
155
  assert.equal(dsproofAttachStub.getCall(0).args[0], app)
156
+ assert.isTrue(encryptionAttachStub.calledOnce)
157
+ assert.equal(encryptionAttachStub.getCall(0).args[0], app)
145
158
  assert.isTrue(fulcrumAttachStub.calledOnce)
146
159
  assert.equal(fulcrumAttachStub.getCall(0).args[0], app)
147
160
  assert.isTrue(miningAttachStub.calledOnce)
148
161
  assert.equal(miningAttachStub.getCall(0).args[0], app)
162
+ assert.isTrue(priceAttachStub.calledOnce)
163
+ assert.equal(priceAttachStub.getCall(0).args[0], app)
149
164
  assert.isTrue(rawtransactionsAttachStub.calledOnce)
150
165
  assert.equal(rawtransactionsAttachStub.getCall(0).args[0], app)
151
166
  assert.isTrue(slpAttachStub.calledOnce)
@@ -0,0 +1,247 @@
1
+ /*
2
+ Unit tests for EncryptionUseCases.
3
+ */
4
+
5
+ import { assert } from 'chai'
6
+ import sinon from 'sinon'
7
+
8
+ import EncryptionUseCases from '../../../src/use-cases/encryption-use-cases.js'
9
+
10
+ describe('#encryption-use-cases.js', () => {
11
+ let sandbox
12
+ let mockAdapters
13
+ let mockUseCases
14
+ let mockBchjs
15
+ let uut
16
+
17
+ beforeEach(() => {
18
+ sandbox = sinon.createSandbox()
19
+ mockAdapters = {}
20
+
21
+ // Mock bchjs
22
+ mockBchjs = {
23
+ Address: {
24
+ toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
25
+ },
26
+ ECPair: {
27
+ fromPublicKey: sandbox.stub().returns({}),
28
+ toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
29
+ }
30
+ }
31
+
32
+ // Mock use cases
33
+ mockUseCases = {
34
+ fulcrum: {
35
+ getTransactions: sandbox.stub().resolves({
36
+ transactions: [
37
+ { tx_hash: 'abc123def456' }
38
+ ]
39
+ })
40
+ },
41
+ rawtransactions: {
42
+ getRawTransaction: sandbox.stub().resolves({
43
+ vin: [
44
+ {
45
+ scriptSig: {
46
+ asm: 'signature 02abc123def456789'
47
+ }
48
+ }
49
+ ]
50
+ })
51
+ }
52
+ }
53
+
54
+ uut = new EncryptionUseCases({
55
+ adapters: mockAdapters,
56
+ useCases: mockUseCases,
57
+ bchjs: mockBchjs
58
+ })
59
+ })
60
+
61
+ afterEach(() => {
62
+ sandbox.restore()
63
+ })
64
+
65
+ describe('#constructor()', () => {
66
+ it('should require adapters', () => {
67
+ assert.throws(() => {
68
+ // eslint-disable-next-line no-new
69
+ new EncryptionUseCases({ useCases: mockUseCases })
70
+ }, /Adapters instance required/)
71
+ })
72
+
73
+ it('should require useCases', () => {
74
+ assert.throws(() => {
75
+ // eslint-disable-next-line no-new
76
+ new EncryptionUseCases({ adapters: mockAdapters })
77
+ }, /UseCases instance required/)
78
+ })
79
+ })
80
+
81
+ describe('#getPublicKey()', () => {
82
+ it('should return public key when found', async () => {
83
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
84
+
85
+ assert.isTrue(result.success)
86
+ assert.equal(result.publicKey, '02abc123def456789')
87
+ assert.isTrue(mockBchjs.Address.toCashAddress.calledOnce)
88
+ assert.isTrue(mockUseCases.fulcrum.getTransactions.calledOnce)
89
+ assert.isTrue(mockUseCases.rawtransactions.getRawTransaction.calledOnce)
90
+ })
91
+
92
+ it('should return not found when public key does not match', async () => {
93
+ // Make the ECPair.toCashAddress return a different address
94
+ mockBchjs.ECPair.toCashAddress.returns('bitcoincash:qqq000000000000000000000000000000000000000')
95
+
96
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
97
+
98
+ assert.isFalse(result.success)
99
+ assert.equal(result.publicKey, 'not found')
100
+ })
101
+
102
+ it('should throw error when no transaction history', async () => {
103
+ mockUseCases.fulcrum.getTransactions.resolves({
104
+ transactions: []
105
+ })
106
+
107
+ try {
108
+ await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
109
+ assert.fail('Should have thrown an error')
110
+ } catch (err) {
111
+ assert.equal(err.message, 'No transaction history.')
112
+ }
113
+ })
114
+
115
+ it('should handle transactions without scriptSig', async () => {
116
+ mockUseCases.rawtransactions.getRawTransaction.resolves({
117
+ vin: [
118
+ { txid: 'coinbase' } // No scriptSig
119
+ ]
120
+ })
121
+
122
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
123
+
124
+ assert.isFalse(result.success)
125
+ assert.equal(result.publicKey, 'not found')
126
+ })
127
+
128
+ it('should handle invalid public key hex gracefully', async () => {
129
+ mockUseCases.rawtransactions.getRawTransaction.resolves({
130
+ vin: [
131
+ {
132
+ scriptSig: {
133
+ asm: 'signature NOT_VALID_HEX'
134
+ }
135
+ }
136
+ ]
137
+ })
138
+
139
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
140
+
141
+ assert.isFalse(result.success)
142
+ assert.equal(result.publicKey, 'not found')
143
+ })
144
+
145
+ it('should handle ECPair.fromPublicKey throwing error', async () => {
146
+ mockBchjs.ECPair.fromPublicKey.throws(new Error('Invalid public key'))
147
+
148
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
149
+
150
+ assert.isFalse(result.success)
151
+ assert.equal(result.publicKey, 'not found')
152
+ })
153
+
154
+ it('should search through multiple transactions', async () => {
155
+ // First transaction has no matching public key
156
+ mockUseCases.fulcrum.getTransactions.resolves({
157
+ transactions: [
158
+ { tx_hash: 'tx1' },
159
+ { tx_hash: 'tx2' }
160
+ ]
161
+ })
162
+
163
+ // Return different data for each tx - first tx has input with non-matching key
164
+ mockUseCases.rawtransactions.getRawTransaction
165
+ .onFirstCall().resolves({
166
+ vin: [
167
+ {
168
+ scriptSig: {
169
+ asm: 'sig 02aaa111bbb222ccc'
170
+ }
171
+ }
172
+ ]
173
+ })
174
+ .onSecondCall().resolves({
175
+ vin: [
176
+ {
177
+ scriptSig: {
178
+ asm: 'sig 02abc123def456789'
179
+ }
180
+ }
181
+ ]
182
+ })
183
+
184
+ // First tx doesn't match, second tx matches
185
+ mockBchjs.ECPair.toCashAddress
186
+ .onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
187
+ .onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
188
+
189
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
190
+
191
+ assert.isTrue(result.success)
192
+ assert.equal(result.publicKey, '02abc123def456789')
193
+ assert.equal(mockUseCases.rawtransactions.getRawTransaction.callCount, 2)
194
+ })
195
+
196
+ it('should search through multiple inputs in a transaction', async () => {
197
+ mockUseCases.rawtransactions.getRawTransaction.resolves({
198
+ vin: [
199
+ {
200
+ scriptSig: {
201
+ asm: 'sig 02aaa111bbb222ccc'
202
+ }
203
+ },
204
+ {
205
+ scriptSig: {
206
+ asm: 'sig 02abc123def456789'
207
+ }
208
+ }
209
+ ]
210
+ })
211
+
212
+ // First input doesn't match, second input matches
213
+ mockBchjs.ECPair.toCashAddress
214
+ .onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
215
+ .onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
216
+
217
+ const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
218
+
219
+ assert.isTrue(result.success)
220
+ assert.equal(result.publicKey, '02abc123def456789')
221
+ })
222
+
223
+ it('should propagate fulcrum errors', async () => {
224
+ const error = new Error('Fulcrum API error')
225
+ mockUseCases.fulcrum.getTransactions.rejects(error)
226
+
227
+ try {
228
+ await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
229
+ assert.fail('Should have thrown an error')
230
+ } catch (err) {
231
+ assert.equal(err.message, 'Fulcrum API error')
232
+ }
233
+ })
234
+
235
+ it('should propagate rawtransactions errors', async () => {
236
+ const error = new Error('RawTransactions API error')
237
+ mockUseCases.rawtransactions.getRawTransaction.rejects(error)
238
+
239
+ try {
240
+ await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
241
+ assert.fail('Should have thrown an error')
242
+ } catch (err) {
243
+ assert.equal(err.message, 'RawTransactions API error')
244
+ }
245
+ })
246
+ })
247
+ })
@@ -24,7 +24,7 @@ describe('#fulcrum-use-cases.js', () => {
24
24
  }
25
25
 
26
26
  // Create a mock BCHJS instance with stubbed sortAllTxs method
27
- const mockBchjs = new BCHJS()
27
+ const mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
28
28
  if (!mockBchjs.Electrumx) {
29
29
  mockBchjs.Electrumx = {}
30
30
  }
@@ -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
+ })
@@ -32,7 +32,7 @@ describe('#slp-use-cases.js', () => {
32
32
  }
33
33
 
34
34
  // Create mock BCHJS
35
- mockBchjs = new BCHJS()
35
+ mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
36
36
  mockBchjs.Electrumx = {
37
37
  txData: sandbox.stub().resolves({
38
38
  details: {
File without changes