@unicitylabs/nostr-js-sdk 0.2.4 → 0.3.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.
@@ -5606,6 +5606,8 @@
5606
5606
  const FILE_METADATA = 31114;
5607
5607
  /** Unicity: Payment request */
5608
5608
  const PAYMENT_REQUEST = 31115;
5609
+ /** Unicity: Payment request response (accept/decline) */
5610
+ const PAYMENT_REQUEST_RESPONSE = 31116;
5609
5611
  // ============================================================================
5610
5612
  // Event Kind Classification Functions
5611
5613
  // ============================================================================
@@ -5681,6 +5683,8 @@
5681
5683
  return 'File Metadata';
5682
5684
  case PAYMENT_REQUEST:
5683
5685
  return 'Payment Request';
5686
+ case PAYMENT_REQUEST_RESPONSE:
5687
+ return 'Payment Request Response';
5684
5688
  default:
5685
5689
  if (isReplaceable(kind)) {
5686
5690
  return `Replaceable (${kind})`;
@@ -5707,6 +5711,7 @@
5707
5711
  FILE_METADATA: FILE_METADATA,
5708
5712
  GIFT_WRAP: GIFT_WRAP,
5709
5713
  PAYMENT_REQUEST: PAYMENT_REQUEST,
5714
+ PAYMENT_REQUEST_RESPONSE: PAYMENT_REQUEST_RESPONSE,
5710
5715
  PROFILE: PROFILE,
5711
5716
  REACTION: REACTION,
5712
5717
  READ_RECEIPT: READ_RECEIPT,
@@ -6260,14 +6265,16 @@
6260
6265
  return;
6261
6266
  }
6262
6267
  // Send a subscription request as a ping (relays respond with EOSE)
6263
- // Using a unique subscription ID that we immediately close
6268
+ // Use a single fixed subscription ID per relay to avoid accumulating subscriptions
6269
+ // Note: limit:1 is used because some relays don't respond to limit:0
6264
6270
  try {
6265
- const pingSubId = `ping-${Date.now()}`;
6266
- const pingMessage = JSON.stringify(['REQ', pingSubId, { limit: 0 }]);
6267
- relay.socket.send(pingMessage);
6268
- // Immediately close the subscription
6271
+ const pingSubId = `ping`;
6272
+ // First close any existing ping subscription to ensure we don't accumulate
6269
6273
  const closeMessage = JSON.stringify(['CLOSE', pingSubId]);
6270
6274
  relay.socket.send(closeMessage);
6275
+ // Then send the new ping request (limit:1 ensures relay sends EOSE)
6276
+ const pingMessage = JSON.stringify(['REQ', pingSubId, { limit: 1 }]);
6277
+ relay.socket.send(pingMessage);
6271
6278
  }
6272
6279
  catch {
6273
6280
  // Send failed, connection likely dead
@@ -6567,6 +6574,41 @@
6567
6574
  const event = await PaymentRequestProtocol$1.createPaymentRequestEvent(this.keyManager, targetPubkeyHex, request);
6568
6575
  return this.publishEvent(event);
6569
6576
  }
