Skip to content
Open
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
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/data/keychain/Keychain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class Keychain @Inject constructor(
PAYKIT_SESSION,
PAYKIT_RECEIVER_NOISE_SECRET_KEY,
PAYKIT_SDK_STATE,
PAYKIT_PENDING_PAYMENT_PROOFS,
PAYKIT_PRESENTED_PAYMENT_REQUESTS,
PUBKY_SECRET_KEY,
}
Expand Down
375 changes: 375 additions & 0 deletions app/src/main/java/to/bitkit/repositories/PaykitPaymentProofRepo.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package to.bitkit.repositories

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import to.bitkit.data.keychain.Keychain
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PaykitPaymentProofStore @Inject constructor(
private val keychain: Keychain,
) {
@Serializable
private data class State(
val proofs: List<PendingPaykitPaymentProof> = emptyList(),
)

fun load(): List<PendingPaykitPaymentProof> {
val value = keychain.loadString(Keychain.Key.PAYKIT_PENDING_PAYMENT_PROOFS.name) ?: return emptyList()
return Json.decodeFromString<State>(value).proofs
}

suspend fun save(proofs: List<PendingPaykitPaymentProof>) {
keychain.upsertString(
Keychain.Key.PAYKIT_PENDING_PAYMENT_PROOFS.name,
Json.encodeToString(State(proofs)),
)
}
}
25 changes: 25 additions & 0 deletions app/src/main/java/to/bitkit/services/PaykitSdkService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.synonym.paykit.PaykitSdk
import com.synonym.paykit.PaykitSdkDefaults
import com.synonym.paykit.PaymentAmountContext
import com.synonym.paykit.PaymentPayload
import com.synonym.paykit.PaymentProofSubmission
import com.synonym.paykit.PaymentReference
import com.synonym.paykit.PaymentRequestAmount
import com.synonym.paykit.PaymentRequestFilter
Expand Down Expand Up @@ -654,6 +655,30 @@ class PaykitSdkService @Inject constructor(
}
}

suspend fun submitPaymentProof(
counterparty: String,
counterpartyReceiverPath: String,
paymentRequestId: String,
paymentEndpointIdentifier: String,
proofJson: String,
): PaymentRequestRecord {
isSetup.await()
return operationMutex.withLock {
withStateRevisionTracking { handle ->
handle.submitPaymentProof(
counterparty,
counterpartyReceiverPath,
paymentRequestId,
PaymentProofSubmission(
billingPeriod = null,
paymentEndpointIdentifier = paymentEndpointIdentifier,
proof = PrivateJsonObject(proofJson),
),
)
}
}
}

