@bsv/message-box-client 1.1.10 → 1.2.0

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.
Files changed (36) hide show
  1. package/dist/cjs/package.json +4 -4
  2. package/dist/cjs/src/MessageBoxClient.js +747 -129
  3. package/dist/cjs/src/MessageBoxClient.js.map +1 -1
  4. package/dist/cjs/src/PeerPayClient.js +61 -28
  5. package/dist/cjs/src/PeerPayClient.js.map +1 -1
  6. package/dist/cjs/src/Utils/logger.js +22 -21
  7. package/dist/cjs/src/Utils/logger.js.map +1 -1
  8. package/dist/cjs/src/types/permissions.js +6 -0
  9. package/dist/cjs/src/types/permissions.js.map +1 -0
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/MessageBoxClient.js +636 -57
  12. package/dist/esm/src/MessageBoxClient.js.map +1 -1
  13. package/dist/esm/src/PeerPayClient.js +1 -1
  14. package/dist/esm/src/PeerPayClient.js.map +1 -1
  15. package/dist/esm/src/Utils/logger.js +17 -19
  16. package/dist/esm/src/Utils/logger.js.map +1 -1
  17. package/dist/esm/src/types/permissions.js +5 -0
  18. package/dist/esm/src/types/permissions.js.map +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/MessageBoxClient.d.ts +235 -24
  21. package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
  22. package/dist/types/src/PeerPayClient.d.ts.map +1 -1
  23. package/dist/types/src/Utils/logger.d.ts +5 -8
  24. package/dist/types/src/Utils/logger.d.ts.map +1 -1
  25. package/dist/types/src/types/permissions.d.ts +75 -0
  26. package/dist/types/src/types/permissions.d.ts.map +1 -0
  27. package/dist/types/src/types.d.ts +80 -2
  28. package/dist/types/src/types.d.ts.map +1 -1
  29. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  30. package/dist/umd/bundle.js +1 -1
  31. package/package.json +4 -4
  32. package/src/MessageBoxClient.ts +781 -68
  33. package/src/PeerPayClient.ts +11 -11
  34. package/src/Utils/logger.ts +17 -19
  35. package/src/types/permissions.ts +81 -0
  36. package/src/types.ts +87 -2
@@ -21,9 +21,9 @@
21
21
  * @author Project Babbage
22
22
  * @license Open BSV License
23
23
  */
24
- import { WalletClient, AuthFetch, LookupResolver, TopicBroadcaster, Utils, Transaction, PushDrop } from '@bsv/sdk';
24
+ import { WalletClient, AuthFetch, LookupResolver, TopicBroadcaster, Utils, Transaction, PushDrop, P2PKH, PublicKey, ProtoWallet, Random } from '@bsv/sdk';
25
25
  import { AuthSocketClient } from '@bsv/authsocket-client';
26
- import { Logger } from './Utils/logger.js';
26
+ import * as Logger from './Utils/logger.js';
27
27
  const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems';
28
28
  const DEFAULT_TESTNET_HOST = 'https://staging-messagebox.babbage.systems';
29
29
  /**
@@ -67,7 +67,7 @@ export class MessageBoxClient {
67
67
  * @constructor
68
68
  * @param {Object} options - Initialization options for the MessageBoxClient.
69
69
  * @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts.
70
- * @param {WalletClient} options.walletClient - Wallet instance used for authentication, signing, and encryption.
70
+ * @param {WalletInterface} options.walletClient - Wallet instance used for authentication, signing, and encryption.
71
71
  * @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console.
72
72
  * @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup.
73
73
  *
@@ -88,12 +88,12 @@ export class MessageBoxClient {
88
88
  * await client.init()
89
89
  */
