From ede19c2c61107b2a26b98054a8ac0ce74c6c243e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 03:28:05 +0200 Subject: [PATCH 1/4] fix: require onchain broadcast acceptance --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 3 +- .../bitkit/repositories/LightningRepoTest.kt | 68 +++++++++++++++++- .../viewmodels/AppViewModelSendFlowTest.kt | 69 ++++++++++++++++++ changelog.d/next/1211.fixed.md | 1 + journeys/onchain-send/README.md | 27 +++++++ journeys/onchain-send/broadcast-accepted.xml | 23 ++++++ journeys/onchain-send/broadcast-rejected.xml | 24 +++++++ scripts/reject-electrum-broadcast.js | 71 +++++++++++++++++++ 8 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 changelog.d/next/1211.fixed.md create mode 100644 journeys/onchain-send/README.md create mode 100644 journeys/onchain-send/broadcast-accepted.xml create mode 100644 journeys/onchain-send/broadcast-rejected.xml create mode 100755 scripts/reject-electrum-broadcast.js diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index dae74658dc..8cc4d4a464 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -3406,7 +3406,8 @@ class AppViewModel @Inject constructor( toast( type = Toast.ToastType.ERROR, title = context.getString(R.string.wallet__error_sending_title), - description = e.message ?: context.getString(R.string.common__error_body) + description = e.message ?: context.getString(R.string.common__error_body), + testTag = "OnchainSendFailedToast", ) hideSheet() } diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 76c9c99ad0..702505a9a9 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -29,6 +29,7 @@ import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.Node +import org.lightningdevkit.ldknode.NodeException import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PeerDetails @@ -893,15 +894,16 @@ class LightningRepoTest : BaseUnitTest() { } @Test - fun `sendOnChain should cache activity meta data`() = test { + fun `accepted onchain send should create metadata and sent activity`() = test { val mockSettingsData = SettingsData( defaultTransactionSpeed = TransactionSpeed.Fast, coinSelectAuto = false // Disable auto coin selection to simplify the test ) + val activityService = mock() whenever(settingsStore.data).thenReturn(flowOf(mockSettingsData)) whenever(preActivityMetadataRepo.addPreActivityMetadata(any())).thenReturn(Result.success(Unit)) - whenever(coreService.activity).thenReturn(mock()) + whenever(coreService.activity).thenReturn(activityService) whenever( lightningService.send( @@ -937,6 +939,68 @@ class LightningRepoTest : BaseUnitTest() { verifyBlocking(preActivityMetadataRepo) { addPreActivityMetadata(any()) } + verify(activityService).createSentOnchainActivityFromSendResult( + txid = "testPaymentId", + address = "test_address", + amount = 1000uL, + fee = 0uL, + feeRate = 10uL, + isTransfer = true, + channelId = "test_channel_id", + ) + } + + @Test + fun `unsuccessful onchain send should not create metadata or sent activity`() = test { + val errors = listOf( + NodeException.OnchainTxBroadcastRejected("Broadcast rejected"), + NodeException.OnchainTxBroadcastFailed("Broadcast failed"), + NodeException.OnchainTxBroadcastTimeout("Broadcast timed out"), + ) + val activityService = mock() + whenever(settingsStore.data).thenReturn( + flowOf( + SettingsData( + defaultTransactionSpeed = TransactionSpeed.Fast, + coinSelectAuto = false, + ) + ) + ) + whenever(coreService.activity).thenReturn(activityService) + startNodeForTesting() + val spySut = spy(sut) + doReturn(Result.success(10uL)).whenever(spySut).getFeeRateForSpeed(any(), anyOrNull()) + + errors.forEach { error -> + whenever( + lightningService.send( + address = any(), + sats = any(), + satsPerVByte = any(), + utxosToSpend = anyOrNull(), + isMaxAmount = any(), + ) + ).thenAnswer { throw error } + + val result = spySut.sendOnChain( + address = "test_address", + sats = 1000uL, + speed = TransactionSpeed.Fast, + ) + + assertEquals(error, result.exceptionOrNull()) + } + verifyBlocking(preActivityMetadataRepo, never()) { addPreActivityMetadata(any()) } + verify(activityService, never()).createSentOnchainActivityFromSendResult( + txid = any(), + address = any(), + amount = any(), + fee = any(), + feeRate = any(), + isTransfer = any(), + channelId = anyOrNull(), + walletId = any(), + ) } @Test diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 76c59f2e27..2fd52a2754 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -4516,6 +4516,75 @@ class AppViewModelSendFlowTest : BaseUnitTest() { confirmCurrentPayment() } + @Test + fun `accepted onchain send presents success`() = test { + val address = "bcrt1qaccepted" + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u) + whenever { + lightningRepo.sendOnChain( + address = address, + sats = 1000u, + speed = TransactionSpeed.Medium, + utxosToSpend = null, + isMaxAmount = false, + tags = emptyList(), + ) + }.thenReturn(Result.success("accepted-txid")) + whenever(lightningRepo.sync()).thenReturn(Result.success(Unit)) + whenever(activityRepo.syncActivities()).thenReturn(Result.success(Unit)) + setSendState( + SendUiState( + address = address, + amount = 1000u, + payMethod = SendMethod.ONCHAIN, + speed = TransactionSpeed.Medium, + ), + ) + + sut.sendEffect.test { + confirmCurrentPayment() + + assertEquals(SendEffect.PaymentSuccess, awaitItem()) + } + assertEquals("accepted-txid", sut.successSendUiState.value.paymentHashOrTxId) + } + + @Test + fun `rejected onchain send does not present success`() = test { + val address = "bcrt1qrejected" + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u) + whenever { + lightningRepo.sendOnChain( + address = address, + sats = 1000u, + speed = TransactionSpeed.Medium, + utxosToSpend = null, + isMaxAmount = false, + tags = emptyList(), + ) + }.thenReturn(Result.failure(AppError("Broadcast rejected"))) + setSendState( + SendUiState( + address = address, + amount = 1000u, + payMethod = SendMethod.ONCHAIN, + speed = TransactionSpeed.Medium, + ), + ) + + sut.sendEffect.test { + confirmCurrentPayment() + + expectNoEvents() + } + verify(toastManager).enqueue( + check { + assertEquals("OnchainSendFailedToast", it.testTag) + } + ) + assertNull(sut.successSendUiState.value.paymentHashOrTxId) + } + @Test fun `private lightning contact payment consumes private list before send`() = test { val bolt11 = "lnbcrt1privatecontact" diff --git a/changelog.d/next/1211.fixed.md b/changelog.d/next/1211.fixed.md new file mode 100644 index 0000000000..a3635c63bd --- /dev/null +++ b/changelog.d/next/1211.fixed.md @@ -0,0 +1 @@ +Prevented rejected, failed, or timed-out on-chain broadcasts from appearing as successful sends. diff --git a/journeys/onchain-send/README.md b/journeys/onchain-send/README.md new file mode 100644 index 0000000000..8afadda4c0 --- /dev/null +++ b/journeys/onchain-send/README.md @@ -0,0 +1,27 @@ +# On-chain Send Journeys + +These journeys verify the user-visible boundary introduced by synonymdev/ldk-node#112: + +- `broadcast-accepted.xml` requires explicit backend acceptance before `SendSuccess`. +- `broadcast-rejected.xml` requires a backend rejection to show `OnchainSendFailedToast` without `SendSuccess`. + +Run both against an Android build consuming the Maven Local artifact from the exact LDK Node #112 +head under validation. The accepted fixture uses a clean funded regtest wallet connected directly to +the local Electrum backend at `tcp://10.0.2.2:60001`. + +For the rejected fixture, run: + +```sh +node scripts/reject-electrum-broadcast.js +``` + +The proxy listens on host port `61001`, forwards normal Electrum traffic to host port `60001`, and +returns a deterministic RPC `-26 non-final` rejection for every transaction broadcast. Configure +Bitkit to use `tcp://10.0.2.2:61001` before running `broadcast-rejected.xml`. + +The app result is necessary but not sufficient evidence. After each journey, record the transaction +id when present and query the active backend. The accepted transaction must be present in its mempool +or chain. The rejected transaction must be absent, and Bitkit must not create a sent activity for it. + +Test tags: `Send`, `RecipientManual`, `RecipientInput`, `AddressContinue`, `send_amount_screen`, +`N1`, `N000`, `ContinueAmount`, `GRAB`, `SendSuccess`, and `OnchainSendFailedToast`. diff --git a/journeys/onchain-send/broadcast-accepted.xml b/journeys/onchain-send/broadcast-accepted.xml new file mode 100644 index 0000000000..50b18a01ce --- /dev/null +++ b/journeys/onchain-send/broadcast-accepted.xml @@ -0,0 +1,23 @@ + + + Verifies that a normal on-chain send reaches Bitcoin Sent only after the configured regtest + backend accepts the transaction. Requires a funded wallet, a valid destination address, camera + permission already resolved, and the LDK Node broadcast-result contract from + synonymdev/ldk-node#112. + + + Verify the Bitkit wallet home screen is visible + Tap the Send button (testTag "Send") + Tap Enter Manually (testTag "RecipientManual") + Type the valid regtest destination address into the recipient field (testTag "RecipientInput") + Tap Continue (testTag "AddressContinue") + Verify the amount screen is visible (testTag "send_amount_screen") + Tap the 1 key (testTag "N1"), then the triple-zero key (testTag "N000"), to enter 1,000 sats + Tap Continue (testTag "ContinueAmount") + Verify the send review screen is visible + Swipe the confirm handle (testTag "GRAB") from left to right + Verify the accepted transaction success screen is visible (testTag "SendSuccess") + Close the send sheet and open All Activity + Verify the accepted transaction appears as a pending sent on-chain activity + + diff --git a/journeys/onchain-send/broadcast-rejected.xml b/journeys/onchain-send/broadcast-rejected.xml new file mode 100644 index 0000000000..27f9cca581 --- /dev/null +++ b/journeys/onchain-send/broadcast-rejected.xml @@ -0,0 +1,24 @@ + + + Verifies that a deterministic backend rejection never reaches Bitcoin Sent. Requires a funded + wallet, a valid destination address, camera permission already resolved, and the LDK Node + broadcast-result contract from synonymdev/ldk-node#112. Run scripts/reject-electrum-broadcast.js + on the host and configure Bitkit to use tcp://10.0.2.2:61001 before starting the send. + + + Verify the Bitkit wallet home screen is visible + Tap the Send button (testTag "Send") + Tap Enter Manually (testTag "RecipientManual") + Type the valid regtest destination address into the recipient field (testTag "RecipientInput") + Tap Continue (testTag "AddressContinue") + Verify the amount screen is visible (testTag "send_amount_screen") + Tap the 1 key (testTag "N1"), then the triple-zero key (testTag "N000"), to enter 1,000 sats + Tap Continue (testTag "ContinueAmount") + Verify the send review screen is visible + Swipe the confirm handle (testTag "GRAB") from left to right + Verify the transaction failure feedback is visible (testTag "OnchainSendFailedToast") + Verify the success screen is absent (testTag "SendSuccess") + Close the send sheet and open All Activity + Verify the rejected transaction is not listed as a sent on-chain activity + + diff --git a/scripts/reject-electrum-broadcast.js b/scripts/reject-electrum-broadcast.js new file mode 100755 index 0000000000..f8366a4978 --- /dev/null +++ b/scripts/reject-electrum-broadcast.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const net = require("node:net") + +function option(name, fallback) { + const index = process.argv.indexOf(`--${name}`) + return index >= 0 ? process.argv[index + 1] : fallback +} + +const listenHost = option("listen-host", "127.0.0.1") +const listenPort = Number(option("listen-port", "61001")) +const upstreamHost = option("upstream-host", "127.0.0.1") +const upstreamPort = Number(option("upstream-port", "60001")) +const rejectionMessage = option("message", "non-final") + +function rejection(request) { + return { + jsonrpc: request.jsonrpc ?? "2.0", + id: request.id, + error: { code: -26, message: rejectionMessage }, + } +} + +function forwardClientLines(client, upstream) { + let buffered = "" + + client.on("data", chunk => { + buffered += chunk.toString("utf8") + const lines = buffered.split("\n") + buffered = lines.pop() ?? "" + + for (const line of lines) { + if (line.length === 0) continue + + let request + try { + request = JSON.parse(line) + } catch { + upstream.write(`${line}\n`) + continue + } + + if (!Array.isArray(request) && request.method === "blockchain.transaction.broadcast") { + client.write(`${JSON.stringify(rejection(request))}\n`) + } else { + upstream.write(`${line}\n`) + } + } + }) +} + +const server = net.createServer(client => { + const upstream = net.createConnection({ host: upstreamHost, port: upstreamPort }) + + forwardClientLines(client, upstream) + upstream.pipe(client) + + client.on("error", () => upstream.destroy()) + upstream.on("error", error => client.destroy(error)) + client.on("close", () => upstream.destroy()) + upstream.on("close", () => client.destroy()) +}) + +server.listen(listenPort, listenHost, () => { + process.stdout.write( + `Electrum rejection proxy listening on ${listenHost}:${listenPort}, forwarding to ${upstreamHost}:${upstreamPort}\n` + ) +}) + +process.on("SIGINT", () => server.close()) +process.on("SIGTERM", () => server.close()) From 07dcd8b2aec91f2894a6be7555a599b28e99ecc7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 2 Sep 2026 10:30:46 +0200 Subject: [PATCH 2/4] docs: fix rejected journey wording --- journeys/onchain-send/broadcast-rejected.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/journeys/onchain-send/broadcast-rejected.xml b/journeys/onchain-send/broadcast-rejected.xml index 27f9cca581..a2d0d29d51 100644 --- a/journeys/onchain-send/broadcast-rejected.xml +++ b/journeys/onchain-send/broadcast-rejected.xml @@ -18,7 +18,7 @@ Swipe the confirm handle (testTag "GRAB") from left to right Verify the transaction failure feedback is visible (testTag "OnchainSendFailedToast") Verify the success screen is absent (testTag "SendSuccess") - Close the send sheet and open All Activity + Verify the send sheet dismisses automatically, then open All Activity Verify the rejected transaction is not listed as a sent on-chain activity From 051d3621843f8f920abab0c007f374ccef3995a8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 5 Sep 2026 04:28:08 +0200 Subject: [PATCH 3/4] chore: update ldk node artifact --- .../bitkit/ext/PaymentFailureReasonExtTest.kt | 6 +-- .../bitkit/repositories/LightningRepoTest.kt | 1 + .../bitkit/repositories/QuickPayRepoTest.kt | 40 +++++++++---------- .../bitkit/services/LightningServiceTest.kt | 4 +- gradle/libs.versions.toml | 2 +- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt b/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt index 2fe3a8c264..5fafab6dcd 100644 --- a/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt +++ b/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt @@ -43,7 +43,7 @@ class PaymentFailureReasonExtTest { assertEquals(message, Exception(" ").toSendFailureMessage(context)) assertEquals( message, - LdkError(NodeException.DuplicatePayment("Duplicate payment.")).toSendFailureMessage(context), + LdkError(NodeException.DuplicatePayment()).toSendFailureMessage(context), ) } @@ -56,11 +56,11 @@ class PaymentFailureReasonExtTest { fun `compact failure types use android ldk error classes`() { assertEquals( "DuplicatePayment", - LdkError(NodeException.DuplicatePayment("Duplicate payment.")).toCompactFailureType(), + LdkError(NodeException.DuplicatePayment()).toCompactFailureType(), ) assertEquals( "InvalidCustomTlvs", - LdkError(NodeException.InvalidCustomTlvs("Invalid custom TLVs")).toCompactFailureType(), + LdkError(NodeException.InvalidCustomTlvs()).toCompactFailureType(), ) } diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 702505a9a9..d1cbb270aa 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -954,6 +954,7 @@ class LightningRepoTest : BaseUnitTest() { fun `unsuccessful onchain send should not create metadata or sent activity`() = test { val errors = listOf( NodeException.OnchainTxBroadcastRejected("Broadcast rejected"), + NodeException.OnchainTxBroadcastNotDispatched("Broadcast not dispatched"), NodeException.OnchainTxBroadcastFailed("Broadcast failed"), NodeException.OnchainTxBroadcastTimeout("Broadcast timed out"), ) diff --git a/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt b/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt index e004753765..23156ca831 100644 --- a/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt @@ -291,7 +291,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `acknowledge during delivery clears the unacked failure`() = test { val (bolt11, _) = testInvoice() - stubPayInvoiceFailure(NodeException.InvalidInvoice("bad")) + stubPayInvoiceFailure(NodeException.InvalidInvoice()) val session = QuickPaySession() val flushed = mutableListOf() val flushJob = launch { sut.unhandledFailures.collect { flushed += it } } @@ -507,7 +507,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `lookup throw on duplicate still emits pending`() = test { val (bolt11, _) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) whenever { lightningRepo.listPaymentsOrNull() }.thenAnswer { error("uniffi") } val session = QuickPaySession() sut.attach(session).test { @@ -561,27 +561,27 @@ class QuickPayRepoTest : BaseUnitTest() { fun `classifies wrapped and unwrapped ldk errors`() { assertEquals( QuickPayDispatchClass.PRE_DISPATCH_REJECTION, - classifyDispatchError(NodeException.InvalidInvoice("bad")), + classifyDispatchError(NodeException.InvalidInvoice()), ) assertEquals( QuickPayDispatchClass.PRE_DISPATCH_REJECTION, - classifyDispatchError(LdkError(NodeException.InvalidInvoice("bad"))), + classifyDispatchError(LdkError(NodeException.InvalidInvoice())), ) assertEquals( QuickPayDispatchClass.DUPLICATE_PAYMENT, - classifyDispatchError(NodeException.DuplicatePayment("dup")), + classifyDispatchError(NodeException.DuplicatePayment()), ) assertEquals( QuickPayDispatchClass.DUPLICATE_PAYMENT, - classifyDispatchError(LdkError(NodeException.DuplicatePayment("dup"))), + classifyDispatchError(LdkError(NodeException.DuplicatePayment())), ) assertEquals( QuickPayDispatchClass.AMBIGUOUS, - classifyDispatchError(NodeException.PersistenceFailed("io")), + classifyDispatchError(NodeException.PersistenceFailed()), ) assertEquals( QuickPayDispatchClass.AMBIGUOUS, - classifyDispatchError(LdkError(NodeException.PaymentSendingFailed("send"))), + classifyDispatchError(LdkError(NodeException.PaymentSendingFailed())), ) } @@ -597,7 +597,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `duplicate payment with pending ldk does not refund`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(pendingRow(hash)) val session = QuickPaySession() @@ -613,7 +613,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `duplicate payment with succeeded ldk refunds a fresh reserve and emits already paid`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(succeededRow(hash)) val session = QuickPaySession() @@ -633,7 +633,7 @@ class QuickPayRepoTest : BaseUnitTest() { assertNotNull(sut.reserveBound(hash, 500u).getOrThrow()) sut.signalCompletion(paymentId = null, paymentHash = hash, success = true) assertEquals(250L, spentCents()) - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(succeededRow(hash)) val session = QuickPaySession() @@ -649,7 +649,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `ambiguous pending emits pending and keeps spend`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.PaymentSendingFailed("send")) + stubPayInvoiceFailure(NodeException.PaymentSendingFailed()) paymentRows = listOf(pendingRow(hash)) val session = QuickPaySession() @@ -665,7 +665,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `sync dispatch failure with failed ldk row refunds immediately`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.PaymentSendingFailed("send")) + stubPayInvoiceFailure(NodeException.PaymentSendingFailed()) paymentRows = listOf(failedRow(hash)) val session = QuickPaySession() @@ -720,7 +720,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `rescan of a pending hash replays pending to a new session`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(pendingRow(hash)) val first = QuickPaySession() val second = QuickPaySession() @@ -741,7 +741,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `rescan pending then success settles once`() = test { val (bolt11, hash) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(pendingRow(hash)) val first = QuickPaySession() val second = QuickPaySession() @@ -797,7 +797,7 @@ class QuickPayRepoTest : BaseUnitTest() { val first = QuickPaySession() sut.attach(first) sut.detachAll() - stubPayInvoiceFailure(NodeException.InvalidInvoice("bad")) + stubPayInvoiceFailure(NodeException.InvalidInvoice()) val second = QuickPaySession() sut.attach(second).test { sut.payNow(second, QuickPayPayRequest.Bolt11(bolt11 = testInvoice().first, amountSats = 500u)) @@ -809,7 +809,7 @@ class QuickPayRepoTest : BaseUnitTest() { fun `hasOpen is true for a live op or recovered row`() = test { val (bolt11, hash) = testInvoice() assertFalse(sut.hasOpen(hash)) - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) paymentRows = listOf(pendingRow(hash)) val session = QuickPaySession() sut.attach(session) @@ -887,7 +887,7 @@ class QuickPayRepoTest : BaseUnitTest() { assertEquals(250L, spentCents()) assertEquals(1, cacheStore.data.first().quickPayLedger!!.records.size) if (!onBeforeSend()) return@doSuspendableAnswer Result.failure(PaymentAbortedBeforeSend()) - Result.failure(LdkError(NodeException.InvalidInvoice("done"))) + Result.failure(LdkError(NodeException.InvalidInvoice())) } val session = QuickPaySession() sut.attach(session) @@ -958,7 +958,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `pre-dispatch rejection refunds after dispatch`() = test { val (bolt11, _) = testInvoice() - stubPayInvoiceFailure(NodeException.InvalidInvoice("bad")) + stubPayInvoiceFailure(NodeException.InvalidInvoice()) val session = QuickPaySession() sut.attach(session).test { sut.payNow(session, QuickPayPayRequest.Bolt11(bolt11 = bolt11, amountSats = 500u)) @@ -971,7 +971,7 @@ class QuickPayRepoTest : BaseUnitTest() { @Test fun `null payment rows mutate nothing on duplicate`() = test { val (bolt11, _) = testInvoice() - stubPayInvoiceFailure(NodeException.DuplicatePayment("dup")) + stubPayInvoiceFailure(NodeException.DuplicatePayment()) val session = QuickPaySession() sut.attach(session).test { sut.payNow(session, QuickPayPayRequest.Bolt11(bolt11 = bolt11, amountSats = 500u)) diff --git a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt index d4285c2efd..114202690f 100644 --- a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt +++ b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt @@ -119,7 +119,7 @@ class LightningServiceTest : BaseUnitTest() { @Test fun `stop destroys the node handle when it is already not running`() = test { - whenever(node.stop()).thenThrow(NodeException.NotRunning("not running")) + whenever(node.stop()).thenThrow(NodeException.NotRunning()) sut.stop() @@ -403,7 +403,7 @@ class LightningServiceTest : BaseUnitTest() { // Regression: a failing node stop must still release the handle instead of rethrowing and leaking it @Test fun `stop destroys the node handle when node stop throws`() = test { - whenever(node.stop()).thenThrow(NodeException.ConnectionFailed("boom")) + whenever(node.stop()).thenThrow(NodeException.ConnectionFailed()) sut.stop() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17cbb15651..2bffdea6b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.66" } +ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.67" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } From b8ed18263218bda517da9e2686c248e6de23f9ac Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 5 Sep 2026 04:28:47 +0200 Subject: [PATCH 4/4] chore: rename changelog fragment --- changelog.d/next/{1211.fixed.md => 1225.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1211.fixed.md => 1225.fixed.md} (100%) diff --git a/changelog.d/next/1211.fixed.md b/changelog.d/next/1225.fixed.md similarity index 100% rename from changelog.d/next/1211.fixed.md rename to changelog.d/next/1225.fixed.md