@bsv/message-box-client 1.4.5 → 2.0.1

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.
@@ -53,7 +53,7 @@ import {
53
53
  ListPermissionsParams,
54
54
  GetQuoteParams,
55
55
  SendListParams,
56
- SendListResult,
56
+ SendListResult
57
57
  } from './types/permissions.js'
58
58
 
59
59
  const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems'
@@ -1002,172 +1002,174 @@ export class MessageBoxClient {
1002
1002
  }
1003
1003
  }
1004
1004
 
1005
-
1006
1005
  /**
1007
1006
  * Multi-recipient sender. Uses the multi-quote route to:
1008
1007
  * - identify blocked recipients
1009
1008
  * - compute per-recipient payment
1010
1009
  * Then sends to the allowed recipients with payment attached.
1011
1010
  */
1012
- async sendMesagetoRecepients(
1013
- params: SendListParams,
1014
- overrideHost?: string
1015
- ): Promise<SendListResult> {
1016
- await this.assertInitialized()
1017
-
1018
- const { recipients, messageBox, body, skipEncryption } = params
1019
- if (!Array.isArray(recipients) || recipients.length === 0) {
1020
- throw new Error('You must provide at least one recipient!')
1021
- }
1022
- if (!messageBox || messageBox.trim() === '') {
1023
- throw new Error('You must provide a messageBox to send this message into!')
1024
- }
1025
- if (body == null || (typeof body === 'string' && body.trim().length === 0)) {
1026
- throw new Error('Every message must have a body!')
1027
- }
1028
-
1029
- // 1) Multi-quote for all recipients
1030
- const quoteResponse = await this.getMessageBoxQuote({
1031
- recipient: recipients,
1032
- messageBox
1033
- }, overrideHost) as MessageBoxMultiQuote
1034
-
1035
- const quotesByRecipient = Array.isArray(quoteResponse?.quotesByRecipient)
1036
- ? quoteResponse.quotesByRecipient : []
1011
+ async sendMesagetoRecepients (
1012
+ params: SendListParams,
1013
+ overrideHost?: string
1014
+ ): Promise<SendListResult> {
1015
+ await this.assertInitialized()
1037
1016
 
1038
- const blocked = (quoteResponse?.blockedRecipients ?? []) as string[]
1039
- const totals = quoteResponse?.totals
1017
+ const { recipients, messageBox, body, skipEncryption } = params
1018
+ if (!Array.isArray(recipients) || recipients.length === 0) {
1019
+ throw new Error('You must provide at least one recipient!')
1020
+ }
1021
+ if (!messageBox || messageBox.trim() === '') {
1022
+ throw new Error('You must provide a messageBox to send this message into!')
1023
+ }
1024
+ if (body == null || (typeof body === 'string' && body.trim().length === 0)) {
1025
+ throw new Error('Every message must have a body!')
1026
+ }
1040
1027
 
1041
- // 2) Filter allowed recipients
1042
- const allowedRecipients = recipients.filter(r => !blocked.includes(r))
1043
- if (allowedRecipients.length === 0) {
1044
- return {
1045
- status: 'error',
1046
- description: `All ${recipients.length} recipients are blocked.`,
1047
- sent: [],
1048
- blocked,
1049
- failed: recipients.map(r => ({ recipient: r, error: 'blocked' })),
1050
- totals
1028
+ // 1) Multi-quote for all recipients
1029
+ const quoteResponse = await this.getMessageBoxQuote({
1030
+ recipient: recipients,
1031
+ messageBox
1032
+ }, overrideHost) as MessageBoxMultiQuote
1033
+
1034
+ const quotesByRecipient = Array.isArray(quoteResponse?.quotesByRecipient)
1035
+ ? quoteResponse.quotesByRecipient
1036
+ : []
1037
+
1038
+ const blocked = (quoteResponse?.blockedRecipients ?? [])
1039
+ const totals = quoteResponse?.totals
1040
+
1041
+ // 2) Filter allowed recipients
1042
+ const allowedRecipients = recipients.filter(r => !blocked.includes(r))
1043
+ if (allowedRecipients.length === 0) {
1044
+ return {
1045
+ status: 'error',
1046
+ description: `All ${recipients.length} recipients are blocked.`,
1047
+ sent: [],
1048
+ blocked,
1049
+ failed: recipients.map(r => ({ recipient: r, error: 'blocked' })),
1050
+ totals
1051
+ }
1051
1052
  }
1052
- }
1053
1053
 
1054
- // 3) Map recipient -> fees
1055
- const perRecipientQuotes = new Map<string, { recipientFee: number, deliveryFee: number }>()
1056
- for (const q of quotesByRecipient) {
1057
- perRecipientQuotes.set(q.recipient, { recipientFee: q.recipientFee, deliveryFee: q.deliveryFee })
1058
- }
1054
+ // 3) Map recipient -> fees
1055
+ const perRecipientQuotes = new Map<string, { recipientFee: number, deliveryFee: number }>()
1056
+ for (const q of quotesByRecipient) {
1057
+ perRecipientQuotes.set(q.recipient, { recipientFee: q.recipientFee, deliveryFee: q.deliveryFee })
1058
+ }
1059
1059
 
1060
- // 4) One delivery agent only (batch goes to one server)
1061
- const { deliveryAgentIdentityKeyByHost } = quoteResponse
1062
- if (!deliveryAgentIdentityKeyByHost || Object.keys(deliveryAgentIdentityKeyByHost).length === 0) {
1063
- throw new Error('Missing delivery agent identity keys in quote response.')
1064
- }
1065
- if (Object.keys(deliveryAgentIdentityKeyByHost).length > 1 && !overrideHost) {
1060
+ // 4) One delivery agent only (batch goes to one server)
1061
+ const { deliveryAgentIdentityKeyByHost } = quoteResponse
1062
+ if (!deliveryAgentIdentityKeyByHost || Object.keys(deliveryAgentIdentityKeyByHost).length === 0) {
1063
+ throw new Error('Missing delivery agent identity keys in quote response.')
1064
+ }
1065
+ if (Object.keys(deliveryAgentIdentityKeyByHost).length > 1 && !overrideHost) {
1066
1066
  // To keep the single-POST invariant, we require all recipients to share a host
1067
- throw new Error('Recipients resolve to multiple hosts. Use overrideHost to force a single server or split by host.')
1068
- }
1067
+ throw new Error('Recipients resolve to multiple hosts. Use overrideHost to force a single server or split by host.')
1068
+ }
1069
1069
 
1070
- // pick the host to POST to
1071
- const finalHost = (overrideHost ?? await this.resolveHostForRecipient(allowedRecipients[0])).replace(/\/+$/,'')
1072
- const singleDeliveryKey = deliveryAgentIdentityKeyByHost[finalHost]
1073
- ?? Object.values(deliveryAgentIdentityKeyByHost)[0]
1070
+ // pick the host to POST to
1071
+ const finalHost = (overrideHost ?? await this.resolveHostForRecipient(allowedRecipients[0])).replace(/\/+$/, '')
1072
+ const singleDeliveryKey = deliveryAgentIdentityKeyByHost[finalHost] ??
1073
+ Object.values(deliveryAgentIdentityKeyByHost)[0]
1074
1074
 
1075
- if (!singleDeliveryKey) {
1076
- throw new Error('Could not determine server delivery agent identity key.')
1077
- }
1075
+ if (!singleDeliveryKey) {
1076
+ throw new Error('Could not determine server delivery agent identity key.')
1077
+ }
1078
1078
 
1079
- // 5) Identity key (sender)
1080
- if (!this.myIdentityKey) {
1081
- const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, this.originator)
1082
- this.myIdentityKey = keyResult.publicKey
1083
- }
1079
+ // 5) Identity key (sender)
1080
+ if (!this.myIdentityKey) {
1081
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, this.originator)
1082
+ this.myIdentityKey = keyResult.publicKey
1083
+ }
1084
1084
 
1085
- // 6) Build per-recipient messageIds (HMAC), same order as allowedRecipients
1086
- const messageIds: string[] = []
1087
- for (const r of allowedRecipients) {
1088
- const hmac = await this.walletClient.createHmac({
1089
- data: Array.from(new TextEncoder().encode(JSON.stringify(body))),
1090
- protocolID: [1, 'messagebox'],
1091
- keyID: '1',
1092
- counterparty: r
1093
- }, this.originator)
1094
- const mid = Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('')
1095
- messageIds.push(mid)
1096
- }
1085
+ // 6) Build per-recipient messageIds (HMAC), same order as allowedRecipients
1086
+ const bodyBytes = Array.from(new TextEncoder().encode(JSON.stringify(body)))
1087
+ const messageIds: string[] = await this.mapWithConcurrency(allowedRecipients, 8, async (r) => {
1088
+ const hmac = await this.walletClient.createHmac({
1089
+ data: bodyBytes,
1090
+ protocolID: [1, 'messagebox'],
1091
+ keyID: '1',
1092
+ counterparty: r
1093
+ }, this.originator)
1094
+ return Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('')
1095
+ })
1097
1096
 
1098
- // 7) Body: for batch route the server expects a single shared body
1099
- // NOTE: If you need per-recipient encryption, we must change the server payload shape.
1100
- let finalBody: string
1101
- if (skipEncryption === true) {
1102
- finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1103
- } else {
1097
+ // 7) Body: for batch route the server expects a single shared body
1098
+ // NOTE: If you need per-recipient encryption, we must change the server payload shape.
1099
+ let finalBody: string
1100
+ if (skipEncryption === true) {
1101
+ finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1102
+ } else {
1104
1103
  // safest for now: send plaintext; the recipients can decrypt payload fields client-side if needed
1105
- finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1106
- }
1104
+ finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1105
+ }
1107
1106
 
1108
- // 8) ONE batch payment with server output at index 0
1109
- const paymentData = await this.createMessagePaymentBatch(
1110
- allowedRecipients,
1111
- perRecipientQuotes,
1112
- singleDeliveryKey
1113
- )
1114
-
1115
- // 9) Single POST to /sendMessage with recipients[] + messageId[]
1116
- const requestBody = {
1117
- message: {
1118
- recipients: allowedRecipients,
1119
- messageBox,
1120
- messageId: messageIds, // aligned by index with recipients
1121
- body: finalBody
1122
- },
1123
- payment: paymentData
1124
- }
1107
+ // 8) ONE batch payment with server output at index 0
1108
+ const paymentData = await this.createMessagePaymentBatch(
1109
+ allowedRecipients,
1110
+ perRecipientQuotes,
1111
+ singleDeliveryKey
1112
+ )
1125
1113
 
1126
- Logger.log('[MB CLIENT] Sending HTTP request to:', `${finalHost}/sendMessage`)
1127
- Logger.log('[MB CLIENT] Request Body (batch):', JSON.stringify({ ...requestBody, payment: { ...paymentData, tx: '<omitted>' } }, null, 2))
1114
+ // 9) Single POST to /sendMessage with recipients[] + messageId[]
1115
+ const requestBody = {
1116
+ message: {
1117
+ recipients: allowedRecipients,
1118
+ messageBox,
1119
+ messageId: messageIds, // aligned by index with recipients
1120
+ body: finalBody
1121
+ },
1122
+ payment: paymentData
1123
+ }
1128
1124
 
1129
- try {
1130
- const response = await this.authFetch.fetch(`${finalHost}/sendMessage`, {
1131
- method: 'POST',
1132
- headers: { 'Content-Type': 'application/json' },
1133
- body: JSON.stringify(requestBody)
1134
- })
1125
+ Logger.log('[MB CLIENT] Sending HTTP request to:', `${finalHost}/sendMessage`)
1126
+ Logger.log('[MB CLIENT] Request Body (batch):', JSON.stringify({ ...requestBody, payment: { ...paymentData, tx: '<omitted>' } }, null, 2))
1135
1127
 
1136
- const parsed = await response.json().catch(() => ({} as any))
1137
- if (!response.ok || parsed.status !== 'success') {
1138
- const msg = !response.ok ? `HTTP ${response.status} - ${response.statusText}` : (parsed.description ?? 'Unknown server error')
1139
- throw new Error(msg)
1140
- }
1128
+ try {
1129
+ const response = await this.authFetch.fetch(`${finalHost}/sendMessage`, {
1130
+ method: 'POST',
1131
+ headers: { 'Content-Type': 'application/json' },
1132
+ body: JSON.stringify(requestBody)
1133
+ })
1141
1134
 
1142
- // server returns { results: [{ recipient, messageId }] }
1143
- const sent = Array.isArray(parsed.results) ? parsed.results : []
1144
- const failed: Array<{ recipient: string, error: string }> = [] // handled server-side now
1135
+ const parsed = await response.json().catch(() => ({} as any))
1136
+ if (!response.ok || parsed.status !== 'success') {
1137
+ const msg = !response.ok ? `HTTP ${response.status} - ${response.statusText}` : (parsed.description ?? 'Unknown server error')
1138
+ throw new Error(msg)
1139
+ }
1145
1140
 
1146
- const status: SendListResult['status'] =
1147
- sent.length === allowedRecipients.length ? 'success'
1148
- : sent.length > 0 ? 'partial'
1149
- : 'error'
1141
+ // server returns { results: [{ recipient, messageId }] }
1142
+ const sent = Array.isArray(parsed.results) ? parsed.results : []
1143
+ const failed: Array<{ recipient: string, error: string }> = [] // handled server-side now
1150
1144
 
1151
- const description =
1145
+ const status: SendListResult['status'] =
1146
+ sent.length === allowedRecipients.length
1147
+ ? 'success'
1148
+ : sent.length > 0
1149
+ ? 'partial'
1150
+ : 'error'
1151
+
1152
+ const description =
1152
1153
  status === 'success'
1153
1154
  ? `Sent to ${sent.length} recipients.`
1154
1155
  : status === 'partial'
1155
1156
  ? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
1156
1157
  : `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`
1157
1158
 
1158
- return { status, description, sent, blocked, failed, totals }
1159
- } catch (err) {
1160
- const msg = err instanceof Error ? err.message : 'Unknown error'
1161
- return {
1162
- status: 'error',
1163
- description: `Batch send failed: ${msg}`,
1164
- sent: [],
1165
- blocked,
1166
- failed: allowedRecipients.map(r => ({ recipient: r, error: msg })),
1167
- totals
1159
+ return { status, description, sent, blocked, failed, totals }
1160
+ } catch (err) {
1161
+ const msg = err instanceof Error ? err.message : 'Unknown error'
1162
+ return {
1163
+ status: 'error',
1164
+ description: `Batch send failed: ${msg}`,
1165
+ sent: [],
1166
+ blocked,
1167
+ failed: allowedRecipients.map(r => ({ recipient: r, error: msg })),
1168
+ totals
1169
+ }
1168
1170
  }
1169
1171
  }
1170
- }
1172
+
1171
1173
  /**
1172
1174
  * @method anointHost
1173
1175
  * @async
@@ -1444,108 +1446,112 @@ async sendMesagetoRecepients(
1444
1446
 
1445
1447
  const messages: PeerMessage[] = Array.from(dedupMap.values())
1446
1448
 
1447
- for (const message of messages) {
1448
- try {
1449
- const parsedBody: unknown =
1450
- typeof message.body === 'string' ? this.tryParse(message.body) : message.body
1449
+ const parsed = messages.map(message => {
1450
+ const parsedBody: unknown =
1451
+ typeof message.body === 'string' ? this.tryParse(message.body) : message.body
1452
+
1453
+ let messageContent: any = parsedBody
1454
+ let paymentData: Payment | undefined
1455
+
1456
+ if (
1457
+ parsedBody != null &&
1458
+ typeof parsedBody === 'object' &&
1459
+ 'message' in parsedBody
1460
+ ) {
1461
+ const wrappedMessage = (parsedBody as any).message
1462
+ messageContent = typeof wrappedMessage === 'string'
1463
+ ? this.tryParse(wrappedMessage)
1464
+ : wrappedMessage
1465
+ paymentData = (parsedBody as any).payment
1466
+ }
1451
1467
 
1452
- let messageContent: any = parsedBody
1453
- let paymentData: Payment | undefined
1468
+ return { message, parsedBody, messageContent, paymentData }
1469
+ })
1454
1470
 
1455
- if (
1456
- parsedBody != null &&
1457
- typeof parsedBody === 'object' &&
1458
- 'message' in parsedBody
1459
- ) {
1460
- // Handle wrapped message format (with payment data)
1461
- const wrappedMessage = (parsedBody as any).message
1462
- messageContent = typeof wrappedMessage === 'string'
1463
- ? this.tryParse(wrappedMessage)
1464
- : wrappedMessage
1465
- paymentData = (parsedBody as any).payment
1466
- }
1471
+ if (acceptPayments) {
1472
+ const paymentJobs = parsed
1473
+ .filter(p => p.paymentData?.tx != null && p.paymentData.outputs != null)
1474
+
1475
+ await this.mapWithConcurrency(paymentJobs, 2, async (p) => {
1476
+ try {
1477
+ Logger.log(
1478
+ `[MB CLIENT] Processing recipient payment in message from ${String(p.message.sender)}…`
1479
+ )
1480
+
1481
+ const recipientOutputs = (p.paymentData as Payment).outputs.filter(
1482
+ output => output.protocol === 'wallet payment'
1483
+ )
1467
1484
 
1468
- // Process payment if present - server now only stores recipient payments
1469
- if (acceptPayments && paymentData?.tx != null && paymentData.outputs != null) {
1470
- try {
1485
+ if (recipientOutputs.length > 0) {
1471
1486
  Logger.log(
1472
- `[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`
1487
+ `[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`
1473
1488
  )
1474
1489
 
1475
- // All outputs in the stored payment data are for the recipient
1476
- // (delivery fees are already processed by the server)
1477
- const recipientOutputs = paymentData.outputs.filter(
1478
- output => output.protocol === 'wallet payment'
1479
- )
1490
+ const internalizeResult = await this.walletClient.internalizeAction({
1491
+ tx: (p.paymentData as Payment).tx,
1492
+ outputs: recipientOutputs,
1493
+ description: (p.paymentData as Payment).description ?? 'MessageBox recipient payment'
1494
+ }, this.originator)
1480
1495
 
1481
- if (recipientOutputs.length > 0) {
1496
+ if (internalizeResult.accepted) {
1482
1497
  Logger.log(
1483
- `[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`
1498
+ '[MB CLIENT] Successfully internalized recipient payment'
1484
1499
  )
1485
-
1486
- const internalizeResult = await this.walletClient.internalizeAction({
1487
- tx: paymentData.tx,
1488
- outputs: recipientOutputs,
1489
- description: paymentData.description ?? 'MessageBox recipient payment'
1490
- }, this.originator)
1491
-
1492
- if (internalizeResult.accepted) {
1493
- Logger.log(
1494
- '[MB CLIENT] Successfully internalized recipient payment'
1495
- )
1496
- } else {
1497
- Logger.warn(
1498
- '[MB CLIENT] Recipient payment internalization was not accepted'
1499
- )
1500
- }
1501
1500
  } else {
1502
- Logger.log(
1503
- '[MB CLIENT] No wallet payment outputs found in payment data'
1501
+ Logger.warn(
1502
+ '[MB CLIENT] Recipient payment internalization was not accepted'
1504
1503
  )
1505
1504
  }
1506
- } catch (paymentError) {
1507
- Logger.error(
1508
- '[MB CLIENT ERROR] Failed to internalize recipient payment:',
1509
- paymentError
1505
+ } else {
1506
+ Logger.log(
1507
+ '[MB CLIENT] No wallet payment outputs found in payment data'
1510
1508
  )
1511
- // Continue processing the message even if payment fails
1512
1509
  }
1510
+ } catch (paymentError) {
1511
+ Logger.error(
1512
+ '[MB CLIENT ERROR] Failed to internalize recipient payment:',
1513
+ paymentError
1514
+ )
1513
1515
  }
1516
+ return null
1517
+ })
1518
+ }
1514
1519
 
1515
- // Handle message decryption
1520
+ await this.mapWithConcurrency(parsed, 4, async (p) => {
1521
+ try {
1516
1522
  if (
1517
- messageContent != null &&
1518
- typeof messageContent === 'object' &&
1519
- typeof (messageContent).encryptedMessage === 'string'
1523
+ p.messageContent != null &&
1524
+ typeof p.messageContent === 'object' &&
1525
+ typeof (p.messageContent).encryptedMessage === 'string'
1520
1526
  ) {
1521
1527
  Logger.log(
1522
- `[MB CLIENT] Decrypting message from ${String(message.sender)}…`
1528
+ `[MB CLIENT] Decrypting message from ${String(p.message.sender)}…`
1523
1529
  )
1524
1530
 
1525
1531
  const decrypted = await this.walletClient.decrypt({
1526
1532
  protocolID: [1, 'messagebox'],
1527
1533
  keyID: '1',
1528
- counterparty: message.sender,
1534
+ counterparty: p.message.sender,
1529
1535
  ciphertext: Utils.toArray(
1530
- messageContent.encryptedMessage,
1536
+ (p.messageContent).encryptedMessage,
1531
1537
  'base64'
1532
1538
  )
1533
1539
  }, this.originator)
1534
1540
 
1535
1541
  const decryptedText = Utils.toUTF8(decrypted.plaintext)
1536
- message.body = this.tryParse(decryptedText)
1542
+ p.message.body = this.tryParse(decryptedText)
1537
1543
  } else {
1538
- // For non-encrypted messages, use the processed content
1539
- message.body = messageContent ?? parsedBody
1544
+ p.message.body = p.messageContent ?? p.parsedBody
1540
1545
  }
1541
1546
  } catch (err) {
1542
1547
  Logger.error(
1543
1548
  '[MB CLIENT ERROR] Failed to parse or decrypt message in list:',
1544
1549
  err
1545
1550
  )
1546
- message.body = '[Error: Failed to decrypt or parse message]'
1551
+ p.message.body = '[Error: Failed to decrypt or parse message]'
1547
1552
  }
1548
- }
1553
+ return null
1554
+ })
1549
1555
 
1550
1556
  // Sort newest‑first for a deterministic order
1551
1557
  messages.sort(
@@ -1603,7 +1609,8 @@ async sendMesagetoRecepients(
1603
1609
  return raw
1604
1610
  }
1605
1611
  }
1606
- for (const message of messages) {
1612
+
1613
+ await this.mapWithConcurrency(messages, 4, async (message) => {
1607
1614
  try {
1608
1615
  const parsedBody: unknown =
1609
1616
  typeof message.body === 'string' ? tryParse(message.body) : message.body
@@ -1613,13 +1620,11 @@ async sendMesagetoRecepients(
1613
1620
  typeof parsedBody === 'object' &&
1614
1621
  'message' in parsedBody
1615
1622
  ) {
1616
- // Handle wrapped message format (with payment data)
1617
1623
  const wrappedMessage = (parsedBody as any).message
1618
1624
  messageContent = typeof wrappedMessage === 'string'
1619
1625
  ? tryParse(wrappedMessage)
1620
1626
  : wrappedMessage
1621
1627
  }
1622
- // Handle message decryption
1623
1628
  if (
1624
1629
  messageContent != null &&
1625
1630
  typeof messageContent === 'object' &&
@@ -1637,7 +1642,6 @@ async sendMesagetoRecepients(
1637
1642
  const decryptedText = Utils.toUTF8(decrypted.plaintext)
1638
1643
  message.body = tryParse(decryptedText)
1639
1644
  } else {
1640
- // For non-encrypted messages, use the processed content
1641
1645
  message.body = messageContent ?? parsedBody
1642
1646
  }
1643
1647
  } catch (err) {
@@ -1647,7 +1651,8 @@ async sendMesagetoRecepients(
1647
1651
  )
1648
1652
  message.body = '[Error: Failed to decrypt or parse message]'
1649
1653
  }
1650
- }
1654
+ return null
1655
+ })
1651
1656
  return messages
1652
1657
  }
1653
1658
 
@@ -1676,6 +1681,33 @@ async sendMesagetoRecepients(
1676
1681
  }
1677
1682
  }
1678
1683
 
1684
+ private async mapWithConcurrency<T, R> (
1685
+ items: T[],
1686
+ limit: number,
1687
+ fn: (item: T, index: number) => Promise<R>
1688
+ ): Promise<R[]> {
1689
+ if (items.length === 0) return []
1690
+ if (!Number.isFinite(limit) || limit >= items.length) {
1691
+ return await Promise.all(items.map(fn))
1692
+ }
1693
+
1694
+ const workerCount = Math.max(1, Math.min(limit, items.length))
1695
+ const results: R[] = new Array(items.length)
1696
+ let nextIndex = 0
1697
+
1698
+ const workers = Array.from({ length: workerCount }, async () => {
1699
+ while (true) {
1700
+ const currentIndex = nextIndex
1701
+ nextIndex++
1702
+ if (currentIndex >= items.length) return
1703
+ results[currentIndex] = await fn(items[currentIndex], currentIndex)
1704
+ }
1705
+ })
1706
+
1707
+ await Promise.all(workers)
1708
+ return results
1709
+ }
1710
+
1679
1711
  /**
1680
1712
  * @method acknowledgeNotification
1681
1713
  * @async
@@ -1961,172 +1993,178 @@ async sendMesagetoRecepients(
1961
1993
  * messageBox: 'notifications'
1962
1994
  * })
1963
1995
  */
1964
- async getMessageBoxQuote(
1965
- params: GetQuoteParams,
1966
- overrideHost?: string
1967
- ): Promise<MessageBoxQuote | MessageBoxMultiQuote> {
1996
+ async getMessageBoxQuote (
1997
+ params: GetQuoteParams,
1998
+ overrideHost?: string
1999
+ ): Promise<MessageBoxQuote | MessageBoxMultiQuote> {
1968
2000
  // ---------- SINGLE RECIPIENT (back-compat) ----------
1969
- if (!Array.isArray(params.recipient)) {
1970
- const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
1971
- const queryParams = new URLSearchParams({
1972
- recipient: params.recipient,
1973
- messageBox: params.messageBox
1974
- })
2001
+ if (!Array.isArray(params.recipient)) {
2002
+ const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
2003
+ const queryParams = new URLSearchParams({
2004
+ recipient: params.recipient,
2005
+ messageBox: params.messageBox
2006
+ })
1975
2007
 
1976
- Logger.log('[MB CLIENT] Getting messageBox quote (single)...')
1977
- console.log("HELP IM QUOTING",`${finalHost}/permissions/quote?${queryParams.toString()}`)
1978
- const response = await this.authFetch.fetch(
2008
+ Logger.log('[MB CLIENT] Getting messageBox quote (single)...')
2009
+ console.log('HELP IM QUOTING', `${finalHost}/permissions/quote?${queryParams.toString()}`)
2010
+ const response = await this.authFetch.fetch(
1979
2011
  `${finalHost}/permissions/quote?${queryParams.toString()}`,
1980
2012
  { method: 'GET' }
1981
- )
1982
- console.log("server response from getquote]",response)
1983
- if (!response.ok) {
1984
- const errorData = await response.json().catch(() => ({}))
1985
- throw new Error(
1986
- `Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
1987
2013
  )
1988
- }
2014
+ console.log('server response from getquote]', response)
2015
+ if (!response.ok) {
2016
+ const errorData = await response.json().catch(() => ({}))
2017
+ throw new Error(
2018
+ `Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
2019
+ )
2020
+ }
1989
2021
 
1990
- const { status, description, quote } = await response.json()
1991
- if (status === 'error') {
1992
- throw new Error(description ?? 'Failed to get quote')
1993
- }
2022
+ const { status, description, quote } = await response.json()
2023
+ if (status === 'error') {
2024
+ throw new Error(description ?? 'Failed to get quote')
2025
+ }
1994
2026
 
1995
- const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key')
1996
- console.log("deliveryAgentIdentityKey",deliveryAgentIdentityKey)
1997
- if (deliveryAgentIdentityKey == null) {
1998
- throw new Error('Failed to get quote: Delivery agent did not provide their identity key')
2027
+ const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key')
2028
+ console.log('deliveryAgentIdentityKey', deliveryAgentIdentityKey)
2029
+ if (deliveryAgentIdentityKey == null) {
2030
+ throw new Error('Failed to get quote: Delivery agent did not provide their identity key')
2031
+ }
2032
+
2033
+ return {
2034
+ recipientFee: quote.recipientFee,
2035
+ deliveryFee: quote.deliveryFee,
2036
+ deliveryAgentIdentityKey
2037
+ }
1999
2038
  }
2000
2039
 
2001
- return {
2002
- recipientFee: quote.recipientFee,
2003
- deliveryFee: quote.deliveryFee,
2004
- deliveryAgentIdentityKey
2040
+ // ---------- MULTI RECIPIENTS ----------
2041
+ const recipients = params.recipient
2042
+ if (recipients.length === 0) {
2043
+ throw new Error('At least one recipient is required.')
2005
2044
  }
2006
- }
2007
2045
 
2008
- // ---------- MULTI RECIPIENTS ----------
2009
- const recipients = params.recipient
2010
- if (recipients.length === 0) {
2011
- throw new Error('At least one recipient is required.')
2012
- }
2046
+ Logger.log('[MB CLIENT] Getting messageBox quotes (multi)...')
2047
+ console.log('[MB CLIENT] Getting messageBox quotes (multi)...')
2048
+ // Resolve host per recipient (unless caller forces overrideHost)
2049
+ // Group recipients by host so we call each overlay once.
2050
+ const hostGroups = new Map<string, PubKeyHex[]>()
2013
2051
 
2014
- Logger.log('[MB CLIENT] Getting messageBox quotes (multi)...')
2015
- console.log("[MB CLIENT] Getting messageBox quotes (multi)...")
2016
- // Resolve host per recipient (unless caller forces overrideHost)
2017
- // Group recipients by host so we call each overlay once.
2018
- const hostGroups = new Map<string, PubKeyHex[]>()
2019
- for (const r of recipients) {
2020
- const host = overrideHost ?? await this.resolveHostForRecipient(r)
2021
- const list = hostGroups.get(host)
2022
- if (list) list.push(r)
2023
- else hostGroups.set(host, [r])
2024
- }
2052
+ const resolvedHosts = overrideHost != null
2053
+ ? recipients.map(() => overrideHost)
2054
+ : await this.mapWithConcurrency(recipients, 8, async (r) => await this.resolveHostForRecipient(r))
2025
2055
 
2026
- const deliveryAgentIdentityKeyByHost: Record<string, string> = {}
2027
- const quotesByRecipient: Array<{
2028
- recipient: PubKeyHex
2029
- messageBox: string
2030
- deliveryFee: number
2031
- recipientFee: number
2032
- status: 'blocked' | 'always_allow' | 'payment_required'
2033
- }> = []
2034
- const blockedRecipients: PubKeyHex[] = []
2056
+ for (let i = 0; i < recipients.length; i++) {
2057
+ const r = recipients[i]
2058
+ const host = resolvedHosts[i]
2059
+ const list = hostGroups.get(host)
2060
+ if (list != null) list.push(r)
2061
+ else hostGroups.set(host, [r])
2062
+ }
2035
2063
 
2036
- let totalDeliveryFees = 0
2037
- let totalRecipientFees = 0
2064
+ const deliveryAgentIdentityKeyByHost: Record<string, string> = {}
2065
+ const quotesByRecipient: Array<{
2066
+ recipient: PubKeyHex
2067
+ messageBox: string
2068
+ deliveryFee: number
2069
+ recipientFee: number
2070
+ status: 'blocked' | 'always_allow' | 'payment_required'
2071
+ }> = []
2072
+ const blockedRecipients: PubKeyHex[] = []
2038
2073
 
2039
- // Helper to fetch one host group
2040
- const fetchGroup = async (host: string, groupRecipients: PubKeyHex[]) => {
2041
- const qp = new URLSearchParams()
2042
- for (const r of groupRecipients) qp.append('recipient', r)
2043
- qp.set('messageBox', params.messageBox)
2074
+ let totalDeliveryFees = 0
2075
+ let totalRecipientFees = 0
2044
2076
 
2045
- const url = `${host}/permissions/quote?${qp.toString()}`
2046
- Logger.log('[MB CLIENT] Multi-quote GET:', url)
2077
+ // Helper to fetch one host group
2078
+ const fetchGroup = async (host: string, groupRecipients: PubKeyHex[]) => {
2079
+ const qp = new URLSearchParams()
2080
+ for (const r of groupRecipients) qp.append('recipient', r)
2081
+ qp.set('messageBox', params.messageBox)
2047
2082
 
2048
- const resp = await this.authFetch.fetch(url, { method: 'GET' })
2083
+ const url = `${host}/permissions/quote?${qp.toString()}`
2084
+ Logger.log('[MB CLIENT] Multi-quote GET:', url)
2049
2085
 
2050
- if (!resp.ok) {
2051
- const errorData = await resp.json().catch(() => ({}))
2052
- throw new Error(
2086
+ const resp = await this.authFetch.fetch(url, { method: 'GET' })
2087
+
2088
+ if (!resp.ok) {
2089
+ const errorData = await resp.json().catch(() => ({}))
2090
+ throw new Error(
2053
2091
  `Failed to get quote (host ${host}): HTTP ${resp.status} - ${String(errorData.description) ?? resp.statusText}`
2054
- )
2055
- }
2092
+ )
2093
+ }
2056
2094
 
2057
- const deliveryAgentKey = resp.headers.get('x-bsv-auth-identity-key')
2058
- if (!deliveryAgentKey) {
2059
- throw new Error(`Failed to get quote (host ${host}): missing delivery agent identity key`)
2060
- }
2061
- deliveryAgentIdentityKeyByHost[host] = deliveryAgentKey
2095
+ const deliveryAgentKey = resp.headers.get('x-bsv-auth-identity-key')
2096
+ if (!deliveryAgentKey) {
2097
+ throw new Error(`Failed to get quote (host ${host}): missing delivery agent identity key`)
2098
+ }
2099
+ deliveryAgentIdentityKeyByHost[host] = deliveryAgentKey
2062
2100
 
2063
- const payload = await resp.json()
2101
+ const payload = await resp.json()
2064
2102
 
2065
- // Server supports both shapes. For multi we expect:
2066
- // { quotesByRecipient, totals, blockedRecipients }
2067
- if (Array.isArray(payload?.quotesByRecipient)) {
2103
+ // Server supports both shapes. For multi we expect:
2104
+ // { quotesByRecipient, totals, blockedRecipients }
2105
+ if (Array.isArray(payload?.quotesByRecipient)) {
2068
2106
  // merge quotes
2069
- for (const q of payload.quotesByRecipient) {
2070
- quotesByRecipient.push({
2071
- recipient: q.recipient,
2072
- messageBox: q.messageBox,
2073
- deliveryFee: q.deliveryFee,
2074
- recipientFee: q.recipientFee,
2075
- status: q.status
2076
- })
2077
- // aggregate client-side totals as well (in case we hit multiple hosts)
2078
- totalDeliveryFees += q.deliveryFee
2079
- if (q.recipientFee === -1) {
2080
- if (!blockedRecipients.includes(q.recipient)) blockedRecipients.push(q.recipient)
2081
- } else {
2082
- totalRecipientFees += q.recipientFee
2107
+ for (const q of payload.quotesByRecipient) {
2108
+ quotesByRecipient.push({
2109
+ recipient: q.recipient,
2110
+ messageBox: q.messageBox,
2111
+ deliveryFee: q.deliveryFee,
2112
+ recipientFee: q.recipientFee,
2113
+ status: q.status
2114
+ })
2115
+ // aggregate client-side totals as well (in case we hit multiple hosts)
2116
+ totalDeliveryFees += q.deliveryFee
2117
+ if (q.recipientFee === -1) {
2118
+ if (!blockedRecipients.includes(q.recipient)) blockedRecipients.push(q.recipient)
2119
+ } else {
2120
+ totalRecipientFees += q.recipientFee
2121
+ }
2083
2122
  }
2084
- }
2085
2123
 
2086
- // Also merge server totals if present (they are per-host); we already aggregated above,
2087
- // so we don’t need to use payload.totals except for sanity/logging.
2088
- if (Array.isArray(payload?.blockedRecipients)) {
2089
- for (const br of payload.blockedRecipients) {
2090
- if (!blockedRecipients.includes(br)) blockedRecipients.push(br)
2124
+ // Also merge server totals if present (they are per-host); we already aggregated above,
2125
+ // so we don’t need to use payload.totals except for sanity/logging.
2126
+ if (Array.isArray(payload?.blockedRecipients)) {
2127
+ for (const br of payload.blockedRecipients) {
2128
+ if (!blockedRecipients.includes(br)) blockedRecipients.push(br)
2129
+ }
2091
2130
  }
2092
- }
2093
- } else if (payload?.quote) {
2131
+ } else if (payload?.quote) {
2094
2132
  // Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
2095
2133
  // we map it to each recipient in the group uniformly.
2096
- for (const r of groupRecipients) {
2097
- const { deliveryFee, recipientFee } = payload.quote
2098
- const status =
2134
+ for (const r of groupRecipients) {
2135
+ const { deliveryFee, recipientFee } = payload.quote
2136
+ const status =
2099
2137
  recipientFee === -1 ? 'blocked' : recipientFee === 0 ? 'always_allow' : 'payment_required'
2100
- quotesByRecipient.push({
2101
- recipient: r,
2102
- messageBox: params.messageBox,
2103
- deliveryFee,
2104
- recipientFee,
2105
- status
2106
- })
2107
- totalDeliveryFees += deliveryFee
2108
- if (recipientFee === -1) blockedRecipients.push(r)
2109
- else totalRecipientFees += recipientFee
2138
+ quotesByRecipient.push({
2139
+ recipient: r,
2140
+ messageBox: params.messageBox,
2141
+ deliveryFee,
2142
+ recipientFee,
2143
+ status
2144
+ })
2145
+ totalDeliveryFees += deliveryFee
2146
+ if (recipientFee === -1) blockedRecipients.push(r)
2147
+ else totalRecipientFees += recipientFee
2148
+ }
2149
+ } else {
2150
+ throw new Error(`Unexpected quote response shape from host ${host}`)
2110
2151
  }
2111
- } else {
2112
- throw new Error(`Unexpected quote response shape from host ${host}`)
2113
2152
  }
2114
- }
2115
2153
 
2116
- // Run all host groups (in parallel, but you can limit if needed)
2117
- await Promise.all(Array.from(hostGroups.entries()).map(([host, group]) => fetchGroup(host, group)))
2118
-
2119
- return {
2120
- quotesByRecipient,
2121
- totals: {
2122
- deliveryFees: totalDeliveryFees,
2123
- recipientFees: totalRecipientFees,
2124
- totalForPayableRecipients: totalDeliveryFees + totalRecipientFees
2125
- },
2126
- blockedRecipients,
2127
- deliveryAgentIdentityKeyByHost
2154
+ // Run all host groups (in parallel, but you can limit if needed)
2155
+ await Promise.all(Array.from(hostGroups.entries()).map(async ([host, group]) => await fetchGroup(host, group)))
2156
+
2157
+ return {
2158
+ quotesByRecipient,
2159
+ totals: {
2160
+ deliveryFees: totalDeliveryFees,
2161
+ recipientFees: totalRecipientFees,
2162
+ totalForPayableRecipients: totalDeliveryFees + totalRecipientFees
2163
+ },
2164
+ blockedRecipients,
2165
+ deliveryAgentIdentityKeyByHost
2166
+ }
2128
2167
  }
2129
- }
2130
2168
 
2131
2169
  /**
2132
2170
  * @method listMessageBoxPermissions
@@ -2291,31 +2329,31 @@ async getMessageBoxQuote(
2291
2329
  * // Send with maximum payment limit for safety
2292
2330
  * await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
2293
2331
  */
2294
- async sendNotification(
2295
- recipient: PubKeyHex | PubKeyHex[],
2296
- body: string | object,
2297
- overrideHost?: string
2298
- ): Promise<SendMessageResponse | SendListResult> {
2299
- await this.assertInitialized()
2300
-
2301
- // Single recipient → keep original flow
2302
- if (!Array.isArray(recipient)) {
2303
- return await this.sendMessage({
2304
- recipient,
2332
+ async sendNotification (
2333
+ recipient: PubKeyHex | PubKeyHex[],
2334
+ body: string | object,
2335
+ overrideHost?: string
2336
+ ): Promise<SendMessageResponse | SendListResult> {
2337
+ await this.assertInitialized()
2338
+
2339
+ // Single recipient → keep original flow
2340
+ if (!Array.isArray(recipient)) {
2341
+ return await this.sendMessage({
2342
+ recipient,
2343
+ messageBox: 'notifications',
2344
+ body,
2345
+ checkPermissions: true
2346
+ }, overrideHost)
2347
+ }
2348
+
2349
+ // Multiple recipients → new flow
2350
+ return await this.sendMesagetoRecepients({
2351
+ recipients: recipient,
2305
2352
  messageBox: 'notifications',
2306
- body,
2307
- checkPermissions: true
2353
+ body
2308
2354
  }, overrideHost)
2309
2355
  }
2310
2356
 
2311
- // Multiple recipients → new flow
2312
- return await this.sendMesagetoRecepients({
2313
- recipients: recipient,
2314
- messageBox: 'notifications',
2315
- body
2316
- }, overrideHost)
2317
- }
2318
-
2319
2357
  /**
2320
2358
  * Register a device for FCM push notifications.
2321
2359
  *
@@ -2579,22 +2617,22 @@ async getMessageBoxQuote(
2579
2617
  }
2580
2618
  }
2581
2619
 
2582
- private async createMessagePaymentBatch(
2583
- recipients: string[],
2584
- perRecipientQuotes: Map<string, { recipientFee: number; deliveryFee: number }>,
2585
- // server (delivery agent) identity key to pay the delivery fee to
2586
- serverIdentityKey: string,
2587
- description = 'MessageBox delivery payment (batch)'
2620
+ private async createMessagePaymentBatch (
2621
+ recipients: string[],
2622
+ perRecipientQuotes: Map<string, { recipientFee: number, deliveryFee: number }>,
2623
+ // server (delivery agent) identity key to pay the delivery fee to
2624
+ serverIdentityKey: string,
2625
+ description = 'MessageBox delivery payment (batch)'
2588
2626
  ): Promise<Payment> {
2589
2627
  const outputs: InternalizeOutput[] = []
2590
2628
  const createActionOutputs: CreateActionOutput[] = []
2591
2629
 
2592
2630
  // figure out the per-request delivery fee (take it from any quoted recipient)
2593
2631
  const deliveryFeeOnce =
2594
- recipients.reduce((acc, r) => {
2632
+ recipients.reduce<number | undefined>((acc, r) => {
2595
2633
  const q = perRecipientQuotes.get(r)
2596
- return q ? (acc ?? q.deliveryFee) : acc
2597
- }, undefined as number | undefined) ?? 0
2634
+ return (q != null) ? (acc ?? q.deliveryFee) : acc
2635
+ }, undefined) ?? 0
2598
2636
 
2599
2637
  const senderIdentityKey = await this.getIdentityKey()
2600
2638
  let outputIndex = 0
@@ -2636,7 +2674,7 @@ async getMessageBoxQuote(
2636
2674
 
2637
2675
  for (const r of recipients) {
2638
2676
  const q = perRecipientQuotes.get(r)
2639
- if (!q || q.recipientFee <= 0) continue
2677
+ if ((q == null) || q.recipientFee <= 0) continue
2640
2678
 
2641
2679
  const derivationPrefix = Utils.toBase64(Random(32))
2642
2680
  const derivationSuffix = Utils.toBase64(Random(32))
@@ -2677,7 +2715,7 @@ async getMessageBoxQuote(
2677
2715
  options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
2678
2716
  }, this.originator)
2679
2717
 
2680
- if (!tx) throw new Error('Failed to create payment transaction')
2718
+ if (tx == null) throw new Error('Failed to create payment transaction')
2681
2719
 
2682
2720
  return { tx, outputs, description }
2683
2721
  }