suspend fun rejectPaymentRequest(
counterparty: String,
counterpartyReceiverPath: String,
Expand Down
87 changes: 86 additions & 1 deletion app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ import to.bitkit.repositories.HealthRepo
import to.bitkit.repositories.HwWalletRepo
import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.LnurlPayInvoiceMismatchError
import to.bitkit.repositories.MethodId
import to.bitkit.repositories.NodeEventUpdate
import to.bitkit.repositories.PaykitPaymentProofKind
import to.bitkit.repositories.PaykitPaymentProofRepo
import to.bitkit.repositories.PaykitPaymentRequest
import to.bitkit.repositories.PaykitPaymentRequestCreation
import to.bitkit.repositories.PaykitPaymentRequestDraft
Expand Down Expand Up @@ -231,6 +234,7 @@ class AppViewModel @Inject constructor(
private val publicPaykitRepo: PublicPaykitRepo,
private val privatePaykitRepo: PrivatePaykitRepo,
private val paykitPaymentRequestRepo: PaykitPaymentRequestRepo,
private val paykitPaymentProofRepo: PaykitPaymentProofRepo,
private val refreshContactPaykitReceivers: RefreshContactPaykitReceiversUseCase,
private val samRockRepo: SamRockRepo,
private val appUpdateSheet: AppUpdateTimedSheet,
Expand Down Expand Up @@ -687,6 +691,7 @@ class AppViewModel @Inject constructor(

private suspend fun refreshIncomingPaykitPaymentRequests(): Boolean {
if (!isPaykitEnabled.value || pubkyRepo.publicKey.value == null || !walletRepo.walletExists()) return false
paykitPaymentProofRepo.reconcile()
val previousRequests = paykitPaymentRequestRepo.pendingRequests.value
val savedPublicKeys = pubkyRepo.contacts.value.map { it.publicKey }
return paykitPaymentRequestRepo.refresh(savedPublicKeys).fold(
Expand Down Expand Up @@ -1300,6 +1305,9 @@ class AppViewModel @Inject constructor(
}

private suspend fun handlePaymentFailed(event: Event.PaymentFailed) {
(event.paymentHash ?: event.paymentId)?.let { paymentHash ->
viewModelScope.launch { paykitPaymentProofRepo.failLightningPayment(paymentHash) }
}
event.paymentHash?.let { paymentHash ->
activityRepo.handlePaymentEvent(paymentHash)
if (pendingPaymentRepo.isPending(paymentHash)) {
Expand Down Expand Up @@ -1376,6 +1384,9 @@ class AppViewModel @Inject constructor(
}

private suspend fun handlePaymentSuccessful(event: Event.PaymentSuccessful) {
viewModelScope.launch {
paykitPaymentProofRepo.completeLightningPayment(event.paymentHash, event.paymentPreimage)
}
event.paymentHash.let { paymentHash ->
activityRepo.handlePaymentEvent(paymentHash)
if (pendingPaymentRepo.isPending(paymentHash)) {
Expand Down Expand Up @@ -2924,18 +2935,29 @@ class AppViewModel @Inject constructor(
}
}

@Suppress("LongMethod")
@Suppress("LongMethod", "ReturnCount")
private suspend fun proceedWithPayment(contactPaymentContext: ContactPaymentContext?) {
delay(SCREEN_TRANSITION_DELAY) // wait for screen transitions when applicable

if (!validateIncomingPaymentRequest(contactPaymentContext)) return

val incomingPaymentRequest = contactPaymentContext?.incomingPaymentRequest
var preparedPaymentProofRequest = preparePaymentProof(incomingPaymentRequest).fold(
onSuccess = { it },
onFailure = {
handlePaymentPreparationFailure(it)
return
},
)

consumePrivatePaymentListIfNeeded(contactPaymentContext).onFailure {
cancelPaymentProofPreparation(preparedPaymentProofRequest)
handlePaymentPreparationFailure(it)
return
}

acceptIncomingPaymentRequestIfNeeded(contactPaymentContext).onFailure {
cancelPaymentProofPreparation(preparedPaymentProofRequest)
handlePaymentPreparationFailure(it)
return
}
Expand All @@ -2956,6 +2978,7 @@ class AppViewModel @Inject constructor(
it.copy(decodedInvoice = invoice)
}
}.onFailure {
cancelPaymentProofPreparation(preparedPaymentProofRequest)
val message = getLnurlInvoiceFetchErrorMessage(it)
toast(Exception(message))
hideSheet()
Expand All @@ -2969,6 +2992,8 @@ class AppViewModel @Inject constructor(
val tags = _sendUiState.value.selectedTags
sendOnchain(address, amount, tags = tags)
.onSuccess { txId ->
preparedPaymentProofRequest = null
completeOnchainPaymentProof(incomingPaymentRequest, txId)
Logger.info("Onchain send result txid: $txId", context = TAG)
onSendSuccess(
NewTransactionSheetDetails(
Expand All @@ -2983,6 +3008,7 @@ class AppViewModel @Inject constructor(
activityRepo.syncActivities()
_successSendUiState.update { it.copy(isLoadingDetails = false) }
}.onFailure { e ->
cancelPaymentProofPreparation(preparedPaymentProofRequest)
Logger.error("Error sending onchain payment", e, context = TAG)
toast(
type = Toast.ToastType.ERROR,
Expand All @@ -3005,6 +3031,11 @@ class AppViewModel @Inject constructor(

// Extract payment hash from invoice for pre-activity metadata
val paymentHash = decodedInvoice.paymentHash.toHex()
associateLightningPaymentProof(incomingPaymentRequest, paymentHash).onFailure {
Comment thread
ben-kaufman marked this conversation as resolved.
cancelPaymentProofPreparation(preparedPaymentProofRequest)
handlePaymentPreparationFailure(it)
return
}

// Create pre-activity metadata before sending
if (tags.isNotEmpty()) {
Expand All @@ -3020,6 +3051,7 @@ class AppViewModel @Inject constructor(
}

sendLightning(bolt11, paymentAmount).onSuccess { actualPaymentHash ->
preparedPaymentProofRequest = null
Logger.info("Lightning send result payment hash: $actualPaymentHash", context = TAG)
onSendSuccess(
NewTransactionSheetDetails(
Expand All @@ -3031,12 +3063,15 @@ class AppViewModel @Inject constructor(
)
}.onFailure {
if (it is PaymentPendingException) {
preparedPaymentProofRequest = null
Logger.info("Lightning payment pending", context = TAG)
pendingPaymentRepo.track(it.paymentHash)
preserveContactPaymentContext(it.paymentHash)
setSendEffect(SendEffect.NavigateToPending(it.paymentHash, displayAmountSats.toLong()))
return@onFailure
}
paykitPaymentProofRepo.failLightningPayment(paymentHash)
cancelPaymentProofPreparation(preparedPaymentProofRequest)
// Delete pre-activity metadata on failure
if (createdMetadataPaymentId != null) {
preActivityMetadataRepo.deletePreActivityMetadata(createdMetadataPaymentId)
Expand All @@ -3054,6 +3089,51 @@ class AppViewModel @Inject constructor(
}
}

private suspend fun preparePaymentProof(request: PaykitPaymentRequest?): Result<PaykitPaymentRequest?> {
if (request == null) return Result.success(null)
val preparation = paymentProofPreparation()
return paykitPaymentProofRepo.prepare(
request = request,
paymentEndpointIdentifier = preparation.endpointIdentifier,
kind = preparation.kind,
).map { request }
}

private suspend fun associateLightningPaymentProof(
request: PaykitPaymentRequest?,
paymentHash: String,
): Result<Unit> = request?.let { paykitPaymentProofRepo.associateLightningPayment(it, paymentHash) }
?: Result.success(Unit)

private suspend fun completeOnchainPaymentProof(request: PaykitPaymentRequest?, txId: String) {
request?.let {
paykitPaymentProofRepo.completeOnchainPayment(
request = it,
txid = txId,
paymentEndpointIdentifier = paymentProofPreparation().endpointIdentifier,
)
}
}

private suspend fun cancelPaymentProofPreparation(request: PaykitPaymentRequest?) {
request?.let { paykitPaymentProofRepo.cancelPreparation(it) }
}

private fun paymentProofPreparation(): PaymentProofPreparation {
val methodId = when (_sendUiState.value.payMethod) {
SendMethod.ONCHAIN -> PublicPaykitRepo.onchainMethodId(_sendUiState.value.address)
SendMethod.LIGHTNING -> if (_sendUiState.value.lnurl is LnurlParams.LnurlPay) {
MethodId.Lnurl
} else {
MethodId.Bolt11
}
}
return PaymentProofPreparation(
endpointIdentifier = methodId.rawValue,
kind = if (methodId.isOnchain) PaykitPaymentProofKind.Onchain else PaykitPaymentProofKind.Lightning,
)
}

private suspend fun hasMismatchedIncomingPaymentRequest(contactPaymentContext: ContactPaymentContext?): Boolean {
val incomingPaymentRequest = contactPaymentContext?.incomingPaymentRequest ?: return false
if (!incomingPaymentRequest.acceptsPaymentAmount(_sendUiState.value.amount)) return true
Expand Down Expand Up @@ -4244,6 +4324,11 @@ data class ContactPaymentContext(
val incomingPaymentRequest: PaykitPaymentRequest? = null,
)

private data class PaymentProofPreparation(
val endpointIdentifier: String,
val kind: PaykitPaymentProofKind,
)

private data class PaykitContactSyncState(
val publicKey: String?,
val contactKeys: Set<String>,
Expand Down
Loading
Loading