Skip to content

Commit 024b6bb

Browse files
committed
fix: settle a spark send that finished before it was watched
1 parent ae55b36 commit 024b6bb

4 files changed

Lines changed: 63 additions & 10 deletions

File tree

‎app/src/main/java/to/bitkit/repositories/LightningRepo.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2309,6 +2309,9 @@ class LightningRepo @Inject constructor(
23092309
fun getChannels(): List<ChannelDetails>? =
23102310
if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.channels else null
23112311

2312+
/** The completion event of a Spark send that settled before its caller started watching for it. */
2313+
suspend fun settledSparkSendEvent(paymentHash: String): Event? = sparkRepo.settledSendEvent(paymentHash)
2314+
23122315
/**
23132316
* Whether Lightning can be received, through a usable channel or Spark; Paykit publishes a Lightning endpoint
23142317
* only then.

‎app/src/main/java/to/bitkit/repositories/SparkRepo.kt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ class SparkRepo @Inject constructor(
290290
}
291291
}
292292

293+
/**
294+
* The completion event of a Spark Lightning send that has already settled. A Spark-to-Spark payment settles inside
295+
* the send call, so its event can fire before the caller starts watching for it.
296+
*/
297+
suspend fun settledSendEvent(paymentHash: String): Event? = withContext(bgDispatcher) {
298+
if (!sparkService.isConnected) return@withContext null
299+
runSuspendCatching { sparkService.listPayments() }
300+
.onFailure { Logger.warn("Failed to list Spark payments", it, context = TAG) }
301+
.getOrNull()
302+
?.firstOrNull {
303+
val hash = (it.details as? PaymentDetails.Lightning)?.htlcDetails?.paymentHash
304+
it.paymentType == SparkPaymentType.SEND && hash.equals(paymentHash, ignoreCase = true)
305+
}
306+
?.toNodeEvent()
307+
}
308+
293309
/** Payment hashes of the Lightning payments Spark received. */
294310
suspend fun receivedLightningPaymentHashes(): Set<String> =
295311
lightningPaymentHashes { it.paymentType == SparkPaymentType.RECEIVE && it.status == PaymentStatus.COMPLETED }

‎app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6682,18 +6682,22 @@ class AppViewModel @Inject constructor(
66826682
paymentDeadlineAt = paymentDeadlineAt,
66836683
onBeforeSend = onBeforeSend,
66846684
).onSuccess { hash ->
6685-
// Wait until matching payment event is received (with timeout for hold invoices)
6686-
val result = lightningRepo.nodeEvents.watchUntil(LightningRepo.SEND_LN_TIMEOUT) {
6687-
when (it) {
6688-
is Event.PaymentSuccessful if it.paymentHash == hash -> WatchResult.Complete(Result.success(hash))
6689-
is Event.PaymentFailed if it.paymentHash == hash -> WatchResult.Complete(
6690-
Result.failure(
6691-
LightningPaymentFailedError(reason = it.reason, paymentRequest = bolt11)
6692-
)
6693-
)
6685+
fun outcome(event: Event): Result<PaymentId>? = when (event) {
6686+
is Event.PaymentSuccessful if event.paymentHash == hash -> Result.success(hash)
6687+
is Event.PaymentFailed if event.paymentHash == hash -> Result.failure(
6688+
LightningPaymentFailedError(reason = event.reason, paymentRequest = bolt11)
6689+
)
66946690

6695-
else -> WatchResult.Continue()
6691+
else -> null
6692+
}
6693+
// Wait until matching payment event is received (with timeout for hold invoices)
6694+
val result = coroutineScope {
6695+
val watch = async(start = CoroutineStart.UNDISPATCHED) {
6696+
lightningRepo.nodeEvents.watchUntil(LightningRepo.SEND_LN_TIMEOUT) {
6697+
outcome(it)?.let { result -> WatchResult.Complete(result) } ?: WatchResult.Continue()
6698+
}
66966699
}
6700+
lightningRepo.settledSparkSendEvent(hash)?.let(::outcome)?.also { watch.cancel() } ?: watch.await()
66976701
}
66986702
return result ?: Result.failure(PaymentPendingException(hash))
66996703
}

‎app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13094,6 +13094,36 @@ class AppViewModelSendFlowTest : BaseUnitTest() {
1309413094
verify(privatePaykitRepo).consumePrivatePaymentList(contactKey, privateContext)
1309513095
}
1309613096

13097+
@Test
13098+
fun `spark send that settled before the watch started completes without pending`() = test {
13099+
val bolt11 = "lnbcrt1sparktospark"
13100+
val paymentHash = "spark_hash"
13101+
balanceState.value = BalanceState(maxSendLightningSats = 100_000u)
13102+
whenever(lightningRepo.payInvoice(bolt11 = bolt11, sats = null)).thenReturn(Result.success(paymentHash))
13103+
whenever(lightningRepo.settledSparkSendEvent(paymentHash)).thenReturn(
13104+
Event.PaymentSuccessful(
13105+
paymentId = paymentHash,
13106+
paymentHash = paymentHash,
13107+
paymentPreimage = "preimage",
13108+
feePaidMsat = 0uL,
13109+
),
13110+
)
13111+
setSendState(
13112+
SendUiState(
13113+
address = bolt11,
13114+
amount = 1000u,
13115+
payMethod = SendMethod.LIGHTNING,
13116+
decodedInvoice = lightningInvoice(bolt11, amountSats = 1000u),
13117+
),
13118+
)
13119+
13120+
sut.setSendEvent(SendEvent.PayConfirmed)
13121+
advanceTimeBy(LightningRepo.SEND_LN_TIMEOUT.inWholeMilliseconds + 1_000)
13122+
runCurrent()
13123+
13124+
verify(pendingPaymentRepo, never()).track(paymentHash)
13125+
}
13126+
1309713127
@Test
1309813128
fun `private lightning pending payment consumes private list`() = test {
1309913129
val bolt11 = "lnbcrt1pending"

0 commit comments

Comments
 (0)