@bsv/message-box-client 1.1.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.
@@ -0,0 +1,245 @@
1
+ /* eslint-env jest */
2
+ import { PeerPayClient } from '../PeerPayClient.js'
3
+ import { WalletClient, CreateHmacResult, PrivateKey } from '@bsv/sdk'
4
+ import { jest } from '@jest/globals'
5
+
6
+ const toArray = (msg: any, enc?: 'hex' | 'utf8' | 'base64'): any[] => {
7
+ if (Array.isArray(msg)) return msg.slice()
8
+ if (msg === undefined) return []
9
+
10
+ if (typeof msg !== 'string') {
11
+ return Array.from(msg, (item: any) => item | 0)
12
+ }
13
+
14
+ switch (enc) {
15
+ case 'hex': {
16
+ const matches = msg.match(/.{1,2}/g)
17
+ return matches != null ? matches.map(byte => parseInt(byte, 16)) : []
18
+ }
19
+ case 'base64':
20
+ return Array.from(Buffer.from(msg, 'base64'))
21
+ default:
22
+ return Array.from(Buffer.from(msg, 'utf8'))
23
+ }
24
+ }
25
+
26
+ // Mock dependencies
27
+ jest.mock('@bsv/sdk', () => {
28
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
29
+ const actualSDK = jest.requireActual('@bsv/sdk') as any
30
+
31
+ return {
32
+ ...actualSDK,
33
+ WalletClient: jest.fn().mockImplementation(() => ({
34
+ getPublicKey: jest.fn(),
35
+ createAction: jest.fn(),
36
+ internalizeAction: jest.fn(),
37
+ createHmac: jest.fn<() => Promise<CreateHmacResult>>().mockResolvedValue({
38
+ hmac: [1, 2, 3, 4, 5]
39
+ })
40
+ }))
41
+ }
42
+ })
43
+
44
+ describe('PeerPayClient Unit Tests', () => {
45
+ let peerPayClient: PeerPayClient
46
+ let mockWalletClient: jest.Mocked<WalletClient>
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks()
50
+
51
+ mockWalletClient = new WalletClient() as jest.Mocked<WalletClient>
52
+
53
+ // Ensure a valid compressed public key (33 bytes, hex format)
54
+ mockWalletClient.getPublicKey.mockResolvedValue({
55
+ publicKey: PrivateKey.fromRandom().toPublicKey().toString()
56
+ })
57
+
58
+ mockWalletClient.createAction.mockResolvedValue({
59
+ tx: toArray('mockedTransaction', 'utf8')
60
+ })
61
+
62
+ peerPayClient = new PeerPayClient({
63
+ messageBoxHost: 'https://messagebox.babbage.systems',
64
+ walletClient: mockWalletClient
65
+ })
66
+ })
67
+
68
+ describe('createPaymentToken', () => {
69
+ it('should create a valid payment token', async () => {
70
+ mockWalletClient.getPublicKey.mockResolvedValue({
71
+ publicKey: PrivateKey.fromRandom().toPublicKey().toString()
72
+ })
73
+ mockWalletClient.createAction.mockResolvedValue({ tx: toArray('mockedTransaction', 'utf8') })
74
+
75
+ const payment = { recipient: PrivateKey.fromRandom().toPublicKey().toString(), amount: 5 }
76
+ const token = await peerPayClient.createPaymentToken(payment)
77
+
78
+ expect(token).toHaveProperty('amount', 5)
79
+ expect(mockWalletClient.getPublicKey).toHaveBeenCalledWith(expect.any(Object))
80
+ expect(mockWalletClient.createAction).toHaveBeenCalledWith(expect.any(Object))
81
+ })
82
+
83
+ it('should throw an error if recipient public key cannot be derived', async () => {
84
+ mockWalletClient.getPublicKey.mockResolvedValue({ publicKey: '' }) // Empty key
85
+
86
+ await expect(peerPayClient.createPaymentToken({ recipient: 'invalid', amount: 5 }))
87
+ .rejects.toThrow('Failed to derive recipient’s public key')
88
+ })
89
+
90
+ it('should throw an error if amount is <= 0', async () => {
91
+ (mockWalletClient.getPublicKey as jest.MockedFunction<typeof mockWalletClient.getPublicKey>)
92
+ .mockResolvedValue({
93
+ publicKey: PrivateKey.fromRandom().toPublicKey().toString()
94
+ })
95
+
96
+ await expect(peerPayClient.createPaymentToken({
97
+ recipient: PrivateKey.fromRandom().toPublicKey().toString(),
98
+ amount: 0
99
+ }))
100
+ .rejects.toThrow('Invalid payment details: recipient and valid amount are required')
101
+ })
102
+ })
103
+
104
+ // Test: sendPayment
105
+ describe('sendPayment', () => {
106
+ it('should call sendMessage with valid payment', async () => {
107
+ const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
108
+ status: 'success',
109
+ messageId: 'mockedMessageId'
110
+ })
111
+
112
+ const payment = { recipient: 'recipientKey', amount: 3 }
113
+
114
+ console.log('[TEST] Calling sendPayment...')
115
+ await peerPayClient.sendPayment(payment)
116
+ console.log('[TEST] sendPayment finished.')
117
+
118
+ expect(sendMessageSpy).toHaveBeenCalledWith({
119
+ recipient: 'recipientKey',
120
+ messageBox: 'payment_inbox',
121
+ body: expect.any(String)
122
+ })
123
+ }, 10000)
124
+ })
125
+
126
+ // Test: sendLivePayment
127
+ describe('sendLivePayment', () => {
128
+ it('should call createPaymentToken and sendLiveMessage with correct parameters', async () => {
129
+ jest.spyOn(peerPayClient, 'createPaymentToken').mockResolvedValue({
130
+ customInstructions: {
131
+ derivationPrefix: 'prefix',
132
+ derivationSuffix: 'suffix'
133
+ },
134
+ transaction: Array.from(new Uint8Array([1, 2, 3, 4, 5])),
135
+ amount: 2
136
+ })
137
+
138
+ jest.spyOn(peerPayClient, 'sendLiveMessage').mockResolvedValue({
139
+ status: 'success',
140
+ messageId: 'mockedMessageId'
141
+ })
142
+
143
+ const payment = { recipient: 'recipientKey', amount: 2 }
144
+ await peerPayClient.sendLivePayment(payment)
145
+
146
+ expect(peerPayClient.createPaymentToken).toHaveBeenCalledWith(payment)
147
+ expect(peerPayClient.sendLiveMessage).toHaveBeenCalledWith({
148
+ recipient: 'recipientKey',
149
+ messageBox: 'payment_inbox',
150
+ body: "{\"customInstructions\":{\"derivationPrefix\":\"prefix\",\"derivationSuffix\":\"suffix\"},\"transaction\":[1,2,3,4,5],\"amount\":2}"
151
+ })
152
+ })
153
+ })
154
+
155
+ // Test: acceptPayment
156
+ describe('acceptPayment', () => {
157
+ it('should call internalizeAction and acknowledgeMessage', async () => {
158
+ mockWalletClient.internalizeAction.mockResolvedValue({ accepted: true })
159
+ jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
160
+
161
+ const payment = {
162
+ messageId: '123',
163
+ sender: 'senderKey',
164
+ token: {
165
+ customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
166
+ transaction: toArray('mockedTransaction', 'utf8'),
167
+ amount: 6
168
+ }
169
+ }
170
+
171
+ await peerPayClient.acceptPayment(payment)
172
+
173
+ expect(mockWalletClient.internalizeAction).toHaveBeenCalled()
174
+ expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({ messageIds: ['123'] })
175
+ })
176
+ })
177
+
178
+ // Test: rejectPayment
179
+ describe('rejectPayment', () => {
180
+ it('should refund payment minus fee', async () => {
181
+ jest.spyOn(peerPayClient, 'acceptPayment').mockResolvedValue(undefined)
182
+ jest.spyOn(peerPayClient, 'sendPayment').mockResolvedValue(undefined)
183
+ jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
184
+
185
+ const payment = {
186
+ messageId: '123',
187
+ sender: 'senderKey',
188
+ token: {
189
+ customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
190
+ transaction: toArray('mockedTransaction', 'utf8'),
191
+ amount: 2000
192
+ }
193
+ }
194
+
195
+ await peerPayClient.rejectPayment(payment)
196
+
197
+ expect(peerPayClient.acceptPayment).toHaveBeenCalledWith(payment)
198
+ expect(peerPayClient.sendPayment).toHaveBeenCalledWith({
199
+ recipient: 'senderKey',
200
+ amount: 1000 // Deduct satoshi fee
201
+ })
202
+ expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({
203
+ messageIds: ['123']
204
+ })
205
+ })
206
+ })
207
+
208
+ // Test: listIncomingPayments
209
+ describe('listIncomingPayments', () => {
210
+ it('should return parsed payment messages', async () => {
211
+ jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
212
+ {
213
+ messageId: '1',
214
+ sender: 'sender1',
215
+ created_at: '2025-03-05T12:00:00Z',
216
+ updated_at: '2025-03-05T12:05:00Z',
217
+ body: JSON.stringify({
218
+ customInstructions: { derivationPrefix: 'prefix1', derivationSuffix: 'suffix1' },
219
+ transaction: toArray('mockedTransaction1', 'utf8'),
220
+ amount: 3
221
+ })
222
+ },
223
+ {
224
+ messageId: '2',
225
+ sender: 'sender2',
226
+ created_at: '2025-03-05T12:10:00Z',
227
+ updated_at: '2025-03-05T12:15:00Z',
228
+ body: JSON.stringify({
229
+ customInstructions: { derivationPrefix: 'prefix2', derivationSuffix: 'suffix2' },
230
+ transaction: toArray('mockedTransaction2', 'utf8'),
231
+ amount: 9
232
+ })
233
+ }
234
+ ])
235
+
236
+ const payments = await peerPayClient.listIncomingPayments()
237
+
238
+ expect(payments).toHaveLength(2)
239
+ expect(payments[0]).toHaveProperty('sender', 'sender1')
240
+ expect(payments[0].token.amount).toBe(3)
241
+ expect(payments[1]).toHaveProperty('sender', 'sender2')
242
+ expect(payments[1].token.amount).toBe(9)
243
+ })
244
+ })
245
+ })
@@ -0,0 +1,103 @@
1
+ /* eslint-env jest */
2
+ import { MessageBoxClient } from '../../MessageBoxClient.js'
3
+ import { WalletClient } from '@bsv/sdk'
4
+ import { webcrypto } from 'crypto'
5
+ import { expect, test, describe, beforeAll } from '@jest/globals'
6
+
7
+ (global as any).self = { crypto: webcrypto }
8
+
9
+ jest.setTimeout(20000)
10
+
11
+ const walletClient = new WalletClient('json-api', 'https://messagebox.babbage.systems')
12
+ const messageBoxClient = new MessageBoxClient({
13
+ host: 'https://messagebox.babbage.systems',
14
+ walletClient,
15
+ enableLogging: true,
16
+ networkPreset: 'local'
17
+ })
18
+
19
+ let identityKey: string
20
+
21
+ describe('Encryption Integration Tests', () => {
22
+ const messageBox = 'testBox'
23
+ const plaintext = 'This is a secure test message.'
24
+
25
+ beforeAll(async () => {
26
+ const result = await walletClient.getPublicKey({ identityKey: true })
27
+ identityKey = result.publicKey
28
+
29
+ await messageBoxClient.initializeConnection()
30
+ })
31
+
32
+ test('encrypts and decrypts a message to self successfully', async () => {
33
+ // Encrypt
34
+ const { ciphertext } = await walletClient.encrypt({
35
+ plaintext: Array.from(new TextEncoder().encode(plaintext)),
36
+ protocolID: [1, 'messagebox'],
37
+ keyID: '1',
38
+ counterparty: 'self'
39
+ })
40
+
41
+ expect(Array.isArray(ciphertext)).toBe(true)
42
+ expect(ciphertext.length).toBeGreaterThan(0)
43
+
44
+ // Decrypt
45
+ const { plaintext: decryptedBytes } = await walletClient.decrypt({
46
+ ciphertext,
47
+ protocolID: [1, 'messagebox'],
48
+ keyID: '1',
49
+ counterparty: 'self'
50
+ })
51
+
52
+ const decrypted = new TextDecoder().decode(Uint8Array.from(decryptedBytes))
53
+ expect(decrypted).toBe(plaintext)
54
+ })
55
+
56
+ test('sends and receives encrypted message using MessageBoxClient', async () => {
57
+ // Send message to self
58
+ const sendResult = await messageBoxClient.sendMessage({
59
+ recipient: identityKey,
60
+ messageBox,
61
+ body: plaintext
62
+ })
63
+
64
+ expect(sendResult.status).toBe('success')
65
+ expect(typeof sendResult.messageId).toBe('string')
66
+
67
+ // List and decrypt
68
+ const messages = await messageBoxClient.listMessages({ messageBox })
69
+ const last = messages.at(-1)
70
+
71
+ expect(last).toBeDefined()
72
+ expect(last?.body).toBe(plaintext)
73
+ })
74
+
75
+ test('encrypted message is not stored or transmitted as plaintext', async () => {
76
+ // Send encrypted message to self
77
+ const sendResult = await messageBoxClient.sendMessage({
78
+ recipient: identityKey,
79
+ messageBox,
80
+ body: plaintext
81
+ })
82
+
83
+ expect(sendResult.status).toBe('success')
84
+
85
+ // Manually fetch raw HTTP response
86
+ const fetch = await messageBoxClient.authFetch.fetch(
87
+ 'https://messagebox.babbage.systems/listMessages',
88
+ {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ messageBox })
92
+ }
93
+ )
94
+
95
+ const raw = await fetch.json()
96
+ const rawBody = raw.messages.at(-1)?.body
97
+
98
+ expect(typeof rawBody).toBe('string')
99
+ const parsed = JSON.parse(rawBody)
100
+ expect(typeof parsed.encryptedMessage).toBe('string')
101
+ expect(parsed.encryptedMessage.includes(plaintext)).toBe(false)
102
+ })
103
+ })
@@ -0,0 +1,158 @@
1
+ /* eslint-env jest */
2
+ import { MessageBoxClient } from '../../MessageBoxClient.js'
3
+ import { WalletClient } from '@bsv/sdk'
4
+ import { webcrypto } from 'crypto'
5
+
6
+ // Ensure Jest doesn't mock WalletClient
7
+ jest.unmock('@bsv/sdk');
8
+
9
+ (global as any).self = { crypto: webcrypto }
10
+
11
+ jest.setTimeout(20000)
12
+
13
+ // Explicitly initialize WalletClient with Meta Net Client (MNC)
14
+ const walletClient = new WalletClient('json-api', 'localhost')
15
+
16
+ // Initialize MessageBoxClient for HTTP-Only Testing
17
+ const messageBoxClient = new MessageBoxClient({
18
+ host: 'https://messagebox.babbage.systems',
19
+ walletClient
20
+ })
21
+
22
+ describe('MessageBoxClient HTTP Integration Tests (No WebSocket)', () => {
23
+ let recipientKey: string
24
+ let testMessageId: string
25
+ const messageBox = 'testBox'
26
+ const testMessage = 'Hello, this is an integration test.'
27
+ const testMessage2 = 'Another test message to avoid duplicates.'
28
+
29
+ beforeAll(async () => {
30
+ try {
31
+ console.log('[DEBUG] Retrieving public key...')
32
+ const publicKeyResponse = await walletClient.getPublicKey({ identityKey: true })
33
+
34
+ if (
35
+ publicKeyResponse?.publicKey == null ||
36
+ typeof publicKeyResponse.publicKey !== 'string' ||
37
+ publicKeyResponse.publicKey.trim() === ''
38
+ ) {
39
+ throw new Error('[ERROR] getPublicKey returned an invalid key!')
40
+ }
41
+
42
+ recipientKey = publicKeyResponse.publicKey.trim()
43
+ console.log('[DEBUG] Successfully assigned recipientKey:', recipientKey)
44
+
45
+ // Ensure identity key is set internally in MessageBoxClient
46
+ await messageBoxClient.initializeConnection()
47
+ } catch (error) {
48
+ console.error('[ERROR] Failed to set up test:', error)
49
+ throw error
50
+ }
51
+ })
52
+
53
+ afterAll(async () => {
54
+ try {
55
+ if (testMessageId !== undefined && testMessageId !== '') {
56
+ console.log('[DEBUG] Cleaning up test messages...')
57
+ }
58
+ } catch (error) {
59
+ console.error('[ERROR] Failed to acknowledge test message:', error)
60
+ }
61
+ })
62
+
63
+ /** TEST 1: Send a Message with Correct Payment **/
64
+ test('should send a message successfully with correct payment', async () => {
65
+ const response = await messageBoxClient.sendMessage({
66
+ recipient: recipientKey,
67
+ messageBox,
68
+ body: testMessage,
69
+ skipEncryption: true // TEMPORARY to test if this fixes the 400
70
+ })
71
+
72
+ console.log('[DEBUG] SendMessage Response:', response)
73
+
74
+ expect(response).toHaveProperty('status', 'success')
75
+ expect(response).toHaveProperty('messageId', expect.any(String))
76
+
77
+ testMessageId = response.messageId // Store for cleanup
78
+ }, 30000)
79
+
80
+ /** TEST 2: List Messages **/
81
+ test('should list messages from messageBox', async () => {
82
+ const messages = await messageBoxClient.listMessages({ messageBox })
83
+ expect(messages.length).toBeGreaterThan(0)
84
+ expect(messages.some(msg => msg.body === testMessage)).toBe(true)
85
+ }, 15000)
86
+
87
+ /** TEST 3: List Messages from an Empty MessageBox **/
88
+ test('should return an empty list if no messages exist', async () => {
89
+ const messages = await messageBoxClient.listMessages({ messageBox: 'emptyBox' })
90
+ expect(messages).toEqual([])
91
+ }, 15000)
92
+
93
+ /** TEST 4: Acknowledge a Message **/
94
+ test('should acknowledge (delete) a message', async () => {
95
+ const ackResponse = await messageBoxClient.acknowledgeMessage({ messageIds: [testMessageId] })
96
+ expect(ackResponse).toBe('success')
97
+ }, 15000)
98
+
99
+ /** TEST 5: Acknowledge a Nonexistent Message **/
100
+ test('should fail to acknowledge a nonexistent message', async () => {
101
+ await expect(
102
+ messageBoxClient.acknowledgeMessage({ messageIds: ['fakeMessageId'] })
103
+ ).rejects.toThrow('Message not found!')
104
+ }, 15000)
105
+
106
+ /** TEST 6: Send Message with Invalid Recipient **/
107
+ test('should fail if recipient is invalid', async () => {
108
+ await expect(
109
+ messageBoxClient.sendMessage({
110
+ recipient: '', // Invalid recipient
111
+ messageBox,
112
+ body: testMessage
113
+ })
114
+ ).rejects.toThrow('You must provide a message recipient!')
115
+ }, 15000)
116
+
117
+ /** TEST 7: Send Message with Empty Body **/
118
+ test('should fail if message body is empty', async () => {
119
+ await expect(
120
+ messageBoxClient.sendMessage({
121
+ recipient: recipientKey,
122
+ messageBox,
123
+ body: '' // Empty message
124
+ })
125
+ ).rejects.toThrow('Every message must have a body!')
126
+ }, 15000)
127
+
128
+ /** TEST 8: Send Message with Excessive Payment (Should still succeed) **/
129
+ test('should send a message even if payment is more than required', async () => {
130
+ const response = await messageBoxClient.sendMessage({
131
+ recipient: recipientKey,
132
+ messageBox,
133
+ body: testMessage2,
134
+ skipEncryption: true
135
+ })
136
+
137
+ console.log('[DEBUG] Overpayment SendMessage Response:', response)
138
+
139
+ expect(response.status).toBe('success')
140
+ }, 15000)
141
+
142
+ /** TEST: Send a message without encryption **/
143
+ test('should send a message without encryption when skipEncryption is true', async () => {
144
+ const plaintextMessage = 'Unencrypted test message'
145
+ const response = await messageBoxClient.sendMessage({
146
+ recipient: recipientKey,
147
+ messageBox,
148
+ body: plaintextMessage,
149
+ skipEncryption: true // Bypass encryption
150
+ })
151
+
152
+ expect(response).toHaveProperty('status', 'success')
153
+ expect(response).toHaveProperty('messageId', expect.any(String))
154
+
155
+ const messages = await messageBoxClient.listMessages({ messageBox })
156
+ expect(messages.some(msg => msg.body === plaintextMessage)).toBe(true)
157
+ }, 30000)
158
+ })
@@ -0,0 +1,163 @@
1
+ /* eslint-env jest */
2
+ import { MessageBoxClient } from '../../MessageBoxClient.js'
3
+ import { WalletClient } from '@bsv/sdk'
4
+ import { webcrypto } from 'crypto'
5
+
6
+ (global as any).self = { crypto: webcrypto }
7
+
8
+ jest.setTimeout(20000)
9
+
10
+ const MESSAGEBOX_HOST = 'http://localhost:5001'
11
+
12
+ const walletA = new WalletClient('json-api', 'localhost')
13
+ const walletB = new WalletClient('json-api', 'localhost')
14
+
15
+ const clientA = new MessageBoxClient({
16
+ host: MESSAGEBOX_HOST,
17
+ walletClient: walletA,
18
+ networkPreset: 'local',
19
+ enableLogging: true
20
+ })
21
+
22
+ const clientB = new MessageBoxClient({
23
+ host: MESSAGEBOX_HOST,
24
+ walletClient: walletB,
25
+ networkPreset: 'local',
26
+ enableLogging: true
27
+ })
28
+
29
+ let identityKeyA: string
30
+ let identityKeyB: string
31
+
32
+ beforeAll(async () => {
33
+ identityKeyA = (await walletA.getPublicKey({ identityKey: true })).publicKey
34
+ identityKeyB = (await walletB.getPublicKey({ identityKey: true })).publicKey
35
+ await clientA.initializeConnection()
36
+ await clientB.initializeConnection()
37
+ })
38
+
39
+ afterAll(async () => {
40
+ await clientA.disconnectWebSocket()
41
+ await clientB.disconnectWebSocket()
42
+ })
43
+
44
+ describe('Overlay Integration Tests', () => {
45
+ const selfBox = 'overlay_self_box'
46
+ const peerBox = 'forwarded_overlay_box'
47
+
48
+ test('clientA broadcasts overlay advertisement', async () => {
49
+ const result = await clientA.anointHost(MESSAGEBOX_HOST)
50
+ expect(result).toHaveProperty('txid')
51
+ await new Promise(resolve => setTimeout(resolve, 3000))
52
+ })
53
+
54
+ test('clientA resolves own host via overlay', async () => {
55
+ const resolved = await (clientA as any).resolveHostForRecipient(identityKeyA)
56
+ console.log('[TEST] Resolved host:', resolved)
57
+ expect(resolved).toBe(MESSAGEBOX_HOST)
58
+ })
59
+
60
+ test('clientA sends message to self via overlay', async () => {
61
+ const response = await clientA.sendMessage({
62
+ recipient: identityKeyA,
63
+ messageBox: selfBox,
64
+ body: 'hello via overlay'
65
+ })
66
+ expect(response.status).toBe('success')
67
+ })
68
+
69
+ test('clientA lists self messages via overlay', async () => {
70
+ const messages = await clientA.listMessages({ messageBox: selfBox })
71
+ expect(messages.length).toBeGreaterThan(0)
72
+ expect(messages.at(-1)?.body).toContain('hello via overlay')
73
+ })
74
+
75
+ test('clientA acknowledges self messages via overlay', async () => {
76
+ const messages = await clientA.listMessages({ messageBox: selfBox })
77
+ const ids = messages.map(m => m.messageId).filter(Boolean)
78
+ expect(ids.length).toBeGreaterThan(0)
79
+ const status = await clientA.acknowledgeMessage({ messageIds: ids })
80
+ expect(status).toBe('success')
81
+ })
82
+
83
+ test('clientB broadcasts overlay advertisement', async () => {
84
+ const result = await clientB.anointHost(MESSAGEBOX_HOST)
85
+ expect(result).toHaveProperty('txid')
86
+ await new Promise(resolve => setTimeout(resolve, 3000))
87
+ })
88
+
89
+ test('clientA sends message to clientB via overlay', async () => {
90
+ const response = await clientA.sendMessage({
91
+ recipient: identityKeyB,
92
+ messageBox: peerBox,
93
+ body: 'delivered to peer via overlay'
94
+ })
95
+ expect(response.status).toBe('success')
96
+ })
97
+
98
+ test('clientB receives overlay message from clientA', async () => {
99
+ const messages = await clientB.listMessages({ messageBox: peerBox })
100
+ expect(messages.some(m => m.body.includes('delivered to peer via overlay'))).toBe(true)
101
+ })
102
+
103
+ test('clientB acknowledges overlay message from clientA', async () => {
104
+ const messages = await clientB.listMessages({ messageBox: peerBox })
105
+ const ids = messages.map(m => m.messageId).filter(Boolean)
106
+ const result = await clientB.acknowledgeMessage({ messageIds: ids })
107
+ expect(result).toBe('success')
108
+ })
109
+
110
+ test('clientA verifies clientB host resolution', async () => {
111
+ const resolved = await (clientA as any).resolveHostForRecipient(identityKeyB)
112
+ expect(resolved).toBe(MESSAGEBOX_HOST)
113
+ })
114
+
115
+ test('overlay advertisement is idempotent', async () => {
116
+ const result1 = await clientA.anointHost(MESSAGEBOX_HOST)
117
+ const result2 = await clientA.anointHost(MESSAGEBOX_HOST)
118
+ expect(result1.txid).not.toBe(result2.txid)
119
+ })
120
+
121
+ test('clientA sends and acknowledges multiple messages to clientB', async () => {
122
+ const contents = ['msg1', 'msg2', 'msg3']
123
+ for (const msg of contents) {
124
+ const response = await clientA.sendMessage({
125
+ recipient: identityKeyB,
126
+ messageBox: peerBox,
127
+ body: msg
128
+ })
129
+ expect(response.status).toBe('success')
130
+ }
131
+
132
+ await new Promise(resolve => setTimeout(resolve, 1000))
133
+ const messages = await clientB.listMessages({ messageBox: peerBox })
134
+ const matched = contents.every(c => messages.some(m => m.body.includes(c)))
135
+ expect(matched).toBe(true)
136
+
137
+ const ids = messages.map(m => m.messageId).filter(Boolean)
138
+ const status = await clientB.acknowledgeMessage({ messageIds: ids })
139
+ expect(status).toBe('success')
140
+ })
141
+
142
+ test('clientA reinitializes with init() and correctly anoints host', async () => {
143
+ const tempClient = new MessageBoxClient({
144
+ walletClient: walletA,
145
+ networkPreset: 'local',
146
+ enableLogging: true
147
+ // No host provided here!
148
+ })
149
+
150
+ // Call init manually
151
+ await tempClient.init(MESSAGEBOX_HOST)
152
+
153
+ // Verify client is initialized and host is correct
154
+ expect((tempClient as any).initialized).toBe(true)
155
+ expect((tempClient as any).host).toBe(MESSAGEBOX_HOST)
156
+
157
+ // Optionally, test that resolving our own identity also works
158
+ const resolvedHost = await (tempClient as any).resolveHostForRecipient(identityKeyA)
159
+ expect(resolvedHost).toBe(MESSAGEBOX_HOST)
160
+
161
+ await tempClient.disconnectWebSocket()
162
+ })
163
+ })