90
90
  constructor(options = {}) {
91
- const { host, walletClient, enableLogging = false, networkPreset = 'mainnet' } = options;
91
+ const { host, walletClient, enableLogging = false, networkPreset = 'mainnet', originator = undefined } = options;
92
92
  const defaultHost = this.networkPreset === 'testnet'
93
93
  ? DEFAULT_TESTNET_HOST
94
94
  : DEFAULT_MAINNET_HOST;
95
95
  this.host = host?.trim() ?? defaultHost;
96
- this.walletClient = walletClient ?? new WalletClient();
96
+ this.walletClient = walletClient ?? new WalletClient('auto', originator);
97
97
  this.authFetch = new AuthFetch(this.walletClient);
98
98
  this.networkPreset = networkPreset;
99
99
  this.lookupResolver = new LookupResolver({
@@ -107,6 +107,7 @@ export class MessageBoxClient {
107
107
  * @method init
108
108
  * @async
109
109
  * @param {string} [targetHost] - Optional host to set or override the default host.
110
+ * @param {string} [originator] - Optional originator to use with walletClient.
110
111
  * @returns {Promise<void>}
111
112
  *
112
113
  * @description
@@ -125,7 +126,7 @@ export class MessageBoxClient {
125
126
  * await client.init()
126
127
  * await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
127
128
  */
128
- async init(targetHost = this.host) {
129
+ async init(targetHost = this.host, originator) {
129
130
  const normalizedHost = targetHost?.trim();
130
131
  if (normalizedHost === '') {
131
132
  throw new Error('Cannot anoint host: No valid host provided');
@@ -138,13 +139,13 @@ export class MessageBoxClient {
138
139
  if (this.initialized)
139
140
  return;
140
141
  // 1. Get our identity key
141
- const identityKey = await this.getIdentityKey();
142
+ const identityKey = await this.getIdentityKey(originator);
142
143
  // 2. Check for any matching advertisements for the given host
143
- const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost);
144
+ const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost, originator);
144
145
  // 3. If none our found, anoint this host
145
146
  if (firstAdvertisement == null || firstAdvertisement?.host?.trim() === '' || firstAdvertisement?.host !== normalizedHost) {
146
147
  Logger.log('[MB CLIENT] Anointing host:', normalizedHost);
147
- const { txid } = await this.anointHost(normalizedHost);
148
+ const { txid } = await this.anointHost(normalizedHost, originator);
148
149
  if (txid == null || txid.trim() === '') {
149
150
  throw new Error('Failed to anoint host: No transaction ID returned');
150
151
  }
@@ -179,18 +180,19 @@ export class MessageBoxClient {
179
180
  }
180
181
  /**
181
182
  * @method getIdentityKey
183
+ * @param {string} [originator] - Optional originator to use for identity key lookup
182
184
  * @returns {Promise<string>} The identity public key of the user
183
185
  * @description
184
186
  * Returns the client's identity key, used for signing, encryption, and addressing.
185
187
  * If not already loaded, it will fetch and cache it.
186
188
  */
187
- async getIdentityKey() {
189
+ async getIdentityKey(originator) {
188
190
  if (this.myIdentityKey != null && this.myIdentityKey.trim() !== '') {
189
191
  return this.myIdentityKey;
190
192
  }
191
193
  Logger.log('[MB CLIENT] Fetching identity key...');
192
194
  try {
193
- const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
195
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator);
194
196
  this.myIdentityKey = keyResult.publicKey;
195
197
  Logger.log(`[MB CLIENT] Identity key fetched: ${this.myIdentityKey}`);
196
198
  return this.myIdentityKey;
@@ -216,6 +218,7 @@ export class MessageBoxClient {
216
218
  }
217
219
  /**
218
220
  * @method initializeConnection
221
+ * @param {string} [originator] - Optional originator to use for authentication.
219
222
  * @async
220
223
  * @returns {Promise<void>}
221
224
  * @description
@@ -237,20 +240,11 @@ export class MessageBoxClient {
237
240
  * await mb.initializeConnection()
238
241
  * // WebSocket is now ready for use
239
242
  */
240
- async initializeConnection() {
243
+ async initializeConnection(originator) {
241
244
  await this.assertInitialized();
242
245
  Logger.log('[MB CLIENT] initializeConnection() STARTED');
243
246
  if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
244
- Logger.log('[MB CLIENT] Fetching identity key...');
245
- try {
246
- const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
247
- this.myIdentityKey = keyResult.publicKey;
248
- Logger.log(`[MB CLIENT] Identity key fetched successfully: ${this.myIdentityKey}`);
249
- }
250
- catch (error) {
251
- Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error);
252
- throw new Error('Identity key retrieval failed');
253
- }
247
+ await this.getIdentityKey(originator);
254
248
  }
255
249
  if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
256
250
  Logger.error('[MB CLIENT ERROR] Identity key is still missing after retrieval!');
@@ -314,6 +308,7 @@ export class MessageBoxClient {
314
308
  * @method resolveHostForRecipient
315
309
  * @async
316
310
  * @param {string} identityKey - The public identity key of the intended recipient.
311
+ * @param {string} [originator] - The originator to use for the WalletClient.
317
312
  * @returns {Promise<string>} - A fully qualified host URL for the recipient's MessageBox server.
318
313
  *
319
314
  * @description
@@ -328,8 +323,8 @@ export class MessageBoxClient {
328
323
  * @example
329
324
  * const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host
330
325
  */
331
- async resolveHostForRecipient(identityKey) {
332
- const advertisementTokens = await this.queryAdvertisements(identityKey);
326
+ async resolveHostForRecipient(identityKey, originator) {
327
+ const advertisementTokens = await this.queryAdvertisements(identityKey, undefined, originator);
333
328
  if (advertisementTokens.length === 0) {
334
329
  Logger.warn(`[MB CLIENT] No advertisements for ${identityKey}, using default host ${this.host}`);
335
330
  return this.host;
@@ -345,10 +340,10 @@ export class MessageBoxClient {
345
340
  * @param host? if passed, only look for adverts anointed at that host
346
341
  * @returns 0-length array if nothing valid was found
347
342
  */
348
- async queryAdvertisements(identityKey, host) {
343
+ async queryAdvertisements(identityKey, host, originator) {
349
344
  const hosts = [];
350
345
  try {
351
- const query = { identityKey: identityKey ?? await this.getIdentityKey() };
346
+ const query = { identityKey: identityKey ?? await this.getIdentityKey(originator) };
352
347
  if (host != null && host.trim() !== '')
353
348
  query.host = host;
354
349
  const result = await this.lookupResolver.query({
@@ -356,7 +351,7 @@ export class MessageBoxClient {
356
351
  query
357
352
  });
358
353
  if (result.type !== 'output-list') {
359
- throw new Error(`Unexpected result type: ${result.type}`);
354
+ throw new Error(`Unexpected result type: ${String(result.type)}`);
360
355
  }
361
356
  for (const output of result.outputs) {
362
357
  try {
@@ -458,7 +453,7 @@ export class MessageBoxClient {
458
453
  * onMessage: (msg) => console.log('Received live message:', msg)
459
454
  * })
460
455
  */
461
- async listenForLiveMessages({ onMessage, messageBox }) {
456
+ async listenForLiveMessages({ onMessage, messageBox, originator }) {
462
457
  await this.assertInitialized();
463
458
  Logger.log(`[MB CLIENT] Setting up listener for WebSocket room: ${messageBox}`);
464
459
  // Ensure WebSocket connection and room join
@@ -491,7 +486,7 @@ export class MessageBoxClient {
491
486
  keyID: '1',
492
487
  counterparty: message.sender,
493
488
  ciphertext: Utils.toArray(parsedBody.encryptedMessage, 'base64')
494
- });
489
+ }, originator);
495
490
  message.body = Utils.toUTF8(decrypted.plaintext);
496
491
  }
497
492
  else {
@@ -543,7 +538,7 @@ export class MessageBoxClient {
543
538
  * body: { amount: 1000 }
544
539
  * })
545
540
  */
546
- async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }) {
541
+ async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption, checkPermissions, originator }) {
547
542
  await this.assertInitialized();
548
543
  if (recipient == null || recipient.trim() === '') {
549
544
  throw new Error('[MB CLIENT ERROR] Recipient identity key is required');
@@ -569,7 +564,7 @@ export class MessageBoxClient {
569
564
  protocolID: [1, 'messagebox'],
570
565
  keyID: '1',
571
566
  counterparty: recipient
572
- });
567
+ }, originator);
573
568
  finalMessageId = messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
574
569
  }
575
570
  catch (error) {
@@ -588,7 +583,7 @@ export class MessageBoxClient {
588
583
  keyID: '1',
589
584
  counterparty: recipient,
590
585
  plaintext: Utils.toArray(typeof body === 'string' ? body : JSON.stringify(body), 'utf8')
591
- });
586
+ }, originator);
592
587
  outgoingBody = JSON.stringify({
593
588
  encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext)
594
589
  });
@@ -612,7 +607,8 @@ export class MessageBoxClient {
612
607
  messageBox,
613
608
  body,
614
609
  messageId: finalMessageId,
615
- skipEncryption
610
+ skipEncryption,
611
+ checkPermissions
616
612
  };
617
613
  this.resolveHostForRecipient(recipient)
618
614
  .then(async (host) => {
@@ -651,7 +647,8 @@ export class MessageBoxClient {
651
647
  messageBox,
652
648
  body,
653
649
  messageId: finalMessageId,
654
- skipEncryption
650
+ skipEncryption,
651
+ checkPermissions
655
652
  };
656
653
  this.resolveHostForRecipient(recipient)
657
654
  .then(async (host) => {
@@ -744,7 +741,7 @@ export class MessageBoxClient {
744
741
  * body: { type: 'ping' }
745
742
  * })
746
743
  */
747
- async sendMessage(message, overrideHost) {
744
+ async sendMessage(message, overrideHost, originator) {
748
745
  await this.assertInitialized();
749
746
  if (message.recipient == null || message.recipient.trim() === '') {
750
747
  throw new Error('You must provide a message recipient!');
@@ -755,6 +752,33 @@ export class MessageBoxClient {
755
752
  if (message.body == null || (typeof message.body === 'string' && message.body.trim().length === 0)) {
756
753
  throw new Error('Every message must have a body!');
757
754
  }
755
+ // Optional permission checking for backwards compatibility
756
+ let paymentData;
757
+ if (message.checkPermissions === true) {
758
+ try {
759
+ Logger.log('[MB CLIENT] Checking permissions and fees for message...');
760
+ // Get quote to check if payment is required
761
+ const quote = await this.getMessageBoxQuote({
762
+ recipient: message.recipient,
763
+ messageBox: message.messageBox
764
+ });
765
+ if (quote.recipientFee === -1) {
766
+ throw new Error('You have been blocked from sending messages to this recipient.');
767
+ }
768
+ if (quote.recipientFee > 0 || quote.deliveryFee > 0) {
769
+ const requiredPayment = quote.recipientFee + quote.deliveryFee;
770
+ if (requiredPayment > 0) {
771
+ Logger.log(`[MB CLIENT] Creating payment of ${requiredPayment} sats for message...`);
772
+ // Create payment using helper method
773
+ paymentData = await this.createMessagePayment(message.recipient, quote, overrideHost);
774
+ Logger.log('[MB CLIENT] Payment data prepared:', paymentData);
775
+ }
776
+ }
777
+ }
778
+ catch (error) {
779
+ throw new Error(`Permission check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
780
+ }
781
+ }
758
782
  let messageId;
759
783
  try {
760
784
  const hmac = await this.walletClient.createHmac({
@@ -762,7 +786,7 @@ export class MessageBoxClient {
762
786
  protocolID: [1, 'messagebox'],
763
787
  keyID: '1',
764
788
  counterparty: message.recipient
765
- });
789
+ }, originator);
766
790
  messageId = message.messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
767
791
  }
768
792
  catch (error) {
@@ -779,7 +803,7 @@ export class MessageBoxClient {
779
803
  keyID: '1',
780
804
  counterparty: message.recipient,
781
805
  plaintext: Utils.toArray(typeof message.body === 'string' ? message.body : JSON.stringify(message.body), 'utf8')
782
- });
806
+ }, originator);
783
807
  finalBody = JSON.stringify({ encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext) });
784
808
  }
785
809
  const requestBody = {
@@ -787,7 +811,8 @@ export class MessageBoxClient {
787
811
  ...message,
788
812
  messageId,
789
813
  body: finalBody
790
- }
814
+ },
815
+ ...(paymentData != null && { payment: paymentData })
791
816
  };
792
817
  try {
793
818
  const finalHost = overrideHost ?? await this.resolveHostForRecipient(message.recipient);
@@ -795,7 +820,7 @@ export class MessageBoxClient {
795
820
  Logger.log('[MB CLIENT] Request Body:', JSON.stringify(requestBody, null, 2));
796
821
  if (this.myIdentityKey == null || this.myIdentityKey === '') {
797
822
  try {
798
- const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
823
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator);
799
824
  this.myIdentityKey = keyResult.publicKey;
800
825
  Logger.log(`[MB CLIENT] Fetched identity key before sending request: ${this.myIdentityKey}`);
801
826
  }
@@ -856,19 +881,19 @@ export class MessageBoxClient {
856
881
  * @example
857
882
  * const { txid } = await client.anointHost('https://my-messagebox.io')
858
883
  */
859
- async anointHost(host) {
884
+ async anointHost(host, originator) {
860
885
  Logger.log('[MB CLIENT] Starting anointHost...');
861
886
  try {
862
887
  if (!host.startsWith('http')) {
863
888
  throw new Error('Invalid host URL');
864
889
  }
865
- const identityKey = await this.getIdentityKey();
890
+ const identityKey = await this.getIdentityKey(originator);
866
891
  Logger.log('[MB CLIENT] Fields - Identity:', identityKey, 'Host:', host);
867
892
  const fields = [
868
893
  Utils.toArray(identityKey, 'hex'),
869
894
  Utils.toArray(host, 'utf8')
870
895
  ];
871
- const pushdrop = new PushDrop(this.walletClient);
896
+ const pushdrop = new PushDrop(this.walletClient, originator);
872
897
  Logger.log('Fields:', fields.map(a => Utils.toHex(a)));
873
898
  Logger.log('ProtocolID:', [1, 'messagebox advertisement']);
874
899
  Logger.log('KeyID:', '1');
@@ -886,7 +911,7 @@ export class MessageBoxClient {
886
911
  outputDescription: 'Overlay advertisement output'
887
912
  }],
888
913
  options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
889
- });
914
+ }, originator);
890
915
  Logger.log('[MB CLIENT] Transaction created:', txid);
891
916
  if (tx !== undefined) {
892
917
  const broadcaster = new TopicBroadcaster(['tm_messagebox'], {
@@ -910,6 +935,7 @@ export class MessageBoxClient {
910
935
  * @method revokeHostAdvertisement
911
936
  * @async
912
937
  * @param {AdvertisementToken} advertisementToken - The advertisement token containing the messagebox host to revoke.
938
+ * @param {string} [originator] - Optional originator to use with walletClient.
913
939
  * @returns {Promise<{ txid: string }>} - The transaction ID of the revocation broadcast to the overlay network.
914
940
  *
915
941
  * @description
@@ -919,7 +945,7 @@ export class MessageBoxClient {
919
945
  * @example
920
946
  * const { txid } = await client.revokeHost('https://my-messagebox.io')
921
947
  */
922
- async revokeHostAdvertisement(advertisementToken) {
948
+ async revokeHostAdvertisement(advertisementToken, originator) {
923
949
  Logger.log('[MB CLIENT] Starting revokeHost...');
924
950
  const outpoint = `${advertisementToken.txid}.${advertisementToken.outputIndex}`;
925
951
  try {
@@ -933,13 +959,13 @@ export class MessageBoxClient {
933
959
  inputDescription: 'Revoking host advertisement token'
934
960
  }
935
961
  ]
936
- });
962
+ }, originator);
937
963
  if (signableTransaction === undefined) {
938
964
  throw new Error('Failed to create signable transaction.');
939
965
  }
940
966
  const partialTx = Transaction.fromBEEF(signableTransaction.tx);
941
967
  // Prepare the unlocker
942
- const pushdrop = new PushDrop(this.walletClient);
968
+ const pushdrop = new PushDrop(this.walletClient, originator);
943
969
  const unlocker = await pushdrop.unlock([1, 'messagebox advertisement'], '1', 'anyone', 'all', false, advertisementToken.outputIndex, advertisementToken.lockingScript);
944
970
  // Convert to Transaction, apply signature
945
971
  const finalUnlockScript = await unlocker.sign(partialTx, advertisementToken.outputIndex);
@@ -954,7 +980,7 @@ export class MessageBoxClient {
954
980
  options: {
955
981
  acceptDelayedBroadcast: false
956
982
  }
957
- });
983
+ }, originator);
958
984
  if (signedTx === undefined) {
959
985
  throw new Error('Failed to finalize the transaction signature.');
960
986
  }
@@ -985,9 +1011,16 @@ export class MessageBoxClient {
985
1011
  *
986
1012
  * Each message is:
987
1013
  * - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
1014
+ * - Automatically processed for payments: if the message includes recipient fee payments, they are internalized using `walletClient.internalizeAction()`.
988
1015
  * - Returned as a normalized `PeerMessage` with readable string body content.
989
1016
  *
990
- * Decryption automatically derives a shared secret using the sender’s identity key and the receiver’s child private key.
1017
+ * Payment Processing:
1018
+ * - Detects messages that include payment data (from paid message delivery).
1019
+ * - Automatically internalizes recipient payment outputs, allowing you to receive payments without additional API calls.
1020
+ * - Only recipient payments are stored with messages - delivery fees are already processed by the server.
1021
+ * - Continues processing messages even if payment internalization fails.
1022
+ *
1023
+ * Decryption automatically derives a shared secret using the sender's identity key and the receiver's child private key.
991
1024
  * If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
992
1025
  *
993
1026
  * @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
@@ -995,15 +1028,16 @@ export class MessageBoxClient {
995
1028
  * @example
996
1029
  * const messages = await client.listMessages({ messageBox: 'inbox' })
997
1030
  * messages.forEach(msg => console.log(msg.sender, msg.body))
1031
+ * // Payments included with messages are automatically received
998
1032
  */
999
- async listMessages({ messageBox, host }) {
1033
+ async listMessages({ messageBox, host, originator }) {
1000
1034
  await this.assertInitialized();
1001
1035
  if (messageBox.trim() === '') {
1002
1036
  throw new Error('MessageBox cannot be empty');
1003
1037
  }
1004
1038
  let hosts = host != null ? [host] : [];
1005
1039
  if (hosts.length === 0) {
1006
- const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey());
1040
+ const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey(originator), originator);
1007
1041
  hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
1008
1042
  }
1009
1043
  // Query each host in parallel
@@ -1066,21 +1100,65 @@ export class MessageBoxClient {
1066
1100
  for (const message of messages) {
1067
1101
  try {
1068
1102
  const parsedBody = typeof message.body === 'string' ? tryParse(message.body) : message.body;
1103
+ let messageContent = parsedBody;
1104
+ let paymentData;
1069
1105
  if (parsedBody != null &&
1070
1106
  typeof parsedBody === 'object' &&
1071
- typeof parsedBody.encryptedMessage === 'string') {
1107
+ 'message' in parsedBody) {
1108
+ // Handle wrapped message format (with payment data)
1109
+ const wrappedMessage = parsedBody.message;
1110
+ messageContent = typeof wrappedMessage === 'string'
1111
+ ? tryParse(wrappedMessage)
1112
+ : wrappedMessage;
1113
+ paymentData = parsedBody.payment;
1114
+ }
1115
+ // Process payment if present - server now only stores recipient payments
1116
+ if (paymentData?.tx != null && paymentData.outputs != null) {
1117
+ try {
1118
+ Logger.log(`[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`);
1119
+ // All outputs in the stored payment data are for the recipient
1120
+ // (delivery fees are already processed by the server)
1121
+ const recipientOutputs = paymentData.outputs.filter(output => output.protocol === 'wallet payment');
1122
+ if (recipientOutputs.length > 0) {
1123
+ Logger.log(`[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`);
1124
+ const internalizeResult = await this.walletClient.internalizeAction({
1125
+ tx: paymentData.tx,
1126
+ outputs: recipientOutputs,
1127
+ description: paymentData.description ?? 'MessageBox recipient payment'
1128
+ }, originator);
1129
+ if (internalizeResult.accepted) {
1130
+ Logger.log('[MB CLIENT] Successfully internalized recipient payment');
1131
+ }
1132
+ else {
1133
+ Logger.warn('[MB CLIENT] Recipient payment internalization was not accepted');
1134
+ }
1135
+ }
1136
+ else {
1137
+ Logger.log('[MB CLIENT] No wallet payment outputs found in payment data');
1138
+ }
1139
+ }
1140
+ catch (paymentError) {
1141
+ Logger.error('[MB CLIENT ERROR] Failed to internalize recipient payment:', paymentError);
1142
+ // Continue processing the message even if payment fails
1143
+ }
1144
+ }
1145
+ // Handle message decryption
1146
+ if (messageContent != null &&
1147
+ typeof messageContent === 'object' &&
1148
+ typeof messageContent.encryptedMessage === 'string') {
1072
1149
  Logger.log(`[MB CLIENT] Decrypting message from ${String(message.sender)}…`);
1073
1150
  const decrypted = await this.walletClient.decrypt({
1074
1151
  protocolID: [1, 'messagebox'],
1075
1152
  keyID: '1',
1076
1153
  counterparty: message.sender,
1077
- ciphertext: Utils.toArray(parsedBody.encryptedMessage, 'base64')
1078
- });
1154
+ ciphertext: Utils.toArray(messageContent.encryptedMessage, 'base64')
1155
+ }, originator);
1079
1156
  const decryptedText = Utils.toUTF8(decrypted.plaintext);
1080
1157
  message.body = tryParse(decryptedText);
1081
1158
  }
1082
1159
  else {
1083
- message.body = parsedBody;
1160
+ // For non-encrypted messages, use the processed content
1161
+ message.body = messageContent ?? parsedBody;
1084
1162
  }
1085
1163
  }
1086
1164
  catch (err) {
@@ -1113,7 +1191,7 @@ export class MessageBoxClient {
1113
1191
  * @example
1114
1192
  * await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] })
1115
1193
  */
1116
- async acknowledgeMessage({ messageIds, host }) {
1194
+ async acknowledgeMessage({ messageIds, host, originator }) {
1117
1195
  await this.assertInitialized();
1118
1196
  if (!Array.isArray(messageIds) || messageIds.length === 0) {
1119
1197
  throw new Error('Message IDs array cannot be empty');
@@ -1122,8 +1200,8 @@ export class MessageBoxClient {
1122
1200
  let hosts = host != null ? [host] : [];
1123
1201
  if (hosts.length === 0) {
1124
1202
  // 1. Determine all hosts (advertised + default)
1125
- const identityKey = await this.getIdentityKey();
1126
- const advertisedHosts = await this.queryAdvertisements(identityKey);
1203
+ const identityKey = await this.getIdentityKey(originator);
1204
+ const advertisedHosts = await this.queryAdvertisements(identityKey, undefined, originator);
1127
1205
  hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
1128
1206
  }
1129
1207
  // 2. Dispatch parallel acknowledge requests
@@ -1161,5 +1239,506 @@ export class MessageBoxClient {
1161
1239
  }
1162
1240
  throw new Error(`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`);
1163
1241
  }
1242
+ // ===========================
1243
+ // PERMISSION MANAGEMENT METHODS
1244
+ // ===========================
1245
+ /**
1246
+ * @method setMessageBoxPermission
1247
+ * @async
1248
+ * @param {SetMessageBoxPermissionParams} params - Permission configuration
1249
+ * @param {string} [overrideHost] - Optional host override
1250
+ * @returns {Promise<void>} Permission status after setting
1251
+ *
1252
+ * @description
1253
+ * Sets permission for receiving messages in a specific messageBox.
1254
+ * Can set sender-specific permissions or box-wide defaults.
1255
+ *
1256
+ * @example
1257
+ * // Set box-wide default: allow notifications for 10 sats
1258
+ * await client.setMessageBoxPermission({ messageBox: 'notifications', recipientFee: 10 })
1259
+ *
1260
+ * // Block specific sender
1261
+ * await client.setMessageBoxPermission({
1262
+ * messageBox: 'notifications',
1263
+ * sender: '03abc123...',
1264
+ * recipientFee: -1
1265
+ * })
1266
+ */
1267
+ async setMessageBoxPermission(params, overrideHost) {
1268
+ await this.assertInitialized();
1269
+ const finalHost = overrideHost ?? this.host;
1270
+ Logger.log('[MB CLIENT] Setting messageBox permission...');
1271
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/set`, {
1272
+ method: 'POST',
1273
+ headers: { 'Content-Type': 'application/json' },
1274
+ body: JSON.stringify({
1275
+ messageBox: params.messageBox,
1276
+ recipientFee: params.recipientFee,
1277
+ ...(params.sender != null && { sender: params.sender })
1278
+ })
1279
+ });
1280
+ if (!response.ok) {
1281
+ const errorData = await response.json().catch(() => ({}));
1282
+ throw new Error(`Failed to set permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1283
+ }
1284
+ const { status, description } = await response.json();
1285
+ if (status === 'error') {
1286
+ throw new Error(description ?? 'Failed to set permission');
1287
+ }
1288
+ }
1289
+ /**
1290
+ * @method getMessageBoxPermission
1291
+ * @async
1292
+ * @param {GetMessageBoxPermissionParams} params - Permission query parameters
1293
+ * @param {string} [overrideHost] - Optional host override
1294
+ * @returns {Promise<MessageBoxPermission | null>} Permission data (null if not set)
1295
+ *
1296
+ * @description
1297
+ * Gets current permission data for a sender/messageBox combination.
1298
+ * Returns null if no permission is set.
1299
+ *
1300
+ * @example
1301
+ * const status = await client.getMessageBoxPermission({
1302
+ * recipient: '03def456...',
1303
+ * messageBox: 'notifications',
1304
+ * sender: '03abc123...'
1305
+ * })
1306
+ */
1307
+ async getMessageBoxPermission(params, overrideHost) {
1308
+ await this.assertInitialized();
1309
+ const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
1310
+ const queryParams = new URLSearchParams({
1311
+ recipient: params.recipient,
1312
+ messageBox: params.messageBox,
1313
+ ...(params.sender != null && { sender: params.sender })
1314
+ });
1315
+ Logger.log('[MB CLIENT] Getting messageBox permission...');
1316
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/get?${queryParams.toString()}`, {
1317
+ method: 'GET'
1318
+ });
1319
+ if (!response.ok) {
1320
+ const errorData = await response.json().catch(() => ({}));
1321
+ throw new Error(`Failed to get permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1322
+ }
1323
+ const data = await response.json();
1324
+ if (data.status === 'error') {
1325
+ throw new Error(data.description ?? 'Failed to get permission');
1326
+ }
1327
+ return data.permission;
1328
+ }
1329
+ /**
1330
+ * @method getMessageBoxQuote
1331
+ * @async
1332
+ * @param {GetQuoteParams} params - Quote request parameters
1333
+ * @returns {Promise<MessageBoxQuote>} Fee quote and permission status
1334
+ *
1335
+ * @description
1336
+ * Gets a fee quote for sending a message, including delivery and recipient fees.
1337
+ *
1338
+ * @example
1339
+ * const quote = await client.getMessageBoxQuote({
1340
+ * recipient: '03def456...',
1341
+ * messageBox: 'notifications'
1342
+ * })
1343
+ */
1344
+ async getMessageBoxQuote(params, overrideHost) {
1345
+ await this.assertInitialized();
1346
+ const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
1347
+ const queryParams = new URLSearchParams({
1348
+ recipient: params.recipient,
1349
+ messageBox: params.messageBox
1350
+ });
1351
+ Logger.log('[MB CLIENT] Getting messageBox quote...');
1352
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
1353
+ method: 'GET'
1354
+ });
1355
+ if (!response.ok) {
1356
+ const errorData = await response.json().catch(() => ({}));
1357
+ throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`);
1358
+ }
1359
+ const { status, description, quote } = await response.json();
1360
+ if (status === 'error') {
1361
+ throw new Error(description ?? 'Failed to get quote');
1362
+ }
1363
+ const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key');
1364
+ if (deliveryAgentIdentityKey == null) {
1365
+ throw new Error('Failed to get quote: Delivery agent did not provide their identity key');
1366
+ }
1367
+ return {
1368
+ recipientFee: quote.recipientFee,
1369
+ deliveryFee: quote.deliveryFee,
1370
+ deliveryAgentIdentityKey
1371
+ };
1372
+ }
1373
+ /**
1374
+ * @method listMessageBoxPermissions
1375
+ * @async
1376
+ * @param {ListPermissionsParams} [params] - Optional filtering and pagination parameters
1377
+ * @returns {Promise<MessageBoxPermission[]>} List of current permissions
1378
+ *
1379
+ * @description
1380
+ * Lists permissions for the authenticated user's messageBoxes with optional pagination.
1381
+ *
1382
+ * @example
1383
+ * // List all permissions
1384
+ * const all = await client.listMessageBoxPermissions()
1385
+ *
1386
+ * // List only notification permissions with pagination
1387
+ * const notifications = await client.listMessageBoxPermissions({
1388
+ * messageBox: 'notifications',
1389
+ * limit: 50,
1390
+ * offset: 0
1391
+ * })
1392
+ */
1393
+ async listMessageBoxPermissions(params, overrideHost) {
1394
+ await this.assertInitialized();
1395
+ const finalHost = overrideHost ?? this.host;
1396
+ const queryParams = new URLSearchParams();
1397
+ if (params?.messageBox != null) {
1398
+ queryParams.set('message_box', params.messageBox);
1399
+ }
1400
+ if (params?.limit !== undefined) {
1401
+ queryParams.set('limit', params.limit.toString());
1402
+ }
1403
+ if (params?.offset !== undefined) {
1404
+ queryParams.set('offset', params.offset.toString());
1405
+ }
1406
+ Logger.log('[MB CLIENT] Listing messageBox permissions with params:', queryParams.toString());
1407
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/list?${queryParams.toString()}`, {
1408
+ method: 'GET'
1409
+ });
1410
+ if (!response.ok) {
1411
+ const errorData = await response.json().catch(() => ({}));
1412
+ throw new Error(`Failed to list permissions: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1413
+ }
1414
+ const data = await response.json();
1415
+ if (data.status === 'error') {
1416
+ throw new Error(data.description ?? 'Failed to list permissions');
1417
+ }
1418
+ return data.permissions.map((p) => ({
1419
+ sender: p.sender,
1420
+ messageBox: p.message_box,
1421
+ recipientFee: p.recipient_fee,
1422
+ status: MessageBoxClient.getStatusFromFee(p.recipient_fee),
1423
+ createdAt: p.created_at,
1424
+ updatedAt: p.updated_at
1425
+ }));
1426
+ }
1427
+ // ===========================
1428
+ // NOTIFICATION CONVENIENCE METHODS
1429
+ // ===========================
1430
+ /**
1431
+ * @method allowNotificationsFromPeer
1432
+ * @async
1433
+ * @param {PubKeyHex} identityKey - Sender's identity key to allow
1434
+ * @param {number} [recipientFee=0] - Fee to charge (0 for always allow)
1435
+ * @param {string} [overrideHost] - Optional host override
1436
+ * @returns {Promise<void>} Permission status after allowing
1437
+ *
1438
+ * @description
1439
+ * Convenience method to allow notifications from a specific peer.
1440
+ *
1441
+ * @example
1442
+ * await client.allowNotificationsFromPeer('03abc123...') // Always allow
1443
+ * await client.allowNotificationsFromPeer('03def456...', 5) // Allow for 5 sats
1444
+ */
1445
+ async allowNotificationsFromPeer(identityKey, recipientFee = 0, overrideHost) {
1446
+ await this.setMessageBoxPermission({
1447
+ messageBox: 'notifications',
1448
+ sender: identityKey,
1449
+ recipientFee
1450
+ }, overrideHost);
1451
+ }
1452
+ /**
1453
+ * @method denyNotificationsFromPeer
1454
+ * @async
1455
+ * @param {PubKeyHex} identityKey - Sender's identity key to block
1456
+ * @returns {Promise<void>} Permission status after denying
1457
+ *
1458
+ * @description
1459
+ * Convenience method to block notifications from a specific peer.
1460
+ *
1461
+ * @example
1462
+ * await client.denyNotificationsFromPeer('03spam123...')
1463
+ */
1464
+ async denyNotificationsFromPeer(identityKey, overrideHost) {
1465
+ await this.setMessageBoxPermission({
1466
+ messageBox: 'notifications',
1467
+ sender: identityKey,
1468
+ recipientFee: -1
1469
+ }, overrideHost);
1470
+ }
1471
+ /**
1472
+ * @method checkPeerNotificationStatus
1473
+ * @async
1474
+ * @param {PubKeyHex} identityKey - Sender's identity key to check
1475
+ * @returns {Promise<MessageBoxPermission>} Current permission status
1476
+ *
1477
+ * @description
1478
+ * Convenience method to check notification permission for a specific peer.
1479
+ *
1480
+ * @example
1481
+ * const status = await client.checkPeerNotificationStatus('03abc123...')
1482
+ * console.log(status.allowed) // true/false
1483
+ */
1484
+ async checkPeerNotificationStatus(identityKey, overrideHost) {
1485
+ const myIdentityKey = await this.getIdentityKey();
1486
+ return await this.getMessageBoxPermission({
1487
+ recipient: myIdentityKey,
1488
+ messageBox: 'notifications',
1489
+ sender: identityKey
1490
+ }, overrideHost);
1491
+ }
1492
+ /**
1493
+ * @method listPeerNotifications
1494
+ * @async
1495
+ * @returns {Promise<MessageBoxPermission[]>} List of notification permissions
1496
+ *
1497
+ * @description
1498
+ * Convenience method to list all notification permissions.
1499
+ *
1500
+ * @example
1501
+ * const notifications = await client.listPeerNotifications()
1502
+ */
1503
+ async listPeerNotifications(overrideHost) {
1504
+ return await this.listMessageBoxPermissions({ messageBox: 'notifications' }, overrideHost);
1505
+ }
1506
+ /**
1507
+ * @method sendNotification
1508
+ * @async
1509
+ * @param {PubKeyHex} recipient - Recipient's identity key
1510
+ * @param {string | object} body - Notification content
1511
+ * @param {string} [overrideHost] - Optional host override
1512
+ * @returns {Promise<SendMessageResponse>} Send result
1513
+ *
1514
+ * @description
1515
+ * Convenience method to send a notification with automatic quote fetching and payment handling.
1516
+ * Automatically determines the required payment amount and creates the payment if needed.
1517
+ *
1518
+ * @example
1519
+ * // Send notification (auto-determines payment needed)
1520
+ * await client.sendNotification('03def456...', 'Hello!')
1521
+ *
1522
+ * // Send with maximum payment limit for safety
1523
+ * await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
1524
+ */
1525
+ async sendNotification(recipient, body, overrideHost) {
1526
+ await this.assertInitialized();
1527
+ // Use sendMessage with permission checking enabled
1528
+ // This eliminates duplication of quote fetching and payment logic
1529
+ return await this.sendMessage({
1530
+ recipient,
1531
+ messageBox: 'notifications',
1532
+ body,
1533
+ checkPermissions: true
1534
+ }, overrideHost);
1535
+ }
1536
+ /**
1537
+ * Register a device for FCM push notifications.
1538
+ *
1539
+ * @async
1540
+ * @param {DeviceRegistrationParams} params - Device registration parameters
1541
+ * @param {string} [overrideHost] - Optional host override
1542
+ * @returns {Promise<DeviceRegistrationResponse>} Registration response
1543
+ *
1544
+ * @description
1545
+ * Registers a device with the message box server to receive FCM push notifications.
1546
+ * The FCM token is obtained from Firebase SDK on the client side.
1547
+ *
1548
+ * @example
1549
+ * const result = await client.registerDevice({
1550
+ * fcmToken: 'eBo8F...',
1551
+ * platform: 'ios',
1552
+ * deviceId: 'iPhone15Pro'
1553
+ * })
1554
+ */
1555
+ async registerDevice(params, overrideHost) {
1556
+ await this.assertInitialized();
1557
+ if (params.fcmToken == null || params.fcmToken.trim() === '') {
1558
+ throw new Error('fcmToken is required and must be a non-empty string');
1559
+ }
1560
+ // Validate platform if provided
1561
+ const validPlatforms = ['ios', 'android', 'web'];
1562
+ if (params.platform != null && !validPlatforms.includes(params.platform)) {
1563
+ throw new Error('platform must be one of: ios, android, web');
1564
+ }
1565
+ const finalHost = overrideHost ?? this.host;
1566
+ Logger.log('[MB CLIENT] Registering device for FCM notifications...');
1567
+ const response = await this.authFetch.fetch(`${finalHost}/registerDevice`, {
1568
+ method: 'POST',
1569
+ headers: { 'Content-Type': 'application/json' },
1570
+ body: JSON.stringify({
1571
+ fcmToken: params.fcmToken.trim(),
1572
+ deviceId: params.deviceId?.trim() ?? undefined,
1573
+ platform: params.platform ?? undefined
1574
+ })
1575
+ });
1576
+ if (!response.ok) {
1577
+ const errorData = await response.json().catch(() => ({}));
1578
+ const description = String(errorData.description) ?? response.statusText;
1579
+ throw new Error(`Failed to register device: HTTP ${response.status} - ${description}`);
1580
+ }
1581
+ const data = await response.json();
1582
+ if (data.status === 'error') {
1583
+ throw new Error(data.description ?? 'Failed to register device');
1584
+ }
1585
+ Logger.log('[MB CLIENT] Device registered successfully');
1586
+ return {
1587
+ status: data.status,
1588
+ message: data.message,
1589
+ deviceId: data.deviceId
1590
+ };
1591
+ }
1592
+ /**
1593
+ * List all registered devices for push notifications.
1594
+ *
1595
+ * @async
1596
+ * @param {string} [overrideHost] - Optional host override
1597
+ * @returns {Promise<RegisteredDevice[]>} Array of registered devices
1598
+ *
1599
+ * @description
1600
+ * Retrieves all devices registered by the authenticated user for FCM push notifications.
1601
+ * Only shows devices belonging to the current user (authenticated via AuthFetch).
1602
+ *
1603
+ * @example
1604
+ * const devices = await client.listRegisteredDevices()
1605
+ * console.log(`Found ${devices.length} registered devices`)
1606
+ * devices.forEach(device => {
1607
+ * console.log(`Device: ${device.platform} - ${device.fcmToken}`)
1608
+ * })
1609
+ */
1610
+ async listRegisteredDevices(overrideHost) {
1611
+ await this.assertInitialized();
1612
+ const finalHost = overrideHost ?? this.host;
1613
+ Logger.log('[MB CLIENT] Listing registered devices...');
1614
+ const response = await this.authFetch.fetch(`${finalHost}/devices`, {
1615
+ method: 'GET'
1616
+ });
1617
+ if (!response.ok) {
1618
+ const errorData = await response.json().catch(() => ({}));
1619
+ const description = String(errorData.description) ?? response.statusText;
1620
+ throw new Error(`Failed to list devices: HTTP ${response.status} - ${description}`);
1621
+ }
1622
+ const data = await response.json();
1623
+ if (data.status === 'error') {
1624
+ throw new Error(data.description ?? 'Failed to list devices');
1625
+ }
1626
+ Logger.log(`[MB CLIENT] Found ${data.devices.length} registered devices`);
1627
+ return data.devices;
1628
+ }
1629
+ // ===========================
1630
+ // PRIVATE HELPER METHODS
1631
+ // ===========================
1632
+ static getStatusFromFee(fee) {
1633
+ if (fee === -1)
1634
+ return 'blocked';
1635
+ if (fee === 0)
1636
+ return 'always_allow';
1637
+ return 'payment_required';
1638
+ }
1639
+ /**
1640
+ * Creates payment transaction for message delivery fees
1641
+ * TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
1642
+ * @private
1643
+ * @param {string} recipient - Recipient identity key
1644
+ * @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
1645
+ * @param {string} description - Description for the payment transaction
1646
+ * @returns {Promise<Payment>} Payment transaction data
1647
+ */
1648
+ async createMessagePayment(recipient, quote, description = 'MessageBox delivery payment', originator) {
1649
+ if (quote.recipientFee <= 0 && quote.deliveryFee <= 0) {
1650
+ throw new Error('No payment required');
1651
+ }
1652
+ Logger.log(`[MB CLIENT] Creating payment transaction for ${quote.recipientFee} sats (delivery: ${quote.deliveryFee}, recipient: ${quote.recipientFee})`);
1653
+ const outputs = [];
1654
+ const createActionOutputs = [];
1655
+ // Get sender identity key for remittance data
1656
+ const senderIdentityKey = await this.getIdentityKey();
1657
+ // Add server delivery fee output if > 0
1658
+ let outputIndex = 0;
1659
+ if (quote.deliveryFee > 0) {
1660
+ const derivationPrefix = Utils.toBase64(Random(32));
1661
+ const derivationSuffix = Utils.toBase64(Random(32));
1662
+ // Get host's derived public key
1663
+ const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
1664
+ protocolID: [2, '3241645161d8'],
1665
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
1666
+ counterparty: quote.deliveryAgentIdentityKey
1667
+ }, originator);
1668
+ // Create locking script using host's public key
1669
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
1670
+ // Add to createAction outputs
1671
+ createActionOutputs.push({
1672
+ satoshis: quote.deliveryFee,
1673
+ lockingScript,
1674
+ outputDescription: 'MessageBox server delivery fee',
1675
+ customInstructions: JSON.stringify({
1676
+ derivationPrefix,
1677
+ derivationSuffix,
1678
+ recipientIdentityKey: quote.deliveryAgentIdentityKey
1679
+ })
1680
+ });
1681
+ outputs.push({
1682
+ outputIndex: outputIndex++,
1683
+ protocol: 'wallet payment',
1684
+ paymentRemittance: {
1685
+ derivationPrefix,
1686
+ derivationSuffix,
1687
+ senderIdentityKey
1688
+ }
1689
+ });
1690
+ }
1691
+ // Add recipient fee output if > 0
1692
+ if (quote.recipientFee > 0) {
1693
+ const derivationPrefix = Utils.toBase64(Random(32));
1694
+ const derivationSuffix = Utils.toBase64(Random(32));
1695
+ // Get a derived public key for the recipient that "anyone" can verify
1696
+ const anyoneWallet = new ProtoWallet('anyone');
1697
+ const { publicKey: derivedKeyResult } = await anyoneWallet.getPublicKey({
1698
+ protocolID: [2, '3241645161d8'],
1699
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
1700
+ counterparty: recipient
1701
+ });
1702
+ if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
1703
+ throw new Error('Failed to derive recipient\'s public key');
1704
+ }
1705
+ // Create locking script using recipient's public key
1706
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
1707
+ // Add to createAction outputs
1708
+ createActionOutputs.push({
1709
+ satoshis: quote.recipientFee,
1710
+ lockingScript,
1711
+ outputDescription: 'Recipient message fee',
1712
+ customInstructions: JSON.stringify({
1713
+ derivationPrefix,
1714
+ derivationSuffix,
1715
+ recipientIdentityKey: recipient
1716
+ })
1717
+ });
1718
+ outputs.push({
1719
+ outputIndex: outputIndex++,
1720
+ protocol: 'wallet payment',
1721
+ paymentRemittance: {
1722
+ derivationPrefix,
1723
+ derivationSuffix,
1724
+ senderIdentityKey: (await anyoneWallet.getPublicKey({ identityKey: true })).publicKey
1725
+ }
1726
+ });
1727
+ }
1728
+ const { tx } = await this.walletClient.createAction({
1729
+ description,
1730
+ outputs: createActionOutputs,
1731
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
1732
+ }, originator);
1733
+ if (tx == null) {
1734
+ throw new Error('Failed to create payment transaction');
1735
+ }
1736
+ return {
1737
+ tx,
1738
+ outputs,
1739
+ description
1740
+ // labels
1741
+ };
1742
+ }
1164
1743
  }
1165
1744
  //# sourceMappingURL=MessageBoxClient.js.map