6577
+ /**
6578
+ * Send a payment request response (decline/expiration notification).
6579
+ * @param targetPubkeyHex Original requester's public key
6580
+ * @param response Response details
6581
+ * @returns Promise that resolves with the event ID
6582
+ */
6583
+ async sendPaymentRequestResponse(targetPubkeyHex, response) {
6584
+ const PaymentRequestProtocol$1 = await Promise.resolve().then(function () { return PaymentRequestProtocol; });
6585
+ const event = await PaymentRequestProtocol$1.createPaymentRequestResponseEvent(this.keyManager, targetPubkeyHex, {
6586
+ requestId: response.requestId,
6587
+ originalEventId: response.originalEventId,
6588
+ status: response.status === 'DECLINED'
6589
+ ? PaymentRequestProtocol$1.ResponseStatus.DECLINED
6590
+ : PaymentRequestProtocol$1.ResponseStatus.EXPIRED,
6591
+ reason: response.reason,
6592
+ });
6593
+ return this.publishEvent(event);
6594
+ }
6595
+ /**
6596
+ * Send a payment request decline response.
6597
+ * Convenience method for declining a payment request.
6598
+ * @param originalRequestSenderPubkey Pubkey of who sent the original payment request
6599
+ * @param originalEventId Event ID of the original payment request
6600
+ * @param requestId Request ID from the original payment request
6601
+ * @param reason Optional reason for declining
6602
+ * @returns Promise that resolves with the event ID
6603
+ */
6604
+ async sendPaymentRequestDecline(originalRequestSenderPubkey, originalEventId, requestId, reason) {
6605
+ return this.sendPaymentRequestResponse(originalRequestSenderPubkey, {
6606
+ requestId,
6607
+ originalEventId,
6608
+ status: 'DECLINED',
6609
+ reason,
6610
+ });
6611
+ }
6570
6612
  /**
6571
6613
  * Publish a nametag binding.
6572
6614
  * @param nametagId Nametag identifier
@@ -10202,6 +10244,20 @@
10202
10244
  */
10203
10245
  /** Prefix for payment request messages */
10204
10246
  const MESSAGE_PREFIX = 'payment_request:';
10247
+ /** Prefix for payment request response messages */
10248
+ const RESPONSE_PREFIX = 'payment_request_response:';
10249
+ /** Default deadline duration: 5 minutes in milliseconds */
10250
+ const DEFAULT_DEADLINE_MS = 5 * 60 * 1000;
10251
+ /**
10252
+ * Payment request response status.
10253
+ */
10254
+ var ResponseStatus;
10255
+ (function (ResponseStatus) {
10256
+ /** Payment request was declined by the recipient */
10257
+ ResponseStatus["DECLINED"] = "DECLINED";
10258
+ /** Payment request expired (deadline passed) */
10259
+ ResponseStatus["EXPIRED"] = "EXPIRED";
10260
+ })(ResponseStatus || (ResponseStatus = {}));
10205
10261
  /**
10206
10262
  * Generate a short unique request ID.
10207
10263
  */
@@ -10242,6 +10298,13 @@
10242
10298
  async function createPaymentRequestEvent(keyManager, targetPubkeyHex, request) {
10243
10299
  // Generate request ID if not provided
10244
10300
  const requestId = request.requestId || generateRequestId();
10301
+ // Calculate deadline: use provided value, or default to 5 minutes from now
10302
+ // If explicitly set to null, no deadline
10303
+ const deadline = request.deadline === null
10304
+ ? null
10305
+ : request.deadline !== undefined
10306
+ ? request.deadline
10307
+ : Date.now() + DEFAULT_DEADLINE_MS;
10245
10308
  // Serialize request to JSON
10246
10309
  const requestJson = JSON.stringify({
10247
10310
  amount: String(request.amount), // Convert to string for JSON compatibility with bigint
@@ -10249,6 +10312,7 @@
10249
10312
  message: request.message,
10250
10313
  recipientNametag: request.recipientNametag,
10251
10314
  requestId: requestId,
10315
+ deadline: deadline,
10252
10316
  });
10253
10317
  // Add prefix and encrypt
10254
10318
  const message = MESSAGE_PREFIX + requestJson;
@@ -10320,6 +10384,7 @@
10320
10384
  senderPubkey: event.pubkey,
10321
10385
  timestamp: event.created_at * 1000, // Convert to milliseconds
10322
10386
  eventId: event.id,
10387
+ deadline: parsed.deadline !== undefined ? parsed.deadline : null,
10323
10388
  };
10324
10389
  }
