@bsv/message-box-client 2.0.5 → 2.0.7

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 (32) hide show
  1. package/dist/cjs/package.json +2 -2
  2. package/dist/cjs/src/PeerPayClient.js +463 -62
  3. package/dist/cjs/src/PeerPayClient.js.map +1 -1
  4. package/dist/cjs/src/__tests/PeerPayClientRequestIntegration.test.js +317 -0
  5. package/dist/cjs/src/__tests/PeerPayClientRequestIntegration.test.js.map +1 -0
  6. package/dist/cjs/src/__tests/PeerPayClientUnit.test.js +505 -1
  7. package/dist/cjs/src/__tests/PeerPayClientUnit.test.js.map +1 -1
  8. package/dist/cjs/src/types.js +5 -0
  9. package/dist/cjs/src/types.js.map +1 -1
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/PeerPayClient.js +459 -61
  12. package/dist/esm/src/PeerPayClient.js.map +1 -1
  13. package/dist/esm/src/__tests/PeerPayClientRequestIntegration.test.js +312 -0
  14. package/dist/esm/src/__tests/PeerPayClientRequestIntegration.test.js.map +1 -0
  15. package/dist/esm/src/__tests/PeerPayClientUnit.test.js +505 -1
  16. package/dist/esm/src/__tests/PeerPayClientUnit.test.js.map +1 -1
  17. package/dist/esm/src/types.js +4 -1
  18. package/dist/esm/src/types.js.map +1 -1
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/PeerPayClient.d.ts +160 -0
  21. package/dist/types/src/PeerPayClient.d.ts.map +1 -1
  22. package/dist/types/src/__tests/PeerPayClientRequestIntegration.test.d.ts +10 -0
  23. package/dist/types/src/__tests/PeerPayClientRequestIntegration.test.d.ts.map +1 -0
  24. package/dist/types/src/types.d.ts +88 -0
  25. package/dist/types/src/types.d.ts.map +1 -1
  26. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  27. package/dist/umd/bundle.js +1 -1
  28. package/package.json +2 -2
  29. package/src/PeerPayClient.ts +526 -69
  30. package/src/__tests/PeerPayClientRequestIntegration.test.ts +364 -0
  31. package/src/__tests/PeerPayClientUnit.test.ts +594 -1
  32. package/src/types.ts +95 -0
