@bsv/message-box-client 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/message-box-client",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -75,6 +75,6 @@
75
75
  },
76
76
  "dependencies": {
77
77
  "@bsv/authsocket-client": "^1.0.12",
78
- "@bsv/sdk": "^1.8.2"
78
+ "@bsv/sdk": "^1.8.8"
79
79
  }
80
80
  }
@@ -48,9 +48,12 @@ import {
48
48
  SetMessageBoxPermissionParams,
49
49
  GetMessageBoxPermissionParams,
50
50
  MessageBoxPermission,
51
+ MessageBoxMultiQuote,
51
52
  MessageBoxQuote,
52
53
  ListPermissionsParams,
53
- GetQuoteParams
54
+ GetQuoteParams,
55
+ SendListParams,
56
+ SendListResult,
54
57
  } from './types/permissions.js'
55
58
 
56
59
  const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems'
@@ -884,7 +887,7 @@ export class MessageBoxClient {
884
887
  const quote = await this.getMessageBoxQuote({
885
888
  recipient: message.recipient,
886
889
  messageBox: message.messageBox
887
- }, overrideHost)
890
+ }, overrideHost) as MessageBoxQuote
888
891
 
889
892
  if (quote.recipientFee === -1) {
890
893
  throw new Error('You have been blocked from sending messages to this recipient.')
@@ -999,6 +1002,172 @@ export class MessageBoxClient {
999
1002
  }
1000
1003
  }
1001
1004
 
1005
+
1006
+ /**
1007
+ * Multi-recipient sender. Uses the multi-quote route to:
1008
+ * - identify blocked recipients
1009
+ * - compute per-recipient payment
1010
+ * Then sends to the allowed recipients with payment attached.
1011
+ */
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 : []
1037
+
1038
+ const blocked = (quoteResponse?.blockedRecipients ?? []) as string[]
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
+ }
1052
+ }
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
+ }
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) {
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
+ }
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]
1074
+
1075
+ if (!singleDeliveryKey) {
1076
+ throw new Error('Could not determine server delivery agent identity key.')
1077
+ }
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
+ }
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
+ }
1097
+
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 {
1104
+ // 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
+ }
1107
+
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
+ }
1125
+
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))
1128
+
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
+ })
1135
+
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
+ }
1141
+
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
1145
+
1146
+ const status: SendListResult['status'] =
1147
+ sent.length === allowedRecipients.length ? 'success'
1148
+ : sent.length > 0 ? 'partial'
1149
+ : 'error'
1150
+
1151
+ const description =
1152
+ status === 'success'
1153
+ ? `Sent to ${sent.length} recipients.`
1154
+ : status === 'partial'
1155
+ ? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
1156
+ : `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`
1157
+
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
1168
+ }
1169
+ }
1170
+ }
1002
1171
  /**
1003
1172
  * @method anointHost
1004
1173
  * @async
@@ -1792,22 +1961,30 @@ export class MessageBoxClient {
1792
1961
  * messageBox: 'notifications'
1793
1962
  * })
1794
1963
  */
1795
- async getMessageBoxQuote (params: GetQuoteParams, overrideHost?: string): Promise<MessageBoxQuote> {
1964
+ async getMessageBoxQuote(
1965
+ params: GetQuoteParams,
1966
+ overrideHost?: string
1967
+ ): Promise<MessageBoxQuote | MessageBoxMultiQuote> {
1968
+ // ---------- SINGLE RECIPIENT (back-compat) ----------
1969
+ if (!Array.isArray(params.recipient)) {
1796
1970
  const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
1797
1971
  const queryParams = new URLSearchParams({
1798
1972
  recipient: params.recipient,
1799
1973
  messageBox: params.messageBox
1800
1974
  })
1801
1975
 
1802
- Logger.log('[MB CLIENT] Getting messageBox quote...')
1803
-
1804
- const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
1805
- method: 'GET'
1806
- })
1807
-
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(
1979
+ `${finalHost}/permissions/quote?${queryParams.toString()}`,
1980
+ { method: 'GET' }
1981
+ )
1982
+ console.log("server response from getquote]",response)
1808
1983
  if (!response.ok) {
1809
1984
  const errorData = await response.json().catch(() => ({}))
1810
- throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`)
1985
+ throw new Error(
1986
+ `Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
1987
+ )
1811
1988
  }