10325
10390
  /**
@@ -10417,18 +10482,166 @@
10417
10482
  const fractionalPart = BigInt(fractionalStr);
10418
10483
  return wholePart * multiplier + fractionalPart;
10419
10484
  }
10485
+ // ============================================================================
10486
+ // Payment Request Response Functions
10487
+ // ============================================================================
10488
+ /**
10489
+ * Check if a parsed payment request has expired.
10490
+ * @param request Parsed payment request
10491
+ * @returns true if the request has a deadline and it has passed
10492
+ */
10493
+ function isExpired(request) {
10494
+ return request.deadline !== null && Date.now() > request.deadline;
10495
+ }
10496
+ /**
10497
+ * Get remaining time until deadline in milliseconds.
10498
+ * @param request Parsed payment request
10499
+ * @returns Remaining time in ms, 0 if expired, null if no deadline
10500
+ */
10501
+ function getRemainingTimeMs(request) {
10502
+ if (request.deadline === null)
10503
+ return null;
10504
+ const remaining = request.deadline - Date.now();
10505
+ return remaining > 0 ? remaining : 0;
10506
+ }
10507
+ /**
10508
+ * Create a payment request response event (for decline/expiration).
10509
+ *
10510
+ * Event structure:
10511
+ * - Kind: 31116 (PAYMENT_REQUEST_RESPONSE)
10512
+ * - Tags:
10513
+ * - ["p", "<target_pubkey_hex>"] - Original requester
10514
+ * - ["type", "payment_request_response"]
10515
+ * - ["status", "DECLINED" | "EXPIRED"]
10516
+ * - ["e", "<original_event_id>", "", "reply"] - Reference to original request
10517
+ * - Content: NIP-04 encrypted response JSON
10518
+ *
10519
+ * @param keyManager Key manager with signing keys
10520
+ * @param targetPubkeyHex Original requester's public key
10521
+ * @param response Response details
10522
+ * @returns Signed event
10523
+ */
10524
+ async function createPaymentRequestResponseEvent(keyManager, targetPubkeyHex, response) {
10525
+ // Serialize response to JSON
10526
+ const responseJson = JSON.stringify({
10527
+ requestId: response.requestId,
10528
+ originalEventId: response.originalEventId,
10529
+ status: response.status,
10530
+ reason: response.reason,
10531
+ });
10532
+ // Add prefix and encrypt
10533
+ const message = RESPONSE_PREFIX + responseJson;
10534
+ const encryptedContent = await keyManager.encryptHex(message, targetPubkeyHex);
10535
+ // Build tags
10536
+ const tags = [
10537
+ ['p', targetPubkeyHex],
10538
+ ['type', 'payment_request_response'],
10539
+ ['status', response.status],
10540
+ ];
10541
+ // Add reference to original event
10542
+ if (response.originalEventId) {
10543
+ tags.push(['e', response.originalEventId, '', 'reply']);
10544
+ }
10545
+ const event = Event.create(keyManager, {
10546
+ kind: PAYMENT_REQUEST_RESPONSE,
10547
+ tags,
10548
+ content: encryptedContent,
10549
+ });
10550
+ return event;
10551
+ }
10552
+ /**
10553
+ * Parse a payment request response event.
10554
+ * Decrypts and parses the response data.
10555
+ *
10556
+ * @param event Payment request response event
10557
+ * @param keyManager Key manager for decryption
10558
+ * @returns Parsed payment request response
10559
+ * @throws Error if the event is not a valid payment request response
10560
+ */
10561
+ async function parsePaymentRequestResponse(event, keyManager) {
10562
+ // Verify event kind
10563
+ if (event.kind !== PAYMENT_REQUEST_RESPONSE) {
10564
+ throw new Error('Event is not a payment request response');
10565
+ }
10566
+ // Determine the peer's public key for decryption
10567
+ let peerPubkeyHex;
10568
+ if (keyManager.isMyPublicKey(event.pubkey)) {
10569
+ // We sent this response, decrypt with target's key
10570
+ const targetPubkey = event.getTagValue('p');
10571
+ if (!targetPubkey) {
10572
+ throw new Error('No target found in event');
10573
+ }
10574
+ peerPubkeyHex = targetPubkey;
10575
+ }
10576
+ else {
10577
+ // We received this response, decrypt with sender's key
10578
+ peerPubkeyHex = event.pubkey;
10579
+ }
10580
+ // Decrypt the content
10581
+ const decrypted = await keyManager.decryptHex(event.content, peerPubkeyHex);
10582
+ // Validate prefix
10583
+ if (!decrypted.startsWith(RESPONSE_PREFIX)) {
10584
+ throw new Error('Invalid payment request response format: missing prefix');
10585
+ }
10586
+ // Parse JSON
10587
+ const responseJson = decrypted.slice(RESPONSE_PREFIX.length);
10588
+ const parsed = JSON.parse(responseJson);
10589
+ return {
10590
+ requestId: parsed.requestId,
10591
+ originalEventId: parsed.originalEventId,
10592
+ status: parsed.status,
10593
+ reason: parsed.reason,
10594
+ senderPubkey: event.pubkey,
10595
+ eventId: event.id,
10596
+ timestamp: event.created_at * 1000,
10597
+ };
10598
+ }
10599
+ /**
10600
+ * Check if an event is a payment request response.
10601
+ * @param event Event to check
10602
+ * @returns true if the event is a payment request response
10603
+ */
10604
+ function isPaymentRequestResponse(event) {
10605
+ return (event.kind === PAYMENT_REQUEST_RESPONSE &&
10606
+ event.getTagValue('type') === 'payment_request_response');
10607
+ }
10608
+ /**
10609
+ * Get the response status from a payment request response event (from unencrypted tag).
10610
+ * @param event Payment request response event
10611
+ * @returns Status string, or undefined if not found
10612
+ */
10613
+ function getResponseStatus(event) {
10614
+ return event.getTagValue('status');
10615
+ }
10616
+ /**
10617
+ * Get the referenced original event ID from the response event.
10618
+ * @param event Payment request response event
10619
+ * @returns Original event ID, or undefined if not found
10620
+ */
10621
+ function getOriginalEventId(event) {
10622
+ return event.getTagValue('e');
10623
+ }
10420
10624
 
