Skip to content

fix: use saved fees after gateway errors - #1701

Merged
ovitrif merged 10 commits into
masterfrom
fix/blocktank-fee-cache
Oct 28, 2026
Merged

ovitrif merged 10 commits into
masterfrom
fix/blocktank-fee-cache

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Parent #1685 merged; this PR now targets master.

Description

Ordinary Bitcoin sending could fail when Blocktank info returned HTTP 502 even though a fee schedule was already saved. Use that existing schedule for the two recognized native HTML/JSON info-502 responses, preserving the selected speed. Missing cache, cancellation, unauthorized responses, malformed data and unrelated errors still fail.

Payment links and scan/request preparation retain their original wallet generation across suspension and UI dispatch. Required or unreadable WALLET/USDT recovery blocks preparation before its payment-specific fee lookup. Late work cannot present a payment, report stale success or continue under a replacement wallet. A refused or superseded send event now leaves the collector active for a later fresh action; cancellation of the owning scope still stops it.

No default fee, cache-lifetime change, general HTTP retry, payment replay, backup-format change or contact CAS is introduced. iOS already attempts its existing saved schedule after fresh lookup failure.

Out of Scope

  • Send-entry restore-marker storage reads: move blocking reads off the main thread while preserving fail-closed recovery, wallet ownership and cancellation checks. Tracked in #1719, milestone 3.1.0, from piotr's review and talos's review. This PR does not establish a measured UI stall.

QA Notes

Final head: cce1c8d3d9148de9703ca3c708b104279de67456, on live master 2622c040cc80b234443e668435991267947aaf10. Parent #1685 merged externally; the signed additive master merge resolves the actual request-confirmation conflict while retaining the original wallet generation and upstream fixed-amount routing. Combined tree: d5b6f5d3e8af8e0e400e074eca95ae9e623a968a. All ten current-base commits verify locally and on GitHub (verified=true, reason=valid); no history rewrite.

Final combined validation — October 28

Required just format, just compile, just test and just lint passed: 4,663 tests / 256 suites, zero failures/errors/skips, zero format/Detekt/Android Lint findings. Complete logs, XML/SARIF/HTML and embedded Gradle diagnostics were inspected once. All 1,757 frozen entries match. The 562 send-flow cases include a new subscription confirmation case proving a replacement wallet cannot receive a delayed old confirmation. Independent affected source review is clean; unchanged ownership/fee coverage is reused by verified identity.

The final arm64 APK 63b3a093545914358d93be12284136f7403cc3dcaefe8e47ba51dd2808497c76 was installed over the owned disposable regtest wallet without clearing data, and its installed bytes match. The changed ordinary confirmation UI was exercised: a 10,000-sat payment reached Normal / 562 sats under actual HTML502, then Back cancelled to Home with the unchanged 936,628-sat balance and zero observed broadcasts. An independent evidence inspection verified the pictures, native public log evidence, all 2,357 APK member CRCs/hashes and the exact fee-control events. The new lane was returned healthy/accept and its emulator shut down with data retained.

Initial setup failures are retained: the restarted emulator lacked its prior Electrum port mapping and refused node startup until that exact mapping was restored; Argent's isolated-device tap service required the documented adb fallback; a fresh XML dump exited 137, so no XML is credited; a control-state read used the wrong header once and was corrected read-only. These are not product failures or hidden passes.

Manual 3's negative restore phases remain at prior combined APK 379014fb… / tree fbf7c62c…: real native authentication and owned VSS/PostgreSQL rejected the exact USDT listing, retaining both required markers and blocking a Bitcoin link. Retry plus a held WALLET read exposed genuine WALLET-only pending after the empty-USDT listing was acknowledged; a second link stayed blocked. Releasing reads acknowledged WALLET and reached Home without payment replay. The guard, restore, scan and fee paths relevant to these phases are byte-identical; only the shared ordinary confirmation positive control was repeated on the new binary. The earlier audit inspected all 801 VSS / 113 fee events, retained ten intentional rejections, six held reads and two post-upstream-200 BrokenPipe delivery errors. No faulted mutation was replayed.

This is an affirmative empty-USDT-listing restore, not a nonempty import or complete-WALLET redesign test. Manual 1's six settled sends and Manual 2's cold-cache/error controls remain scoped to their recorded binaries and unchanged fee/native implementation; no new six-send campaign or signing/broadcast claim is made. Three JVM and four actual-base-identical provider cursor notices remain documented.

Ready status remains verified (draft=false). Current-head CI 40755558405 passed 4,663 tests / 256 suites, zero failures/errors/skips; lint 40755558440 has zero findings. E2E 40755559443 completed with 39 eventual passes and one explicit settings skip. Two initial failures remain recorded: backup_1 could not find PriceWidget before the backup/restore portion, and multi_address_3 could not find Sent after SendSuccess/Close but before transaction details/RBF. Both passed the workflow's automatic second attempt; eight already-passed cases were marker-skipped. No workflow rerun was requested and no cause or first-attempt-clean claim is made. All complete logs, attempts, annotations and digest/CRC-verified artifacts were inspected. Synthetic merge c3d93e3b has the exact final head tree against master 2622c040. Failure-only videos/app logs were not retained after eventual success. Migration remained skipped. Prior de28c103 CI 40745891791 passed 4,661 tests, lint 40745891804 had zero findings, and E2E 40745892403 passed 39 first-attempt cases with one disabled settings skip, no failed executions. All completed logs, annotations and verified artifacts were inspected. That executed merge was based on old parent c747cc0b; it is not current-master validation. Older failed runs below remain preserved.

The separate #1719 issue remains deferred. External review 5686754597 reports additional old-head cases; its raw media/transaction artifacts were not independently inspected here and add no author acceptance credit.

Setup

The dev/regtest app uses Core 0.8.0-rc11, Paykit 0.1.1-rc2 and VSS 0.5.26-rc1. Acceptance used disposable owned wallets and synthetic Bitcoin regtest funds, a real native node, owned recipient, authenticated native VSS/RS256/PostgreSQL and narrowly selected HTTP faults. The committed debug-only Blocktank endpoint seam permits the explicit loopback fixture; no TLS verification was bypassed and these cases do not claim TLS coverage. Existing wallets and signed operations were preserved.