1812
1989
 
1813
1990
  const { status, description, quote } = await response.json()
@@ -1816,7 +1993,7 @@ export class MessageBoxClient {
1816
1993
  }
1817
1994
 
1818
1995
  const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key')
1819
-
1996
+ console.log("deliveryAgentIdentityKey",deliveryAgentIdentityKey)
1820
1997
  if (deliveryAgentIdentityKey == null) {
1821
1998
  throw new Error('Failed to get quote: Delivery agent did not provide their identity key')
1822
1999
  }
@@ -1828,6 +2005,129 @@ export class MessageBoxClient {
1828
2005
  }
1829
2006
  }
1830
2007
 
2008
+ // ---------- MULTI RECIPIENTS ----------
2009
+ const recipients = params.recipient
2010
+ if (recipients.length === 0) {
2011
+ throw new Error('At least one recipient is required.')
2012
+ }
2013
+
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
+ }
2025
+
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[] = []
2035
+
2036
+ let totalDeliveryFees = 0
2037
+ let totalRecipientFees = 0
2038
+
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)
2044
+
2045
+ const url = `${host}/permissions/quote?${qp.toString()}`
2046
+ Logger.log('[MB CLIENT] Multi-quote GET:', url)
2047
+
2048
+ const resp = await this.authFetch.fetch(url, { method: 'GET' })
2049
+
2050
+ if (!resp.ok) {
2051
+ const errorData = await resp.json().catch(() => ({}))
2052
+ throw new Error(
2053
+ `Failed to get quote (host ${host}): HTTP ${resp.status} - ${String(errorData.description) ?? resp.statusText}`
2054
+ )
2055
+ }
2056
+
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
2062
+
2063
+ const payload = await resp.json()
2064
+
2065
+ // Server supports both shapes. For multi we expect:
2066
+ // { quotesByRecipient, totals, blockedRecipients }
2067
+ if (Array.isArray(payload?.quotesByRecipient)) {
2068
+ // 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
2083
+ }
2084
+ }
2085
+
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)
2091
+ }
2092
+ }
2093
+ } else if (payload?.quote) {
2094
+ // Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
2095
+ // 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 =
2099
+ 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
2110
+ }
2111
+ } else {
2112
+ throw new Error(`Unexpected quote response shape from host ${host}`)
2113
+ }
2114
+ }
2115
+
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
2128
+ }
2129
+ }
2130
+
1831
2131
  /**
1832
2132
  * @method listMessageBoxPermissions
1833
2133
  * @async
@@ -1991,15 +2291,15 @@ export class MessageBoxClient {
1991
2291
  * // Send with maximum payment limit for safety
1992
2292
  * await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
1993
2293
  */