@@ -0,0 +1,364 @@
1
+ /* eslint-env jest */
2
+ /**
3
+ * Integration tests for the PeerPayClient payment request flow.
4
+ *
5
+ * Uses a MockMessageBus to simulate the MessageBox server in memory,
6
+ * wiring the PeerPayClient methods (sendMessage, listMessages, acknowledgeMessage,
7
+ * getIdentityKey) to the mock bus so that full round-trip flows can be tested
8
+ * without hitting a real server.
9
+ */
10
+
11
+ import { PeerPayClient, PAYMENT_REQUESTS_MESSAGEBOX, PAYMENT_REQUEST_RESPONSES_MESSAGEBOX, STANDARD_PAYMENT_MESSAGEBOX } from '../PeerPayClient.js'
12
+ import { PeerMessage } from '../types.js'
13
+ import { PrivateKey, CreateHmacResult, WalletClient } from '@bsv/sdk'
14
+ import { jest } from '@jest/globals'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Mock @bsv/sdk the same way as PeerPayClientUnit.test.ts
18
+ // ---------------------------------------------------------------------------
19
+ jest.mock('@bsv/sdk', () => {
20
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
21
+ const actualSDK = jest.requireActual('@bsv/sdk') as any
22
+ return {
23
+ ...actualSDK,
24
+ WalletClient: jest.fn().mockImplementation(() => ({
25
+ getPublicKey: jest.fn(),
26
+ createAction: jest.fn(),
27
+ internalizeAction: jest.fn(),
28
+ createHmac: jest.fn<() => Promise<CreateHmacResult>>().mockResolvedValue({
29
+ hmac: [1, 2, 3, 4, 5]
30
+ }),
31
+ verifyHmac: jest.fn<() => Promise<{ valid: true }>>().mockResolvedValue({ valid: true as const })
32
+ }))
33
+ }
34
+ })
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // MockMessageBus
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Stores messages per (recipient, messageBox) pair in memory.
41
+ * Provides send / list / ack helpers that the wired client delegates to.
42
+ */
43
+ class MockMessageBus {
44
+ private counter = 0
45
+ // key: `${recipient}::${messageBox}`
46
+ private readonly store = new Map<string, PeerMessage[]>()
47
+
48
+ private key (recipient: string, messageBox: string): string {
49
+ return `${recipient}::${messageBox}`
50
+ }
51
+
52
+ send (params: { recipient: string, messageBox: string, body: string, sender: string }): { status: string, messageId: string } {
53
+ const { recipient, messageBox, body, sender } = params
54
+ const k = this.key(recipient, messageBox)
55
+ const messageId = `msg-${++this.counter}`
56
+ const now = new Date().toISOString()
57
+ const msg: PeerMessage = { messageId, sender, body, created_at: now, updated_at: now }
58
+ const existing = this.store.get(k)
59
+ if (existing != null) {
60
+ existing.push(msg)
61
+ } else {
62
+ this.store.set(k, [msg])
63
+ }
64
+ return { status: 'success', messageId }
65
+ }
66
+
67
+ list (recipient: string, messageBox: string): PeerMessage[] {
68
+ return this.store.get(this.key(recipient, messageBox)) ?? []
69
+ }
70
+
71
+ ack (recipient: string, messageBox: string, messageIds: string[]): void {
72
+ const k = this.key(recipient, messageBox)
73
+ const msgs = this.store.get(k)
74
+ if (msgs == null) return
75
+ const remaining = msgs.filter(m => !messageIds.includes(m.messageId))
76
+ this.store.set(k, remaining)
77
+ }
78
+
79
+ /** Convenience: clear everything */
80
+ reset (): void {
81
+ this.store.clear()
82
+ this.counter = 0
83
+ }
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // createWiredClient
88
+ // ---------------------------------------------------------------------------
89
+ /**
90
+ * Creates a PeerPayClient whose sendMessage, listMessages, acknowledgeMessage,
91
+ * and getIdentityKey are wired to the provided MockMessageBus instance.
92
+ *
93
+ * The identityKey is the "address" used as recipient/sender for messages
94
+ * sent through the bus.
95
+ *
96
+ * sendPayment is mocked to be a no-op so tests that call fulfillPaymentRequest
97
+ * don't need a real wallet.
98
+ */
99
+ function createWiredClient (params: {
100
+ bus: MockMessageBus
101
+ identityKey: string
102
+ walletClient: jest.Mocked<WalletClient>
103
+ }): PeerPayClient {
104
+ const { bus, identityKey, walletClient } = params
105
+
106
+ const client = new PeerPayClient({
107
+ messageBoxHost: 'https://messagebox.babbage.systems',
108
+ walletClient
109
+ })
110
+
111
+ // Wire getIdentityKey
112
+ jest.spyOn(client, 'getIdentityKey').mockResolvedValue(identityKey)
113
+
114
+ // Wire sendMessage: route to the bus using the current client identity as sender
115
+ jest.spyOn(client, 'sendMessage').mockImplementation(async (sendParams: any) => {
116
+ const body = typeof sendParams.body === 'string' ? sendParams.body : JSON.stringify(sendParams.body)
117
+ return bus.send({
118
+ recipient: sendParams.recipient,
119
+ messageBox: sendParams.messageBox,
120
+ body,
121
+ sender: identityKey
122
+ })
123
+ })
124
+
125
+ // Wire listMessages: retrieve from bus for this identity as recipient
126
+ jest.spyOn(client, 'listMessages').mockImplementation(async (listParams: any) => {
127
+ return bus.list(identityKey, listParams.messageBox)
128
+ })
129
+
130
+ // Wire acknowledgeMessage: remove messages from bus
131
+ // We need to know which messageBox to remove from — scan all boxes for this recipient
132
+ jest.spyOn(client, 'acknowledgeMessage').mockImplementation(async (ackParams: any) => {
133
+ const { messageIds } = ackParams
134
+ // Attempt to ack from all known messageBoxes
135
+ for (const mb of [PAYMENT_REQUESTS_MESSAGEBOX, PAYMENT_REQUEST_RESPONSES_MESSAGEBOX, STANDARD_PAYMENT_MESSAGEBOX]) {
136
+ bus.ack(identityKey, mb, messageIds)
137
+ }
138
+ return 'acknowledged'
139
+ })
140
+
141
+ // Mock sendPayment to be a no-op (avoids needing real wallet for tx creation)
142
+ jest.spyOn(client, 'sendPayment').mockResolvedValue(undefined)
143
+
144
+ return client
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Tests
149
+ // ---------------------------------------------------------------------------
150
+ describe('PeerPayClient — Integration: payment request flow', () => {
151
+ let bus: MockMessageBus
152
+ let mockWalletRequester: jest.Mocked<WalletClient>
153
+ let mockWalletPayer: jest.Mocked<WalletClient>
154
+ const REQUESTER_KEY = PrivateKey.fromRandom().toPublicKey().toString()
155
+ const PAYER_KEY = PrivateKey.fromRandom().toPublicKey().toString()
156
+
157
+ beforeEach(() => {
158
+ jest.clearAllMocks()
159
+ bus = new MockMessageBus()
160
+
161
+ mockWalletRequester = new WalletClient() as jest.Mocked<WalletClient>
162
+ mockWalletRequester.getPublicKey.mockResolvedValue({ publicKey: REQUESTER_KEY })
163
+
164
+ mockWalletPayer = new WalletClient() as jest.Mocked<WalletClient>
165
+ mockWalletPayer.getPublicKey.mockResolvedValue({ publicKey: PAYER_KEY })
166
+ })
167
+
168
+ // -------------------------------------------------------------------------
169
+ // Test 1: Full round-trip — request → fulfill → requester sees paid response
170
+ // -------------------------------------------------------------------------
171
+ it('Test 1: full round-trip: request → fulfill → requester sees paid response', async () => {
172
+ const requester = createWiredClient({ bus, identityKey: REQUESTER_KEY, walletClient: mockWalletRequester })
173
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
174
+
175
+ // Requester sends a payment request to payer
176
+ const { requestId } = await requester.requestPayment({
177
+ recipient: PAYER_KEY,
178
+ amount: 5000,
179
+ description: 'Invoice #1',
180
+ expiresAt: Date.now() + 60000
181
+ })
182
+
183
+ expect(requestId).toBeTruthy()
184
+
185
+ // Payer lists incoming requests
186
+ const incoming = await payer.listIncomingPaymentRequests()
187
+ expect(incoming).toHaveLength(1)
188
+ expect(incoming[0].requestId).toBe(requestId)
189
+ expect(incoming[0].amount).toBe(5000)
190
+
191
+ // Payer fulfills the request
192
+ await payer.fulfillPaymentRequest({ request: incoming[0] })
193
+
194
+ // Requester checks for responses
195
+ const responses = await requester.listPaymentRequestResponses()
196
+ expect(responses).toHaveLength(1)
197
+ expect(responses[0]).toMatchObject({ requestId, status: 'paid', amountPaid: 5000 })
198
+ })
199
+
200
+ // -------------------------------------------------------------------------
201
+ // Test 2: Full round-trip — request → decline → requester sees declined
202
+ // -------------------------------------------------------------------------
203
+ it('Test 2: full round-trip: request → decline → requester sees declined response', async () => {
204
+ const requester = createWiredClient({ bus, identityKey: REQUESTER_KEY, walletClient: mockWalletRequester })
205
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
206
+
207
+ const { requestId } = await requester.requestPayment({
208
+ recipient: PAYER_KEY,
209
+ amount: 3000,
210
+ description: 'Invoice #2',
211
+ expiresAt: Date.now() + 60000
212
+ })
213
+
214
+ const incoming = await payer.listIncomingPaymentRequests()
215
+ expect(incoming).toHaveLength(1)
216
+
217
+ await payer.declinePaymentRequest({ request: incoming[0], note: 'No funds' })
218
+
219
+ const responses = await requester.listPaymentRequestResponses()
220
+ expect(responses).toHaveLength(1)
221
+ expect(responses[0]).toMatchObject({ requestId, status: 'declined', note: 'No funds' })
222
+ })
223
+
224
+ // -------------------------------------------------------------------------
225
+ // Test 3: Request → cancel → payer no longer sees the request
226
+ // -------------------------------------------------------------------------
227
+ it('Test 3: request → cancel → payer no longer sees the request', async () => {
228
+ const requester = createWiredClient({ bus, identityKey: REQUESTER_KEY, walletClient: mockWalletRequester })
229
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
230
+
231
+ const { requestId, requestProof } = await requester.requestPayment({
232
+ recipient: PAYER_KEY,
233
+ amount: 2000,
234
+ description: 'Cancellable request',
235
+ expiresAt: Date.now() + 60000
236
+ })
237
+
238
+ // Confirm payer can see it before cancellation
239
+ const beforeCancel = await payer.listIncomingPaymentRequests()
240
+ expect(beforeCancel).toHaveLength(1)
241
+
242
+ // Requester cancels the request
243
+ await requester.cancelPaymentRequest({ recipient: PAYER_KEY, requestId, requestProof })
244
+
245
+ // Payer should now see zero active requests (the cancel message causes filtering)
246
+ const afterCancel = await payer.listIncomingPaymentRequests()
247
+ expect(afterCancel).toHaveLength(0)
248
+ })
249
+
250
+ // -------------------------------------------------------------------------
251
+ // Test 4: Expired request is filtered out automatically
252
+ // -------------------------------------------------------------------------
253
+ it('Test 4: expired request is filtered out automatically', async () => {
254
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
255
+
256
+ // Inject an already-expired request directly onto the bus (expiresAt in the past)
257
+ const expiredBody = JSON.stringify({
258
+ requestId: 'expired-req-1',
259
+ amount: 4000,
260
+ description: 'Already expired',
261
+ expiresAt: Date.now() - 10000, // in the past
262
+ senderIdentityKey: REQUESTER_KEY,
263
+ requestProof: 'mock-proof'
264
+ })
265
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: expiredBody, sender: REQUESTER_KEY })
266
+
267
+ // Also inject a valid request so the filter has something to keep
268
+ const validBody = JSON.stringify({
269
+ requestId: 'valid-req-1',
270
+ amount: 4000,
271
+ description: 'Still valid',
272
+ expiresAt: Date.now() + 60000,
273
+ senderIdentityKey: REQUESTER_KEY,
274
+ requestProof: 'mock-proof'
275
+ })
276
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: validBody, sender: REQUESTER_KEY })
277
+
278
+ const requests = await payer.listIncomingPaymentRequests()
279
+ expect(requests).toHaveLength(1)
280
+ expect(requests[0].requestId).toBe('valid-req-1')
281
+ })
282
+
283
+ // -------------------------------------------------------------------------
284
+ // Test 5: Requests below minAmount are auto-acknowledged and excluded
285
+ // -------------------------------------------------------------------------
286
+ it('Test 5: requests below minAmount are auto-acknowledged and excluded', async () => {
287
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
288
+
289
+ // Inject a request below the minAmount threshold
290
+ const smallBody = JSON.stringify({
291
+ requestId: 'req-small',
292
+ amount: 100, // below minAmount of 1000
293
+ description: 'Too small',
294
+ expiresAt: Date.now() + 60000,
295
+ senderIdentityKey: REQUESTER_KEY,
296
+ requestProof: 'mock-proof'
297
+ })
298
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: smallBody, sender: REQUESTER_KEY })
299
+
300
+ // Inject a valid request that passes the filter
301
+ const okBody = JSON.stringify({
302
+ requestId: 'req-ok',
303
+ amount: 5000,
304
+ description: 'Just right',
305
+ expiresAt: Date.now() + 60000,
306
+ senderIdentityKey: REQUESTER_KEY,
307
+ requestProof: 'mock-proof'
308
+ })
309
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: okBody, sender: REQUESTER_KEY })
310
+
311
+ const requests = await payer.listIncomingPaymentRequests(undefined, { minAmount: 1000, maxAmount: 10000 })
312
+ expect(requests).toHaveLength(1)
313
+ expect(requests[0].requestId).toBe('req-ok')
314
+
315
+ // The small request should have been auto-acknowledged (removed from bus)
316
+ const remaining = bus.list(PAYER_KEY, PAYMENT_REQUESTS_MESSAGEBOX)
317
+ const remainingIds = remaining.map(m => {
318
+ const b = JSON.parse(m.body as string)
319
+ return b.requestId
320
+ })
321
+ expect(remainingIds).not.toContain('req-small')
322
+ })
323
+
324
+ // -------------------------------------------------------------------------
325
+ // Test 6: Requests above maxAmount are auto-acknowledged and excluded
326
+ // -------------------------------------------------------------------------
327
+ it('Test 6: requests above maxAmount are auto-acknowledged and excluded', async () => {
328
+ const payer = createWiredClient({ bus, identityKey: PAYER_KEY, walletClient: mockWalletPayer })
329
+
330
+ // Inject a request above the maxAmount threshold
331
+ const largeBody = JSON.stringify({
332
+ requestId: 'req-large',
333
+ amount: 99999, // above maxAmount of 10000
334
+ description: 'Too large',
335
+ expiresAt: Date.now() + 60000,
336
+ senderIdentityKey: REQUESTER_KEY,
337
+ requestProof: 'mock-proof'
338
+ })
339
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: largeBody, sender: REQUESTER_KEY })
340
+
341
+ // Inject a valid request that passes the filter
342
+ const okBody = JSON.stringify({
343
+ requestId: 'req-ok-2',
344
+ amount: 5000,
345
+ description: 'Just right',
346
+ expiresAt: Date.now() + 60000,
347
+ senderIdentityKey: REQUESTER_KEY,
348
+ requestProof: 'mock-proof'
349
+ })
350
+ bus.send({ recipient: PAYER_KEY, messageBox: PAYMENT_REQUESTS_MESSAGEBOX, body: okBody, sender: REQUESTER_KEY })
351
+
352
+ const requests = await payer.listIncomingPaymentRequests(undefined, { minAmount: 1000, maxAmount: 10000 })
353
+ expect(requests).toHaveLength(1)
354
+ expect(requests[0].requestId).toBe('req-ok-2')
355
+
356
+ // The large request should have been auto-acknowledged (removed from bus)
357
+ const remaining = bus.list(PAYER_KEY, PAYMENT_REQUESTS_MESSAGEBOX)
358
+ const remainingIds = remaining.map(m => {
359
+ const b = JSON.parse(m.body as string)
360
+ return b.requestId
361
+ })
362
+ expect(remainingIds).not.toContain('req-large')
363
+ })
364
+ })