10421
10625
  var PaymentRequestProtocol = /*#__PURE__*/Object.freeze({
10422
10626
  __proto__: null,
10627
+ DEFAULT_DEADLINE_MS: DEFAULT_DEADLINE_MS,
10628
+ get ResponseStatus () { return ResponseStatus; },
10423
10629
  createPaymentRequestEvent: createPaymentRequestEvent,
10630
+ createPaymentRequestResponseEvent: createPaymentRequestResponseEvent,
10424
10631
  formatAmount: formatAmount,
10425
10632
  getAmount: getAmount,
10633
+ getOriginalEventId: getOriginalEventId,
10426
10634
  getRecipientNametag: getRecipientNametag,
10635
+ getRemainingTimeMs: getRemainingTimeMs,
10636
+ getResponseStatus: getResponseStatus,
10427
10637
  getSender: getSender,
10428
10638
  getTarget: getTarget,
10639
+ isExpired: isExpired,
10429
10640
  isPaymentRequest: isPaymentRequest,
10641
+ isPaymentRequestResponse: isPaymentRequestResponse,
10430
10642
  parseAmount: parseAmount,
10431
- parsePaymentRequest: parsePaymentRequest
10643
+ parsePaymentRequest: parsePaymentRequest,
10644
+ parsePaymentRequestResponse: parsePaymentRequestResponse
10432
10645
  });
10433
10646
 
10434
10647
  /**
@@ -10474,6 +10687,7 @@
10474
10687
  exports.NostrKeyManager = NostrKeyManager;
10475
10688
  exports.OPEN = OPEN;
10476
10689
  exports.PAYMENT_REQUEST = PAYMENT_REQUEST;
10690
+ exports.PAYMENT_REQUEST_RESPONSE = PAYMENT_REQUEST_RESPONSE;
10477
10691
  exports.PROFILE = PROFILE;
10478
10692
  exports.PaymentRequestProtocol = PaymentRequestProtocol;
10479
10693
  exports.REACTION = REACTION;