@@ -455,7 +455,9 @@ class DirectoryService @Inject constructor(
455455 throw DirectoryError .EncryptionFailed (" Encryption failed: ${e.message} " )
456456 }
457457
458- val result = adapter.put(path, encryptedEnvelope)
458+ val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers .IO ) {
459+ adapter.put(path, encryptedEnvelope)
460+ }
459461 if (! result.success) {
460462 Logger .error(" Failed to publish payment request ${request.id} : ${result.error} " , context = TAG )
461463 throw DirectoryError .PublishFailed (result.error ? : " Unknown error" )
@@ -557,6 +559,99 @@ class DirectoryService @Inject constructor(
557559 Logger .info(" Removed payment request: $requestId " , context = TAG )
558560 }
559561
562+ /* *
563+ * List all payment request IDs on the homeserver for a given recipient.
564+ *
565+ * Used to compare with locally tracked requests and find orphaned ones.
566+ *
567+ * @param recipientPubkey The recipient pubkey (used for scope computation)
568+ * @return List of request IDs found on the homeserver
569+ */
570+ suspend fun listRequestsOnHomeserver (recipientPubkey : String ): List <String > {
571+ if (! isConfigured) tryRestoreFromKeychain()
572+ val myPubkey = keyManager.getCurrentPublicKeyZ32() ? : throw DirectoryError .NotConfigured
573+
574+ val effectiveHomeserver = homeserverURL ? : HomeserverDefaults .defaultHomeserverURL
575+ val adapter = pubkyStorage.createUnauthenticatedAdapter(effectiveHomeserver)
576+ val recipientScope = PaykitV0Protocol .recipientScope(recipientPubkey)
577+ val requestsPath =
578+ " ${PaykitV0Protocol .PAYKIT_V0_PREFIX } /${PaykitV0Protocol .REQUESTS_SUBPATH } /$recipientScope /"
579+
580+ return try {
581+ val requestFiles = pubkyStorage.listDirectory(requestsPath, adapter, myPubkey)
582+ Logger .info(
583+ " Found ${requestFiles.size} requests on homeserver for recipient ${recipientPubkey.take(12 )} ..." ,
584+ context = TAG ,
585+ )
586+ requestFiles
587+ } catch (e: Exception ) {
588+ Logger .debug(
589+ " No requests directory found for recipient ${recipientPubkey.take(12 )} ..." ,
590+ context = TAG ,
591+ )
592+ emptyList()
593+ }
594+ }
595+
596+ /* *
597+ * Delete a payment request from OUR storage (as sender).
598+ *
599+ * Used when the sender wants to cancel a pending request they sent.
600+ *
601+ * @param requestId The request ID to delete
602+ * @param recipientPubkey The recipient pubkey (used for scope computation)
603+ */
604+ suspend fun deletePaymentRequest (requestId : String , recipientPubkey : String ) {
605+ // Auto-restore from keychain if not configured
606+ if (! isConfigured) tryRestoreFromKeychain()
607+ val adapter = authenticatedAdapter ? : throw DirectoryError .NotConfigured
608+ val path = PaykitV0Protocol .paymentRequestPath(recipientPubkey, requestId)
609+
610+ val result = adapter.delete(path)
611+ if (! result.success) {
612+ Logger .error(" Failed to delete payment request $requestId : ${result.error} " , context = TAG )
613+ throw DirectoryError .PublishFailed (result.error ? : " Unknown error" )
614+ }
615+ Logger .info(" Deleted payment request: $requestId for recipient ${recipientPubkey.take(12 )} ..." , context = TAG )
616+ }
617+
618+ /* *
619+ * Delete multiple payment requests in batch.
620+ *
621+ * Used to clean up orphaned requests that exist on homeserver but aren't tracked locally.
622+ *
623+ * @param requestIds List of request IDs to delete
624+ * @param recipientPubkey The recipient pubkey (used for scope computation)
625+ * @return Number of successfully deleted requests
626+ */
627+ suspend fun deleteRequestsBatch (requestIds : List <String >, recipientPubkey : String ): Int {
628+ if (! isConfigured) tryRestoreFromKeychain()
629+ val adapter = authenticatedAdapter ? : throw DirectoryError .NotConfigured
630+
631+ var deletedCount = 0
632+ for (requestId in requestIds) {
633+ try {
634+ val path = PaykitV0Protocol .paymentRequestPath(recipientPubkey, requestId)
635+ val result = adapter.delete(path)
636+ if (result.success) {
637+ deletedCount++
638+ Logger .info(
639+ " Deleted orphaned request: $requestId for recipient ${recipientPubkey.take(12 )} ..." ,
640+ context = TAG ,
641+ )
642+ } else {
643+ Logger .error(
644+ " Failed to delete orphaned request $requestId : ${result.error} " ,
645+ context = TAG ,
646+ )
647+ }
648+ } catch (e: Exception ) {
649+ Logger .error(" Failed to delete orphaned request $requestId " , e, context = TAG )
650+ }
651+ }
652+ return deletedCount
653+ }
654+
560655 /* *
561656 * Delete a subscription proposal from OUR storage (as provider).
562657 *
@@ -663,12 +758,14 @@ class DirectoryService @Inject constructor(
663758 suspend fun discoverPendingRequestsFromPeer (
664759 peerPubkey : String ,
665760 myPubkey : String ,
666- ): List <to.bitkit.paykit.workers.DiscoveredRequest > {
761+ ): List <to.bitkit.paykit.workers.DiscoveredRequest > = kotlinx.coroutines.withContext(
762+ kotlinx.coroutines.Dispatchers .IO
763+ ) {
667764 val adapter = pubkyStorage.createUnauthenticatedAdapter(homeserverURL)
668765 val myScope = PaykitV0Protocol .recipientScope(myPubkey)
669766 val requestsPath = " ${PaykitV0Protocol .PAYKIT_V0_PREFIX } /${PaykitV0Protocol .REQUESTS_SUBPATH } /$myScope /"
670767
671- return try {
768+ try {
672769 val requestFiles = pubkyStorage.listDirectory(requestsPath, adapter, peerPubkey)
673770
674771 requestFiles.mapNotNull { requestId ->
@@ -894,6 +991,28 @@ class DirectoryService @Inject constructor(
894991 val myNoiseSk = PubkyRingBridge .hexStringToByteArray(noiseKeypair.secretKey)
895992 val aad = PaykitV0Protocol .paymentRequestAad(recipientPubkey, requestId)
896993
994+ // Detailed logging for debugging decryption issues
995+ Logger .info(" Decrypting request $requestId :" , context = TAG )
996+ Logger .info(" - recipientPubkey: ${recipientPubkey.take(12 )} ..." , context = TAG )
997+ Logger .info(" - myNoisePk (first 16 hex): ${noiseKeypair.publicKey.take(32 )} ..." , context = TAG )
998+ Logger .info(" - epoch: 0" , context = TAG )
999+ Logger .info(" - AAD: $aad " , context = TAG )
1000+
1001+ // Check if our local key matches the published endpoint (key sync issue detection)
1002+ val publishedEndpoint = try {
1003+ discoverNoiseEndpoint(recipientPubkey)
1004+ } catch (e: Exception ) {
1005+ null
1006+ }
1007+ if (publishedEndpoint != null && publishedEndpoint.serverNoisePubkey != noiseKeypair.publicKey) {
1008+ Logger .error(
1009+ " KEY MISMATCH: Local key ${noiseKeypair.publicKey.take(16 )} ... != published ${publishedEndpoint.serverNoisePubkey.take(16 )} ..." ,
1010+ context = TAG ,
1011+ )
1012+ Logger .error(" Senders are encrypting with published key but we have a different local key!" , context = TAG )
1013+ Logger .error(" Please reconnect to Pubky Ring to fix key sync" , context = TAG )
1014+ }
1015+
8971016 return try {
8981017 val plaintextBytes = com.pubky.noise.sealedBlobDecrypt(myNoiseSk, envelopeJson, aad)
8991018 val plaintextJson = String (plaintextBytes)
@@ -902,10 +1021,10 @@ class DirectoryService @Inject constructor(
9021021 to.bitkit.paykit.workers.DiscoveredRequest (
9031022 requestId = requestId,
9041023 type = to.bitkit.paykit.workers.RequestType .PaymentRequest ,
905- fromPubkey = obj.optString(" from_pubkey " , " " ),
906- amountSats = obj.optLong(" amount_sats " , 0 ),
1024+ fromPubkey = obj.optString(" fromPubkey " , " " ),
1025+ amountSats = obj.optLong(" amountSats " , 0 ),
9071026 description = if (obj.has(" description" )) obj.getString(" description" ) else null ,
908- createdAt = obj.optLong(" created_at " , System .currentTimeMillis()),
1027+ createdAt = obj.optLong(" createdAt " , System .currentTimeMillis()),
9091028 )
9101029 } catch (e: Exception ) {
9111030 Logger .error(" Failed to decrypt/parse payment request $requestId " , e, context = TAG )
0 commit comments