1994
- async sendNotification (
1995
- recipient: PubKeyHex,
1996
- body: string | object,
1997
- overrideHost?: string
1998
- ): Promise<SendMessageResponse> {
1999
- await this.assertInitialized()
2000
-
2001
- // Use sendMessage with permission checking enabled
2002
- // This eliminates duplication of quote fetching and payment logic
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)) {
2003
2303
  return await this.sendMessage({
2004
2304
  recipient,
2005
2305
  messageBox: 'notifications',
@@ -2008,6 +2308,14 @@ export class MessageBoxClient {
2008
2308
  }, overrideHost)
2009
2309
  }
2010
2310
 
2311
+ // Multiple recipients → new flow
2312
+ return await this.sendMesagetoRecepients({
2313
+ recipients: recipient,
2314
+ messageBox: 'notifications',
2315
+ body
2316
+ }, overrideHost)
2317
+ }
2318
+
2011
2319
  /**
2012
2320
  * Register a device for FCM push notifications.
2013
2321
  *
@@ -2178,6 +2486,7 @@ export class MessageBoxClient {
2178
2486
  const derivationSuffix = Utils.toBase64(Random(32))
2179
2487
 
2180
2488
  // Get host's derived public key
2489
+ console.log('delivery agent:', quote.deliveryAgentIdentityKey)
2181
2490
  const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
2182
2491
  protocolID: [2, '3241645161d8'],
2183
2492
  keyID: `${derivationPrefix} ${derivationSuffix}`,
@@ -2269,4 +2578,107 @@ export class MessageBoxClient {
2269
2578
  // labels
2270
2579
  }
2271
2580
  }
2581
+
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)'
2588
+ ): Promise<Payment> {
2589
+ const outputs: InternalizeOutput[] = []
2590
+ const createActionOutputs: CreateActionOutput[] = []
2591
+
2592
+ // figure out the per-request delivery fee (take it from any quoted recipient)
2593
+ const deliveryFeeOnce =
2594
+ recipients.reduce((acc, r) => {
2595
+ const q = perRecipientQuotes.get(r)
2596
+ return q ? (acc ?? q.deliveryFee) : acc
2597
+ }, undefined as number | undefined) ?? 0
2598
+
2599
+ const senderIdentityKey = await this.getIdentityKey()
2600
+ let outputIndex = 0
2601
+
2602
+ // index 0: server delivery fee (if any)
2603
+ if (deliveryFeeOnce > 0) {
2604
+ const derivationPrefix = Utils.toBase64(Random(32))
2605
+ const derivationSuffix = Utils.toBase64(Random(32))
2606
+
2607
+ const { publicKey: agentDerived } = await this.walletClient.getPublicKey({
2608
+ protocolID: [2, '3241645161d8'],
2609
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
2610
+ counterparty: serverIdentityKey
2611
+ }, this.originator)
2612
+
2613
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(agentDerived).toAddress()).toHex()
2614
+
2615
+ createActionOutputs.push({
2616
+ satoshis: deliveryFeeOnce,
2617
+ lockingScript,
2618
+ outputDescription: 'MessageBox server delivery fee (batch)',
2619
+ customInstructions: JSON.stringify({
2620
+ derivationPrefix,
2621
+ derivationSuffix,
2622
+ recipientIdentityKey: serverIdentityKey
2623
+ })
2624
+ })
2625
+
2626
+ outputs.push({
2627
+ outputIndex: outputIndex++,
2628
+ protocol: 'wallet payment',
2629
+ paymentRemittance: { derivationPrefix, derivationSuffix, senderIdentityKey }
2630
+ })
2631
+ }
2632
+
2633
+ // recipient outputs start at index 1 (or 0 if no delivery fee)
2634
+ const anyoneWallet = new ProtoWallet('anyone')
2635
+ const anyoneIdKey = (await anyoneWallet.getPublicKey({ identityKey: true })).publicKey
2636
+
2637
+ for (const r of recipients) {
2638
+ const q = perRecipientQuotes.get(r)
2639
+ if (!q || q.recipientFee <= 0) continue
2640
+
2641
+ const derivationPrefix = Utils.toBase64(Random(32))
2642
+ const derivationSuffix = Utils.toBase64(Random(32))
2643
+
2644
+ const { publicKey: recipientDerived } = await anyoneWallet.getPublicKey({
2645
+ protocolID: [2, '3241645161d8'],
2646
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
2647
+ counterparty: r
2648
+ })
2649
+
2650
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(recipientDerived).toAddress()).toHex()
2651
+
2652
+ createActionOutputs.push({
2653
+ satoshis: q.recipientFee,
2654
+ lockingScript,
2655
+ outputDescription: `Recipient message fee (${r.slice(0, 8)}…)`,
2656
+ customInstructions: JSON.stringify({
2657
+ derivationPrefix,
2658
+ derivationSuffix,
2659
+ recipientIdentityKey: r
2660
+ })
2661
+ })
2662
+
2663
+ outputs.push({
2664
+ outputIndex: outputIndex++,
2665
+ protocol: 'wallet payment',
2666
+ paymentRemittance: {
2667
+ derivationPrefix,
2668
+ derivationSuffix,
2669
+ senderIdentityKey: anyoneIdKey
2670
+ }
2671
+ })
2672
+ }
2673
+
2674
+ const { tx } = await this.walletClient.createAction({
2675
+ description,
2676
+ outputs: createActionOutputs,
2677
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
2678
+ }, this.originator)
2679
+
2680
+ if (!tx) throw new Error('Failed to create payment transaction')
2681
+
2682
+ return { tx, outputs, description }
2683
+ }
2272
2684
  }
@@ -59,6 +59,7 @@ export interface PaymentToken {
59
59
  }
60
60
  transaction: AtomicBEEF
61
61
  amount: number
62
+ outputIndex?: number
62
63
  }
63
64
 
64
65
  /**
@@ -68,6 +69,7 @@ export interface IncomingPayment {
68
69
  messageId: string
69
70
  sender: string
70
71
  token: PaymentToken
72
+ outputIndex?: number
71
73
  }
72
74
 
73
75
  /**
@@ -296,7 +298,7 @@ export class PeerPayClient extends MessageBoxClient {
296
298
  derivationSuffix: payment.token.customInstructions.derivationSuffix,
297
299
  senderIdentityKey: payment.sender
298
300
  },
299
- outputIndex: STANDARD_PAYMENT_OUTPUT_INDEX,
301
+ outputIndex: payment.token.outputIndex ?? STANDARD_PAYMENT_OUTPUT_INDEX,
300
302
  protocol: 'wallet payment'
301
303
  }],
302
304
  labels: ['peerpay'],
@@ -75,7 +75,47 @@ export interface ListPermissionsParams {
75
75
  */
76
76
  export interface GetQuoteParams {
77
77
  /** Recipient identity key */
78
- recipient: string
78
+ recipient: string | string[]
79
79
  /** MessageBox type */
80
80
  messageBox: string
81
81
  }
82
+ export interface SendListParams {
83
+ recipients: PubKeyHex[]
84
+ messageBox: string
85
+ body: string | object
86
+ skipEncryption?: boolean
87
+ }
88
+
89
+ export interface SendListResult {
90
+ status: 'success' | 'partial' | 'error'
91
+ description: string
92
+ sent: Array<{ recipient: PubKeyHex, messageId: string }>
93
+ blocked: PubKeyHex[]
94
+ failed: Array<{ recipient: PubKeyHex, error: string }>
95
+ totals?: {
96
+ deliveryFees: number
97
+ recipientFees: number
98
+ totalForPayableRecipients: number
99
+ }
100
+ }
101
+ export interface MessageBoxMultiQuote {
102
+ quotesByRecipient: Array<{
103
+ recipient: PubKeyHex
104
+ messageBox: string
105
+ deliveryFee: number
106
+ recipientFee: number
107
+ status: 'blocked' | 'always_allow' | 'payment_required'
108
+ }>
109
+ totals?: {
110
+ deliveryFees: number
111
+ recipientFees: number
112
+ totalForPayableRecipients: number
113
+ }
114
+ blockedRecipients: PubKeyHex[]
115
+ /**
116
+ * When multiple overlays are involved, each host returns its own
117
+ * delivery agent identity key. This map preserves them.
118
+ * If all recipients resolve to one host, you’ll just have one entry.
119
+ */
120
+ deliveryAgentIdentityKeyByHost: Record<string, string>
121
+ }