Repository navigation
fix: refresh lightning capacity before send - #1808
Conversation
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryRefreshes Lightning channels before Send trusts a cached usable channel, so a recent receipt updates the spending limit.
No actionable issues found. Checks in this review were source-based; no tests were run. Reviews (1) · Last reviewed commit: "docs: add lightning spending limit chang..." · Reviewed by Greptile |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. waitForUsableChannels now re-reads channels and peers on every call instead of returning early on a cached usable channel, so a send prepared right after a receipt derives its Spending cap from the current nextOutboundHtlcLimitMsat. Reviewed b5ff8114be9a1392adc2caef371d5b367868c7dd, full tier, statically.
What I checked, and 4 candidates I ruled out
Read in full: waitForUsableChannels, awaitUsableChannels, waitForChannelsToLoadIfNeeded, refreshChannelsAndPeers, syncState, canSend in LightningRepo.kt; WalletRepo.refreshMaxSendLightning; the six new cases in LightningRepoTest.kt.
Call sites traced: AppViewModel.waitForUsableChannels (unified invoice, Paykit request, bolt11, LNURL pay) all call walletRepo.refreshMaxSendLightning() right after, which reads the channels this refresh publishes, so the cap and canSend see the same snapshot; payInvoice calls it before onBeforeSend.
Build: not built here. Bitkit Android reviews are static on this side: Gradle would run the branch's own build scripts. lint, detekt and build are green; build-and-test and compose-ui were still pending at review time.
Ruled out
- Empty-channel path lost
syncState(): the old branch also refreshedbalances,nodeStatusand scheduled the ordinary follow-up retry. The callers only need channels (refreshMaxSendLightning,canSendreadchannels),syncState()still runs after everysend, and avoiding the balance read is the stated intent of8d0fcabfe. - Refresh while the node is starting:
refreshChannelsAndPeersreturns success without reading when the lifecycle is not running, so a cached-channel state behaves as before. - Refresh failure: the exception is caught inside
refreshChannelsAndPeersand the last snapshot stays published, covered bykeeps the last usable snapshot when refreshing fails. - Node stopping mid-refresh: the lifecycle is rechecked after the refresh and takes the feedback-delay path, covered by
rechecks lifecycle when the node stops during refresh.
Merge confidence: 4/5, no findings and the change is covered by the new LightningRepoTest.kt cases, but build-and-test had not finished and the branch was not built here.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the runtime does not report it to the reviewer)
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews, nothing blocking a merge.
worth doing, does not block
- Android-only journey not in cross-platform table and needs unlisted capability (journeys/amount-limits/send-spending-after-receive.xml:1)
nits
- Node-stop test passes without the new post-refresh lifecycle check (app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt:1664)
- Unusable-channel path reads channels twice back to back (app/src/main/java/to/bitkit/repositories/LightningRepo.kt:1467). When the new refresh returns non-empty channels with none usable,
awaitUsableChannels()callsrefreshChannelsAndPeers()again before its first delay. Every wait now starts with two consecutive node reads of the same data. Harmless; the loop could start with the delay since the caller has just refreshed.
|
For the duplicate-read nit in the review summary: out of scope: tracked in #1811 for 3.2.0, because this is a LOW redundant-read optimization. Source inspection confirms that a refreshed nonempty/unusable snapshot enters awaitUsableChannels(), which refreshes once more before the first delay. It does not establish an incorrect spending cap; changing polling order deserves its own focused call-count, recovery and lifecycle checks. Deferred under AGENTS.md Review Findings, with current polling/error behavior and every existing assertion preserved. The journey-documentation and stop-test findings are answered in their original inline threads and tracked in #1809 and #1810. |
Ready-for-review CI follow-upFull local E2E run 42580213958, workflow attempt 1, finished failed at app
Fresh build/unit/Compose UI CI passes, and lint remains green at this head. Human approval is present, but required |
|
Triage: in 3.2.0, a non-blocking bug found after the 3.1 cut. This PR closed no issue, so I filed #1812 to carry the milestone and added |
|
The shared Home/USDT-intro fixture correction is now in standalone synonymdev/bitkit-e2e-tests#300, based on E2E Workflow 42580213958 is running as attempt 2, with app head still |
jvsena42
left a comment
There was a problem hiding this comment.
Code scan at b5ff8114b: no findings.
Checked and clean:
- Root cause matches:
DeriveBalanceStateUseCasesets the cap from live channels, thenwaitForUsableChannels→refreshMaxSendLightningre-derived it from the cached_lightningState.channels, so a stalenextOutboundHtlcLimitMsat = 0snapshot zeroed it andsetMaxAmount(0)fell back toMAX_AMOUNT. TherefreshChannelsAndPeers()call atLightningRepo.kt:1441, before the usable check, closes that window. v3.1.0 still has the early return, so the fix is user-facing from 3.2.0. - Overspend:
canSendandrefreshMaxSendLightningboth sumtotalNextOutboundHtlcLimitSats()over usable channels read live from the node, so the cap can only get closer to LDK's limit. - Valid sends: cached-usable → live-usable returns immediately; cached-usable → live-unusable now waits up to 15 s behind the sync overlay instead of failing at
send; a removed channel drops to the insufficient-balance path. Back during the wait still aborts throughrequireCurrentPaymentPreparation. - Concurrency:
refreshChannelsAndPeersandsyncStateboth publish withupdateAndGetfrom live reads; a concurrentstop()resolves to empty channels. No persisted state is touched. - Dropping
syncState()on the empty-channel branch: callers only readchannels, andpayInvoicestill syncs aftersend. - Journey
send-spending-after-receive.xml: every tag it names exists at head, and the 10 001 sats keypress exceeds the cap only when the fix is present. - iOS: no twin needed.
BalanceManagerderives max send straight from node channels, with no cached snapshot to overwrite it.
perf: relevant (adds list_peers + list_channels before every prepared Lightning send). To measure on device: AddressContinue tap → SendNumberField visible, PR vs master.
Device gate (partial): b5ff8114b — not run yet; amount-limits/send-spending-after-receive.xml and the send-prep timing are pending. Device result follows in a second review.
Shared fixture correction validatedStandalone synonymdev/bitkit-e2e-tests#300 is ready for review. Workflow 42580213958, workflow attempt 2, passed 39/39 cases on their first case attempt across all seven shards; all 14 retry steps were skipped. Every shard checked out E2E App PR head remains The failed attempt 1 (E2E |
pwltr
left a comment
There was a problem hiding this comment.
Reviewed commit b5ff811. No findings.
Before a send, the usable-channel wait now re-reads channels and peers even when the cached channels are already usable, so the Lightning spending limit comes from the current outbound limits rather than a snapshot from before a receive. The re-read is a no-op while the node is not running and reads no balances.
Review details
General review
Model: Claude Opus 5.5
Reasoning effort: high
jvsena42
left a comment
There was a problem hiding this comment.
Device gate passed and the code scan was clean (see my earlier review). Approving.
Device gate: b5ff8114b — journeys: 1 passed (amount-limits/send-spending-after-receive.xml, 11/11 actions); perf: AddressContinue → SendNumberField median 0.70 s (master 74c8b2cdf 0.60 s, 5 runs each; the ranges overlap, 0.58–0.95 s); Figma: n/a; l10n: no UI text changed; iOS: no twin needed.
Setup: Gate_Throwaway emulator on local docker regtest (Electrum 10.0.2.2:60001). Fresh wallet with Savings 100 000, a private 100k channel from LND, and 10 000 received over it with no restart. On the amount screen Spending showed 8 998 available. The single N1 after 1 000 raised the exceeded toast, and 2 000 reached review. The crash buffer stayed empty.
The extra ~0.1 s is the list_peers + list_channels re-read before the amount screen, and it sits within run-to-run spread.
CI: build-local is still pending at b5ff8114b; everything else is green.
|
The companion fixture conflict is resolved in synonymdev/bitkit-e2e-tests#300 at signed/Verified Full workflow attempt 3 finished red in all seven shards. All checkout SHAs are confirmed. Eleven Settings cases passed; setup-hook failures prevented a complete cohort execution. The artifact-backed investigation records probable external VSS dependency trouble with explicit limits on the initial receipt/Boost failures and an isolated onboarding-slide timeout. Merging remains blocked; the old |
Closes #1812
Companion: synonymdev/bitkit-e2e-tests#298, synonymdev/bitkit-e2e-tests#300
This PR refreshes Lightning channel capacity before preparing a send, so a newly received payment immediately has the correct spending limit.
Description
Out of Scope
Design
N/A — no UI changes.
Preview
Existing Send amount UI; the over-limit digit now consistently triggers the existing warning and leaves the accepted prefix unchanged. Local Send2 observed
1000retained when entering10001against a 10,000-sat Spending balance.QA Notes
Setup
Use the local-regtest Send2 fixture from
synonymdev/bitkit-e2e-testsate560ece2b7cc1fb5bbc1b72e8205abea544e7ae3. It funds Savings, opens a private channel from local LND to Bitkit, then pays Bitkit 10,000 sats. This does not purchase a Blocktank connection.Build this app with
E2E=true E2E_BACKEND=local ./gradlew assembleDevDebug; place its universal debug APK at the E2E checkout'saut/bitkit_e2e.apk. With Node 22, dependencies installed, a Pixel 6/API33 emulator booted, the checkout's Docker stack running and LND credentials ready, runBACKEND=local ./ci_run_android.sh --mochaOpts.grep '@send_2 '. The checked-in fixture pins Bitcoin26.0, electrs0.10.2 and LND0.18.0-beta. UI waits remain 30s. The targeted CI run builds appb5ff8114band checks out that exact E2E commit.For J1, retain the running app and channel after receipt: do not restart or manually refresh before opening the amountless invoice.
Journeys
send-spending-after-receive.xml— a fresh Lightning receipt keeps its correct Send cap; the first over-limit key is rejected with a warning and normal amount entry/reentry still works.Manual Tests
N/A
Automated Checks
LightningRepoTest.kt— six regression cases cover fresh capacity, usability, removal, lifecycle changes and failed-refresh fallback; all 11 focused wait-path cases pass.send.e2e.ts— unchanged rewritten Send2 passes against the fixed app with every financial, fee, invoice, history and negative-validation assertion retained.Evidence
Original APK at app
ebe9bc09850704d1e96a2bb1697fd5d5a664fd1a: old Send2 (849806e) accepted 10,001 with 9,000 available, then passed after its internal extra-digit fallback. Rewritten Send2 (e560ece) exposed the same failure. Both used the identical APK/device/runtime/backend configuration.A logging-only local diagnostic build confirmed fresh cap 9,000 → cached usable-channel cap 0 → unrestricted amount input until periodic correction. Receive and Send used different amount-input VMs. Diagnostic logging is absent from this PR.
At app
8d0fcabfe8bcc5c1b7e3ddbe39506f11b8940401and E2Ee560ece, full local Send2 passed on its first case attempt: 90/90 recorded outcomes, retained 1,000 and observed the warning in 820ms, no case retry. Pixel 6/API33 ARM64, Node 22.23.4,30s UI waits, fresh isolated backend. The local build additionally enabledTREZOR_BRIDGE=true; Send2 uses no hardware-wallet flow. The only later source delta is the changelog fragment.just compile,just test(4,944 tests; zero failures/errors/skips) andjust lint(detekt + Android lint) passed at 8d0fcab. Independent implementation review is complete with no remaining findings; signed commits verified locally. The changelog-only publication delta is checked separately.Android CI Send2 passed on its first attempt: 1/1 cases and 90/90 recorded outcomes, with attempts 2/3 skipped. App
b5ff8114be9a1392adc2caef371d5b367868c7dd, E2Ee560ece2b7cc1fb5bbc1b72e8205abea544e7ae3, workflowdbd0ae8be421b7be3f68451c77af1b83a7eb5632; Node 22.23.4, Pixel 6/API33 x86_64, 30s UI waits, standard local E2E build flags. The single over-limit digit retained1 000and observedSendAmountExceededToastin 717ms. Reported suite time 8m 25.6s; worker 8m 44s. All three PR commits are GitHub Verified. The temporary workflow only pins both sources and selects Send2; it is not part of this PR.Ready-for-review CI run 42580213587 passes build, unit and Compose UI tests at the same
b5ff8114bhead. Default full local E2E run 42580213958, attempt 1, using E2Emainat849806e70784da0960f44da6206e2346b1e0ba3e, failed: 27/39 first-attempt and 35/39 eventual passes. All 22 failure screenshots and sampled frames from all 22 recordings show an undismissed USDT introduction covering Home; device logs and missing balance/menu/backup selectors agree. See the artifact-backed diagnosis. Default Send2 uses its historical extra-key fallback, so its pass does not replace the separate clean pinned Send2 evidence.Full local Android workflow 42580213958, attempt 2 passed with E2E
407d4a05fdcf6796960df0afbda552f575c93e97: 39/39 first-attempt cases, all seven shards, no case retries and no legacy toast fallback invocation. App merge checkout2353ee4c30a71c7c09c709cc3403f3231080f94band app headb5ff8114bshare tree813c7ff6e9aac702bd0be30422812c0dfb528936. The shared fixture was independently reviewed and passes 70 repository tests plus 4 boundary probes on Node22.23.4, TypeScript and scoped lint. See the validated correction.Independent device review at
b5ff8114b: J1amount-limits/send-spending-after-receive.xmlpassed 11/11 actions on Gate_Throwaway with local Docker regtest and a private LND channel. After the 10,000-sat receipt without a restart, Spending showed 8,998; the single over-limit digit after 1,000 raised the warning, and 2,000 reached review. The reviewer reports an empty crash buffer and send-preparation medians of 0.70s for this PR versus 0.60s on master74c8b2cdf, five runs each with overlapping 0.58–0.95s ranges. These are reviewer-published results, separate from this chat's pinned Send2 execution.Historical
fba9a6epaired Android CI pass: workflow 42580213958, attempt 5 completed 39/39 cases on their first attempt at exact E2Efba9a6efde8fdd9066a0408e3f5f1fe1b5ec23ac; every shard checkout is confirmed, all 14 retry steps skipped, and no legacy toast fallback ran. It reused the successful app build of merge2353ee4c30a71c7c09c709cc3403f3231080f94b(same source tree as app headb5ff8114b).Current paired Android CI passes: workflow 42580213958, attempt 6, exact E2E
7f61681bb8d4b0d31dc836a1c03a8488170baf66paired with unchanged appb5ff8114be9a1392adc2caef371d5b367868c7dd. All seven checkouts are verified: 38/39 first-attempt cases, 39/39 eventual passes, one built-in retry and 13 retry steps skipped; no legacy toast fallback ran. The fresh app build checked out merge2353ee4c30a71c7c09c709cc3403f3231080f94b, whose tree matchesb5ff8114b.The retried
multi_address_3case timed out waiting forReceivedTransaction. Its failure screenshot and sampled recording show uncovered Home, and the device log records an incremental Electrum on-chain sync timeout during that wait. The same case passed on its built-in retry with unchanged sources. The artifact-backed disposition is flake, with the underlying sync-timeout cause unisolated; this is not proof of staging VSS causality. The exported app log starts after the next reinstall. Three recording samples and the failure screenshot were inspected; a fourth requested frame did not decode.Gaps
Android master has additionally advanced to
36a2c4d, adding USDT first-sync receipt celebration/restore-window handling and loaded-account on-chain address search. These do not alter this PR's Lightning capacity refresh or Progress cards #300's Home-introduction controls. Existing CI remains pinned to the actual app merge2353ee4c/ headb5ff8114b; the combination with the newer master changes has not been rerun.Companion fix: handle late usdt introductions during home checks bitkit-e2e-tests#300 is conflict-free at signed/Verified
7f61681bb8d4b0d31dc836a1c03a8488170baf66, with current main0e83ea6integrated. Its 82 local checks, static validation, four independent implementation reviews, current-head Talos review and full paired Android attempt 6 pass. Fresh current-head approvals on Progress cards #300 are now present from ben-kaufman and pwltr; GitHub reports APPROVED/CLEAN. Android fix: refresh lightning capacity before send #1808 already has current-head approvals. The historical attempt-5 result remains pinned tofba9a6e.Companion fix: handle late usdt introductions during home checks bitkit-e2e-tests#300 now incorporates main's Move-to-Spark recovery in signed conflict-resolution head
fba9a6efde8fdd9066a0408e3f5f1fe1b5ec23ac. Independent full reassessment and 78 repository tests plus 4 boundary probes pass. Full workflow attempt 3 previously finished failure in all seven shards against the combined helper; it remains historical alongside the later 39/39 attempt-5 pass at the same E2E head. All seven shard logs confirm E2Efba9a6e. Eleven Settings cases passed; setup-hook failures prevented a complete 39-case execution. All seven failed-shard artifacts, 13 screenshots and sampled frames from all 13 recordings were inspected. Later reinstalled app processes stall in VSS-backed node construction; saved read-only staging VSS GET probes also time out. External dependency trouble is likely, but the first receipt failures are not fully isolated. The older407d4a0runtime result below remains historical. Previously assessed Android master3f2009e: its delta from the originally tested base consists of release 3.1.0 version/changelog bookkeeping and test: drop blocktank-dependent tests #1817 instrumentation-test/local-regtest workflow changes. No production app source, dependencies or standard local E2E workflow changed. Those new instrumentation fixtures are outside this Appium cohort. The rerun does not execute current-master version metadata, migration behavior or those new instrumentation fixtures.Historical shared-fixture correction fix: handle late usdt introductions during home checks bitkit-e2e-tests#300 was validated at
407d4a05fdcf6796960df0afbda552f575c93e97. Workflow42580213958attempt 2 passes 39/39 cases on their first case attempt, all seven shards, with all 14 retry steps skipped. No legacy toast fallback message occurred. Every shard's checkout SHA is confirmed. The normal PR merge checkout2353ee4chas the exact same source tree as app headb5ff8114b.Historical workflow attempt 1 remains a failed result at E2E
main/849806e:lnurl_1,security_1,send_3andonchain_2were still failed with the USDT intro covering Home. Its 27/39 first-attempt and 35/39 eventual outcomes and all inspected failure evidence remain attributed to that test revision; the corrected attempt 2 is recorded separately.The separate successful pinned Send2 run covers only that case at app
b5ff8114b/ E2Ee560ece. The later full-suite success uses E2E407d4a0; neither one first-attempt pass nor one full cohort establishes a durable reliability rate. Experimental E2E adoption gaps remain separate.This chat's earlier J1 coverage came through Send2 and XML parsing; the later independent device review above closes the separate manual J1 execution gap at
b5ff8114b. No new iOS run is claimed.Models used
gpt-6.1-sol(reasoning:high)gpt-6.1-sol(reasoning:high)gpt-6.1-sol(reasoning:high)