Earlier completed acceptance

  • Manual 1: prime a real fee cache, then return HTML and JSON info-502 responses. Six actual 10,000-sat sends cover Fast/Normal/Slow for both response forms, with 843/562/281-sat fees. Raw signed transactions, owned outputs and exactly one broadcast acknowledgement per operation were inspected; all six settled.
  • Manual 2: the fresh wallet before its first successful cache prime failed on info-502 without broadcast. Unauthorized, malformed-data and unrelated HTTP 500 responses also failed. That wallet was then primed for Manual 1; no reset or manufactured cache was used.
  • Manual 3: a separate fresh target restored through the actual app and authenticated native backups. Rejected USDT-record listing kept both required markers pending and blocked a Bitcoin link. A held subsequent read then exposed genuine WALLET-only pending state after USDT acknowledgement; the same link stayed blocked. Releasing the read completed required restoration. A fresh link then reached real 10,000-sat payment review at Normal/562 sats during HTML502, and review was cancelled. No seventh broadcast occurred.

Manual 1/2 used APK d201a18b…; Manual 3 and its positive control used then-current APK SHA-256 40b5484fffc7a4de41da9d46dae95884648c45d12c83a2527b6fd89b74272cc0, independently matched to installed bytes. The fee service, native Core and send implementation supporting reuse of the earlier six transfers are unchanged. That APK also exercised the exact parent foreground repair: two native startup failures retained ordinary wallet/menu/Settings; restoring the endpoint started the node. A visual toast count was not captured.

The ordinary source genuinely had no USDT wallet or Pubky identity. Its successful authenticated zero-record USDT listing is an affirmative native result, not a fallback for failed reads. No nonempty USDT import or full-app USDT-only intermediate phase is claimed. Generic background fee-refresh traffic was distinguished from the blocked payment-specific path.

Prior validated head 258f8a9

Required compile, full applicable tests, formatting, Detekt and Android Lint passed: 4,753 tests / 259 suites, zero failures/errors/skips; zero lint/format findings. All 1,777 frozen inputs match the final tree and actual APK build. Complete logs, JUnit, SARIF/XML and Gradle diagnostics were inspected. Existing JVM/Gradle and provider cursor notices remain documented against actual-base evidence.

Two consecutive independent final source reviews (9/10) are clean. A separate independent acceptance review verified the raw transaction/acknowledgement records, restoration phases, installed bytes and reuse boundaries. Every commit in the complete four-commit live-base PR range verifies locally and on GitHub (verified=true, reason=valid).

Evidence retained in work/validation/android1701-app-acceptance/, android1701-restore-final-h/, android1701-payment-entry-final-h/, android1701-ready-publication/ and work/android1701-readiness-evidence-review-20261027.md. Controlled earlier failures, intermediate fixture/compiler/formatter failures and the parent's historical nine 502 E2E failures remain recorded. This repair does not prove all historical failures had cached rates.

Hosted CI

Ready-for-review is verified (draft=false). Prior-head 258f8a9 CI 40520195678 passed 4,753 tests / 259 classes with zero failures/skips; lint 40519956652 reports zero findings. The synthetic merge tree exactly matches the validated head.

E2E 40520196618 failed: five shards passed and two exhausted three automatic in-job attempts. Four distinct widget-deletion setup cases account for all twelve failed executions (Yes, Delete did not appear); 35 executions passed and 25 skip records include 24 retry skips of already-passed cases plus settings_08. Logs and videos show the Suggestions delete target followed by Profile navigation. An owned-device control reproduced the Suggestions target under the fixed Profile header, then showed the actual delete dialog after scrolling; it was cancelled without deletion. The shared harness repair belongs to the separately authorized E2E #287 writer. Its publication and new hosted evidence remain outstanding; this failure is not waived. No workflow rerun was requested. All fourteen completed job logs, six checksum/CRC-verified artifacts and 23 annotations were inspected. Earlier draft skips remain skips.

Prior combined-tree validation — head 6bc0821e

Additive merge 6bc0821e incorporates its then-current #1685 parent and its live-master composition. All 13 feature files remain byte-identical to the previously validated feature patch; incoming parent paths match the parent. Full required compile/tests/format/Detekt/Android Lint passed: 4,760 tests / 259 suites, zero failures/errors/skips, zero lint/format findings, all 1,781 frozen inputs unchanged. Complete logs, JUnit, SARIF/XML and Gradle diagnostics were inspected; the three JVM notices, four provider cursor stacks and Gradle deprecation retain their actual-parent comparisons. Two consecutive independent final composition reviews are clean. All five live-base commits are cryptographically valid locally and on GitHub.

That combined APK e0d436a0c317973c7051ded4ce56c1764cafe6432ef7b5d1e8df1eea581ce462 was installed over the owned disposable restore target without clearing data and matched installed bytes. Under actual HTML502, a new 10,000-sat link reached the correct recipient and Normal/562-sat review; cancelling returned Home with the same 936,628-sat balance and six total broadcasts. The controller was restored healthy. This is an additional final-composition check, not a claim that all six earlier sends were repeated. Initial post-install package lookup and launcher-tool failures were retained; resolving the installed MainActivity launched successfully.

Ready status remains verified (draft=false). Hosted CI for that revision was newly triggered and pending complete inspection; earlier hosted results above remain revision-scoped. The shared widget-harness failure must still be closed with the separately owned repair and appropriate new CI evidence.

Prior collector and parent integration validation — head bfa61d3

Full required just format, just compile, just test and just lint passed on the final integrated source: 4,780 tests / 259 suites, zero failures/errors/skips; zero format, Detekt or Android Lint findings. All 1,781 frozen inputs match the final source and APK build. Complete logs, JUnit, SARIF/XML and Gradle diagnostics were inspected. Existing JVM/provider cursor notices and the Detekt Gradle deprecation were checked against retained baseline/live-parent evidence.

