@bsv/message-box-client 1.1.9 → 1.1.11
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 +5 -5
- package/dist/cjs/src/MessageBoxClient.js +707 -87
- package/dist/cjs/src/MessageBoxClient.js.map +1 -1
- package/dist/cjs/src/PeerPayClient.js +61 -28
- package/dist/cjs/src/PeerPayClient.js.map +1 -1
- package/dist/cjs/src/Utils/logger.js +22 -21
- package/dist/cjs/src/Utils/logger.js.map +1 -1
- package/dist/cjs/src/types/permissions.js +6 -0
- package/dist/cjs/src/types/permissions.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/MessageBoxClient.js +593 -12
- package/dist/esm/src/MessageBoxClient.js.map +1 -1
- package/dist/esm/src/PeerPayClient.js +1 -1
- package/dist/esm/src/PeerPayClient.js.map +1 -1
- package/dist/esm/src/Utils/logger.js +17 -19
- package/dist/esm/src/Utils/logger.js.map +1 -1
- package/dist/esm/src/types/permissions.js +5 -0
- package/dist/esm/src/types/permissions.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/MessageBoxClient.d.ts +218 -13
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
- package/dist/types/src/PeerPayClient.d.ts.map +1 -1
- package/dist/types/src/Utils/logger.d.ts +5 -8
- package/dist/types/src/Utils/logger.d.ts.map +1 -1
- package/dist/types/src/types/permissions.d.ts +75 -0
- package/dist/types/src/types/permissions.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +71 -2
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/package.json +5 -5
- package/src/MessageBoxClient.ts +732 -24
- package/src/PeerPayClient.ts +11 -11
- package/src/Utils/logger.ts +17 -19
- package/src/types/permissions.ts +81 -0
- package/src/types.ts +77 -2
package/src/MessageBoxClient.ts
CHANGED
|
@@ -30,25 +30,31 @@ import {
|
|
|
30
30
|
Utils,
|
|
31
31
|
Transaction,
|
|
32
32
|
PushDrop,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
PubKeyHex,
|
|
34
|
+
createNonce,
|
|
35
|
+
P2PKH,
|
|
36
|
+
PublicKey,
|
|
37
|
+
CreateActionOutput,
|
|
38
|
+
WalletInterface,
|
|
39
|
+
ProtoWallet,
|
|
40
|
+
InternalizeOutput,
|
|
41
|
+
Random
|
|
36
42
|
} from '@bsv/sdk'
|
|
37
43
|
import { AuthSocketClient } from '@bsv/authsocket-client'
|
|
38
|
-
import
|
|
39
|
-
import { AcknowledgeMessageParams, EncryptedMessage, ListMessagesParams, MessageBoxClientOptions, PeerMessage, SendMessageParams, SendMessageResponse } from './types.js'
|
|
44
|
+
import * as Logger from './Utils/logger.js'
|
|
45
|
+
import { AcknowledgeMessageParams, AdvertisementToken, EncryptedMessage, ListMessagesParams, MessageBoxClientOptions, Payment, PeerMessage, SendMessageParams, SendMessageResponse, DeviceRegistrationParams, DeviceRegistrationResponse, RegisteredDevice, ListDevicesResponse } from './types.js'
|
|
46
|
+
import {
|
|
47
|
+
SetMessageBoxPermissionParams,
|
|
48
|
+
GetMessageBoxPermissionParams,
|
|
49
|
+
MessageBoxPermission,
|
|
50
|
+
MessageBoxQuote,
|
|
51
|
+
ListPermissionsParams,
|
|
52
|
+
GetQuoteParams
|
|
53
|
+
} from './types/permissions.js'
|
|
40
54
|
|
|
41
55
|
const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems'
|
|
42
56
|
const DEFAULT_TESTNET_HOST = 'https://staging-messagebox.babbage.systems'
|
|
43
57
|
|
|
44
|
-
interface AdvertisementToken {
|
|
45
|
-
host: string
|
|
46
|
-
txid: HexString
|
|
47
|
-
outputIndex: number
|
|
48
|
-
lockingScript: LockingScript
|
|
49
|
-
beef: BEEF
|
|
50
|
-
}
|
|
51
|
-
|
|
52
58
|
/**
|
|
53
59
|
* @class MessageBoxClient
|
|
54
60
|
* @description
|
|
@@ -79,7 +85,7 @@ interface AdvertisementToken {
|
|
|
79
85
|
export class MessageBoxClient {
|
|
80
86
|
private host: string
|
|
81
87
|
public readonly authFetch: AuthFetch
|
|
82
|
-
private readonly walletClient:
|
|
88
|
+
private readonly walletClient: WalletInterface
|
|
83
89
|
private socket?: ReturnType<typeof AuthSocketClient>
|
|
84
90
|
private myIdentityKey?: string
|
|
85
91
|
private readonly joinedRooms: Set<string> = new Set()
|
|
@@ -91,7 +97,7 @@ export class MessageBoxClient {
|
|
|
91
97
|
* @constructor
|
|
92
98
|
* @param {Object} options - Initialization options for the MessageBoxClient.
|
|
93
99
|
* @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts.
|
|
94
|
-
* @param {
|
|
100
|
+
* @param {WalletInterface} options.walletClient - Wallet instance used for authentication, signing, and encryption.
|
|
95
101
|
* @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console.
|
|
96
102
|
* @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup.
|
|
97
103
|
*
|
|
@@ -413,7 +419,7 @@ export class MessageBoxClient {
|
|
|
413
419
|
query
|
|
414
420
|
})
|
|
415
421
|
if (result.type !== 'output-list') {
|
|
416
|
-
throw new Error(`Unexpected result type: ${result.type}`)
|
|
422
|
+
throw new Error(`Unexpected result type: ${String(result.type)}`)
|
|
417
423
|
}
|
|
418
424
|
|
|
419
425
|
for (const output of result.outputs) {
|
|
@@ -623,7 +629,8 @@ export class MessageBoxClient {
|
|
|
623
629
|
messageBox,
|
|
624
630
|
body,
|
|
625
631
|
messageId,
|
|
626
|
-
skipEncryption
|
|
632
|
+
skipEncryption,
|
|
633
|
+
checkPermissions
|
|
627
634
|
}: SendMessageParams): Promise<SendMessageResponse> {
|
|
628
635
|
await this.assertInitialized()
|
|
629
636
|
if (recipient == null || recipient.trim() === '') {
|
|
@@ -701,7 +708,8 @@ export class MessageBoxClient {
|
|
|
701
708
|
messageBox,
|
|
702
709
|
body,
|
|
703
710
|
messageId: finalMessageId,
|
|
704
|
-
skipEncryption
|
|
711
|
+
skipEncryption,
|
|
712
|
+
checkPermissions
|
|
705
713
|
}
|
|
706
714
|
|
|
707
715
|
this.resolveHostForRecipient(recipient)
|
|
@@ -743,7 +751,8 @@ export class MessageBoxClient {
|
|
|
743
751
|
messageBox,
|
|
744
752
|
body,
|
|
745
753
|
messageId: finalMessageId,
|
|
746
|
-
skipEncryption
|
|
754
|
+
skipEncryption,
|
|
755
|
+
checkPermissions
|
|
747
756
|
}
|
|
748
757
|
|
|
749
758
|
this.resolveHostForRecipient(recipient)
|
|
@@ -857,6 +866,43 @@ export class MessageBoxClient {
|
|
|
857
866
|
throw new Error('Every message must have a body!')
|
|
858
867
|
}
|
|
859
868
|
|
|
869
|
+
// Optional permission checking for backwards compatibility
|
|
870
|
+
let paymentData: Payment | undefined
|
|
871
|
+
if (message.checkPermissions === true) {
|
|
872
|
+
try {
|
|
873
|
+
Logger.log('[MB CLIENT] Checking permissions and fees for message...')
|
|
874
|
+
|
|
875
|
+
// Get quote to check if payment is required
|
|
876
|
+
const quote = await this.getMessageBoxQuote({
|
|
877
|
+
recipient: message.recipient,
|
|
878
|
+
messageBox: message.messageBox
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
if (quote.recipientFee === -1) {
|
|
882
|
+
throw new Error('You have been blocked from sending messages to this recipient.')
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (quote.recipientFee > 0 || quote.deliveryFee > 0) {
|
|
886
|
+
const requiredPayment = quote.recipientFee + quote.deliveryFee
|
|
887
|
+
|
|
888
|
+
if (requiredPayment > 0) {
|
|
889
|
+
Logger.log(`[MB CLIENT] Creating payment of ${requiredPayment} sats for message...`)
|
|
890
|
+
|
|
891
|
+
// Create payment using helper method
|
|
892
|
+
paymentData = await this.createMessagePayment(
|
|
893
|
+
message.recipient,
|
|
894
|
+
quote,
|
|
895
|
+
overrideHost
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
Logger.log('[MB CLIENT] Payment data prepared:', paymentData)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
} catch (error) {
|
|
902
|
+
throw new Error(`Permission check failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
860
906
|
let messageId: string
|
|
861
907
|
try {
|
|
862
908
|
const hmac = await this.walletClient.createHmac({
|
|
@@ -890,7 +936,8 @@ export class MessageBoxClient {
|
|
|
890
936
|
...message,
|
|
891
937
|
messageId,
|
|
892
938
|
body: finalBody
|
|
893
|
-
}
|
|
939
|
+
},
|
|
940
|
+
...(paymentData != null && { payment: paymentData })
|
|
894
941
|
}
|
|
895
942
|
|
|
896
943
|
try {
|
|
@@ -1132,9 +1179,16 @@ export class MessageBoxClient {
|
|
|
1132
1179
|
*
|
|
1133
1180
|
* Each message is:
|
|
1134
1181
|
* - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
|
|
1182
|
+
* - Automatically processed for payments: if the message includes recipient fee payments, they are internalized using `walletClient.internalizeAction()`.
|
|
1135
1183
|
* - Returned as a normalized `PeerMessage` with readable string body content.
|
|
1136
1184
|
*
|
|
1137
|
-
*
|
|
1185
|
+
* Payment Processing:
|
|
1186
|
+
* - Detects messages that include payment data (from paid message delivery).
|
|
1187
|
+
* - Automatically internalizes recipient payment outputs, allowing you to receive payments without additional API calls.
|
|
1188
|
+
* - Only recipient payments are stored with messages - delivery fees are already processed by the server.
|
|
1189
|
+
* - Continues processing messages even if payment internalization fails.
|
|
1190
|
+
*
|
|
1191
|
+
* Decryption automatically derives a shared secret using the sender's identity key and the receiver's child private key.
|
|
1138
1192
|
* If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
|
|
1139
1193
|
*
|
|
1140
1194
|
* @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
|
|
@@ -1142,6 +1196,7 @@ export class MessageBoxClient {
|
|
|
1142
1196
|
* @example
|
|
1143
1197
|
* const messages = await client.listMessages({ messageBox: 'inbox' })
|
|
1144
1198
|
* messages.forEach(msg => console.log(msg.sender, msg.body))
|
|
1199
|
+
* // Payments included with messages are automatically received
|
|
1145
1200
|
*/
|
|
1146
1201
|
async listMessages({ messageBox, host }: ListMessagesParams): Promise<PeerMessage[]> {
|
|
1147
1202
|
await this.assertInitialized()
|
|
@@ -1219,10 +1274,70 @@ export class MessageBoxClient {
|
|
|
1219
1274
|
const parsedBody: unknown =
|
|
1220
1275
|
typeof message.body === 'string' ? tryParse(message.body) : message.body
|
|
1221
1276
|
|
|
1277
|
+
let messageContent: any = parsedBody
|
|
1278
|
+
let paymentData: Payment | undefined
|
|
1279
|
+
|
|
1222
1280
|
if (
|
|
1223
1281
|
parsedBody != null &&
|
|
1224
1282
|
typeof parsedBody === 'object' &&
|
|
1225
|
-
|
|
1283
|
+
'message' in parsedBody
|
|
1284
|
+
) {
|
|
1285
|
+
messageContent = (parsedBody as any).message?.body
|
|
1286
|
+
paymentData = (parsedBody as any).payment
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Process payment if present - server now only stores recipient payments
|
|
1290
|
+
if (paymentData?.tx != null && paymentData.outputs != null) {
|
|
1291
|
+
try {
|
|
1292
|
+
Logger.log(
|
|
1293
|
+
`[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
// All outputs in the stored payment data are for the recipient
|
|
1297
|
+
// (delivery fees are already processed by the server)
|
|
1298
|
+
const recipientOutputs = paymentData.outputs.filter(
|
|
1299
|
+
output => output.protocol === 'wallet payment'
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
if (recipientOutputs.length > 0) {
|
|
1303
|
+
Logger.log(
|
|
1304
|
+
`[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
const internalizeResult = await this.walletClient.internalizeAction({
|
|
1308
|
+
tx: paymentData.tx,
|
|
1309
|
+
outputs: recipientOutputs,
|
|
1310
|
+
description: paymentData.description ?? 'MessageBox recipient payment'
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
if (internalizeResult.accepted) {
|
|
1314
|
+
Logger.log(
|
|
1315
|
+
'[MB CLIENT] Successfully internalized recipient payment'
|
|
1316
|
+
)
|
|
1317
|
+
} else {
|
|
1318
|
+
Logger.warn(
|
|
1319
|
+
'[MB CLIENT] Recipient payment internalization was not accepted'
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
} else {
|
|
1323
|
+
Logger.log(
|
|
1324
|
+
'[MB CLIENT] No wallet payment outputs found in payment data'
|
|
1325
|
+
)
|
|
1326
|
+
}
|
|
1327
|
+
} catch (paymentError) {
|
|
1328
|
+
Logger.error(
|
|
1329
|
+
'[MB CLIENT ERROR] Failed to internalize recipient payment:',
|
|
1330
|
+
paymentError
|
|
1331
|
+
)
|
|
1332
|
+
// Continue processing the message even if payment fails
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Handle message decryption
|
|
1337
|
+
if (
|
|
1338
|
+
messageContent != null &&
|
|
1339
|
+
typeof messageContent === 'object' &&
|
|
1340
|
+
typeof (messageContent as any).encryptedMessage === 'string'
|
|
1226
1341
|
) {
|
|
1227
1342
|
Logger.log(
|
|
1228
1343
|
`[MB CLIENT] Decrypting message from ${String(message.sender)}…`
|
|
@@ -1233,7 +1348,7 @@ export class MessageBoxClient {
|
|
|
1233
1348
|
keyID: '1',
|
|
1234
1349
|
counterparty: message.sender,
|
|
1235
1350
|
ciphertext: Utils.toArray(
|
|
1236
|
-
(
|
|
1351
|
+
(messageContent as any).encryptedMessage,
|
|
1237
1352
|
'base64'
|
|
1238
1353
|
)
|
|
1239
1354
|
})
|
|
@@ -1241,7 +1356,10 @@ export class MessageBoxClient {
|
|
|
1241
1356
|
const decryptedText = Utils.toUTF8(decrypted.plaintext)
|
|
1242
1357
|
message.body = tryParse(decryptedText)
|
|
1243
1358
|
} else {
|
|
1244
|
-
|
|
1359
|
+
// Handle both old format (direct content) and new format (message.body)
|
|
1360
|
+
message.body = messageContent != null
|
|
1361
|
+
? (typeof messageContent === 'string' ? messageContent : messageContent)
|
|
1362
|
+
: (parsedBody as string | Record<string, any>)
|
|
1245
1363
|
}
|
|
1246
1364
|
} catch (err) {
|
|
1247
1365
|
Logger.error(
|
|
@@ -1338,4 +1456,594 @@ export class MessageBoxClient {
|
|
|
1338
1456
|
`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`
|
|
1339
1457
|
)
|
|
1340
1458
|
}
|
|
1459
|
+
|
|
1460
|
+
// ===========================
|
|
1461
|
+
// PERMISSION MANAGEMENT METHODS
|
|
1462
|
+
// ===========================
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* @method setMessageBoxPermission
|
|
1466
|
+
* @async
|
|
1467
|
+
* @param {SetMessageBoxPermissionParams} params - Permission configuration
|
|
1468
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1469
|
+
* @returns {Promise<void>} Permission status after setting
|
|
1470
|
+
*
|
|
1471
|
+
* @description
|
|
1472
|
+
* Sets permission for receiving messages in a specific messageBox.
|
|
1473
|
+
* Can set sender-specific permissions or box-wide defaults.
|
|
1474
|
+
*
|
|
1475
|
+
* @example
|
|
1476
|
+
* // Set box-wide default: allow notifications for 10 sats
|
|
1477
|
+
* await client.setMessageBoxPermission({ messageBox: 'notifications', recipientFee: 10 })
|
|
1478
|
+
*
|
|
1479
|
+
* // Block specific sender
|
|
1480
|
+
* await client.setMessageBoxPermission({
|
|
1481
|
+
* messageBox: 'notifications',
|
|
1482
|
+
* sender: '03abc123...',
|
|
1483
|
+
* recipientFee: -1
|
|
1484
|
+
* })
|
|
1485
|
+
*/
|
|
1486
|
+
async setMessageBoxPermission(
|
|
1487
|
+
params: SetMessageBoxPermissionParams,
|
|
1488
|
+
overrideHost?: string
|
|
1489
|
+
): Promise<void> {
|
|
1490
|
+
await this.assertInitialized()
|
|
1491
|
+
const finalHost = overrideHost ?? this.host
|
|
1492
|
+
|
|
1493
|
+
Logger.log('[MB CLIENT] Setting messageBox permission...')
|
|
1494
|
+
|
|
1495
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/set`, {
|
|
1496
|
+
method: 'POST',
|
|
1497
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1498
|
+
body: JSON.stringify({
|
|
1499
|
+
messageBox: params.messageBox,
|
|
1500
|
+
recipientFee: params.recipientFee,
|
|
1501
|
+
...(params.sender != null && { sender: params.sender })
|
|
1502
|
+
})
|
|
1503
|
+
})
|
|
1504
|
+
|
|
1505
|
+
if (!response.ok) {
|
|
1506
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1507
|
+
throw new Error(`Failed to set permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const { status, description } = await response.json()
|
|
1511
|
+
if (status === 'error') {
|
|
1512
|
+
throw new Error(description ?? 'Failed to set permission')
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* @method getMessageBoxPermission
|
|
1518
|
+
* @async
|
|
1519
|
+
* @param {GetMessageBoxPermissionParams} params - Permission query parameters
|
|
1520
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1521
|
+
* @returns {Promise<MessageBoxPermission | null>} Permission data (null if not set)
|
|
1522
|
+
*
|
|
1523
|
+
* @description
|
|
1524
|
+
* Gets current permission data for a sender/messageBox combination.
|
|
1525
|
+
* Returns null if no permission is set.
|
|
1526
|
+
*
|
|
1527
|
+
* @example
|
|
1528
|
+
* const status = await client.getMessageBoxPermission({
|
|
1529
|
+
* recipient: '03def456...',
|
|
1530
|
+
* messageBox: 'notifications',
|
|
1531
|
+
* sender: '03abc123...'
|
|
1532
|
+
* })
|
|
1533
|
+
*/
|
|
1534
|
+
async getMessageBoxPermission(
|
|
1535
|
+
params: GetMessageBoxPermissionParams,
|
|
1536
|
+
overrideHost?: string
|
|
1537
|
+
): Promise<MessageBoxPermission | null> {
|
|
1538
|
+
await this.assertInitialized()
|
|
1539
|
+
|
|
1540
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
|
|
1541
|
+
const queryParams = new URLSearchParams({
|
|
1542
|
+
recipient: params.recipient,
|
|
1543
|
+
messageBox: params.messageBox,
|
|
1544
|
+
...(params.sender != null && { sender: params.sender })
|
|
1545
|
+
})
|
|
1546
|
+
|
|
1547
|
+
Logger.log('[MB CLIENT] Getting messageBox permission...')
|
|
1548
|
+
|
|
1549
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/get?${queryParams.toString()}`, {
|
|
1550
|
+
method: 'GET'
|
|
1551
|
+
})
|
|
1552
|
+
|
|
1553
|
+
if (!response.ok) {
|
|
1554
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1555
|
+
throw new Error(`Failed to get permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const data = await response.json()
|
|
1559
|
+
if (data.status === 'error') {
|
|
1560
|
+
throw new Error(data.description ?? 'Failed to get permission')
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return data.permission
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* @method getMessageBoxQuote
|
|
1568
|
+
* @async
|
|
1569
|
+
* @param {GetQuoteParams} params - Quote request parameters
|
|
1570
|
+
* @returns {Promise<MessageBoxQuote>} Fee quote and permission status
|
|
1571
|
+
*
|
|
1572
|
+
* @description
|
|
1573
|
+
* Gets a fee quote for sending a message, including delivery and recipient fees.
|
|
1574
|
+
*
|
|
1575
|
+
* @example
|
|
1576
|
+
* const quote = await client.getMessageBoxQuote({
|
|
1577
|
+
* recipient: '03def456...',
|
|
1578
|
+
* messageBox: 'notifications'
|
|
1579
|
+
* })
|
|
1580
|
+
*/
|
|
1581
|
+
async getMessageBoxQuote(params: GetQuoteParams, overrideHost?: string): Promise<MessageBoxQuote> {
|
|
1582
|
+
await this.assertInitialized()
|
|
1583
|
+
|
|
1584
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
|
|
1585
|
+
const queryParams = new URLSearchParams({
|
|
1586
|
+
recipient: params.recipient,
|
|
1587
|
+
messageBox: params.messageBox
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
Logger.log('[MB CLIENT] Getting messageBox quote...')
|
|
1591
|
+
|
|
1592
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
|
|
1593
|
+
method: 'GET'
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1596
|
+
if (!response.ok) {
|
|
1597
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1598
|
+
throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const { status, description, quote } = await response.json()
|
|
1602
|
+
if (status === 'error') {
|
|
1603
|
+
throw new Error(description ?? 'Failed to get quote')
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key')
|
|
1607
|
+
|
|
1608
|
+
if (deliveryAgentIdentityKey == null) {
|
|
1609
|
+
throw new Error('Failed to get quote: Delivery agent did not provide their identity key')
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
return {
|
|
1613
|
+
recipientFee: quote.recipientFee,
|
|
1614
|
+
deliveryFee: quote.deliveryFee,
|
|
1615
|
+
deliveryAgentIdentityKey
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* @method listMessageBoxPermissions
|
|
1621
|
+
* @async
|
|
1622
|
+
* @param {ListPermissionsParams} [params] - Optional filtering and pagination parameters
|
|
1623
|
+
* @returns {Promise<MessageBoxPermission[]>} List of current permissions
|
|
1624
|
+
*
|
|
1625
|
+
* @description
|
|
1626
|
+
* Lists permissions for the authenticated user's messageBoxes with optional pagination.
|
|
1627
|
+
*
|
|
1628
|
+
* @example
|
|
1629
|
+
* // List all permissions
|
|
1630
|
+
* const all = await client.listMessageBoxPermissions()
|
|
1631
|
+
*
|
|
1632
|
+
* // List only notification permissions with pagination
|
|
1633
|
+
* const notifications = await client.listMessageBoxPermissions({
|
|
1634
|
+
* messageBox: 'notifications',
|
|
1635
|
+
* limit: 50,
|
|
1636
|
+
* offset: 0
|
|
1637
|
+
* })
|
|
1638
|
+
*/
|
|
1639
|
+
async listMessageBoxPermissions(params?: ListPermissionsParams, overrideHost?: string): Promise<MessageBoxPermission[]> {
|
|
1640
|
+
await this.assertInitialized()
|
|
1641
|
+
|
|
1642
|
+
const finalHost = overrideHost ?? this.host
|
|
1643
|
+
const queryParams = new URLSearchParams()
|
|
1644
|
+
|
|
1645
|
+
if (params?.messageBox != null) {
|
|
1646
|
+
queryParams.set('message_box', params.messageBox)
|
|
1647
|
+
}
|
|
1648
|
+
if (params?.limit !== undefined) {
|
|
1649
|
+
queryParams.set('limit', params.limit.toString())
|
|
1650
|
+
}
|
|
1651
|
+
if (params?.offset !== undefined) {
|
|
1652
|
+
queryParams.set('offset', params.offset.toString())
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
Logger.log('[MB CLIENT] Listing messageBox permissions with params:', queryParams.toString())
|
|
1656
|
+
|
|
1657
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/list?${queryParams.toString()}`, {
|
|
1658
|
+
method: 'GET'
|
|
1659
|
+
})
|
|
1660
|
+
|
|
1661
|
+
if (!response.ok) {
|
|
1662
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1663
|
+
throw new Error(`Failed to list permissions: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const data = await response.json()
|
|
1667
|
+
if (data.status === 'error') {
|
|
1668
|
+
throw new Error(data.description ?? 'Failed to list permissions')
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
return data.permissions.map((p: any) => ({
|
|
1672
|
+
sender: p.sender,
|
|
1673
|
+
messageBox: p.message_box,
|
|
1674
|
+
recipientFee: p.recipient_fee,
|
|
1675
|
+
status: MessageBoxClient.getStatusFromFee(p.recipient_fee),
|
|
1676
|
+
createdAt: p.created_at,
|
|
1677
|
+
updatedAt: p.updated_at
|
|
1678
|
+
}))
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ===========================
|
|
1682
|
+
// NOTIFICATION CONVENIENCE METHODS
|
|
1683
|
+
// ===========================
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* @method allowNotificationsFromPeer
|
|
1687
|
+
* @async
|
|
1688
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to allow
|
|
1689
|
+
* @param {number} [recipientFee=0] - Fee to charge (0 for always allow)
|
|
1690
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1691
|
+
* @returns {Promise<void>} Permission status after allowing
|
|
1692
|
+
*
|
|
1693
|
+
* @description
|
|
1694
|
+
* Convenience method to allow notifications from a specific peer.
|
|
1695
|
+
*
|
|
1696
|
+
* @example
|
|
1697
|
+
* await client.allowNotificationsFromPeer('03abc123...') // Always allow
|
|
1698
|
+
* await client.allowNotificationsFromPeer('03def456...', 5) // Allow for 5 sats
|
|
1699
|
+
*/
|
|
1700
|
+
async allowNotificationsFromPeer(identityKey: PubKeyHex, recipientFee: number = 0, overrideHost?: string): Promise<void> {
|
|
1701
|
+
await this.setMessageBoxPermission({
|
|
1702
|
+
messageBox: 'notifications',
|
|
1703
|
+
sender: identityKey,
|
|
1704
|
+
recipientFee
|
|
1705
|
+
}, overrideHost)
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* @method denyNotificationsFromPeer
|
|
1710
|
+
* @async
|
|
1711
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to block
|
|
1712
|
+
* @returns {Promise<void>} Permission status after denying
|
|
1713
|
+
*
|
|
1714
|
+
* @description
|
|
1715
|
+
* Convenience method to block notifications from a specific peer.
|
|
1716
|
+
*
|
|
1717
|
+
* @example
|
|
1718
|
+
* await client.denyNotificationsFromPeer('03spam123...')
|
|
1719
|
+
*/
|
|
1720
|
+
async denyNotificationsFromPeer(identityKey: PubKeyHex, overrideHost?: string): Promise<void> {
|
|
1721
|
+
await this.setMessageBoxPermission({
|
|
1722
|
+
messageBox: 'notifications',
|
|
1723
|
+
sender: identityKey,
|
|
1724
|
+
recipientFee: -1
|
|
1725
|
+
}, overrideHost)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* @method checkPeerNotificationStatus
|
|
1730
|
+
* @async
|
|
1731
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to check
|
|
1732
|
+
* @returns {Promise<MessageBoxPermission>} Current permission status
|
|
1733
|
+
*
|
|
1734
|
+
* @description
|
|
1735
|
+
* Convenience method to check notification permission for a specific peer.
|
|
1736
|
+
*
|
|
1737
|
+
* @example
|
|
1738
|
+
* const status = await client.checkPeerNotificationStatus('03abc123...')
|
|
1739
|
+
* console.log(status.allowed) // true/false
|
|
1740
|
+
*/
|
|
1741
|
+
async checkPeerNotificationStatus(identityKey: PubKeyHex, overrideHost?: string): Promise<MessageBoxPermission | null> {
|
|
1742
|
+
const myIdentityKey = await this.getIdentityKey()
|
|
1743
|
+
return await this.getMessageBoxPermission({
|
|
1744
|
+
recipient: myIdentityKey,
|
|
1745
|
+
messageBox: 'notifications',
|
|
1746
|
+
sender: identityKey
|
|
1747
|
+
}, overrideHost)
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* @method listPeerNotifications
|
|
1752
|
+
* @async
|
|
1753
|
+
* @returns {Promise<MessageBoxPermission[]>} List of notification permissions
|
|
1754
|
+
*
|
|
1755
|
+
* @description
|
|
1756
|
+
* Convenience method to list all notification permissions.
|
|
1757
|
+
*
|
|
1758
|
+
* @example
|
|
1759
|
+
* const notifications = await client.listPeerNotifications()
|
|
1760
|
+
*/
|
|
1761
|
+
async listPeerNotifications(overrideHost?: string): Promise<MessageBoxPermission[]> {
|
|
1762
|
+
return await this.listMessageBoxPermissions({ messageBox: 'notifications' }, overrideHost)
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* @method sendNotification
|
|
1767
|
+
* @async
|
|
1768
|
+
* @param {PubKeyHex} recipient - Recipient's identity key
|
|
1769
|
+
* @param {string | object} body - Notification content
|
|
1770
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1771
|
+
* @returns {Promise<SendMessageResponse>} Send result
|
|
1772
|
+
*
|
|
1773
|
+
* @description
|
|
1774
|
+
* Convenience method to send a notification with automatic quote fetching and payment handling.
|
|
1775
|
+
* Automatically determines the required payment amount and creates the payment if needed.
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* // Send notification (auto-determines payment needed)
|
|
1779
|
+
* await client.sendNotification('03def456...', 'Hello!')
|
|
1780
|
+
*
|
|
1781
|
+
* // Send with maximum payment limit for safety
|
|
1782
|
+
* await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
|
|
1783
|
+
*/
|
|
1784
|
+
async sendNotification(
|
|
1785
|
+
recipient: PubKeyHex,
|
|
1786
|
+
body: string | object,
|
|
1787
|
+
overrideHost?: string
|
|
1788
|
+
): Promise<SendMessageResponse> {
|
|
1789
|
+
await this.assertInitialized()
|
|
1790
|
+
|
|
1791
|
+
// Use sendMessage with permission checking enabled
|
|
1792
|
+
// This eliminates duplication of quote fetching and payment logic
|
|
1793
|
+
return await this.sendMessage({
|
|
1794
|
+
recipient,
|
|
1795
|
+
messageBox: 'notifications',
|
|
1796
|
+
body,
|
|
1797
|
+
checkPermissions: true
|
|
1798
|
+
}, overrideHost)
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/**
|
|
1802
|
+
* Register a device for FCM push notifications.
|
|
1803
|
+
*
|
|
1804
|
+
* @async
|
|
1805
|
+
* @param {DeviceRegistrationParams} params - Device registration parameters
|
|
1806
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1807
|
+
* @returns {Promise<DeviceRegistrationResponse>} Registration response
|
|
1808
|
+
*
|
|
1809
|
+
* @description
|
|
1810
|
+
* Registers a device with the message box server to receive FCM push notifications.
|
|
1811
|
+
* The FCM token is obtained from Firebase SDK on the client side.
|
|
1812
|
+
*
|
|
1813
|
+
* @example
|
|
1814
|
+
* const result = await client.registerDevice({
|
|
1815
|
+
* fcmToken: 'eBo8F...',
|
|
1816
|
+
* platform: 'ios',
|
|
1817
|
+
* deviceId: 'iPhone15Pro'
|
|
1818
|
+
* })
|
|
1819
|
+
*/
|
|
1820
|
+
async registerDevice(
|
|
1821
|
+
params: DeviceRegistrationParams,
|
|
1822
|
+
overrideHost?: string
|
|
1823
|
+
): Promise<DeviceRegistrationResponse> {
|
|
1824
|
+
await this.assertInitialized()
|
|
1825
|
+
|
|
1826
|
+
if (params.fcmToken == null || params.fcmToken.trim() === '') {
|
|
1827
|
+
throw new Error('fcmToken is required and must be a non-empty string')
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Validate platform if provided
|
|
1831
|
+
const validPlatforms = ['ios', 'android', 'web']
|
|
1832
|
+
if (params.platform != null && !validPlatforms.includes(params.platform)) {
|
|
1833
|
+
throw new Error('platform must be one of: ios, android, web')
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const finalHost = overrideHost ?? this.host
|
|
1837
|
+
|
|
1838
|
+
Logger.log('[MB CLIENT] Registering device for FCM notifications...')
|
|
1839
|
+
|
|
1840
|
+
const response = await this.authFetch.fetch(`${finalHost}/registerDevice`, {
|
|
1841
|
+
method: 'POST',
|
|
1842
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1843
|
+
body: JSON.stringify({
|
|
1844
|
+
fcmToken: params.fcmToken.trim(),
|
|
1845
|
+
deviceId: params.deviceId?.trim() ?? undefined,
|
|
1846
|
+
platform: params.platform ?? undefined
|
|
1847
|
+
})
|
|
1848
|
+
})
|
|
1849
|
+
|
|
1850
|
+
if (!response.ok) {
|
|
1851
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1852
|
+
const description = String(errorData.description) ?? response.statusText
|
|
1853
|
+
throw new Error(`Failed to register device: HTTP ${response.status} - ${description}`)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const data = await response.json()
|
|
1857
|
+
if (data.status === 'error') {
|
|
1858
|
+
throw new Error(data.description ?? 'Failed to register device')
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
Logger.log('[MB CLIENT] Device registered successfully')
|
|
1862
|
+
return {
|
|
1863
|
+
status: data.status,
|
|
1864
|
+
message: data.message,
|
|
1865
|
+
deviceId: data.deviceId
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* List all registered devices for push notifications.
|
|
1871
|
+
*
|
|
1872
|
+
* @async
|
|
1873
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1874
|
+
* @returns {Promise<RegisteredDevice[]>} Array of registered devices
|
|
1875
|
+
*
|
|
1876
|
+
* @description
|
|
1877
|
+
* Retrieves all devices registered by the authenticated user for FCM push notifications.
|
|
1878
|
+
* Only shows devices belonging to the current user (authenticated via AuthFetch).
|
|
1879
|
+
*
|
|
1880
|
+
* @example
|
|
1881
|
+
* const devices = await client.listRegisteredDevices()
|
|
1882
|
+
* console.log(`Found ${devices.length} registered devices`)
|
|
1883
|
+
* devices.forEach(device => {
|
|
1884
|
+
* console.log(`Device: ${device.platform} - ${device.fcmToken}`)
|
|
1885
|
+
* })
|
|
1886
|
+
*/
|
|
1887
|
+
async listRegisteredDevices(
|
|
1888
|
+
overrideHost?: string
|
|
1889
|
+
): Promise<RegisteredDevice[]> {
|
|
1890
|
+
await this.assertInitialized()
|
|
1891
|
+
|
|
1892
|
+
const finalHost = overrideHost ?? this.host
|
|
1893
|
+
|
|
1894
|
+
Logger.log('[MB CLIENT] Listing registered devices...')
|
|
1895
|
+
|
|
1896
|
+
const response = await this.authFetch.fetch(`${finalHost}/devices`, {
|
|
1897
|
+
method: 'GET'
|
|
1898
|
+
})
|
|
1899
|
+
|
|
1900
|
+
if (!response.ok) {
|
|
1901
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1902
|
+
const description = String(errorData.description) ?? response.statusText
|
|
1903
|
+
throw new Error(`Failed to list devices: HTTP ${response.status} - ${description}`)
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const data: ListDevicesResponse = await response.json()
|
|
1907
|
+
if (data.status === 'error') {
|
|
1908
|
+
throw new Error(data.description ?? 'Failed to list devices')
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
Logger.log(`[MB CLIENT] Found ${data.devices.length} registered devices`)
|
|
1912
|
+
return data.devices
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// ===========================
|
|
1916
|
+
// PRIVATE HELPER METHODS
|
|
1917
|
+
// ===========================
|
|
1918
|
+
|
|
1919
|
+
private static getStatusFromFee(fee: number): 'always_allow' | 'blocked' | 'payment_required' {
|
|
1920
|
+
if (fee === -1) return 'blocked'
|
|
1921
|
+
if (fee === 0) return 'always_allow'
|
|
1922
|
+
return 'payment_required'
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Creates payment transaction for message delivery fees
|
|
1927
|
+
* TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
|
|
1928
|
+
* @private
|
|
1929
|
+
* @param {string} recipient - Recipient identity key
|
|
1930
|
+
* @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
|
|
1931
|
+
* @param {string} description - Description for the payment transaction
|
|
1932
|
+
* @returns {Promise<Payment>} Payment transaction data
|
|
1933
|
+
*/
|
|
1934
|
+
private async createMessagePayment(
|
|
1935
|
+
recipient: string,
|
|
1936
|
+
quote: MessageBoxQuote,
|
|
1937
|
+
description: string = 'MessageBox delivery payment'
|
|
1938
|
+
): Promise<Payment> {
|
|
1939
|
+
if (quote.recipientFee <= 0 && quote.deliveryFee <= 0) {
|
|
1940
|
+
throw new Error('No payment required')
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
Logger.log(`[MB CLIENT] Creating payment transaction for ${quote.recipientFee} sats (delivery: ${quote.deliveryFee}, recipient: ${quote.recipientFee})`)
|
|
1944
|
+
|
|
1945
|
+
const outputs: InternalizeOutput[] = []
|
|
1946
|
+
const createActionOutputs: CreateActionOutput[] = []
|
|
1947
|
+
|
|
1948
|
+
// Get sender identity key for remittance data
|
|
1949
|
+
const senderIdentityKey = await this.getIdentityKey()
|
|
1950
|
+
|
|
1951
|
+
// Add server delivery fee output if > 0
|
|
1952
|
+
let outputIndex = 0
|
|
1953
|
+
if (quote.deliveryFee > 0) {
|
|
1954
|
+
const derivationPrefix = Utils.toBase64(Random(32))
|
|
1955
|
+
const derivationSuffix = Utils.toBase64(Random(32))
|
|
1956
|
+
|
|
1957
|
+
// Get host's derived public key
|
|
1958
|
+
const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
|
|
1959
|
+
protocolID: [2, '3241645161d8'],
|
|
1960
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
1961
|
+
counterparty: quote.deliveryAgentIdentityKey
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
// Create locking script using host's public key
|
|
1965
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
|
|
1966
|
+
|
|
1967
|
+
// Add to createAction outputs
|
|
1968
|
+
createActionOutputs.push({
|
|
1969
|
+
satoshis: quote.deliveryFee,
|
|
1970
|
+
lockingScript,
|
|
1971
|
+
outputDescription: 'MessageBox server delivery fee',
|
|
1972
|
+
customInstructions: JSON.stringify({
|
|
1973
|
+
derivationPrefix,
|
|
1974
|
+
derivationSuffix,
|
|
1975
|
+
recipientIdentityKey: quote.deliveryAgentIdentityKey
|
|
1976
|
+
})
|
|
1977
|
+
})
|
|
1978
|
+
|
|
1979
|
+
outputs.push({
|
|
1980
|
+
outputIndex: outputIndex++,
|
|
1981
|
+
protocol: 'wallet payment',
|
|
1982
|
+
paymentRemittance: {
|
|
1983
|
+
derivationPrefix,
|
|
1984
|
+
derivationSuffix,
|
|
1985
|
+
senderIdentityKey
|
|
1986
|
+
}
|
|
1987
|
+
})
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Add recipient fee output if > 0
|
|
1991
|
+
if (quote.recipientFee > 0) {
|
|
1992
|
+
const derivationPrefix = Utils.toBase64(Random(32))
|
|
1993
|
+
const derivationSuffix = Utils.toBase64(Random(32))
|
|
1994
|
+
// Get a derived public key for the recipient that "anyone" can verify
|
|
1995
|
+
const anyoneWallet = new ProtoWallet('anyone')
|
|
1996
|
+
const { publicKey: derivedKeyResult } = await anyoneWallet.getPublicKey({
|
|
1997
|
+
protocolID: [2, '3241645161d8'],
|
|
1998
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
1999
|
+
counterparty: recipient
|
|
2000
|
+
})
|
|
2001
|
+
|
|
2002
|
+
if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
|
|
2003
|
+
throw new Error('Failed to derive recipient\'s public key')
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Create locking script using recipient's public key
|
|
2007
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
|
|
2008
|
+
|
|
2009
|
+
// Add to createAction outputs
|
|
2010
|
+
createActionOutputs.push({
|
|
2011
|
+
satoshis: quote.recipientFee,
|
|
2012
|
+
lockingScript,
|
|
2013
|
+
outputDescription: 'Recipient message fee',
|
|
2014
|
+
customInstructions: JSON.stringify({
|
|
2015
|
+
derivationPrefix,
|
|
2016
|
+
derivationSuffix,
|
|
2017
|
+
recipientIdentityKey: recipient
|
|
2018
|
+
})
|
|
2019
|
+
})
|
|
2020
|
+
|
|
2021
|
+
outputs.push({
|
|
2022
|
+
outputIndex: outputIndex++,
|
|
2023
|
+
protocol: 'wallet payment',
|
|
2024
|
+
paymentRemittance: {
|
|
2025
|
+
derivationPrefix,
|
|
2026
|
+
derivationSuffix,
|
|
2027
|
+
senderIdentityKey
|
|
2028
|
+
}
|
|
2029
|
+
})
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
const { tx } = await this.walletClient.createAction({
|
|
2033
|
+
description,
|
|
2034
|
+
outputs: createActionOutputs,
|
|
2035
|
+
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
|
|
2036
|
+
})
|
|
2037
|
+
|
|
2038
|
+
if (tx == null) {
|
|
2039
|
+
throw new Error('Failed to create payment transaction')
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
return {
|
|
2043
|
+
tx,
|
|
2044
|
+
outputs,
|
|
2045
|
+
description
|
|
2046
|
+
// labels
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1341
2049
|
}
|