@bsv/message-box-client 2.0.2 → 2.0.4
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/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/MessageBoxClient.js +119 -30
- package/dist/cjs/src/MessageBoxClient.js.map +1 -1
- package/dist/cjs/src/RemittanceAdapter.js +137 -0
- package/dist/cjs/src/RemittanceAdapter.js.map +1 -0
- package/dist/cjs/src/__tests/RemittanceAdapter.test.js +133 -0
- package/dist/cjs/src/__tests/RemittanceAdapter.test.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/MessageBoxClient.js +119 -30
- package/dist/esm/src/MessageBoxClient.js.map +1 -1
- package/dist/esm/src/RemittanceAdapter.js +133 -0
- package/dist/esm/src/RemittanceAdapter.js.map +1 -0
- package/dist/esm/src/__tests/RemittanceAdapter.test.js +131 -0
- package/dist/esm/src/__tests/RemittanceAdapter.test.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/MessageBoxClient.d.ts +2 -0
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
- package/dist/types/src/RemittanceAdapter.d.ts +103 -0
- package/dist/types/src/RemittanceAdapter.d.ts.map +1 -0
- package/dist/types/src/__tests/RemittanceAdapter.test.d.ts +2 -0
- package/dist/types/src/__tests/RemittanceAdapter.test.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/mod.ts +1 -0
- package/package.json +1 -1
- package/src/MessageBoxClient.ts +148 -52
- package/src/RemittanceAdapter.ts +164 -0
- package/src/__tests/RemittanceAdapter.test.ts +153 -0
package/mod.ts
CHANGED
package/package.json
CHANGED
package/src/MessageBoxClient.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
/**
|
|
@@ -453,7 +521,29 @@ export class MessageBoxClient {
|
|
|
453
521
|
} catch (err) {
|
|
454
522
|
Logger.error('[MB CLIENT ERROR] _queryAdvertisements failed:', err)
|
|
455
523
|
}
|
|
456
|
-
return hosts
|
|
524
|
+
return hosts.filter(item => {
|
|
525
|
+
const h = item.host.trim().toLowerCase()
|
|
526
|
+
if (!h) return false
|
|
527
|
+
try {
|
|
528
|
+
const url = new URL(h)
|
|
529
|
+
if (url.protocol !== 'https:') return false
|
|
530
|
+
const hostname = url.hostname
|
|
531
|
+
if (
|
|
532
|
+
hostname === 'localhost' ||
|
|
533
|
+
hostname === '127.0.0.1' ||
|
|
534
|
+
hostname === '0.0.0.0' ||
|
|
535
|
+
hostname === '::1' ||
|
|
536
|
+
hostname.endsWith('.local') ||
|
|
537
|
+
hostname.endsWith('.example.com') ||
|
|
538
|
+
hostname.endsWith('.test') ||
|
|
539
|
+
hostname.endsWith('.invalid') ||
|
|
540
|
+
hostname.endsWith('.localhost')
|
|
541
|
+
) return false
|
|
542
|
+
return true
|
|
543
|
+
} catch {
|
|
544
|
+
return false
|
|
545
|
+
}
|
|
546
|
+
})
|
|
457
547
|
}
|
|
458
548
|
|
|
459
549
|
/**
|
|
@@ -700,10 +790,15 @@ export class MessageBoxClient {
|
|
|
700
790
|
return await new Promise((resolve, reject) => {
|
|
701
791
|
const ackEvent = `sendMessageAck-${roomId}`
|
|
702
792
|
let handled = false
|
|
793
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
703
794
|
|
|
704
795
|
const ackHandler = (response?: SendMessageResponse): void => {
|
|
705
796
|
if (handled) return
|
|
706
797
|
handled = true
|
|
798
|
+
if (timeoutId != null) {
|
|
799
|
+
clearTimeout(timeoutId)
|
|
800
|
+
timeoutId = undefined
|
|
801
|
+
}
|
|
707
802
|
|
|
708
803
|
const socketAny = this.socket as any
|
|
709
804
|
if (typeof socketAny?.off === 'function') {
|
|
@@ -749,9 +844,10 @@ export class MessageBoxClient {
|
|
|
749
844
|
})
|
|
750
845
|
|
|
751
846
|
// Timeout: Fallback to HTTP if no acknowledgment received
|
|
752
|
-
setTimeout(() => {
|
|
847
|
+
timeoutId = setTimeout(() => {
|
|
753
848
|
if (!handled) {
|
|
754
849
|
handled = true
|
|
850
|
+
timeoutId = undefined
|
|
755
851
|
const socketAny = this.socket as any
|
|
756
852
|
if (typeof socketAny?.off === 'function') {
|
|
757
853
|
socketAny.off(ackEvent, ackHandler)
|
|
@@ -1063,14 +1159,14 @@ export class MessageBoxClient {
|
|
|
1063
1159
|
throw new Error('Missing delivery agent identity keys in quote response.')
|
|
1064
1160
|
}
|
|
1065
1161
|
if (Object.keys(deliveryAgentIdentityKeyByHost).length > 1 && !overrideHost) {
|
|
1066
|
-
|
|
1162
|
+
// To keep the single-POST invariant, we require all recipients to share a host
|
|
1067
1163
|
throw new Error('Recipients resolve to multiple hosts. Use overrideHost to force a single server or split by host.')
|
|
1068
1164
|
}
|
|
1069
1165
|
|
|
1070
1166
|
// pick the host to POST to
|
|
1071
1167
|
const finalHost = (overrideHost ?? await this.resolveHostForRecipient(allowedRecipients[0])).replace(/\/+$/, '')
|
|
1072
1168
|
const singleDeliveryKey = deliveryAgentIdentityKeyByHost[finalHost] ??
|
|
1073
|
-
|
|
1169
|
+
Object.values(deliveryAgentIdentityKeyByHost)[0]
|
|
1074
1170
|
|
|
1075
1171
|
if (!singleDeliveryKey) {
|
|
1076
1172
|
throw new Error('Could not determine server delivery agent identity key.')
|
|
@@ -1100,7 +1196,7 @@ export class MessageBoxClient {
|
|
|
1100
1196
|
if (skipEncryption === true) {
|
|
1101
1197
|
finalBody = typeof body === 'string' ? body : JSON.stringify(body)
|
|
1102
1198
|
} else {
|
|
1103
|
-
|
|
1199
|
+
// safest for now: send plaintext; the recipients can decrypt payload fields client-side if needed
|
|
1104
1200
|
finalBody = typeof body === 'string' ? body : JSON.stringify(body)
|
|
1105
1201
|
}
|
|
1106
1202
|
|
|
@@ -1143,18 +1239,18 @@ export class MessageBoxClient {
|
|
|
1143
1239
|
const failed: Array<{ recipient: string, error: string }> = [] // handled server-side now
|
|
1144
1240
|
|
|
1145
1241
|
const status: SendListResult['status'] =
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1242
|
+
sent.length === allowedRecipients.length
|
|
1243
|
+
? 'success'
|
|
1244
|
+
: sent.length > 0
|
|
1245
|
+
? 'partial'
|
|
1246
|
+
: 'error'
|
|
1151
1247
|
|
|
1152
1248
|
const description =
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1249
|
+
status === 'success'
|
|
1250
|
+
? `Sent to ${sent.length} recipients.`
|
|
1251
|
+
: status === 'partial'
|
|
1252
|
+
? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
|
|
1253
|
+
: `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`
|
|
1158
1254
|
|
|
1159
1255
|
return { status, description, sent, blocked, failed, totals }
|
|
1160
1256
|
} catch (err) {
|
|
@@ -1681,7 +1777,7 @@ export class MessageBoxClient {
|
|
|
1681
1777
|
}
|
|
1682
1778
|
}
|
|
1683
1779
|
|
|
1684
|
-
private async mapWithConcurrency<T, R>
|
|
1780
|
+
private async mapWithConcurrency<T, R>(
|
|
1685
1781
|
items: T[],
|
|
1686
1782
|
limit: number,
|
|
1687
1783
|
fn: (item: T, index: number) => Promise<R>
|
|
@@ -1997,7 +2093,7 @@ export class MessageBoxClient {
|
|
|
1997
2093
|
params: GetQuoteParams,
|
|
1998
2094
|
overrideHost?: string
|
|
1999
2095
|
): Promise<MessageBoxQuote | MessageBoxMultiQuote> {
|
|
2000
|
-
|
|
2096
|
+
// ---------- SINGLE RECIPIENT (back-compat) ----------
|
|
2001
2097
|
if (!Array.isArray(params.recipient)) {
|
|
2002
2098
|
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
|
|
2003
2099
|
const queryParams = new URLSearchParams({
|
|
@@ -2008,14 +2104,14 @@ export class MessageBoxClient {
|
|
|
2008
2104
|
Logger.log('[MB CLIENT] Getting messageBox quote (single)...')
|
|
2009
2105
|
console.log('HELP IM QUOTING', `${finalHost}/permissions/quote?${queryParams.toString()}`)
|
|
2010
2106
|
const response = await this.authFetch.fetch(
|
|
2011
|
-
|
|
2012
|
-
|
|
2107
|
+
`${finalHost}/permissions/quote?${queryParams.toString()}`,
|
|
2108
|
+
{ method: 'GET' }
|
|
2013
2109
|
)
|
|
2014
2110
|
console.log('server response from getquote]', response)
|
|
2015
2111
|
if (!response.ok) {
|
|
2016
2112
|
const errorData = await response.json().catch(() => ({}))
|
|
2017
2113
|
throw new Error(
|
|
2018
|
-
|
|
2114
|
+
`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
|
|
2019
2115
|
)
|
|
2020
2116
|
}
|
|
2021
2117
|
|
|
@@ -2088,7 +2184,7 @@ export class MessageBoxClient {
|
|
|
2088
2184
|
if (!resp.ok) {
|
|
2089
2185
|
const errorData = await resp.json().catch(() => ({}))
|
|
2090
2186
|
throw new Error(
|
|
2091
|
-
|
|
2187
|
+
`Failed to get quote (host ${host}): HTTP ${resp.status} - ${String(errorData.description) ?? resp.statusText}`
|
|
2092
2188
|
)
|
|
2093
2189
|
}
|
|
2094
2190
|
|
|
@@ -2103,7 +2199,7 @@ export class MessageBoxClient {
|
|
|
2103
2199
|
// Server supports both shapes. For multi we expect:
|
|
2104
2200
|
// { quotesByRecipient, totals, blockedRecipients }
|
|
2105
2201
|
if (Array.isArray(payload?.quotesByRecipient)) {
|
|
2106
|
-
|
|
2202
|
+
// merge quotes
|
|
2107
2203
|
for (const q of payload.quotesByRecipient) {
|
|
2108
2204
|
quotesByRecipient.push({
|
|
2109
2205
|
recipient: q.recipient,
|
|
@@ -2129,12 +2225,12 @@ export class MessageBoxClient {
|
|
|
2129
2225
|
}
|
|
2130
2226
|
}
|
|
2131
2227
|
} else if (payload?.quote) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2228
|
+
// Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
|
|
2229
|
+
// we map it to each recipient in the group uniformly.
|
|
2134
2230
|
for (const r of groupRecipients) {
|
|
2135
2231
|
const { deliveryFee, recipientFee } = payload.quote
|
|
2136
2232
|
const status =
|
|
2137
|
-
|
|
2233
|
+
recipientFee === -1 ? 'blocked' : recipientFee === 0 ? 'always_allow' : 'payment_required'
|
|
2138
2234
|
quotesByRecipient.push({
|
|
2139
2235
|
recipient: r,
|
|
2140
2236
|
messageBox: params.messageBox,
|
|
@@ -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
|
+
})
|