Skip to content

Commit 280cc0a

Browse files
JOHNJOHN
authored andcommitted
chore: update Kotlin code
1 parent 20dece5 commit 280cc0a

23 files changed

+2480
-209
lines changed

app/src/main/java/com/pubky/noise/pubky_noise.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
10971097
if (lib.uniffi_pubky_noise_checksum_func_ed25519_verify() != 14993.toShort()) {
10981098
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
10991099
}
1100-
if (lib.uniffi_pubky_noise_checksum_func_is_sealed_blob() != 59485.toShort()) {
1100+
if (lib.uniffi_pubky_noise_checksum_func_is_sealed_blob() != 27217.toShort()) {
11011101
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
11021102
}
11031103
if (lib.uniffi_pubky_noise_checksum_func_performance_config() != 613.toShort()) {
@@ -1106,10 +1106,10 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
11061106
if (lib.uniffi_pubky_noise_checksum_func_public_key_from_secret() != 12954.toShort()) {
11071107
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
11081108
}
1109-
if (lib.uniffi_pubky_noise_checksum_func_sealed_blob_decrypt() != 36862.toShort()) {
1109+
if (lib.uniffi_pubky_noise_checksum_func_sealed_blob_decrypt() != 39236.toShort()) {
11101110
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
11111111
}
1112-
if (lib.uniffi_pubky_noise_checksum_func_sealed_blob_encrypt() != 44846.toShort()) {
1112+
if (lib.uniffi_pubky_noise_checksum_func_sealed_blob_encrypt() != 19222.toShort()) {
11131113
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
11141114
}
11151115
if (lib.uniffi_pubky_noise_checksum_func_x25519_generate_keypair() != 20350.toShort()) {
@@ -2801,8 +2801,9 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer<List<kotlin.Str
28012801

28022802

28032803
/**
2804-
* Check if a JSON string looks like a sealed blob envelope.
2804+
* Check if a JSON string looks like a sealed blob envelope (v1 or v2).
28052805
*
2806+
* Requires both version field (`"v":1` or `"v":2`) AND ephemeral public key (`"epk":`).
28062807
* This is a quick heuristic check for distinguishing encrypted from legacy plaintext.
28072808
*/ fun `isSealedBlob`(`json`: kotlin.String): kotlin.Boolean {
28082809
return FfiConverterBoolean.lift(
@@ -2841,12 +2842,12 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer<List<kotlin.Str
28412842

28422843

28432844
/**
2844-
* Decrypt a Paykit Sealed Blob v1 envelope.
2845+
* Decrypt a Paykit Sealed Blob v1 or v2 envelope (auto-detects version).
28452846
*
28462847
* # Arguments
28472848
*
28482849
* * `recipient_sk` - Recipient's X25519 secret key (32 bytes)
2849-
* * `envelope_json` - JSON-encoded sealed blob envelope
2850+
* * `envelope_json` - JSON-encoded sealed blob envelope (v1 or v2)
28502851
* * `aad` - Associated authenticated data (must match encryption)
28512852
*
28522853
* # Returns
@@ -2869,7 +2870,7 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer<List<kotlin.Str
28692870

28702871

28712872
/**
2872-
* Encrypt plaintext using Paykit Sealed Blob v1 format.
2873+
* Encrypt plaintext using Paykit Sealed Blob v2 format (XChaCha20-Poly1305).
28732874
*
28742875
* # Arguments
28752876
*
@@ -2880,7 +2881,7 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer<List<kotlin.Str
28802881
*
28812882
* # Returns
28822883
*
2883-
* JSON-encoded sealed blob envelope.
2884+
* JSON-encoded sealed blob v2 envelope.
28842885
*
28852886
* # Errors
28862887
*

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package to.bitkit.fcm
22

33
import android.content.Context
4+
import android.net.Uri
45
import androidx.hilt.work.HiltWorker
56
import androidx.work.CoroutineWorker
67
import androidx.work.WorkerParameters
@@ -59,6 +60,7 @@ class WakeNodeWorker @AssistedInject constructor(
5960
private val self = this
6061

6162
private var bestAttemptContent: NotificationDetails? = null
63+
private var bestAttemptDeepLink: Uri? = null
6264

6365
private var notificationType: BlocktankNotificationType? = null
6466
private var notificationPayload: JsonObject? = null
@@ -253,6 +255,7 @@ class WakeNodeWorker @AssistedInject constructor(
253255
when (self.notificationType) {
254256
paykitPaymentRequest -> {
255257
val requestId = (notificationPayload?.get("requestId") as? JsonPrimitive)?.contentOrNull
258+
val fromPubkey = (notificationPayload?.get("from") as? JsonPrimitive)?.contentOrNull
256259
if (requestId == null) {
257260
Logger.error("Missing requestId for payment request")
258261
return
@@ -266,6 +269,12 @@ class WakeNodeWorker @AssistedInject constructor(
266269
title = appContext.getString(R.string.notification_payment_request_title),
267270
body = appContext.getString(R.string.notification_payment_request_body),
268271
)
272+
// Set deep link for payment request detail (with from if available, else general requests screen)
273+
self.bestAttemptDeepLink = if (fromPubkey != null) {
274+
Uri.parse("bitkit://payment-request?requestId=$requestId&from=$fromPubkey")
275+
} else {
276+
Uri.parse("bitkit://payment-requests")
277+
}
269278
self.deliver()
270279
}
271280

@@ -283,6 +292,7 @@ class WakeNodeWorker @AssistedInject constructor(
283292
title = appContext.getString(R.string.notification_subscription_due_title),
284293
body = appContext.getString(R.string.notification_subscription_due_body),
285294
)
295+
self.bestAttemptDeepLink = Uri.parse("bitkit://subscriptions")
286296
self.deliver()
287297
}
288298

@@ -299,6 +309,7 @@ class WakeNodeWorker @AssistedInject constructor(
299309
title = appContext.getString(R.string.notification_autopay_executed_title),
300310
body = "$BITCOIN_SYMBOL $amount ${appContext.getString(R.string.notification_sent)}",
301311
)
312+
self.bestAttemptDeepLink = Uri.parse("bitkit://payment-requests")
302313
self.deliver()
303314
}
304315

@@ -316,6 +327,7 @@ class WakeNodeWorker @AssistedInject constructor(
316327
title = appContext.getString(R.string.notification_subscription_failed_title),
317328
body = reason,
318329
)
330+
self.bestAttemptDeepLink = Uri.parse("bitkit://subscriptions")
319331
self.deliver()
320332
}
321333

@@ -329,7 +341,11 @@ class WakeNodeWorker @AssistedInject constructor(
329341
lightningRepo.stop()
330342

331343
bestAttemptContent?.run {
332-
appContext.pushNotification(title, body)
344+
appContext.pushNotification(
345+
title = title,
346+
text = body,
347+
deepLinkUri = bestAttemptDeepLink,
348+
)
333349
Logger.info("Delivered notification")
334350
}
335351

app/src/main/java/to/bitkit/paykit/services/DirectoryService.kt

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)