@bsv/message-box-client 2.0.6 → 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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/PeerPayClient.js +405 -5
- package/dist/cjs/src/PeerPayClient.js.map +1 -1
- package/dist/cjs/src/__tests/PeerPayClientRequestIntegration.test.js +317 -0
- package/dist/cjs/src/__tests/PeerPayClientRequestIntegration.test.js.map +1 -0
- package/dist/cjs/src/__tests/PeerPayClientUnit.test.js +505 -1
- package/dist/cjs/src/__tests/PeerPayClientUnit.test.js.map +1 -1
- package/dist/cjs/src/types.js +5 -0
- package/dist/cjs/src/types.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/PeerPayClient.js +401 -5
- package/dist/esm/src/PeerPayClient.js.map +1 -1
- package/dist/esm/src/__tests/PeerPayClientRequestIntegration.test.js +312 -0
- package/dist/esm/src/__tests/PeerPayClientRequestIntegration.test.js.map +1 -0
- package/dist/esm/src/__tests/PeerPayClientUnit.test.js +505 -1
- package/dist/esm/src/__tests/PeerPayClientUnit.test.js.map +1 -1
- package/dist/esm/src/types.js +4 -1
- package/dist/esm/src/types.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/PeerPayClient.d.ts +159 -0
- package/dist/types/src/PeerPayClient.d.ts.map +1 -1
- package/dist/types/src/__tests/PeerPayClientRequestIntegration.test.d.ts +10 -0
- package/dist/types/src/__tests/PeerPayClientRequestIntegration.test.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +88 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/package.json +1 -1
- package/src/PeerPayClient.ts +460 -9
- package/src/__tests/PeerPayClientRequestIntegration.test.ts +364 -0
- package/src/__tests/PeerPayClientUnit.test.ts +594 -1
- package/src/types.ts +95 -0
|
@@ -36,7 +36,8 @@ jest.mock('@bsv/sdk', () => {
|
|
|
36
36
|
internalizeAction: jest.fn(),
|
|
37
37
|
createHmac: jest.fn<() => Promise<CreateHmacResult>>().mockResolvedValue({
|
|
38
38
|
hmac: [1, 2, 3, 4, 5]
|
|
39
|
-
})
|
|
39
|
+
}),
|
|
40
|
+
verifyHmac: jest.fn<() => Promise<{ valid: true }>>().mockResolvedValue({ valid: true as const })
|
|
40
41
|
}))
|
|
41
42
|
}
|
|
42
43
|
})
|
|
@@ -242,4 +243,596 @@ describe('PeerPayClient Unit Tests', () => {
|
|
|
242
243
|
expect(payments[1].token.amount).toBe(9)
|
|
243
244
|
})
|
|
244
245
|
})
|
|
246
|
+
|
|
247
|
+
// Test: listIncomingPaymentRequests
|
|
248
|
+
describe('listIncomingPaymentRequests', () => {
|
|
249
|
+
const futureExpiry = Date.now() + 60000
|
|
250
|
+
const pastExpiry = Date.now() - 60000
|
|
251
|
+
|
|
252
|
+
it('returns parsed request messages from payment_requests box', async () => {
|
|
253
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
254
|
+
{
|
|
255
|
+
messageId: 'msg1',
|
|
256
|
+
sender: 'sender1',
|
|
257
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
258
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
requestId: 'req1',
|
|
261
|
+
amount: 5000,
|
|
262
|
+
description: 'Test request',
|
|
263
|
+
expiresAt: futureExpiry,
|
|
264
|
+
senderIdentityKey: 'sender1',
|
|
265
|
+
requestProof: 'abcd1234'
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
])
|
|
269
|
+
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
270
|
+
|
|
271
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
272
|
+
|
|
273
|
+
expect(requests).toHaveLength(1)
|
|
274
|
+
expect(requests[0]).toMatchObject({
|
|
275
|
+
messageId: 'msg1',
|
|
276
|
+
sender: 'sender1',
|
|
277
|
+
requestId: 'req1',
|
|
278
|
+
amount: 5000,
|
|
279
|
+
description: 'Test request'
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('filters expired requests and acknowledges them', async () => {
|
|
284
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
285
|
+
{
|
|
286
|
+
messageId: 'expired-msg',
|
|
287
|
+
sender: 'sender1',
|
|
288
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
289
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
requestId: 'req-expired',
|
|
292
|
+
amount: 5000,
|
|
293
|
+
description: 'Expired request',
|
|
294
|
+
expiresAt: pastExpiry,
|
|
295
|
+
senderIdentityKey: 'sender1',
|
|
296
|
+
requestProof: 'abcd1234'
|
|
297
|
+
})
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
messageId: 'active-msg',
|
|
301
|
+
sender: 'sender2',
|
|
302
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
303
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
304
|
+
body: JSON.stringify({
|
|
305
|
+
requestId: 'req-active',
|
|
306
|
+
amount: 3000,
|
|
307
|
+
description: 'Active request',
|
|
308
|
+
expiresAt: futureExpiry,
|
|
309
|
+
senderIdentityKey: 'sender2',
|
|
310
|
+
requestProof: 'abcd1234'
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
])
|
|
314
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
315
|
+
|
|
316
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
317
|
+
|
|
318
|
+
expect(requests).toHaveLength(1)
|
|
319
|
+
expect(requests[0].requestId).toBe('req-active')
|
|
320
|
+
expect(ackSpy).toHaveBeenCalledWith({ messageIds: ['expired-msg'] })
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('filters cancelled requests and acknowledges both original and cancel messages', async () => {
|
|
324
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
325
|
+
{
|
|
326
|
+
messageId: 'original-msg',
|
|
327
|
+
sender: 'sender1',
|
|
328
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
329
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
requestId: 'req-cancel',
|
|
332
|
+
amount: 5000,
|
|
333
|
+
description: 'To be cancelled',
|
|
334
|
+
expiresAt: futureExpiry,
|
|
335
|
+
senderIdentityKey: 'sender1',
|
|
336
|
+
requestProof: 'abcd1234'
|
|
337
|
+
})
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
messageId: 'cancel-msg',
|
|
341
|
+
sender: 'sender1',
|
|
342
|
+
created_at: '2025-01-01T00:01:00Z',
|
|
343
|
+
updated_at: '2025-01-01T00:01:00Z',
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
requestId: 'req-cancel',
|
|
346
|
+
senderIdentityKey: 'sender1',
|
|
347
|
+
cancelled: true,
|
|
348
|
+
requestProof: 'abcd1234'
|
|
349
|
+
})
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
messageId: 'other-msg',
|
|
353
|
+
sender: 'sender2',
|
|
354
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
355
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
requestId: 'req-other',
|
|
358
|
+
amount: 2000,
|
|
359
|
+
description: 'Other request',
|
|
360
|
+
expiresAt: futureExpiry,
|
|
361
|
+
senderIdentityKey: 'sender2',
|
|
362
|
+
requestProof: 'abcd1234'
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
])
|
|
366
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
367
|
+
|
|
368
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
369
|
+
|
|
370
|
+
expect(requests).toHaveLength(1)
|
|
371
|
+
expect(requests[0].requestId).toBe('req-other')
|
|
372
|
+
expect(ackSpy).toHaveBeenCalledWith({ messageIds: expect.arrayContaining(['original-msg', 'cancel-msg']) })
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('discards malformed messages (invalid JSON) and acknowledges them', async () => {
|
|
376
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
377
|
+
{
|
|
378
|
+
messageId: 'bad-msg',
|
|
379
|
+
sender: 'sender1',
|
|
380
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
381
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
382
|
+
body: 'NOT VALID JSON {{{}'
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
messageId: 'good-msg',
|
|
386
|
+
sender: 'sender2',
|
|
387
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
388
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
requestId: 'req-good',
|
|
391
|
+
amount: 5000,
|
|
392
|
+
description: 'Valid request',
|
|
393
|
+
expiresAt: Date.now() + 60000,
|
|
394
|
+
senderIdentityKey: 'sender2',
|
|
395
|
+
requestProof: 'abcd1234'
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
])
|
|
399
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
400
|
+
|
|
401
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
402
|
+
|
|
403
|
+
expect(requests).toHaveLength(1)
|
|
404
|
+
expect(requests[0].requestId).toBe('req-good')
|
|
405
|
+
expect(ackSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
406
|
+
messageIds: expect.arrayContaining(['bad-msg'])
|
|
407
|
+
}))
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('discards messages with missing required fields and acknowledges them', async () => {
|
|
411
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
412
|
+
{
|
|
413
|
+
messageId: 'incomplete-msg',
|
|
414
|
+
sender: 'sender1',
|
|
415
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
416
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
417
|
+
body: JSON.stringify({ requestId: 'req-incomplete' })
|
|
418
|
+
}
|
|
419
|
+
])
|
|
420
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
421
|
+
|
|
422
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
423
|
+
|
|
424
|
+
expect(requests).toHaveLength(0)
|
|
425
|
+
expect(ackSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
426
|
+
messageIds: expect.arrayContaining(['incomplete-msg'])
|
|
427
|
+
}))
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('only cancels requests from the same sender', async () => {
|
|
431
|
+
const futureExpiry = Date.now() + 60000
|
|
432
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
433
|
+
{
|
|
434
|
+
messageId: 'original-msg',
|
|
435
|
+
sender: 'sender1',
|
|
436
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
437
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
438
|
+
body: JSON.stringify({
|
|
439
|
+
requestId: 'req-1',
|
|
440
|
+
amount: 5000,
|
|
441
|
+
description: 'Real request',
|
|
442
|
+
expiresAt: futureExpiry,
|
|
443
|
+
senderIdentityKey: 'sender1',
|
|
444
|
+
requestProof: 'abcd1234'
|
|
445
|
+
})
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
messageId: 'spoofed-cancel',
|
|
449
|
+
sender: 'attacker',
|
|
450
|
+
created_at: '2025-01-01T00:01:00Z',
|
|
451
|
+
updated_at: '2025-01-01T00:01:00Z',
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
requestId: 'req-1',
|
|
454
|
+
senderIdentityKey: 'attacker',
|
|
455
|
+
cancelled: true,
|
|
456
|
+
requestProof: 'abcd1234'
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
])
|
|
460
|
+
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
461
|
+
|
|
462
|
+
const requests = await peerPayClient.listIncomingPaymentRequests()
|
|
463
|
+
|
|
464
|
+
// The request should NOT be cancelled because the cancel came from a different sender
|
|
465
|
+
expect(requests).toHaveLength(1)
|
|
466
|
+
expect(requests[0].requestId).toBe('req-1')
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('filters out requests below minAmount and above maxAmount, acknowledges them', async () => {
|
|
470
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
471
|
+
{
|
|
472
|
+
messageId: 'too-small',
|
|
473
|
+
sender: 'sender1',
|
|
474
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
475
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
476
|
+
body: JSON.stringify({
|
|
477
|
+
requestId: 'req-small',
|
|
478
|
+
amount: 100,
|
|
479
|
+
description: 'Too small',
|
|
480
|
+
expiresAt: futureExpiry,
|
|
481
|
+
senderIdentityKey: 'sender1',
|
|
482
|
+
requestProof: 'abcd1234'
|
|
483
|
+
})
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
messageId: 'too-large',
|
|
487
|
+
sender: 'sender2',
|
|
488
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
489
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
490
|
+
body: JSON.stringify({
|
|
491
|
+
requestId: 'req-large',
|
|
492
|
+
amount: 99999,
|
|
493
|
+
description: 'Too large',
|
|
494
|
+
expiresAt: futureExpiry,
|
|
495
|
+
senderIdentityKey: 'sender2',
|
|
496
|
+
requestProof: 'abcd1234'
|
|
497
|
+
})
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
messageId: 'just-right',
|
|
501
|
+
sender: 'sender3',
|
|
502
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
503
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
504
|
+
body: JSON.stringify({
|
|
505
|
+
requestId: 'req-ok',
|
|
506
|
+
amount: 5000,
|
|
507
|
+
description: 'Just right',
|
|
508
|
+
expiresAt: futureExpiry,
|
|
509
|
+
senderIdentityKey: 'sender3',
|
|
510
|
+
requestProof: 'abcd1234'
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
])
|
|
514
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
515
|
+
|
|
516
|
+
const requests = await peerPayClient.listIncomingPaymentRequests(undefined, { minAmount: 1000, maxAmount: 10000 })
|
|
517
|
+
|
|
518
|
+
expect(requests).toHaveLength(1)
|
|
519
|
+
expect(requests[0].requestId).toBe('req-ok')
|
|
520
|
+
expect(ackSpy).toHaveBeenCalledWith({ messageIds: expect.arrayContaining(['too-small', 'too-large']) })
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// Test: fulfillPaymentRequest
|
|
525
|
+
describe('fulfillPaymentRequest', () => {
|
|
526
|
+
const mockRequest = {
|
|
527
|
+
messageId: 'req-msg-1',
|
|
528
|
+
sender: 'senderKey',
|
|
529
|
+
requestId: 'req-id-1',
|
|
530
|
+
amount: 5000,
|
|
531
|
+
description: 'Pay for goods',
|
|
532
|
+
expiresAt: Date.now() + 60000
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
it('sends payment for request.amount, sends paid response, acknowledges', async () => {
|
|
536
|
+
const sendPaymentSpy = jest.spyOn(peerPayClient, 'sendPayment').mockResolvedValue(undefined)
|
|
537
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
538
|
+
status: 'success',
|
|
539
|
+
messageId: 'resp-msg-id'
|
|
540
|
+
})
|
|
541
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
542
|
+
|
|
543
|
+
await peerPayClient.fulfillPaymentRequest({ request: mockRequest })
|
|
544
|
+
|
|
545
|
+
expect(sendPaymentSpy).toHaveBeenCalledWith(
|
|
546
|
+
{ recipient: 'senderKey', amount: 5000 },
|
|
547
|
+
undefined
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
const responseBody = JSON.parse((sendMessageSpy.mock.calls[0][0] as any).body)
|
|
551
|
+
expect(responseBody).toMatchObject({ requestId: 'req-id-1', status: 'paid', amountPaid: 5000 })
|
|
552
|
+
|
|
553
|
+
expect(ackSpy).toHaveBeenCalledWith({ messageIds: ['req-msg-1'] })
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('includes note when provided', async () => {
|
|
557
|
+
jest.spyOn(peerPayClient, 'sendPayment').mockResolvedValue(undefined)
|
|
558
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
559
|
+
status: 'success',
|
|
560
|
+
messageId: 'resp-msg-id'
|
|
561
|
+
})
|
|
562
|
+
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
563
|
+
|
|
564
|
+
await peerPayClient.fulfillPaymentRequest({ request: mockRequest, note: 'Here you go' })
|
|
565
|
+
|
|
566
|
+
const responseBody = JSON.parse((sendMessageSpy.mock.calls[0][0] as any).body)
|
|
567
|
+
expect(responseBody).toMatchObject({ note: 'Here you go' })
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// Test: declinePaymentRequest
|
|
572
|
+
describe('declinePaymentRequest', () => {
|
|
573
|
+
const mockRequest = {
|
|
574
|
+
messageId: 'req-msg-2',
|
|
575
|
+
sender: 'senderKey2',
|
|
576
|
+
requestId: 'req-id-2',
|
|
577
|
+
amount: 3000,
|
|
578
|
+
description: 'Pay for service',
|
|
579
|
+
expiresAt: Date.now() + 60000
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
it('sends declined response to payment_request_responses and acknowledges request', async () => {
|
|
583
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
584
|
+
status: 'success',
|
|
585
|
+
messageId: 'resp-msg-id'
|
|
586
|
+
})
|
|
587
|
+
const ackSpy = jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('ok')
|
|
588
|
+
|
|
589
|
+
await peerPayClient.declinePaymentRequest({ request: mockRequest, note: 'Not today' })
|
|
590
|
+
|
|
591
|
+
expect(sendMessageSpy).toHaveBeenCalledWith(
|
|
592
|
+
expect.objectContaining({
|
|
593
|
+
recipient: 'senderKey2',
|
|
594
|
+
messageBox: 'payment_request_responses'
|
|
595
|
+
}),
|
|
596
|
+
undefined
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
const responseBody = JSON.parse((sendMessageSpy.mock.calls[0][0] as any).body)
|
|
600
|
+
expect(responseBody).toMatchObject({ requestId: 'req-id-2', status: 'declined', note: 'Not today' })
|
|
601
|
+
|
|
602
|
+
expect(ackSpy).toHaveBeenCalledWith({ messageIds: ['req-msg-2'] })
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
// Test: listPaymentRequestResponses
|
|
607
|
+
describe('listPaymentRequestResponses', () => {
|
|
608
|
+
it('returns parsed responses from payment_request_responses box', async () => {
|
|
609
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
610
|
+
{
|
|
611
|
+
messageId: 'resp-1',
|
|
612
|
+
sender: 'payer1',
|
|
613
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
614
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
615
|
+
body: JSON.stringify({ requestId: 'req-1', status: 'paid', amountPaid: 5000 })
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
messageId: 'resp-2',
|
|
619
|
+
sender: 'payer2',
|
|
620
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
621
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
622
|
+
body: JSON.stringify({ requestId: 'req-2', status: 'declined', note: 'No funds' })
|
|
623
|
+
}
|
|
624
|
+
])
|
|
625
|
+
|
|
626
|
+
const responses = await peerPayClient.listPaymentRequestResponses()
|
|
627
|
+
|
|
628
|
+
expect(responses).toHaveLength(2)
|
|
629
|
+
expect(responses[0]).toMatchObject({ requestId: 'req-1', status: 'paid', amountPaid: 5000 })
|
|
630
|
+
expect(responses[1]).toMatchObject({ requestId: 'req-2', status: 'declined', note: 'No funds' })
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
// Test: listenForLivePaymentRequests
|
|
635
|
+
describe('listenForLivePaymentRequests', () => {
|
|
636
|
+
it('calls listenForLiveMessages on payment_requests box and converts messages to IncomingPaymentRequest', async () => {
|
|
637
|
+
const listenSpy = jest.spyOn(peerPayClient, 'listenForLiveMessages').mockResolvedValue(undefined)
|
|
638
|
+
const onRequest = jest.fn()
|
|
639
|
+
|
|
640
|
+
await peerPayClient.listenForLivePaymentRequests({ onRequest })
|
|
641
|
+
|
|
642
|
+
expect(listenSpy).toHaveBeenCalledWith(
|
|
643
|
+
expect.objectContaining({ messageBox: 'payment_requests' })
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
// Simulate a message arriving by calling the onMessage callback
|
|
647
|
+
const { onMessage } = (listenSpy.mock.calls[0][0] as any)
|
|
648
|
+
onMessage({
|
|
649
|
+
messageId: 'live-msg-1',
|
|
650
|
+
sender: 'sender1',
|
|
651
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
652
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
653
|
+
body: JSON.stringify({
|
|
654
|
+
requestId: 'req-live-1',
|
|
655
|
+
amount: 3000,
|
|
656
|
+
description: 'Live request',
|
|
657
|
+
expiresAt: Date.now() + 60000,
|
|
658
|
+
senderIdentityKey: 'sender1',
|
|
659
|
+
requestProof: 'abcd1234'
|
|
660
|
+
})
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
expect(onRequest).toHaveBeenCalledWith(
|
|
664
|
+
expect.objectContaining({ messageId: 'live-msg-1', requestId: 'req-live-1', amount: 3000 })
|
|
665
|
+
)
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
// Test: listenForLivePaymentRequestResponses
|
|
670
|
+
describe('listenForLivePaymentRequestResponses', () => {
|
|
671
|
+
it('calls listenForLiveMessages on payment_request_responses box and parses responses', async () => {
|
|
672
|
+
const listenSpy = jest.spyOn(peerPayClient, 'listenForLiveMessages').mockResolvedValue(undefined)
|
|
673
|
+
const onResponse = jest.fn()
|
|
674
|
+
|
|
675
|
+
await peerPayClient.listenForLivePaymentRequestResponses({ onResponse })
|
|
676
|
+
|
|
677
|
+
expect(listenSpy).toHaveBeenCalledWith(
|
|
678
|
+
expect.objectContaining({ messageBox: 'payment_request_responses' })
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
// Simulate a message arriving
|
|
682
|
+
const { onMessage } = (listenSpy.mock.calls[0][0] as any)
|
|
683
|
+
onMessage({
|
|
684
|
+
messageId: 'live-resp-1',
|
|
685
|
+
sender: 'payer1',
|
|
686
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
687
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
688
|
+
body: JSON.stringify({ requestId: 'req-1', status: 'paid', amountPaid: 5000 })
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
expect(onResponse).toHaveBeenCalledWith(
|
|
692
|
+
expect.objectContaining({ requestId: 'req-1', status: 'paid', amountPaid: 5000 })
|
|
693
|
+
)
|
|
694
|
+
})
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
// Test: allowPaymentRequestsFrom
|
|
698
|
+
describe('allowPaymentRequestsFrom', () => {
|
|
699
|
+
it('calls setMessageBoxPermission with messageBox=payment_requests and recipientFee=0', async () => {
|
|
700
|
+
const setPermSpy = jest.spyOn(peerPayClient, 'setMessageBoxPermission').mockResolvedValue(undefined)
|
|
701
|
+
|
|
702
|
+
await peerPayClient.allowPaymentRequestsFrom({ identityKey: 'trustedKey' })
|
|
703
|
+
|
|
704
|
+
expect(setPermSpy).toHaveBeenCalledWith({
|
|
705
|
+
messageBox: 'payment_requests',
|
|
706
|
+
sender: 'trustedKey',
|
|
707
|
+
recipientFee: 0
|
|
708
|
+
})
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// Test: blockPaymentRequestsFrom
|
|
713
|
+
describe('blockPaymentRequestsFrom', () => {
|
|
714
|
+
it('calls setMessageBoxPermission with recipientFee=-1', async () => {
|
|
715
|
+
const setPermSpy = jest.spyOn(peerPayClient, 'setMessageBoxPermission').mockResolvedValue(undefined)
|
|
716
|
+
|
|
717
|
+
await peerPayClient.blockPaymentRequestsFrom({ identityKey: 'blockedKey' })
|
|
718
|
+
|
|
719
|
+
expect(setPermSpy).toHaveBeenCalledWith({
|
|
720
|
+
messageBox: 'payment_requests',
|
|
721
|
+
sender: 'blockedKey',
|
|
722
|
+
recipientFee: -1
|
|
723
|
+
})
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
// Test: listPaymentRequestPermissions
|
|
728
|
+
describe('listPaymentRequestPermissions', () => {
|
|
729
|
+
it('calls listMessageBoxPermissions and maps to { identityKey, allowed } array', async () => {
|
|
730
|
+
jest.spyOn(peerPayClient, 'listMessageBoxPermissions').mockResolvedValue([
|
|
731
|
+
{
|
|
732
|
+
sender: 'key1',
|
|
733
|
+
messageBox: 'payment_requests',
|
|
734
|
+
recipientFee: 0,
|
|
735
|
+
status: 'always_allow',
|
|
736
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
737
|
+
updatedAt: '2025-01-01T00:00:00Z'
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
sender: 'key2',
|
|
741
|
+
messageBox: 'payment_requests',
|
|
742
|
+
recipientFee: -1,
|
|
743
|
+
status: 'blocked',
|
|
744
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
745
|
+
updatedAt: '2025-01-01T00:00:00Z'
|
|
746
|
+
}
|
|
747
|
+
])
|
|
748
|
+
|
|
749
|
+
const permissions = await peerPayClient.listPaymentRequestPermissions()
|
|
750
|
+
|
|
751
|
+
expect(permissions).toHaveLength(2)
|
|
752
|
+
expect(permissions[0]).toEqual({ identityKey: 'key1', allowed: true })
|
|
753
|
+
expect(permissions[1]).toEqual({ identityKey: 'key2', allowed: false })
|
|
754
|
+
})
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
// Test: requestPayment
|
|
758
|
+
describe('requestPayment', () => {
|
|
759
|
+
it('sends payment request message to payment_requests box with correct body fields', async () => {
|
|
760
|
+
jest.spyOn(peerPayClient, 'getIdentityKey').mockResolvedValue('myIdentityKey')
|
|
761
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
762
|
+
status: 'success',
|
|
763
|
+
messageId: 'mockedMessageId'
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
const result = await peerPayClient.requestPayment({
|
|
767
|
+
recipient: 'recipientKey',
|
|
768
|
+
amount: 1000,
|
|
769
|
+
description: 'Please pay me',
|
|
770
|
+
expiresAt: Date.now() + 60000
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
expect(result).toHaveProperty('requestId')
|
|
774
|
+
expect(typeof result.requestId).toBe('string')
|
|
775
|
+
expect(sendMessageSpy).toHaveBeenCalledWith(
|
|
776
|
+
expect.objectContaining({
|
|
777
|
+
recipient: 'recipientKey',
|
|
778
|
+
messageBox: 'payment_requests',
|
|
779
|
+
body: expect.stringContaining('"amount":1000')
|
|
780
|
+
}),
|
|
781
|
+
undefined
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
const sentBody = JSON.parse((sendMessageSpy.mock.calls[0][0] as any).body)
|
|
785
|
+
expect(sentBody).toHaveProperty('requestId')
|
|
786
|
+
expect(sentBody).toHaveProperty('amount', 1000)
|
|
787
|
+
expect(sentBody).toHaveProperty('description', 'Please pay me')
|
|
788
|
+
expect(sentBody).toHaveProperty('senderIdentityKey', 'myIdentityKey')
|
|
789
|
+
expect(sentBody).toHaveProperty('requestProof')
|
|
790
|
+
expect(typeof sentBody.requestProof).toBe('string')
|
|
791
|
+
expect(sentBody.requestProof.length).toBeGreaterThan(0)
|
|
792
|
+
|
|
793
|
+
expect(result).toHaveProperty('requestProof')
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('throws if amount <= 0', async () => {
|
|
797
|
+
await expect(peerPayClient.requestPayment({
|
|
798
|
+
recipient: 'recipientKey',
|
|
799
|
+
amount: 0,
|
|
800
|
+
description: 'Bad request',
|
|
801
|
+
expiresAt: Date.now() + 60000
|
|
802
|
+
})).rejects.toThrow()
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
// Test: cancelPaymentRequest
|
|
807
|
+
describe('cancelPaymentRequest', () => {
|
|
808
|
+
it('sends cancellation message with requestId, real senderIdentityKey, and cancelled: true', async () => {
|
|
809
|
+
jest.spyOn(peerPayClient, 'getIdentityKey').mockResolvedValue('myIdentityKey')
|
|
810
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
811
|
+
status: 'success',
|
|
812
|
+
messageId: 'mockedMessageId'
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
await peerPayClient.cancelPaymentRequest({
|
|
816
|
+
recipient: 'recipientKey',
|
|
817
|
+
requestId: 'existing-request-id',
|
|
818
|
+
requestProof: 'original-proof-hex'
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
expect(sendMessageSpy).toHaveBeenCalledWith(
|
|
822
|
+
expect.objectContaining({
|
|
823
|
+
recipient: 'recipientKey',
|
|
824
|
+
messageBox: 'payment_requests'
|
|
825
|
+
}),
|
|
826
|
+
undefined
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
const sentBody = JSON.parse((sendMessageSpy.mock.calls[0][0] as any).body)
|
|
830
|
+
expect(sentBody).toEqual({
|
|
831
|
+
requestId: 'existing-request-id',
|
|
832
|
+
senderIdentityKey: 'myIdentityKey',
|
|
833
|
+
requestProof: 'original-proof-hex',
|
|
834
|
+
cancelled: true
|
|
835
|
+
})
|
|
836
|
+
})
|
|
837
|
+
})
|
|
245
838
|
})
|
package/src/types.ts
CHANGED
|
@@ -187,3 +187,98 @@ export interface ListDevicesResponse {
|
|
|
187
187
|
devices: RegisteredDevice[]
|
|
188
188
|
description?: string // For error responses
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Base fields shared by both payment request and cancellation messages.
|
|
193
|
+
*/
|
|
194
|
+
interface PaymentRequestBase {
|
|
195
|
+
/** Unique identifier for this request, generated via createNonce(). */
|
|
196
|
+
requestId: string
|
|
197
|
+
/** Identity key of the requester, used for correlation and cancellation verification. */
|
|
198
|
+
senderIdentityKey: string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A new payment request sent from requester to payer.
|
|
203
|
+
* Carried in the 'payment_requests' message box.
|
|
204
|
+
*/
|
|
205
|
+
export interface PaymentRequestNew extends PaymentRequestBase {
|
|
206
|
+
/** Amount in satoshis being requested. */
|
|
207
|
+
amount: number
|
|
208
|
+
/** Human-readable reason for the request. */
|
|
209
|
+
description: string
|
|
210
|
+
/** Unix timestamp (ms) after which the request expires. Set by the sender. */
|
|
211
|
+
expiresAt: number
|
|
212
|
+
/** HMAC proof tying this request to the sender's identity. Used to authorize cancellations. */
|
|
213
|
+
requestProof: string
|
|
214
|
+
/** Omitted or false for a new payment request. */
|
|
215
|
+
cancelled?: false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A cancellation of a previously sent payment request.
|
|
220
|
+
* Carried in the 'payment_requests' message box.
|
|
221
|
+
*/
|
|
222
|
+
export interface PaymentRequestCancellation extends PaymentRequestBase {
|
|
223
|
+
/** If true, this message cancels a previously sent request with the same requestId. */
|
|
224
|
+
cancelled: true
|
|
225
|
+
/** HMAC proof from the original request, proving cancellation authority. */
|
|
226
|
+
requestProof: string
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Discriminated union: either a new payment request or a cancellation.
|
|
231
|
+
* Discriminant field: `cancelled` (true = cancellation, absent/false = new request).
|
|
232
|
+
*/
|
|
233
|
+
export type PaymentRequestMessage = PaymentRequestNew | PaymentRequestCancellation
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Represents a response to a payment request, sent from the payer back to the requester.
|
|
237
|
+
* Carried in the 'payment_request_responses' message box.
|
|
238
|
+
*/
|
|
239
|
+
export interface PaymentRequestResponse {
|
|
240
|
+
/** The requestId of the original PaymentRequestMessage this responds to. */
|
|
241
|
+
requestId: string
|
|
242
|
+
/** Status of the response. */
|
|
243
|
+
status: 'paid' | 'declined'
|
|
244
|
+
/** Optional note from the payer. */
|
|
245
|
+
note?: string
|
|
246
|
+
/** Actual amount paid in satoshis (may differ from the requested amount). */
|
|
247
|
+
amountPaid?: number
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Represents an incoming payment request as returned by listIncomingPaymentRequests().
|
|
252
|
+
* Combines the transport message metadata with the parsed request body.
|
|
253
|
+
* Only active (non-cancelled) requests are returned, so cancelled field is omitted.
|
|
254
|
+
*/
|
|
255
|
+
export interface IncomingPaymentRequest {
|
|
256
|
+
/** Transport message ID used for acknowledgment. */
|
|
257
|
+
messageId: string
|
|
258
|
+
/** Identity key of the requester. */
|
|
259
|
+
sender: string
|
|
260
|
+
/** Unique identifier for this request. */
|
|
261
|
+
requestId: string
|
|
262
|
+
/** Amount in satoshis requested. */
|
|
263
|
+
amount: number
|
|
264
|
+
/** Human-readable reason for the request. */
|
|
265
|
+
description: string
|
|
266
|
+
/** Unix timestamp (ms) when the request expires. */
|
|
267
|
+
expiresAt: number
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Default minimum satoshis for payment request filtering. */
|
|
271
|
+
export const DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT = 1000
|
|
272
|
+
/** Default maximum satoshis for payment request filtering. */
|
|
273
|
+
export const DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT = 10_000_000
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Configurable min/max amount limits for incoming payment requests.
|
|
277
|
+
* Requests outside these bounds are auto-acknowledged and discarded.
|
|
278
|
+
*/
|
|
279
|
+
export interface PaymentRequestLimits {
|
|
280
|
+
/** Minimum satoshis to accept in a request. Requests below this are discarded. Default: 1000. */
|
|
281
|
+
minAmount?: number
|
|
282
|
+
/** Maximum satoshis to accept in a request. Requests above this are discarded. Default: 10000000. */
|
|
283
|
+
maxAmount?: number
|
|
284
|
+
}
|