Twelve new tests exercise the public send events on one ViewModel: required and unreadable WALLET/USDT recovery for Amount Continue and Coin Selection, fresh continuation after clearing recovery without replay, wallet replacement while fees suspend, and actual owning-scope cancellation. Before the fix, ten regression cases failed while the two scope-cancellation controls passed; all twelve and the focused 546-test set pass afterward. Two consecutive independent final reviews are clean. The five incoming parent source/test files match the parent exactly. All seven commits in the complete live-base PR range verify cryptographically locally and on GitHub (verified=true, reason=valid).

The final APK SHA-256 f5254a044d874152c97c68b50c74bd4ca303df1014810e8be50972bac2f3f337 matched installed bytes on a newly created disposable restore target. Manual 3 was repeated on this final binary: real authenticated backup listing rejection retained both required markers; after retry, USDT acknowledgement and a held metadata read exposed WALLET-only pending. Payment links stayed out of payment review in both phases. Releasing reads produced both marker-deletion acknowledgements and full restore success. Clearing recovery alone did not replay a payment. A fresh 10,000-sat link and Continue then reached the correct recipient and Normal/562-sat review under HTML502, in the same PID and Activity throughout; the review was cancelled with no seventh broadcast. The restored balance remained 936,628 sats, and both fixture controls were restored healthy/accept. No existing wallet was reset, no marker was manufactured and no app hook was added.

An initial fresh-install unresponsive-app dialog recovered after Wait before phrase entry; the same PID subsequently completed the full check. Its logs and initial tool/input failures are retained without a root-cause claim. The earlier final-build warm/cold HTTP500 attempts on the already-used wallet reached review and are inconclusive negative controls, not substitutes for Manual 2. Earlier six-send acceptance remains scoped to its recorded binaries; it was not repeated.

Evidence is retained in the worker's work/child-final-a/, work/child-review/, work/restore-final/, work/device/ and work/publication/. New-head hosted runs are CI 40562290268, Lint 40562290599 and E2E 40562290547, have now completed successfully and been fully inspected. CI passed 4,780 tests, zero failures/skips; lint reports contain zero findings. E2E executed harness 4c0cbef0 and passed 39 cases on their first attempt, zero failed executions or retries, with one explicit settings_08 skip. All 11 E2E logs, annotations, exact app/harness revisions and published artifacts were inspected; unit-report counters and every artifact CRC/digest were verified. The current synthetic merge has the exact final head tree. Migration 40562290865 remains skipped, not passed. The parent #1685's separate WidgetSave failures remain documented and are not diagnosed or waived by this run. Ready status remains verified draft=false.

Design

N/A — no UI changes.

Models used

  • Planning/scoping: gpt-6-astra (reasoning: xhigh).
  • Implementation: gpt-6-astra (reasoning: xhigh).
  • Review: gpt-6-astra (reasoning: xhigh), gpt-6.1-sol (reasoning: high).

Earlier parent integration and request ownership — head 0b963ef9

At 0b963ef9, finalization and both internal subscription handoffs carry their originally captured wallet generation into scan admission. Previously, replacement during finalization could cause the helper's default argument to capture the replacement generation and admit old request details. The normal-source before-fix test reproduced the unwanted address decode with the existing post-proof guard still present. After the fix, both isolated proof-sync/finalization regressions and all 551 send-flow tests pass. A separate earlier reusable-mock failure did not isolate this defect and is retained as fixture evidence, not product causation.

Required final just format, just compile, just test and just lint pass: 4,625 cases / 255 suites, zero failures/errors/skips/retries; zero format, Detekt or Android Lint findings. All 1,751 frozen inputs match. The complete 9,514-line test log, 513 XML snapshots, five SARIF reports and 31 embedded Gradle reports were inspected; duplicated report snapshots are not extra test executions. Compile legitimately reused Gradle UP-TO-DATE outputs. Three JVM notices, the pinned Detekt plugin deprecation and four cursor notices from actual-base-identical test/provider code remain disclosed; no warning-free runtime claim.

The combined-source review plus its affected recheck and a second independent ownership review are clean. All eight commits in the complete live-parent range verify locally and on GitHub. Ready status remains draft=false. Current-head CI 40721227903 passed all 4,625 tests with zero failures/skips, and Lint 40721227979 reports zero findings. E2E 40721228502 passed 39 cases on the first attempt, with one explicit settings_08 skip and no failed test attempts. All complete logs, reports, annotations and digest/CRC-verified artifacts were inspected. The executed synthetic merge 238cef14, against parent 5b4674c3, has the exact head tree. Seven shards used harness 4c0cbef0; recovered toast/permission-helper behavior and infrastructure/npm notices remain disclosed. The E2E APK is distinct from the ordinary CI APK and was verified separately. These results do not waive older failures or prove skipped cases.

A newly verified #1685 recovery-presentation finding is being fixed in the parent: required download could lose the restricted screen if a brief node-initialization state was not observed. Its payment-admission guards remained active. That repair must be incorporated and validated here before requesting the next affected human review; the results above cover only 0b963ef9. Parent integration retains the typed fee fallback and required restore gates; it removes the retired createCjit path without adding a new send or fee mechanism. Earlier full-app Manual 1–3 results retain their recorded binary/revision limits; the new owner regressions use controlled unit resources, not a new authenticated app journey. No existing failed CI attempt is waived by these local results.

@ben-kaufman
ben-kaufman marked this pull request as ready for review October 27, 2026 14:20
@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no actionable issues were found.

Summary

This PR uses saved Bitcoin fees after two recognized Blocktank info-502 responses. It also keeps delayed payment preparation tied to the original wallet and blocks preparation while required recovery remains pending.

  • Bitcoin fee lookups reuse saved rates after recognized Blocktank info errors.
  • Payment requests stay with their starting wallet and wait for required recovery.
  • Debug E2E tests can send Blocktank info requests to a local fixture.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Payment link or scan] --> B{Original wallet current and recovery complete?}
    B -->|No| C[Stop preparation]
    B -->|Yes| D[Prepare payment]
    D --> E[Fetch fresh fee schedule]
    E -->|Success| F[Use fresh schedule]
    E -->|Recognized info 502| G[Read saved schedule]
    E -->|Other error| H[Fail]
    G -->|Missing or unreadable| H
    G -->|Available| I[Keep selected speed]
    F --> J{Original wallet still current?}
    I --> J
    J -->|Yes| K[Present payment]
    J -->|No| C
