File tree Expand file tree Collapse file tree
test/java/to/bitkit/viewmodels Expand file tree Collapse file tree Original file line number Diff line number Diff 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.
Original file line number Diff line number Diff 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 }
Original file line number Diff line number Diff 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 }
Original file line number Diff line number Diff 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"
You can’t perform that action at this time.
0 commit comments