@bsv/message-box-client 1.4.0 → 1.4.2
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/package.json +2 -2
- package/dist/cjs/src/MessageBoxClient.js +371 -29
- package/dist/cjs/src/MessageBoxClient.js.map +1 -1
- package/dist/cjs/src/PeerPayClient.js +7 -5
- package/dist/cjs/src/PeerPayClient.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/MessageBoxClient.js +369 -29
- package/dist/esm/src/MessageBoxClient.js.map +1 -1
- package/dist/esm/src/PeerPayClient.js +7 -5
- package/dist/esm/src/PeerPayClient.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/MessageBoxClient.d.ts +11 -3
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
- package/dist/types/src/PeerPayClient.d.ts +2 -0
- package/dist/types/src/PeerPayClient.d.ts.map +1 -1
- package/dist/types/src/types/permissions.d.ts +46 -1
- package/dist/types/src/types/permissions.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/package.json +2 -2
- package/src/MessageBoxClient.ts +432 -20
- package/src/PeerPayClient.ts +19 -15
- package/src/types/permissions.ts +41 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/message-box-client",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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.
|
|
78
|
+
"@bsv/sdk": "^1.8.8"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/MessageBoxClient.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
1805
|
-
|
|
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(
|
|
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
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
}
|
package/src/PeerPayClient.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { WalletInterface, P2PKH, PublicKey, createNonce, AtomicBEEF, AuthFetch,
|
|
|
16
16
|
|
|
17
17
|
import * as Logger from './Utils/logger.js'
|
|
18
18
|
|
|
19
|
-
function safeParse<T>
|
|
19
|
+
function safeParse<T>(input: any): T {
|
|
20
20
|
try {
|
|
21
21
|
return typeof input === 'string' ? JSON.parse(input) : input
|
|
22
22
|
} catch (e) {
|
|
@@ -35,6 +35,7 @@ const STANDARD_PAYMENT_OUTPUT_INDEX = 0
|
|
|
35
35
|
*/
|
|
36
36
|
export interface PeerPayClientConfig {
|
|
37
37
|
messageBoxHost?: string
|
|
38
|
+
messageBox?: string
|
|
38
39
|
walletClient: WalletInterface
|
|
39
40
|
enableLogging?: boolean // Added optional logging flag,
|
|
40
41
|
originator?: OriginatorDomainNameStringUnder250Bytes
|
|
@@ -75,17 +76,20 @@ export interface IncomingPayment {
|
|
|
75
76
|
export class PeerPayClient extends MessageBoxClient {
|
|
76
77
|
private readonly peerPayWalletClient: WalletInterface
|
|
77
78
|
private _authFetchInstance?: AuthFetch
|
|
78
|
-
|
|
79
|
+
private messageBox: string
|
|
80
|
+
|
|
81
|
+
constructor(config: PeerPayClientConfig) {
|
|
79
82
|
const { messageBoxHost = 'https://messagebox.babbage.systems', walletClient, enableLogging = false, originator } = config
|
|
80
83
|
|
|
81
84
|
// 🔹 Pass enableLogging to MessageBoxClient
|
|
82
85
|
super({ host: messageBoxHost, walletClient, enableLogging, originator })
|
|
83
86
|
|
|
87
|
+
this.messageBox = config.messageBox ?? STANDARD_PAYMENT_MESSAGEBOX
|
|
84
88
|
this.peerPayWalletClient = walletClient
|
|
85
89
|
this.originator = originator
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
private get authFetchInstance
|
|
92
|
+
private get authFetchInstance(): AuthFetch {
|
|
89
93
|
if (this._authFetchInstance === null || this._authFetchInstance === undefined) {
|
|
90
94
|
this._authFetchInstance = new AuthFetch(this.peerPayWalletClient, undefined, undefined, this.originator)
|
|
91
95
|
}
|
|
@@ -104,7 +108,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
104
108
|
* @returns {Promise<PaymentToken>} A valid payment token containing transaction details.
|
|
105
109
|
* @throws {Error} If the recipient's public key cannot be derived.
|
|
106
110
|
*/
|
|
107
|
-
async createPaymentToken
|
|
111
|
+
async createPaymentToken(payment: PaymentParams): Promise<PaymentToken> {
|
|
108
112
|
if (payment.amount <= 0) {
|
|
109
113
|
throw new Error('Invalid payment details: recipient and valid amount are required')
|
|
110
114
|
};
|
|
@@ -181,7 +185,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
181
185
|
* @returns {Promise<any>} Resolves with the payment result.
|
|
182
186
|
* @throws {Error} If the recipient is missing or the amount is invalid.
|
|
183
187
|
*/
|
|
184
|
-
async sendPayment
|
|
188
|
+
async sendPayment(payment: PaymentParams, hostOverride?: string): Promise<any> {
|
|
185
189
|
if (payment.recipient == null || payment.recipient.trim() === '' || payment.amount <= 0) {
|
|
186
190
|
throw new Error('Invalid payment details: recipient and valid amount are required')
|
|
187
191
|
}
|
|
@@ -191,7 +195,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
191
195
|
// Ensure the recipient is included before sendings
|
|
192
196
|
await this.sendMessage({
|
|
193
197
|
recipient: payment.recipient,
|
|
194
|
-
messageBox:
|
|
198
|
+
messageBox: this.messageBox,
|
|
195
199
|
body: JSON.stringify(paymentToken)
|
|
196
200
|
}, hostOverride)
|
|
197
201
|
}
|
|
@@ -210,14 +214,14 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
210
214
|
* @returns {Promise<void>} Resolves when the payment has been sent.
|
|
211
215
|
* @throws {Error} If payment token generation fails.
|
|
212
216
|
*/
|
|
213
|
-
async sendLivePayment
|
|
217
|
+
async sendLivePayment(payment: PaymentParams, overrideHost?: string): Promise<void> {
|
|
214
218
|
const paymentToken = await this.createPaymentToken(payment)
|
|
215
219
|
|
|
216
220
|
try {
|
|
217
221
|
// Attempt WebSocket first
|
|
218
222
|
await this.sendLiveMessage({
|
|
219
223
|
recipient: payment.recipient,
|
|
220
|
-
messageBox:
|
|
224
|
+
messageBox: this.messageBox,
|
|
221
225
|
body: JSON.stringify(paymentToken)
|
|
222
226
|
}, overrideHost)
|
|
223
227
|
} catch (err) {
|
|
@@ -226,7 +230,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
226
230
|
// Fallback to HTTP if WebSocket fails
|
|
227
231
|
await this.sendMessage({
|
|
228
232
|
recipient: payment.recipient,
|
|
229
|
-
messageBox:
|
|
233
|
+
messageBox: this.messageBox,
|
|
230
234
|
body: JSON.stringify(paymentToken)
|
|
231
235
|
}, overrideHost)
|
|
232
236
|
}
|
|
@@ -244,7 +248,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
244
248
|
* @param {string} [obj.overrideHost] - Optional host override for WebSocket connection.
|
|
245
249
|
* @returns {Promise<void>} Resolves when the listener is successfully set up.
|
|
246
250
|
*/
|
|
247
|
-
async listenForLivePayments
|
|
251
|
+
async listenForLivePayments({
|
|
248
252
|
onPayment,
|
|
249
253
|
overrideHost
|
|
250
254
|
}: {
|
|
@@ -252,7 +256,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
252
256
|
overrideHost?: string
|
|
253
257
|
}): Promise<void> {
|
|
254
258
|
await this.listenForLiveMessages({
|
|
255
|
-
messageBox:
|
|
259
|
+
messageBox: this.messageBox,
|
|
256
260
|
overrideHost,
|
|
257
261
|
|
|
258
262
|
// Convert PeerMessage → IncomingPayment before calling onPayment
|
|
@@ -280,7 +284,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
280
284
|
* @returns {Promise<any>} Resolves with the payment result if successful.
|
|
281
285
|
* @throws {Error} If payment processing fails.
|
|
282
286
|
*/
|
|
283
|
-
async acceptPayment
|
|
287
|
+
async acceptPayment(payment: IncomingPayment): Promise<any> {
|
|
284
288
|
try {
|
|
285
289
|
Logger.log(`[PP CLIENT] Processing payment: ${JSON.stringify(payment, null, 2)}`)
|
|
286
290
|
|
|
@@ -321,7 +325,7 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
321
325
|
* @param {IncomingPayment} payment - The payment object containing transaction details.
|
|
322
326
|
* @returns {Promise<void>} Resolves when the payment is either acknowledged or refunded.
|
|
323
327
|
*/
|
|
324
|
-
async rejectPayment
|
|
328
|
+
async rejectPayment(payment: IncomingPayment): Promise<void> {
|
|
325
329
|
Logger.log(`[PP CLIENT] Rejecting payment: ${JSON.stringify(payment, null, 2)}`)
|
|
326
330
|
|
|
327
331
|
if (payment.token.amount - 1000 < 1000) {
|
|
@@ -382,8 +386,8 @@ export class PeerPayClient extends MessageBoxClient {
|
|
|
382
386
|
* @param {string} [overrideHost] - Optional host override to list payments from
|
|
383
387
|
* @returns {Promise<IncomingPayment[]>} Resolves with an array of pending payments.
|
|
384
388
|
*/
|
|
385
|
-
async listIncomingPayments
|
|
386
|
-
const messages = await this.listMessages({ messageBox:
|
|
389
|
+
async listIncomingPayments(overrideHost?: string): Promise<IncomingPayment[]> {
|
|
390
|
+
const messages = await this.listMessages({ messageBox: this.messageBox, host: overrideHost })
|
|
387
391
|
return messages.map((msg: any) => {
|
|
388
392
|
const parsedToken = safeParse<PaymentToken>(msg.body)
|
|
389
393
|
|