Loading

Reviews (1) · Last reviewed commit: "merge: adopt validated foreground recove..." · Reviewed by Greptile

@ovitrif ovitrif added this to the 3.1.0 milestone Oct 27, 2026
@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Triage: 3.1.0, with its dependency #1685. It keeps on-chain sends working when Blocktank's info endpoint returns 502, which matters more now that Blocktank is winding down, and 3.1.0 is cut on 28 Oct. It lands only if #1685 does.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from cce1c8d (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@talosmachina talosmachina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 finding (1 P2). Falls back to the saved Blocktank fee schedule when the info call fails with one of two recognised 502 Bad Gateway forms, and binds payment links, scans and Paykit request preparation to the wallet generation that started them, so they are dropped when required WALLET/USDT recovery is pending or the wallet was replaced. Reviewed 258f8a9, full tier, static only: per this repo's policy I do not build Android PR branches here, so nothing was compiled or run on my side.

What I checked, and 5 candidates I ruled out

Read in full (diff): CoreService.kt (BlocktankService.getFees, isInfoBadGateway), WalletConnectionRecovery.kt, Env.kt, BlocktankTestEndpoint.kt, SendSheet.kt, AppViewModel.kt (all 399 added lines), BlocktankFeeFallbackTest.kt, test names in AppViewModelSendFlowTest.kt
Call sites traced: getFees (LightningRepo.getFeeRates, getFeeRateForSpeed, used by transfer, boost, wallet and send flows), resetSendState (its only caller outside the view model is SendSheet), Keychain.hasPendingWalletRestore -> Keychain.exists, WalletConnectionRecovery.beginWipe (the only place the generation moves)
CI: build, lint and build-local still pending at review time; Greptile passed

Ruled out

  • Fallback widens error handling: any non-502 failure is rethrown exactly as before (the old code let the fees(refresh = true) exception propagate too), and a null fresh result still reads the cache as before.
  • 502 match misses the wrapped error: ServiceQueue.background wraps in AppError(it), and isInfoBadGateway walks AppError.cause up to 8 levels, which the HTML and JSON 502 tests exercise.
  • Stale isLoading after an early return in refreshOnchainSendIfNeeded: the early return only fires when sendStateGeneration moved, and every place that moves it either rebuilds SendUiState (fresh OnchainFeeUi) or leaves on-chain.
  • Debug Blocktank endpoint reaching release: E2E_BLOCKTANK_API_URL is empty for non-debug variants in build.gradle.kts, and Env also requires isDebug && isE2eTest; the URL must be localhost with an explicit port.
  • Deferred scan replayed for a replaced wallet: DeferredScan and ScheduledScan now carry walletGeneration, duplicate merging compares it, and launchScan re-checks it before and inside the scan mutex.

Merge confidence: 4/5, no blocking findings but the hosted build and lint had not finished and I reviewed without building.

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ⛔️ Request changes — one reproduced send-flow regression.

Findings:

  • MEDIUM — Recovery rejection permanently stops send-event handling (app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:6962). requireCurrentPaymentPreparation throws CancellationException directly inside the sole observeSendEvents collector. This check also runs for ordinary on-chain AmountContinue, before the Lightning-only early return. If required recovery becomes pending or unreadable, rejecting that one action ends the collector. Clearing recovery and reopening send do not restart it: later amount/address events and Continue are ignored until the activity's view model is recreated. The post-fee check has the same effect when the wallet changes during Lightning fee estimation. Keep the recovery gate, but stop only the individual stale action.

The separate blocking keychain-read finding is already reported and is not duplicated here.

QA for 258f8a9:

Built from 258f8a9. Android 16 / API 36, disposable regtest wallet with real test funds. The device APK adds a debug-only hook to set/clear the actual required-recovery marker and read send state; the production send handler is unchanged. No payment was broadcast.

Test 4: send after a recovery rejection ❌ Continue remains broken after recovery clears and the send sheet reopens.

I entered 1,000 sats, set the required WALLET marker through the debug hook, and tapped Continue. After clearing the marker, Continue still did nothing. Closing and reopening the same payment reset the amount, but entering a new amount no longer updated the view model. The hook confirmed its amount remained zero; the number field could still change locally.

test4-final-2x.2x.mp4

Recovery rejection, cleared marker, and a fresh send that cannot continue; recording at 2×.

Test 5: fresh-process control ✅ Restarting the same app and wallet restores payment preparation.

With the marker clear, I restarted the app, reopened the same owned recipient, entered 1,000 sats, and reached Confirm. Restart was checked by force-stopping and relaunching the app; the recording shows the resulting confirmation. This isolates the failure to the existing view model's event collector. I cancelled before payment.

test5-control-2x.2x.mp4

The same wallet reaches 1,000-sat payment review after restart; recording at 2×.

Check 1: head build and existing affected unit suites ✅ The unmodified head builds and its existing targeted suites pass.

Ran assembleDevDebug and targeted testDevDebugUnitTest with these suites:

  • BlocktankFeeFallbackTest
  • AppViewModelSendFlowTest
  • WalletConnectionRecoveryTest
  • LightningRepoTest
  • BlocktankTestEndpointTest
Check 2: collector-lifetime regressions ❌ Both added regression checks reproduce ignored later events.

One check replaces the wallet generation while Lightning fee estimation is suspended; the other makes the required-marker read temporarily fail during on-chain AmountContinue. After restoring valid state and calling resetSendState, each emits AddressReset. Both fail because addressInput stays unchanged: the collector has ended. These are temporary review checks, separate from the committed passing suites.

Manual 1–3 were not repeated: the author's acceptance report records the six cached-fee transfers, error controls, and authenticated required-restoration case. Those are author-reported results, not device passes from this review.

