@bsv/message-box-client 2.0.1 → 2.0.3

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 (34) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +5 -5
  4. package/dist/cjs/src/MessageBoxClient.js +94 -29
  5. package/dist/cjs/src/MessageBoxClient.js.map +1 -1
  6. package/dist/cjs/src/RemittanceAdapter.js +137 -0
  7. package/dist/cjs/src/RemittanceAdapter.js.map +1 -0
  8. package/dist/cjs/src/__tests/RemittanceAdapter.test.js +133 -0
  9. package/dist/cjs/src/__tests/RemittanceAdapter.test.js.map +1 -0
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/mod.js +1 -0
  12. package/dist/esm/mod.js.map +1 -1
  13. package/dist/esm/src/MessageBoxClient.js +94 -29
  14. package/dist/esm/src/MessageBoxClient.js.map +1 -1
  15. package/dist/esm/src/RemittanceAdapter.js +133 -0
  16. package/dist/esm/src/RemittanceAdapter.js.map +1 -0
  17. package/dist/esm/src/__tests/RemittanceAdapter.test.js +131 -0
  18. package/dist/esm/src/__tests/RemittanceAdapter.test.js.map +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/mod.d.ts +1 -0
  21. package/dist/types/mod.d.ts.map +1 -1
  22. package/dist/types/src/MessageBoxClient.d.ts +2 -0
  23. package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
  24. package/dist/types/src/RemittanceAdapter.d.ts +103 -0
  25. package/dist/types/src/RemittanceAdapter.d.ts.map +1 -0
  26. package/dist/types/src/__tests/RemittanceAdapter.test.d.ts +2 -0
  27. package/dist/types/src/__tests/RemittanceAdapter.test.d.ts.map +1 -0
  28. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  29. package/dist/umd/bundle.js +1 -1
  30. package/mod.ts +1 -0
  31. package/package.json +5 -5
  32. package/src/MessageBoxClient.ts +102 -28
  33. package/src/RemittanceAdapter.ts +164 -0
  34. package/src/__tests/RemittanceAdapter.test.ts +153 -0
package/mod.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './src/MessageBoxClient.js'
2
2
  export * from './src/PeerPayClient.js'
3
3
  export * from './src/types.js'
4
+ export * from './src/RemittanceAdapter.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/message-box-client",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -54,8 +54,8 @@
54
54
  "author": "BSV Blockchain Association",
55
55
  "license": "SEE LICENSE IN LICENSE.txt",
56
56
  "devDependencies": {
57
- "@bsv/auth-express-middleware": "^2.0.2",
58
- "@bsv/payment-express-middleware": "^2.0.0",
57
+ "@bsv/auth-express-middleware": "^2.0.4",
58
+ "@bsv/payment-express-middleware": "^2.0.1",
59
59
  "@eslint/js": "^9.20.0",
60
60
  "@types/jest": "^29.5.14",
61
61
  "@types/node": "^22.13.2",
@@ -82,7 +82,7 @@
82
82
  "webpack-merge": "^6.0.1"
83
83
  },
84
84
  "dependencies": {
85
- "@bsv/authsocket-client": "^2.0.1",
86
- "@bsv/sdk": "^2.0.3"
85
+ "@bsv/authsocket-client": "^2.0.2",
86
+ "@bsv/sdk": "^2.0.4"
87
87
  }
88
88
  }
