Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions app/src/main/java/to/bitkit/repositories/PaykitPaymentProofRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@

private val operationMutex = Mutex()

// Process-local activity handoffs only; restored proofs must reconstruct their activity.
private val observedHardwareProofs = mutableSetOf<PendingPaykitPaymentProof>()

fun paymentRequestStateChanges(identity: Flow<String?>): Flow<Unit> =
combine(identity, store.backupStateVersion) { publicKey, _ ->
runSuspendCatching {
Expand Down Expand Up @@ -159,7 +162,10 @@

suspend fun restoreBackup(proofs: List<PaykitPaymentStateBackup.Proof>) = withContext(ioDispatcher) {
val restored = proofs.map { it.restored() }
operationMutex.withLock { persist(restored) }
operationMutex.withLock {
observedHardwareProofs.clear()
persist(restored)
}
}
private val _onchainPaymentResolutions = MutableStateFlow<List<PaykitOnchainPaymentProofResolution>>(emptyList())
val onchainPaymentResolutions = _onchainPaymentResolutions.asStateFlow()
Expand Down Expand Up @@ -320,7 +326,7 @@
it.kind == PaykitPaymentProofKind.Lightning &&
it.paymentIdentifier.equals(paymentHash, ignoreCase = true)
}
persistAndSubmit(completedProofs, proofs)
persistAndSubmit(completedProofs, proofs, awaitDelivery = false)
}.onFailure { Logger.warn("Failed to complete a Paykit Lightning payment proof", it, context = TAG) }
}
}
Expand Down Expand Up @@ -477,7 +483,7 @@
}
if (!hasPositiveEvidence(proof)) return@runSuspendCatching false
if (index >= 0) proofs[index] = proof else proofs += proof
val retained = persistAndSubmit(listOf(proof), proofs)
val retained = persistAndSubmit(listOf(proof), proofs, awaitDelivery = false)
if (retained) publishOnchainResolution(proof, txid)
retained
}
Expand Down Expand Up @@ -769,7 +775,7 @@
val pending = original.copy(paymentIdentifier = txid.lowercase())
proofs[index] = pending
persist(proofs)
reconcileHardwareOnchainProof(pending)
reconcileHardwareOnchainProof(pending, awaitDelivery = false)
Comment thread
ben-kaufman marked this conversation as resolved.
}.onFailure { Logger.warn("Failed to retain or observe the original hardware Shop payment", it, context = TAG) }
.getOrDefault(false)
}
Expand Down Expand Up @@ -917,7 +923,7 @@
runSuspendCatching { finishAlreadyQueuedOnchainProof(attempt) }
.onFailure { Logger.warn("Failed to check queued Shop proof", it, context = TAG) }
}
if (!store.hasPendingProofs()) return@withContext
if (!hasPendingProofs()) return@withContext

// Snapshot immutable proofs, finish their exact accepted activity without the proof lock,
// then revalidate the same record under the lock before marking or delivering its proof.
Expand Down Expand Up @@ -1003,7 +1009,7 @@
if (!proof.paymentStarted || !proof.proofData.isHex(HASH_BYTE_COUNT) ||
!proof.paymentIdentifier.equals(proof.proofData, ignoreCase = true)
) return false
if (proof.onchainWalletId != WalletScope.default) return reconcileHardwareOnchainProof(proof)
if (proof.onchainWalletId != WalletScope.default) return reconcileCompletedHardwareProof(proof)
if (proof.onchainAcceptanceVerified != true) {
if (!attempt.matchesPositiveShopProof(proof)) return false
val proofs = loadProofs().toMutableList()
Expand Down Expand Up @@ -1080,7 +1086,13 @@
return retained
}

private suspend fun reconcileHardwareOnchainProof(proof: PendingPaykitPaymentProof): Boolean {
private suspend fun reconcileCompletedHardwareProof(proof: PendingPaykitPaymentProof): Boolean =
if (proof in observedHardwareProofs) submitReady(proof) else reconcileHardwareOnchainProof(proof)

private suspend fun reconcileHardwareOnchainProof(
Comment thread
ben-kaufman marked this conversation as resolved.
proof: PendingPaykitPaymentProof,
awaitDelivery: Boolean = true,
): Boolean {
val txid = proof.paymentIdentifier?.takeIf { it.isHex(HASH_BYTE_COUNT) } ?: return false
if (!proof.paymentStarted || proof.onchainWalletId == WalletScope.default ||
(proof.proofData != null && !proof.proofData.equals(txid, true))
Expand All @@ -1097,8 +1109,11 @@
if (index < 0) return false
val completed = proof.copy(proofData = txid.lowercase(), onchainAcceptanceVerified = true)
proofs[index] = completed
val retained = persistAndSubmit(listOf(completed), proofs)
if (retained) publishOnchainResolution(completed, txid)
val retained = persistAndSubmit(listOf(completed), proofs, awaitDelivery)
if (retained) {
observedHardwareProofs += completed
publishOnchainResolution(completed, txid)
}
// Hardware completion never acknowledges an unrelated node-wallet guard.
return retained
}
Expand Down Expand Up @@ -1257,6 +1272,7 @@
private suspend fun persistAndSubmit(
completedProofs: List<PendingPaykitPaymentProof>,
allProofs: List<PendingPaykitPaymentProof>,
awaitDelivery: Boolean = true,
): Boolean {
val didPersist = runSuspendCatching { persist(allProofs) }
.onFailure {
Expand All @@ -1266,6 +1282,7 @@
context = TAG,
)
}.isSuccess
if (didPersist && !awaitDelivery) return true
var hasUndeliveredProof = false
completedProofs.forEach { proof ->
val wasDelivered = runSuspendCatching { submitReady(proof) }
Expand Down Expand Up @@ -1314,10 +1331,17 @@
)
}

private fun loadProofs(): List<PendingPaykitPaymentProof> = store.load()
private suspend fun hasPendingProofs(): Boolean = operationMutex.withLock {
store.hasPendingProofs().also { if (!it) observedHardwareProofs.clear() }
}

private fun loadProofs(): List<PendingPaykitPaymentProof> = store.load().also {
observedHardwareProofs.retainAll(it.toSet())
}

private suspend fun persist(proofs: List<PendingPaykitPaymentProof>) {
store.save(proofs)
observedHardwareProofs.retainAll(proofs.toSet())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ class PaykitPaymentRequestRepo @Inject constructor(
} ?: return@refresh
val snapshot = runSuspendCatching {
val priority = if (forceFresh) Priority.Ordered else Priority.Background
fetchRequestSnapshot(expectedIdentity, mode, priority, messagePriority)
fetchRequestSnapshot(expectedIdentity, mode, priority, messagePriority, observationOnly = true)
}
operationMutex.withLock {
if (!isCurrentState(generation, expectedIdentity)) return@withLock
Expand Down Expand Up @@ -1137,14 +1137,23 @@ class PaykitPaymentRequestRepo @Inject constructor(
mode: PaykitPaymentRequestRefreshMode,
priority: Priority = Priority.Ordered,
messagePriority: Priority = Priority.Ordered,
observationOnly: Boolean = false,
): RequestSnapshot {
if (mode == PaykitPaymentRequestRefreshMode.FULL) processPendingMessages(priority = messagePriority)
if (mode != PaykitPaymentRequestRefreshMode.STORED) {
paykitSdkService.receivePrivateMessagesFromLinkedPeers(messagePriority).also(::logIntakeFailures)
}
val allRecords = paykitSdkService.allPaymentRequests(expectedIdentity, priority)
val blockedPeers = paykitSdkService.linkedPeers(priority).filter { it.state == LinkedPeerState.BLOCKED }
return RequestSnapshot(allRecords, blockedPeers)
if (observationOnly) {
val observation = paykitSdkService.observeState(requireNotNull(expectedIdentity), priority)
return RequestSnapshot(
observation.paymentRequests,
observation.linkedPeers.filter { it.state == LinkedPeerState.BLOCKED },
)
}
return RequestSnapshot(
paykitSdkService.allPaymentRequests(expectedIdentity, priority),
paykitSdkService.linkedPeers(priority).filter { it.state == LinkedPeerState.BLOCKED },
)
}

@Suppress("LongMethod", "CyclomaticComplexMethod")
Expand Down
17 changes: 9 additions & 8 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1141,9 +1141,10 @@ class PubkyRepo @Inject constructor(
* was resolved less than [CONTACT_PROFILE_FRESHNESS] ago.
*/
suspend fun loadContacts() {
val pk = _publicKey.value ?: return
val signIn = currentSignIn() ?: return
val pk = signIn.publicKey
loadContactsMutex.lock()
if (_publicKey.value != pk) {
if (!isCurrent(signIn)) {
loadContactsMutex.unlock()
return
}
Expand All @@ -1158,12 +1159,12 @@ class PubkyRepo @Inject constructor(
reload = false
runSuspendCatching {
withContext(ioDispatcher) {
val records = pubkyService.contactRecords()
val records = pubkyService.observedContactRecords(pk)
val overrides = pubkyStore.data.first().contactProfileOverrides
records.map { savedContact(it, overrides) }
}
}.onSuccess { savedContacts ->
if (_publicKey.value != pk) {
if (!isCurrent(signIn)) {
Logger.debug("Skipped stale contacts load for '${redacted(pk)}'", context = TAG)
return@onSuccess
}
Expand All @@ -1180,16 +1181,16 @@ class PubkyRepo @Inject constructor(
markContactsLoaded()
shouldMarkLoadCompleted = true
}.onFailure {
shouldMarkLoadCompleted = _publicKey.value == pk
shouldMarkLoadCompleted = isCurrent(signIn)
Logger.error("Failed to load contacts", it, context = TAG)
}
} while (reload && _publicKey.value == pk)
} while (reload && isCurrent(signIn))
} finally {
_isLoadingContacts.update { false }
loadContactsMutex.unlock()
if (shouldMarkLoadCompleted && _publicKey.value == pk) markContactsLoadCompleted()
if (shouldMarkLoadCompleted && isCurrent(signIn)) markContactsLoadCompleted()
}
refreshContactProfiles(pk, contactsToRefresh)
if (isCurrent(signIn)) refreshContactProfiles(pk, contactsToRefresh)
}

/**
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/services/PaykitSdkService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.synonym.paykit.PaykitProfile
import com.synonym.paykit.PaykitProfileRecord
import com.synonym.paykit.PaykitSdk
import com.synonym.paykit.PaykitSdkDefaults
import com.synonym.paykit.PaykitStateObservation
import com.synonym.paykit.PaymentAmountContext
import com.synonym.paykit.PaymentPayload
import com.synonym.paykit.PaymentProofSubmission
Expand Down Expand Up @@ -958,6 +959,31 @@ class PaykitSdkService @Inject constructor(

suspend fun paymentRequests(): List<PaymentRequestRecord> = allPaymentRequests().filter(::isBitkitPaymentRequest)

/** Refreshes display projections only; payment authority remains with the action APIs. */
internal suspend fun observeState(
expectedIdentity: String,
priority: Priority = Priority.Background,
): PaykitStateObservation {
isSetup.await()
val generation = runtimeGeneration
return operationLock.withLock(priority) {
check(runtimeGeneration == generation) { "Paykit runtime changed before observation" }
withPaykitKey { handle ->
val observation = completeSdkCall { handle.observeState() }
check(PubkyPublicKeyFormat.matches(observation.publicKey, expectedIdentity)) {
"Paykit observation identity changed"
}
cachedPaykitKey?.let {
check(
PubkyPublicKeyFormat.matches(observation.publicKey, it.publicKey) &&
observation.keyGeneration == it.generation,
) { "Paykit observation key changed" }
}
observation
}
}
}

suspend fun allPaymentRequests(expectedIdentity: String? = null): List<PaymentRequestRecord> =
allPaymentRequests(expectedIdentity, Priority.Ordered)

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/to/bitkit/services/PubkyService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ class PubkyService @Inject constructor(
paykitSdkService.contactRecords()
}

suspend fun observedContactRecords(expectedIdentity: String): List<ContactRecord> = ServiceQueue.CORE.background {
paykitSdkService.observeState(expectedIdentity).contacts
}

@Suppress("LongParameterList")
suspend fun saveContact(
publicKey: String,
Expand Down
44 changes: 27 additions & 17 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -968,14 +968,15 @@ class AppViewModel @Inject constructor(
forceFresh: Boolean = false,
messagePriority: Priority = Priority.Ordered,
onlyWhileActive: Boolean = false,
reconcileProofs: Boolean = mode == PaykitPaymentRequestRefreshMode.FULL,
): Result<Unit> {
if (!isPaykitEnabled.value || pubkyRepo.publicKey.value == null || !walletRepo.walletExists()) {
return Result.failure(PaykitPaymentRequestError.RequestUnavailable)
}
if (onlyWhileActive && isPaymentRequestPollingStopped.value) {
return Result.failure(PaykitPaymentRequestError.RequestUnavailable)
}
if (mode == PaykitPaymentRequestRefreshMode.FULL) paykitPaymentProofRepo.reconcile()
if (reconcileProofs) paykitPaymentProofRepo.reconcile()
if (onlyWhileActive && isPaymentRequestPollingStopped.value) {
return Result.failure(PaykitPaymentRequestError.RequestUnavailable)
}
Expand Down Expand Up @@ -1137,22 +1138,7 @@ class AppViewModel @Inject constructor(
}
}.filterNotNull().collect(::clearPaymentRequestPreparation)
}
viewModelScope.launch {
var changeVersion = 0L
paykitPaymentProofRepo.paymentRequestStateChanges(pubkyRepo.publicKey)
.map { ++changeVersion }
.combine(paykitPaymentRequestRepo.isPaymentSubmissionActive) { version, active ->
version.takeUnless { active }
}
.combine(isPaymentRequestPollingStopped) { version, stopped ->
version.takeUnless { stopped }
}
.filterNotNull()
.distinctUntilChanged()
.collect {
refreshIncomingPaykitPaymentRequests(PaykitPaymentRequestRefreshMode.STORED, forceFresh = true)
}
}
observePaymentProofChanges()
viewModelScope.launch {
var previousSheet: Sheet? = null
currentSheet.collect { sheet ->
Expand Down Expand Up @@ -1187,6 +1173,30 @@ class AppViewModel @Inject constructor(
}
}

private fun observePaymentProofChanges() {
viewModelScope.launch {
var changeVersion = 0L
paykitPaymentProofRepo.paymentRequestStateChanges(pubkyRepo.publicKey)
.map { ++changeVersion }
.combine(paykitPaymentRequestRepo.isPaymentSubmissionActive) { version, active ->
version.takeUnless { active }
}
.combine(isPaymentRequestPollingStopped) { version, stopped ->
version.takeUnless { stopped }
}
.filterNotNull()
.distinctUntilChanged()
.collect {
refreshIncomingPaykitPaymentRequests(
PaykitPaymentRequestRefreshMode.STORED,
forceFresh = true,
onlyWhileActive = true,
reconcileProofs = true,
)
}
}
}

fun onSheetVisible(sheet: Sheet?) {
if (sheet is Sheet.Subscription && sheet.route is SubscriptionRoute.Review) {
subscription(sheet.route.id)?.let { subscription ->
Expand Down
Loading
Loading