Hosted E2E is also red: onboarding and on-chain fail while waiting for “Yes, Delete”. I have not attributed those failures to this change.

Twin: no matching fee-fallback PR is linked; iOS already retries the saved schedule after fresh lookup fails. The fallback behavior matches; this review does not certify iOS recovery parity.

Coverage: full f9ff9d2..258f8a9, all 13 changed files and affected callers. Device work covers the send-event recovery regression and its control; fee-fallback broadcasts and complete authenticated restoration rely on the author's report above.

Reviewer model: gpt-6.1-sol
Reasoning effort: high


Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Full review of the PR diff against merge base f9ff9d27, at 258f8a9. No earlier assessment baseline was available.

2 actionable findings — resolve or provide an evidence-backed rebuttal.

Continuing an open send after required recovery becomes pending, or after the wallet generation changes, cancels the activity-scoped send-event collector. Amount Continue hits that guard before the Lightning-only return, and clearing recovery does not start the collector again.

BlocktankService.getFees uses the saved Fast, Normal, and Slow schedule for the two recognized info 502 responses and still fails other fresh-lookup errors. Payment-link, scan, and Paykit preparation keep the generation captured when they start and drop the presentation when that generation is no longer current. Generation still advances only in beginWipe on the parent at f9ff9d27 (#1685); that parent was comparison context, not a separate review.

iOS FeeEstimatesManager.getEstimates at 11c27421 returns nil on any fee fetch error and leaves the last published estimate unchanged. It does not special-case these two 502 strings, which is the narrower contract this PR states.

The new fee-fallback and send-preparation unit tests were read and not executed here. GitHub reported success for CI build 40520195678 and lint 40519956652 on this head. E2E 40520196618 failed; those logs were not re-diagnosed here.

Device testing: not performed in this review.

Suggested additional test cases

  • Android. Open an ordinary on-chain send on the amount screen. Set a required WALLET or USDT restore marker, tap Continue, then clear the marker. Tap Continue again and swipe to pay. The first Continue stays off confirmation. After the marker is cleared, Continue opens confirmation and swipe still starts the payment without force-stopping the app.
  • Android. Reach coin selection, set the marker, tap Continue, then clear the marker and continue that send. Coin selection and the later confirm swipe still work.
  • Android. Start a Lightning amount Continue so fee estimation is in progress, and replace the wallet before it finishes. Open a new send on the replacement wallet and tap Continue. That Continue still opens confirmation.

Findings

  • [HIGH] Recovery rejection stops all later send actions — inline at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:6962.
  • [LOW] Send entry reads restore markers on the main thread — inline at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:7103.

Reviewer model: grok-4.7
Reasoning effort: high

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code scan at 258f8a9: no findings.

Checked and clean:

  • The fallback doesn't widen beyond the two recognised 502 responses.
  • Cancellation doesn't leak.
  • The fee path is keyed by network and wallet.
  • The test endpoint can't reach a release build: it needs the debug build type, isDebug && isE2eTest, and a localhost URL.
  • Under a 502, Confirm and the broadcast both resolve to the saved schedule.

The saved schedule has no age limit. That isn't a finding here, because master already falls back to it with no age limit when the fresh read is null, and iOS does the same on any error.

Out of scope, both pre-existing:

  • AppViewModel.sendOnchain never passes feeRates, so the broadcast fetches rates again and can pay a different fee from the one Confirm showed if rates move in between.
  • The fallback covers 502 only, so DNS, connect, timeout, 503 and 504 errors during the Blocktank wind-down still fail an ordinary send even when a saved schedule exists.

The PR body has no #### Journeys section. The 502 cases need the local fixture, so list them under Manual Tests.

Device gate (partial): 258f8a9, not run yet. Pending: an on-chain send on a regtest build pointed at the 502 test endpoint (Confirm shows the saved fee, and the broadcast uses it), plus a send with the endpoint healthy.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Full reassessment of the entire PR diff against merge base 825bb3a9, at 6bc0821e. The merge base changed and no coverage baseline was supplied, so inherited coverage is empty.

No new actionable code findings.

An on-chain send reuses the saved Blocktank schedule only when fresh info fails with one of the two native HTTP 502 texts from bitkit-core 0.8.0-rc11 and rust-blocktank-client 0.0.16. The selected speed is taken from that schedule at send time. A missing cache, cancellation, an unauthorized or malformed response, and any other HTTP error still fail before the cache is read. Payment links, scans, and request preparation keep the wallet generation captured at entry, including across an unauthenticated deferral, and stop before the payment-specific fee lookup when a PAYKIT or USDT restore marker is set or unreadable.

iOS FeeEstimatesManager at 3d16b620 keeps its last in-memory schedule after any fetch error. This PR limits Android to the two 502 responses above. bitkit-android#1685 at 825bb3a9 was checked only for the wipe generation this PR observes.

Validation: Hosted CI, including testDevDebugUnitTest, and Lint succeeded on this revision. Detekt succeeded. E2E failed on this revision: the send shard passed; @backup_1 and @widgets_1 through @widgets_3 did not find "Yes, Delete". Those logs were inspected and not re-run. The new unit tests were read and not executed locally.

Device testing: not performed in this review.

Suggested additional test cases

  • Android dev/regtest. Prime a real fee cache, force Blocktank info to return HTTP 500, and repeat with HTTP 401. Open a 10,000 sat on-chain send and confirm. Info refresh can still show the saved fee, because it keeps the last cache for every error. Confirm must not broadcast and must not spend at that cached rate.

Earlier findings reconciled at this revision: 2 still present.

Findings

  • [HIGH] Recovery rejection stops all later send actions — now at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:6962. Still present from the earlier review.
  • [LOW] Send entry reads restore markers on the main thread — now at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:7103. Still present from the earlier review.

Reviewer model: grok-4.7
Reasoning effort: high

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve

Findings: No new findings. The blocking marker reads remain deferred to synonymdev/bitkit-android#1719.

QA for bfa61d3:

Android 16, disposable regtest wallets and owned recipient. The test build adds debug controls for the real recovery marker; the wallet-replacement case also holds the Lightning fee result in a local test hook. The send-event handler is unchanged. A local endpoint returned the selected HTTP faults after a successful upstream fee refresh.

Test 1: cached fees after gateway errors ✅ Six 10,000-sat transfers reached the recipient under HTML and JSON 502 responses at all three selected speeds.

For each response form, Fast paid 422 sats and Normal and Slow paid 141 sats. The saved schedule was 3, 1 and 1 sat/vB, respectively. Confirm showed those fees; the regtest mempool entries confirmed them and the recipient wallet confirmed each amount. Recordings are 2×.

HTML: Fast, Normal, Slow:

test1a-redo-2x.2x.mp4

test1b-2x.2x.mp4

test1c-final-2x.2x.mp4

JSON: Fast, Normal, Slow:

test1e-redo-2x.2x.mp4

test1d-2x.2x.mp4

test1f-2x.2x.mp4

Test 2: amount Continue after recovery refusal ✅ Clearing the required marker allowed Continue and a successful payment without restarting.

The debug control set and cleared the actual WALLET marker while the amount screen was open. The first Continue stayed on that screen; the fresh Continue reached Confirm and swipe sent 1,000 sats.

test4-pay-2x.2x.mp4

Test 3: coin selection after recovery refusal ✅ Clearing the required marker allowed coin selection to continue and the payment to be sent.

The debug control set and cleared the actual WALLET marker while coin selection was open. The first Continue stayed on selection; the fresh Continue reached Confirm and swipe sent 1,000 sats.

test6-pay-2x.2x.mp4

Test 4: replacement during Lightning fee estimation ✅ The stale result did not open Confirm, and a fresh send on the replacement wallet reached Confirm in the same process.

A local hook held the Lightning fee result after Continue. The funded wallet was wiped and a new wallet created before the result was released. The process ID and changed wallet address were checked through the debug control; the old amount screen did not advance. The new wallet was funded and a fresh 1,000-sat on-chain send reached Confirm; it was cancelled without broadcast. The hook supplies the held fee result, so this checks wallet ownership and collector behavior rather than Lightning fee calculation.

test7-2x.2x.mp4

test7-fresh-2x.2x.mp4

Test 5: non-502 errors with a saved schedule ✅ HTTP 500 and 401 each stopped the send before broadcast.

After a successful fee refresh, each 10,000-sat send reached Confirm and was swiped. The app's fee-lookup logs showed the respective error; the regtest transaction pool remained empty after each attempt.

test8-500-2x.2x.mp4

test8-401-2x.2x.mp4

Check 1: build and send-event tests ✅ The unmodified head built and the affected send-flow suite passed.

The suite covers unreadable and pending WALLET/USDT markers, wallet generation changes during held fee estimation, and cancellation of the owning scope.

The author's final-head report covers the authenticated restore and payment-link case. The earlier fresh-cache and malformed-response controls in that report exercise the unchanged fee-fallback path. The final-head E2E run passed its send shard. The earlier review documented the collector failure reproduced before this fix.

Reviewed the delta from 258f8a9 to bfa61d3 and the earlier open findings. Parent-only changes in the delta were read as integration context; the device cases above focus on fee fallback and send-event recovery.

No matching iOS twin PR was identified. The iOS fee manager retains its published estimate after a fetch error, while Android reuses saved fees only for the two recognized 502 forms.

Reviewer model: gpt-6.1-terra
Reasoning effort: high


Reviewed by gpt-6.1-terra-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utAck

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed to bfa61d3. No new findings. The per-event catch (CancellationException) { ensureActive() } keeps the send-event collector alive while still rethrowing a real scope cancellation, and onAmountContinue / onConfirmPay still reset loading in finally.

Device gate (partial): bfa61d3, not run yet. Pending: an on-chain send against the 502 test endpoint (Confirm and the broadcast both use the saved schedule), and a send with the endpoint healthy.

jvsena42
jvsena42 previously approved these changes Oct 28, 2026

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The code scan had no findings, and the device gate passed.

Device gate: bfa61d3, Gate_Throwaway emulator. I made an e2e debug build of this head pointed at a local stub on E2E_BLOCKTANK_API_URL that proxies to staging. In proxyfee mode the stub serves 7/5/2 sat/vB, so the saved schedule can be told apart from staging's live 3/1/1. Wallet: fresh, funded with 200 000 sat on regtest.

  • HTML 502 on /info: Confirm showed Normal (₿ 703), which is the saved 5 sat/vB, and the broadcast used it.
    07:57:21.896 WARN [CoreService.kt:2182] Failed to fetch fresh fee rate, using cached rate.
    07:57:23.045 DEBUG [LightningRepo.kt:1558] sendOnChain: sats=10000, isTransfer=false, isMaxAmount=false, satsPerVByte=5
    07:57:23.431 INFO [AppViewModel.kt:6021] Accepted on-chain send '1e89f519…7875'
    
  • JSON 502: Confirm used the saved schedule and logged the warning. I cancelled before broadcast.
  • Healthy stub: Confirm showed Normal (₿ 141) at the live 1 sat/vB, with no saved-rate warning, and the send went out (e75c894b…ccb3).

⚠️ Not run: manual cases 2 (a fresh wallet with no saved schedule) and 3 (restore-marker cases). They rest on the code scan and unit tests.

Perf: not relevant to this PR's own change.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Re-reviewed the full pull request at bfa61d31 after no verified baseline coverage was supplied. The comparison base and merge base are both 2986fde9.

No new actionable code findings.

Info HTTP 502 from Blocktank get_info falls back to the saved fee schedule for the HTML and JSON body shapes produced by bitkit-core 0.8.0-rc11 and rust-blocktank-client 0.0.16. Cancellation, unauthorized responses, malformed data, unrelated HTTP failures, and a missing cache still fail. The broadcast uses that cached schedule for the selected speed through getFeeRateForSpeed. Payment links, scans, and request preparation keep the original wallet generation from bitkit-android#1685 and drop late presentation when that generation or a required WALLET/USDT restore marker no longer matches. A refused send event leaves the collector active; cancelling the owning scope still stops it. The debug end-to-end Blocktank URL accepts only an explicit localhost port and path, and non-debug builds leave it empty.

The new generation readers match the parent wipe and admission rules at 2986fde9. That check covers this contract only, not the rest of #1685. Restore-marker reads still run on the send collector's caller thread; that follow-up remains #1719, and this pass did not measure a stall.

Validation: source inspection of the pinned diff, callers, and the core/client error text. This review did not run the unit suite or a device. Hosted checks on this commit include a successful build, lint, detekt, and end-to-end. Migration jobs in 40562290865 were skipped. Those hosted results were not re-executed here, and the end-to-end run does not exercise the gateway-502 fee path.

The current iOS fee-fallback source was not compared in this pass.

Device testing: not performed in this review.

Ready for device testing.

Earlier findings reconciled at this revision: 1 still present, 1 verified fixed.

Findings

  • [LOW] Send entry reads restore markers on the main thread — now at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:7111. Still present from the earlier review.

Reviewer model: grok-4.7
Reasoning effort: high

piotr-iohk
piotr-iohk previously approved these changes Oct 28, 2026

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Re-reviewed the full PR at 0b963ef9 against its merge base 5b4674c3, because the comparison base moved from 2986fde9 to the current #1685 head and that change touches files this PR also edits. This is a full reassessment and no earlier coverage was carried over.

No new actionable code findings.

  • Fee fallback: BlocktankService.getFees() uses the saved schedule only when the error chain is a BlocktankException.DataException with one of the two recognized info-502 messages, and only when a saved schedule exists. Cancellation, other typed errors, unrelated text containing "502" and a missing cache still fail as before. The send flow passes its stored feeRates through, so the speed the user selected is kept. Other callers that look up fees without stored rates (for example boost via getFeeRateForSpeed) now get the same fallback.
  • Wallet-generation gating: WalletConnectionRecovery.generation changes only when the wallet is wiped. Everyday reconnects therefore cannot invalidate scans, links or payment requests that are being prepared. The latest commit passes the captured generation through payment-request finalization, so a default argument no longer picks up the replacement wallet's generation. When preparation is refused, observeSendEvents swallows only the refusal's CancellationException and stops if its own scope is cancelled. The payment-request preparation sheet is still cleared by the existing finally block.
  • iOS parity: at bitkit-ios 25f8225b (source only), iOS falls back to cached estimates after any fresh-fetch failure. Android is deliberately narrower and only handles the two 502 forms, as the PR describes.

Validation: I did not run tests locally because no Android SDK was available. Hosted CI 40721227903 (unit tests), Lint 40721227979 and E2E 40721228502 all passed at this head. I read the new unit tests; I did not run them.

Limitations: I could not check the two matched error prefixes against the source of the Blocktank client (version 0.0.16, used by bitkit-core v0.8.0-rc11). The crates.io download was refused, and the public client repo only has an older version (0.0.12) with a different message format. The match is supported only by the author's device report and the unit tests. Blocking keychain reads in the send-entry restore check are out of scope as stated and tracked in #1719.

Device testing: not performed in this review.

Suggested additional test cases

  • Android, regtest: build a saved fee schedule with a successful Blocktank info fetch, then make Blocktank info return the HTML 502. Open Boost on an unconfirmed outgoing on-chain transaction. Expected: the boost fee uses the saved Fast rate and the sheet does not show a fee-unavailable error.
  • Android, same setup: make Blocktank info return 503 or 504 and attempt a normal on-chain send. Expected: fee preparation fails with no broadcast. Only the two recognized 502 forms may use the saved schedule.

Earlier findings reconciled at this revision: 1 still present.

Findings

  • [LOW] Send entry reads restore markers on the main thread — now at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:7099. Still present from the earlier review.

Reviewer model: claude-opus-5-5
Reasoning effort: high

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve

Findings: No new findings. Blocking restore-marker reads remain deferred to synonymdev/bitkit-android#1719.

QA for 0b963ef:

Android 16, disposable regtest wallets and an owned recipient. I built the head and matched the installed APK to the build. The device build adds a debug-only control for capturing wallet ownership and invoking the payment-scan handoff; the production send and admission code is unchanged.

Test 1: cached fees after gateway errors ✅ Six 10,000-sat transfers confirmed under HTML and JSON info-502 responses at all three speeds.

I primed the native cache with 7/5/2 sat/vB, then verified actual 502 traffic. Confirm showed Fast/Normal/Slow fees of 984/703/281 sats for both response forms. Node logs and the six confirmed transaction records independently matched those fees and each 10,000-sat recipient output. The integrated sendOnchain path passes the rates shown on Confirm into the broadcast.

HTML 502, recordings at 2×:

Fast Normal Slow
test1a-final-2x.2x.mp4
test1b-2x.2x.mp4
test1c-2x.2x.mp4

JSON 502, recordings at 2×:

Fast Normal Slow
test1d-2x.2x.mp4
test1e-2x.2x.mp4
test1f-2x.2x.mp4
Test 9: original owner at the scan handoff ✅ The old wallet owner was refused after replacement, while a fresh owner opened Bitcoin Amount in the same process.

The debug control captured the real wallet generation before an actual wipe and wallet creation. Calling openContactPayment with that captured owner left Home visible; calling it with the replacement owner opened the amount screen. The control receipts verified the changed wallet address, generation and unchanged process. No payment was submitted in this case.

This drives the public handoff's owner admission. The proof-sync and finalization races themselves are covered by the committed unit regressions below; this is not an authenticated Paykit-resolution journey.

Recordings at 2×:

Wallet replacement Old and fresh handoffs
test9a-final-2x.2x.mp4
test9b-final-2x.2x.mp4
Test 10: Boost after gateway errors ✅ Boost used the saved Fast rate after both HTML and JSON 502 responses.

I created a real unconfirmed 10,000-sat outgoing transaction at 2 sat/vB, then opened Boost under each response form. It offered 7 sat/vB from the saved schedule. Native helper logs confirmed cache fallback on each lookup. I cancelled both boosts without submitting a replacement.

Recordings at 2×; the HTML recording includes creation of the unconfirmed transaction:

HTML 502 JSON 502
test10-html-2x.2x.mp4
test10-json-2x.2x.mp4
Test 11: HTTP 503 and 504 controls ✅ Fresh Boost lookups rejected both errors; ordinary sends kept the rates shown on Confirm.

Under each response, Boost dismissed with “Boost Failed” and native fee-lookup errors. An ordinary 10,000-sat send then showed Normal/703 sats and broadcast at exactly that fee; transaction records verified each recipient output and fee.

The ordinary-send expectation in the latest review is superseded by the inherited change:

Recordings at 2×, each showing rejected Boost preparation followed by the ordinary send:

HTTP 503 HTTP 504
test11-503-final-2x.2x.mp4
test11-504-final-2x.2x.mp4
Check 1: head build and affected unit suites ✅ The unmodified head built and all five affected suites passed.

Ran assembleDevDebug and targeted testDevDebugUnitTest for AppViewModelSendFlowTest, BlocktankFeeFallbackTest, WalletConnectionRecoveryTest, BlocktankTestEndpointTest and LightningRepoTest. Coverage includes replacement during proof synchronization and request finalization, required/unreadable restore markers, collector survival and genuine scope cancellation.

Check 2: earlier acceptance ✅ Passing reports cover the unchanged recovery controls.

I relied on the earlier device review at bfa61d3 for Amount and Coin Selection recovery after a refused action, and replacement during Lightning fee estimation. The author's QA notes retain passing missing-cache, malformed-response and authenticated required-restoration controls at their recorded revisions. Those cases were not re-driven here.

The earlier HTTP 500/401 swipe results are not reused: the broadcast now receives the reviewed fee rates. The narrow fee-lookup error controls were rechecked in the unit suites.

Reviewed the delta from bfa61d3 to 0b963ef and earlier open findings. The ownership fix carries the original wallet owner to scan admission through request finalization and both initial-subscription handoffs. The incoming parent changes were checked as integration context; subscription acceptance/retry has no new device result in this pass.

Twin: no matching fee-fallback PR is linked. The iOS fee manager retains its last published schedule after a fetch error; Android's fee helper recognizes the two specific 502 forms.


Reviewed by gpt-6.1-sol-high

ovitrif
ovitrif previously approved these changes Oct 28, 2026

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utAck

@ovitrif
ovitrif changed the base branch from fix/pending-wallet-restore-admission to master October 28, 2026 18:49
@ovitrif
ovitrif dismissed stale reviews from piotr-iohk, jvsena42, and themself October 28, 2026 18:49

The base branch was changed.

@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Retargeted this PR to master before merging its base, #1685, so deleting that branch would not close it. If the base change dismissed the approval, it needs a re-approval.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve

Findings: No new findings. Blocking restore-marker reads remain deferred to bitkit-android#1719 Move send-entry restore-marker reads off the main thread.

QA for cce1c8d:

Test 1: Bitcoin confirmation ✅ Under HTML info-502, Confirm showed 10,000 sats and the saved Normal fee of 703 sats; Back returned Home with the balance unchanged.

Android 16, disposable regtest wallet. I built the clean head with the committed debug endpoint override and matched the installed APK to the build. The native cache was primed with 7/5/2 sat/vB. Endpoint logs verified HTTP 502 responses, and native logs confirmed saved-fee fallback. The recording shows confirmation details and cancellation back to the unchanged 200,000-sat balance. No payment was submitted.

confirm-html-2x.2x.mp4

Confirmation and cancellation, recording at 2×.

Check 1: build and affected regressions ✅ The unmodified head built and all five affected unit suites passed.

Ran assembleDevDebug and targeted testDevDebugUnitTest for AppViewModelSendFlowTest, BlocktankFeeFallbackTest, WalletConnectionRecoveryTest, BlocktankTestEndpointTest and LightningRepoTest. This includes the fixed-USDT and subscription confirmation checks: replacement during the sheet transition cannot present the old payment. Proof synchronization, request finalization, required/unreadable recovery markers, collector survival and genuine scope cancellation also passed.

Check 2: prior acceptance ✅ Earlier passing reports cover the unchanged fee and recovery paths.

I checked implementation and dependency identity before reusing:

  • The previous review at 0b963ef: six HTML/JSON 502 transfers at Fast/Normal/Slow, original scan-owner admission, Boost fallback and HTTP 503/504 controls.
  • The device review at bfa61d3: Amount and Coin Selection after recovery refusal, plus wallet replacement during Lightning fee estimation.
  • The author's revision-scoped QA notes: missing-cache, malformed/unauthorized responses and authenticated required-restoration controls, including both-pending and WALLET-only admission.

Those cases were not re-driven. The changed ordinary confirmation UI was checked above. The earlier HTTP 500/401 ordinary-send rejection expectation remains superseded by the confirmed-rate send implementation; the narrow fee-lookup error controls passed in the unit suites.

Reviewed the contiguous 0b963ef..cce1c8d delta and earlier open findings. The master merge preserves fixed Bitcoin/USDT and subscription confirmation routing while carrying the original wallet generation through both confirmation branches and delayed presentation. Other incoming master changes were checked as integration context; authenticated Paykit request/subscription and complete VSS restoration journeys have no new device result in this pass.

Twin: no matching fee-fallback PR is linked. The iOS fee manager at 85e9d4c retains its published schedule after a refresh error; Android deliberately recognizes only the two typed info-502 forms.


Reviewed by gpt-6.1-sol-high

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utAck

@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman Merging this PR on the approvals of head cce1c8d, with ovi-reviewer's device run on that head, checks green and no open threads. #1725 is retargeted to master first.

@ovitrif
ovitrif merged commit 0924ae5 into master Oct 28, 2026
22 checks passed
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman Merged, as it was approved and green.

@ovitrif
ovitrif deleted the fix/blocktank-fee-cache branch October 28, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants