Skip to content

Commit 9cf060e

Browse files
committed
fix: preserve paykit cleanup retries
1 parent 7f0ab3d commit 9cf060e

2 files changed

Lines changed: 152 additions & 19 deletions

File tree

app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ class PrivatePaykitRepo @Inject constructor(
112112
val firstError: Throwable?,
113113
)
114114

115+
private data class NormalizedPublicKeyBatch(
116+
val publicKeys: List<String>,
117+
val hadInvalidKey: Boolean,
118+
)
119+
120+
private data class LinkedReceiverPathInspection(
121+
val receiverPaths: Set<String>,
122+
val error: Throwable?,
123+
)
124+
115125
private data class PrivateMessageDrainRetryKey(
116126
val publicKey: String,
117127
val receiverPath: String,
@@ -699,7 +709,7 @@ class PrivatePaykitRepo @Inject constructor(
699709
var firstError: Throwable? = null
700710
val updates = mutableListOf<PrivatePaymentListReservationUpdateInput>()
701711
val linkRetryKeys = mutableListOf<PrivateMessageDrainRetryKey>()
702-
val linkedReceiverPaths = linkedReceiverPathsByPublicKey()
712+
val linkedReceiverPathsSnapshot = linkedReceiverPathsSnapshotOrNull(reason)
703713

704714
for (publicKey in publicKeys) {
705715
val receiverPaths = runSuspendCatching { receiverPathsForSavedContact(publicKey) }
@@ -718,22 +728,19 @@ class PrivatePaykitRepo @Inject constructor(
718728
firstError = firstError ?: it
719729
logPrivateReceiverPathSelectionFailure(publicKey, reason, it)
720730
}
731+
val linkedReceiverPathInspection = inspectLinkedReceiverPaths(
732+
publicKey,
733+
linkedReceiverPathsSnapshot,
734+
reason,
735+
)
736+
firstError = firstError ?: linkedReceiverPathInspection.error
721737
val cleanupReceiverPaths = receiverPathsForPrivateEndpointCleanup(
722738
publicKey = publicKey,
723739
excludedReceiverPaths = publicationReceiverPaths + receiverPathSelection.cleanupProtectedReceiverPaths,
724-
linkedReceiverPaths = linkedReceiverPaths[publicKey].orEmpty(),
740+
linkedReceiverPaths = linkedReceiverPathInspection.receiverPaths,
725741
)
726742

727-
(linkableReceiverPaths + cleanupReceiverPaths).distinct().forEach { receiverPath ->
728-
linkRetryKeys += PrivateMessageDrainRetryKey(publicKey, receiverPath)
729-
runSuspendCatching { paykitSdkService.ensureLinkWithPeer(publicKey, receiverPath) }.onFailure {
730-
Logger.warn(
731-
"Failed to prepare private Paykit link for '${redacted(publicKey)}' during '$reason'",
732-
it,
733-
context = TAG,
734-
)
735-
}
736-
}
743+
linkRetryKeys += preparePrivateLinks(publicKey, linkableReceiverPaths + cleanupReceiverPaths, reason)
737744

738745
cleanupReceiverPaths.forEach { receiverPath ->
739746
updates += PrivatePaymentListReservationUpdateInput(
@@ -785,6 +792,21 @@ class PrivatePaykitRepo @Inject constructor(
785792
drainAndSchedulePrivateLinkRetries(reason, retryKeys.distinct())
786793
}
787794

795+
private suspend fun preparePrivateLinks(
796+
publicKey: String,
797+
receiverPaths: Collection<String>,
798+
reason: String,
799+
): List<PrivateMessageDrainRetryKey> = receiverPaths.distinct().map { receiverPath ->
800+
runSuspendCatching { paykitSdkService.ensureLinkWithPeer(publicKey, receiverPath) }.onFailure {
801+
Logger.warn(
802+
"Failed to prepare private Paykit link for '${redacted(publicKey)}' during '$reason'",
803+
it,
804+
context = TAG,
805+
)
806+
}
807+
PrivateMessageDrainRetryKey(publicKey, receiverPath)
808+
}
809+
788810
private suspend fun drainAndSchedulePrivateLinkRetries(
789811
reason: String,
790812
retryKeys: Collection<PrivateMessageDrainRetryKey>,
@@ -1207,13 +1229,20 @@ class PrivatePaykitRepo @Inject constructor(
12071229
private suspend fun removePublishedEndpoints(publicKeys: Collection<String>): Result<Unit> =
12081230
withContext(serializedDispatcher) {
12091231
runSuspendCatching {
1210-
val publicKeys = publicKeys.mapNotNull(::normalizedPublicKey).distinct()
1211-
if (publicKeys.isEmpty()) return@runSuspendCatching Unit
1232+
val normalizedBatch = normalizedPublicKeyBatch(publicKeys)
1233+
val publicKeys = normalizedBatch.publicKeys
1234+
var firstError: Throwable? = PrivatePaykitError.InvalidPublicKey.takeIf {
1235+
normalizedBatch.hadInvalidKey
1236+
}
1237+
if (publicKeys.isEmpty()) {
1238+
firstError?.let { throw it }
1239+
return@runSuspendCatching Unit
1240+
}
12121241

1213-
val linkedReceiverPaths = linkedReceiverPathsByPublicKey()
1214-
val preparation = clearPrivatePaymentLists(publicKeys, linkedReceiverPaths)
1242+
val linkedReceiverPathsSnapshot = linkedReceiverPathsSnapshotOrNull("private endpoint cleanup")
1243+
val preparation = clearPrivatePaymentLists(publicKeys, linkedReceiverPathsSnapshot)
12151244
val failedPublicKeys = preparation.failedPublicKeys.toMutableSet()
1216-
var firstError = preparation.firstError
1245+
firstError = firstError ?: preparation.firstError
12171246

12181247
if (preparation.clearedRetryKeys.isNotEmpty()) {
12191248
drainPendingPrivateMessages(
@@ -1237,16 +1266,28 @@ class PrivatePaykitRepo @Inject constructor(
12371266

12381267
private suspend fun clearPrivatePaymentLists(
12391268
publicKeys: Collection<String>,
1240-
linkedReceiverPaths: Map<String, Set<String>>,
1269+
linkedReceiverPathsSnapshot: Map<String, Set<String>>?,
12411270
): PrivateEndpointCleanupPreparation {
12421271
val failedPublicKeys = mutableSetOf<String>()
12431272
val clearedRetryKeys = mutableListOf<PrivateMessageDrainRetryKey>()
12441273
var firstError: Throwable? = null
12451274

12461275
publicKeys.forEach { publicKey ->
1276+
val contactLinkedReceiverPaths = linkedReceiverPaths(
1277+
publicKey = publicKey,
1278+
snapshot = linkedReceiverPathsSnapshot,
1279+
).onFailure {
1280+
failedPublicKeys += publicKey
1281+
firstError = firstError ?: it
1282+
Logger.warn(
1283+
"Failed to inspect private Paykit links for '${redacted(publicKey)}' during cleanup",
1284+
it,
1285+
context = TAG,
1286+
)
1287+
}.getOrDefault(emptySet())
12471288
receiverPathsForCleanup(
12481289
publicKey = publicKey,
1249-
linkedReceiverPaths = linkedReceiverPaths[publicKey].orEmpty(),
1290+
linkedReceiverPaths = contactLinkedReceiverPaths,
12501291
).forEach { receiverPath ->
12511292
runSuspendCatching {
12521293
val report = paykitSdkService.clearPrivatePaymentList(publicKey, receiverPath)
@@ -1325,6 +1366,53 @@ class PrivatePaykitRepo @Inject constructor(
13251366
return linkedPaths
13261367
}
13271368

1369+
private suspend fun linkedReceiverPathsSnapshotOrNull(reason: String): Map<String, Set<String>>? =
1370+
runSuspendCatching { linkedReceiverPathsByPublicKey() }
1371+
.onFailure {
1372+
Logger.warn(
1373+
"Failed to inspect private Paykit links during '$reason'; retrying per contact",
1374+
it,
1375+
context = TAG,
1376+
)
1377+
}.getOrNull()
1378+
1379+
private suspend fun linkedReceiverPaths(
1380+
publicKey: String,
1381+
snapshot: Map<String, Set<String>>?,
1382+
): Result<Set<String>> = if (snapshot != null) {
1383+
Result.success(snapshot[publicKey].orEmpty())
1384+
} else {
1385+
runSuspendCatching { linkedReceiverPathsByPublicKey()[publicKey].orEmpty() }
1386+
}
1387+
1388+
private suspend fun inspectLinkedReceiverPaths(
1389+
publicKey: String,
1390+
snapshot: Map<String, Set<String>>?,
1391+
reason: String,
1392+
): LinkedReceiverPathInspection {
1393+
val result = linkedReceiverPaths(publicKey, snapshot)
1394+
val error = result.exceptionOrNull()
1395+
if (error != null) {
1396+
Logger.warn(
1397+
"Failed to inspect private Paykit links for '${redacted(publicKey)}' during '$reason'",
1398+
error,
1399+
context = TAG,
1400+
)
1401+
}
1402+
return LinkedReceiverPathInspection(result.getOrDefault(emptySet()), error)
1403+
}
1404+
1405+
private fun normalizedPublicKeyBatch(publicKeys: Collection<String>): NormalizedPublicKeyBatch {
1406+
var hadInvalidKey = false
1407+
val normalizedKeys = publicKeys.mapNotNull { publicKey ->
1408+
normalizedPublicKey(publicKey) ?: run {
1409+
hadInvalidKey = true
1410+
null
1411+
}
1412+
}.distinct()
1413+
return NormalizedPublicKeyBatch(normalizedKeys, hadInvalidKey)
1414+
}
1415+
13281416
private fun publishedPrivatePaymentReceiverPaths(publicKey: String): List<String> {
13291417
val contactState = state?.contacts?.get(publicKey) ?: return emptyList()
13301418
return contactState.publishedPrivatePaymentReceiverPaths.toList()

app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,51 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) {
632632

633633
assertTrue(result.isFailure)
634634
assertTrue(cacheData.value.cleanupPending)
635+
verifyBlocking(paykitSdkService) { clearPrivatePaymentList(CONTACT_KEY, WALLET_RECEIVER_PATH) }
636+
assertEquals(
637+
setOf(WALLET_RECEIVER_PATH),
638+
cacheData.value.contacts.getValue(CONTACT_KEY).publishedPrivatePaymentReceiverPaths,
639+
)
640+
}
641+
642+
@Test
643+
fun `cleanup falls back per contact when shared linked receiver inspection fails`() = test {
644+
cacheData.value = PrivatePaykitCacheData(
645+
contacts = mapOf(
646+
CONTACT_KEY to cachedPublishedContact(WALLET_RECEIVER_PATH),
647+
OTHER_CONTACT_KEY to cachedPublishedContact(SERVER_RECEIVER_PATH),
648+
),
649+
)
650+
sut = createSut()
651+
var linkedPeerReads = 0
652+
whenever { paykitSdkService.linkedPeers() }.thenAnswer {
653+
linkedPeerReads += 1
654+
if (linkedPeerReads <= 2) error("link inspection failed")
655+
emptyList<LinkedPeerRecord>()
656+
}
657+
658+
val result = sut.removePublishedEndpointsForCleanup("test")
659+
660+
assertTrue(result.isFailure)
661+
verifyBlocking(paykitSdkService) { clearPrivatePaymentList(CONTACT_KEY, WALLET_RECEIVER_PATH) }
662+
verifyBlocking(paykitSdkService) { clearPrivatePaymentList(OTHER_CONTACT_KEY, SERVER_RECEIVER_PATH) }
663+
assertTrue(CONTACT_KEY in cacheData.value.contacts)
664+
assertTrue(OTHER_CONTACT_KEY !in cacheData.value.contacts)
665+
assertTrue(cacheData.value.cleanupPending)
666+
}
667+
668+
@Test
669+
fun `invalid deleted contact key remains pending for cleanup`() = test {
670+
val invalidPublicKey = "not-a-pubky"
671+
cacheData.value = PrivatePaykitCacheData(
672+
deletedContactCleanupPendingPublicKeys = setOf(invalidPublicKey),
673+
)
674+
sut = createSut()
675+
676+
val result = sut.retryPendingEndpointRemoval(emptyList())
677+
678+
assertTrue(result.isFailure)
679+
assertEquals(setOf(invalidPublicKey), cacheData.value.deletedContactCleanupPendingPublicKeys)
635680
verifyBlocking(paykitSdkService, never()) { clearPrivatePaymentList(any(), any()) }
636681
}
637682

0 commit comments

Comments
 (0)