@@ -96,6 +96,8 @@ export class MessageBoxClient {
96
96
  private readonly lookupResolver: LookupResolver
97
97
  private readonly networkPreset: 'local' | 'mainnet' | 'testnet'
98
98
  private initialized = false
99
+ private socketAuthenticated = false
100
+ private connectionInitPromise?: Promise<void>
99
101
  protected originator?: OriginatorDomainNameStringUnder250Bytes
100
102
  /**
101
103
  * @constructor
@@ -131,7 +133,7 @@ export class MessageBoxClient {
131
133
  } = options
132
134
 
133
135
  const defaultHost =
134
- this.networkPreset === 'testnet'
136
+ networkPreset === 'testnet'
135
137
  ? DEFAULT_TESTNET_HOST
136
138
  : DEFAULT_MAINNET_HOST
137
139
 
@@ -313,6 +315,15 @@ export class MessageBoxClient {
313
315
 
314
316
  Logger.log('[MB CLIENT] Setting up WebSocket connection...')
315
317
 
318
+ if (this.socketAuthenticated && this.socket != null) {
319
+ return
320
+ }
321
+
322
+ if (this.connectionInitPromise != null) {
323
+ await this.connectionInitPromise
324
+ return
325
+ }
326
+
316
327
  if (this.socket == null) {
317
328
  const targetHost = overrideHost ?? this.host
318
329
  if (typeof targetHost !== 'string' || targetHost.trim() === '') {
@@ -320,58 +331,115 @@ export class MessageBoxClient {
320
331
  }
321
332
  this.socket = AuthSocketClient(targetHost, { wallet: this.walletClient, originator: this.originator })
322
333
 
323
- let identitySent = false
324
- let authenticated = false
325
-
326
334
  this.socket.on('connect', () => {
327
335
  Logger.log('[MB CLIENT] Connected to WebSocket.')
328
336
 
329
- if (!identitySent) {
330
- Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey)
331
- if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
332
- Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!')
333
- } else {
334
- this.socket?.emit('authenticated', { identityKey: this.myIdentityKey })
335
- identitySent = true
336
- }
337
+ Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey)
338
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
339
+ Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!')
340
+ } else {
341
+ this.socket?.emit('authenticated', { identityKey: this.myIdentityKey })
337
342
  }
338
343
  })
339
344
 
340
345
  // Listen for authentication success from the server
341
346
  this.socket.on('authenticationSuccess', (data) => {
342
347
  Logger.log(`[MB CLIENT] WebSocket authentication successful: ${JSON.stringify(data)}`)
343
- authenticated = true
348
+ this.socketAuthenticated = true
344
349
  })
345
350
 
346
351
  // Handle authentication failures
347
352
  this.socket.on('authenticationFailed', (data) => {
348
353
  Logger.error(`[MB CLIENT ERROR] WebSocket authentication failed: ${JSON.stringify(data)}`)
349
- authenticated = false
354
+ this.socketAuthenticated = false
350
355
  })
351
356
 
352
357
  this.socket.on('disconnect', () => {
353
358
  Logger.log('[MB CLIENT] Disconnected from MessageBox server')
354
359
  this.socket = undefined
355
- identitySent = false
356
- authenticated = false
360
+ this.socketAuthenticated = false
357
361
  })
358
362
 
359
363
  this.socket.on('error', (error) => {
360
364
  Logger.error('[MB CLIENT ERROR] WebSocket error:', error)
361
365
  })
366
+ }
362
367
 
363
- // Wait for authentication confirmation before proceeding
364
- await new Promise<void>((resolve, reject) => {
365
- setTimeout(() => {
366
- if (authenticated) {
367
- Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!')
368
- resolve()
369
- } else {
370
- reject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!'))
371
- }
372
- }, 5000) // Timeout after 5 seconds
373
- })
368
+ if (this.socket?.connected && !this.socketAuthenticated) {
369
+ this.socket.emit('authenticated', { identityKey: this.myIdentityKey })
374
370
  }
371
+
372
+ this.connectionInitPromise = new Promise<void>((resolve, reject) => {
373
+ const socketAny = this.socket as any
374
+ let settled = false
375
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
376
+
377
+ const finalizeResolve = (): void => {
378
+ if (settled) return
379
+ settled = true
380
+ if (timeoutId != null) {
381
+ clearTimeout(timeoutId)
382
+ timeoutId = undefined
383
+ }
384
+ if (typeof socketAny?.off === 'function') {
385
+ socketAny.off('authenticationSuccess', onSuccess)
386
+ socketAny.off('authenticationFailed', onFailed)
387
+ socketAny.off('disconnect', onDisconnectBeforeAuth)
388
+ }
389
+ this.connectionInitPromise = undefined
390
+ Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!')
391
+ resolve()
392
+ }
393
+
394
+ const finalizeReject = (error: Error): void => {
395
+ if (settled) return
396
+ settled = true
397
+ if (timeoutId != null) {
398
+ clearTimeout(timeoutId)
399
+ timeoutId = undefined
400
+ }
401
+ if (typeof socketAny?.off === 'function') {
402
+ socketAny.off('authenticationSuccess', onSuccess)
403
+ socketAny.off('authenticationFailed', onFailed)
404
+ socketAny.off('disconnect', onDisconnectBeforeAuth)
405
+ }
406
+ this.connectionInitPromise = undefined
407
+ reject(error)
408
+ }
409
+
410
+ const onSuccess = (): void => {
411
+ this.socketAuthenticated = true
412
+ finalizeResolve()
413
+ }
414
+
415
+ const onFailed = (): void => {
416
+ this.socketAuthenticated = false
417
+ finalizeReject(new Error('[MB CLIENT ERROR] WebSocket authentication failed!'))
418
+ }
419
+
420
+ const onDisconnectBeforeAuth = (): void => {
421
+ this.socketAuthenticated = false
422
+ }
423
+
424
+ if (this.socketAuthenticated) {
425
+ finalizeResolve()
426
+ return
427
+ }
428
+
429
+ socketAny?.on('authenticationSuccess', onSuccess)
430
+ socketAny?.on('authenticationFailed', onFailed)
431
+ socketAny?.on('disconnect', onDisconnectBeforeAuth)
432
+
433
+ timeoutId = setTimeout(() => {
434
+ if (this.socketAuthenticated) {
435
+ finalizeResolve()
436
+ } else {
437
+ finalizeReject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!'))
438
+ }
439
+ }, 5000)
440
+ })
441
+
442
+ await this.connectionInitPromise
375
443
  }
376
444
 
377
445
  /**
@@ -700,10 +768,15 @@ export class MessageBoxClient {
700
768
  return await new Promise((resolve, reject) => {
701
769
  const ackEvent = `sendMessageAck-${roomId}`
702
770
  let handled = false
771
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
703
772
 
704
773
  const ackHandler = (response?: SendMessageResponse): void => {
705
774
  if (handled) return
706
775
  handled = true
776
+ if (timeoutId != null) {
777
+ clearTimeout(timeoutId)
778
+ timeoutId = undefined
779
+ }
707
780
 
708
781
  const socketAny = this.socket as any
709
782
  if (typeof socketAny?.off === 'function') {
@@ -749,9 +822,10 @@ export class MessageBoxClient {
749
822
  })
750
823
 
751
824
  // Timeout: Fallback to HTTP if no acknowledgment received
752
- setTimeout(() => {
825
+ timeoutId = setTimeout(() => {
753
826
  if (!handled) {
754
827
  handled = true
828
+ timeoutId = undefined
755
829
  const socketAny = this.socket as any
756
830
  if (typeof socketAny?.off === 'function') {
757
831
  socketAny.off(ackEvent, ackHandler)
@@ -0,0 +1,164 @@
1
+ /**
2
+ * RemittanceAdapter - Adapts MessageBoxClient to the CommsLayer interface
3
+ *
4
+ * This adapter bridges MessageBoxClient with the ts-sdk RemittanceManager by implementing
5
+ * the CommsLayer interface. It handles the protocol differences between the two systems,
6
+ * particularly around message body format (MessageBoxClient returns parsed objects,
7
+ * RemittanceManager expects JSON strings).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { RemittanceAdapter } from '@bsv/message-box-client'
12
+ * import { RemittanceManager } from '@bsv/sdk'
13
+ * import { MessageBoxClient } from '@bsv/message-box-client'
14
+ * import { WalletClient } from '@bsv/sdk'
15
+ *
16
+ * const wallet = new WalletClient()
17
+ * const messageBox = new MessageBoxClient({ walletClient: wallet })
18
+ * const commsLayer = new RemittanceAdapter(messageBox)
19
+ *
20
+ * const manager = new RemittanceManager(
21
+ * {
22
+ * messageBox: 'remittance_inbox',
23
+ * remittanceModules: [new Brc29RemittanceModule()]
24
+ * },
25
+ * wallet,
26
+ * commsLayer
27
+ * )
28
+ * ```
29
+ */
30
+
31
+ import { PubKeyHex } from '@bsv/sdk'
32
+ import type {
33
+ CommsLayer as SdkCommsLayer,
34
+ PeerMessage as SdkRemittancePeerMessage
35
+ } from '@bsv/sdk'
36
+ import type { MessageBoxClient } from './MessageBoxClient.js'
37
+ import type { PeerMessage as MessageBoxPeerMessage } from './types.js'
38
+
39
+ export type CommsLayer = SdkCommsLayer
40
+ export type RemittancePeerMessage = SdkRemittancePeerMessage
41
+
42
+ /**
43
+ * Adapter that implements the CommsLayer interface for MessageBoxClient
44
+ *
45
+ * This class wraps MessageBoxClient to provide compatibility with the RemittanceManager
46
+ * communications interface. It handles format conversions, particularly ensuring message
47
+ * bodies are properly stringified for the RemittanceManager protocol.
48
+ */
49
+ export class RemittanceAdapter implements SdkCommsLayer {
50
+ /**
51
+ * Creates a new RemittanceAdapter
52
+ * @param messageBox - The MessageBoxClient instance to adapt
53
+ */
54
+ constructor(private readonly messageBox: MessageBoxClient) { }
55
+
56
+ /**
57
+ * Sends a message over the store-and-forward channel
58
+ * @param args - Message parameters (recipient, messageBox, body)
59
+ * @param hostOverride - Optional host override
60
+ * @returns The transport message ID
61
+ */
62
+ async sendMessage(
63
+ args: { recipient: PubKeyHex, messageBox: string, body: string },
64
+ hostOverride?: string
65
+ ): Promise<string> {
66
+ const result = await this.messageBox.sendMessage({
67
+ recipient: args.recipient,
68
+ messageBox: args.messageBox,
69
+ body: args.body
70
+ }, hostOverride)
71
+
72
+ return result.messageId
73
+ }
74
+
75
+ /**
76
+ * Sends a message over the live channel.
77
+ * MessageBoxClient handles transport fallback internally (WebSocket -> HTTP).
78
+ * @param args - Message parameters (recipient, messageBox, body)
79
+ * @param hostOverride - Optional host override
80
+ * @returns The transport message ID
81
+ */
82
+ async sendLiveMessage(
83
+ args: { recipient: PubKeyHex, messageBox: string, body: string },
84
+ hostOverride?: string
85
+ ): Promise<string> {
86
+ const result = await this.messageBox.sendLiveMessage({
87
+ recipient: args.recipient,
88
+ messageBox: args.messageBox,
89
+ body: args.body
90
+ }, hostOverride)
91
+
92
+ return result.messageId
93
+ }
94
+
95
+ /**
96
+ * Lists pending messages for a message box
97
+ *
98
+ * Note: MessageBoxClient returns message bodies as parsed objects, but RemittanceManager
99
+ * expects them as JSON strings. This method handles the conversion.
100
+ *
101
+ * @param args - List parameters (messageBox, optional host)
102
+ * @returns Array of peer messages with stringified bodies
103
+ */
104
+ async listMessages(args: { messageBox: string, host?: string }): Promise<RemittancePeerMessage[]> {
105
+ const defaultRecipient = await this.messageBox.getIdentityKey() as PubKeyHex
106
+ const messages = await this.messageBox.listMessages({
107
+ messageBox: args.messageBox,
108
+ host: args.host
109
+ })
110
+
111
+ return messages.map(msg => this.toRemittancePeerMessage(msg, args.messageBox, defaultRecipient))
112
+ }
113
+
114
+ /**
115
+ * Acknowledges messages (deletes them from the server inbox)
116
+ * @param args - Array of message IDs to acknowledge
117
+ */
118
+ async acknowledgeMessage(args: { messageIds: string[] }): Promise<void> {
119
+ // MessageBoxClient's acknowledgeMessage expects the same format
120
+ await this.messageBox.acknowledgeMessage({ messageIds: args.messageIds })
121
+ }
122
+
123
+ /**
124
+ * Starts a live listener and normalizes inbound messages to the remittance PeerMessage shape.
125
+ */
126
+ async listenForLiveMessages(args: {
127
+ messageBox: string
128
+ overrideHost?: string
129
+ onMessage: (msg: RemittancePeerMessage) => void
130
+ }): Promise<void> {
131
+ const defaultRecipient = await this.messageBox.getIdentityKey() as PubKeyHex
132
+
133
+ await this.messageBox.listenForLiveMessages({
134
+ messageBox: args.messageBox,
135
+ overrideHost: args.overrideHost,
136
+ onMessage: (msg: MessageBoxPeerMessage) => {
137
+ args.onMessage(this.toRemittancePeerMessage(msg, args.messageBox, defaultRecipient))
138
+ }
139
+ })
140
+ }
141
+
142
+ private toRemittancePeerMessage (
143
+ msg: MessageBoxPeerMessage & { recipient?: string, messageBox?: string },
144
+ fallbackMessageBox: string,
145
+ fallbackRecipient: PubKeyHex
146
+ ): RemittancePeerMessage {
147
+ return {
148
+ messageId: msg.messageId,
149
+ sender: msg.sender as PubKeyHex,
150
+ recipient: (msg.recipient ?? fallbackRecipient) as PubKeyHex,
151
+ messageBox: msg.messageBox ?? fallbackMessageBox,
152
+ body: this.toBodyString(msg.body)
153
+ }
154
+ }
155
+
156
+ private toBodyString (body: unknown): string {
157
+ if (typeof body === 'string') return body
158
+ try {
159
+ return JSON.stringify(body)
160
+ } catch {
161
+ return String(body)
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,153 @@
1
+ /* eslint-env jest */
2
+ import { jest } from '@jest/globals'
3
+ import { RemittanceAdapter } from '../RemittanceAdapter.js'
4
+ import type { MessageBoxClient } from '../MessageBoxClient.js'
5
+
6
+ describe('RemittanceAdapter', () => {
7
+ const myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
8
+ const senderKey = '03f5d7a10f8ac22f0785a54d7d30fd009a77da27812f4e2f4ac9327dfcb5f65f86'
9
+
10
+ it('delegates sendMessage and returns the transport messageId', async () => {
11
+ const messageBox = {
12
+ sendMessage: jest.fn<() => Promise<{ status: string, messageId: string }>>().mockResolvedValue({
13
+ status: 'success',
14
+ messageId: 'http-mid'
15
+ })
16
+ } as unknown as MessageBoxClient
17
+
18
+ const adapter = new RemittanceAdapter(messageBox)
19
+ const result = await adapter.sendMessage({
20
+ recipient: senderKey,
21
+ messageBox: 'remittance_inbox',
22
+ body: '{"v":1}'
23
+ }, 'https://override-host')
24
+
25
+ expect(result).toBe('http-mid')
26
+ expect(messageBox.sendMessage).toHaveBeenCalledWith({
27
+ recipient: senderKey,
28
+ messageBox: 'remittance_inbox',
29
+ body: '{"v":1}'
30
+ }, 'https://override-host')
31
+ })
32
+
33
+ it('delegates sendLiveMessage (live path) and returns the transport messageId', async () => {
34
+ const messageBox = {
35
+ sendLiveMessage: jest.fn<() => Promise<{ status: string, messageId: string }>>().mockResolvedValue({
36
+ status: 'success',
37
+ messageId: 'ws-mid'
38
+ })
39
+ } as unknown as MessageBoxClient
40
+
41
+ const adapter = new RemittanceAdapter(messageBox)
42
+ const result = await adapter.sendLiveMessage({
43
+ recipient: senderKey,
44
+ messageBox: 'remittance_inbox',
45
+ body: '{"v":1}'
46
+ }, 'https://override-host')
47
+
48
+ expect(result).toBe('ws-mid')
49
+ expect(messageBox.sendLiveMessage).toHaveBeenCalledWith({
50
+ recipient: senderKey,
51
+ messageBox: 'remittance_inbox',
52
+ body: '{"v":1}'
53
+ }, 'https://override-host')
54
+ })
55
+
56
+ it('normalizes listMessages shape for remittance and forwards host', async () => {
57
+ const messageBox = {
58
+ getIdentityKey: jest.fn<() => Promise<string>>().mockResolvedValue(myIdentityKey),
59
+ listMessages: jest.fn<() => Promise<any[]>>().mockResolvedValue([
60
+ {
61
+ messageId: 'm1',
62
+ sender: senderKey,
63
+ created_at: '2026-01-01T00:00:00Z',
64
+ updated_at: '2026-01-01T00:00:00Z',
65
+ body: { kind: 'invoice', amount: 1 }
66
+ },
67
+ {
68
+ messageId: 'm2',
69
+ sender: senderKey,
70
+ recipient: '020202020202020202020202020202020202020202020202020202020202020202',
71
+ messageBox: 'custom_box',
72
+ created_at: '2026-01-01T00:00:00Z',
73
+ updated_at: '2026-01-01T00:00:00Z',
74
+ body: '{"kind":"receipt"}'
75
+ }
76
+ ])
77
+ } as unknown as MessageBoxClient
78
+
79
+ const adapter = new RemittanceAdapter(messageBox)
80
+ const result = await adapter.listMessages({
81
+ messageBox: 'remittance_inbox',
82
+ host: 'https://remote-host'
83
+ })
84
+
85
+ expect(messageBox.listMessages).toHaveBeenCalledWith({
86
+ messageBox: 'remittance_inbox',
87
+ host: 'https://remote-host'
88
+ })
89
+ expect(result).toEqual([
90
+ {
91
+ messageId: 'm1',
92
+ sender: senderKey,
93
+ recipient: myIdentityKey,
94
+ messageBox: 'remittance_inbox',
95
+ body: '{"kind":"invoice","amount":1}'
96
+ },
97
+ {
98
+ messageId: 'm2',
99
+ sender: senderKey,
100
+ recipient: '020202020202020202020202020202020202020202020202020202020202020202',
101
+ messageBox: 'custom_box',
102
+ body: '{"kind":"receipt"}'
103
+ }
104
+ ])
105
+ })
106
+
107
+ it('forwards live listener setup and normalizes inbound message shape', async () => {
108
+ const onPaymentMessage = jest.fn()
109
+ let forwardedListener: ((msg: {
110
+ messageId: string
111
+ sender: string
112
+ body: unknown
113
+ created_at: string
114
+ updated_at: string
115
+ }) => void) | undefined
116
+
117
+ const messageBox = {
118
+ getIdentityKey: jest.fn<() => Promise<string>>().mockResolvedValue(myIdentityKey),
119
+ listenForLiveMessages: jest.fn().mockImplementation(async ({ onMessage }) => {
120
+ forwardedListener = onMessage
121
+ })
122
+ } as unknown as MessageBoxClient
123
+
124
+ const adapter = new RemittanceAdapter(messageBox)
125
+ await adapter.listenForLiveMessages({
126
+ messageBox: 'remittance_inbox',
127
+ overrideHost: 'https://ws-host',
128
+ onMessage: onPaymentMessage
129
+ })
130
+
131
+ expect(messageBox.listenForLiveMessages).toHaveBeenCalledWith({
132
+ messageBox: 'remittance_inbox',
133
+ overrideHost: 'https://ws-host',
134
+ onMessage: expect.any(Function)
135
+ })
136
+
137
+ forwardedListener?.({
138
+ messageId: 'live-1',
139
+ sender: senderKey,
140
+ created_at: '2026-01-01T00:00:00Z',
141
+ updated_at: '2026-01-01T00:00:00Z',
142
+ body: { kind: 'settlement' }
143
+ })
144
+
145
+ expect(onPaymentMessage).toHaveBeenCalledWith({
146
+ messageId: 'live-1',
147
+ sender: senderKey,
148
+ recipient: myIdentityKey,
149
+ messageBox: 'remittance_inbox',
150
+ body: '{"kind":"settlement"}'
151
+ })
152
+ })
153
+ })