Skip to content

fix: keep fixed paykit requests on confirmation with an asset switch - #1638

Merged
ovitrif merged 14 commits into
masterfrom
fix/paykit-fixed-request-asset-confirm
Oct 28, 2026
Merged

ovitrif merged 14 commits into
masterfrom
fix/paykit-fixed-request-asset-confirm

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1623, closes #1630, closes #1631, closes #1632
Refs:

This PR keeps a fixed-amount payment request, such as a Pubky Market Shop order, on its confirmation step. On 3.0.1 and on master, a request whose payee offers both bitcoin and USDT opened a loading Payment Request screen, then moved to the Amount step on its own (#1623). From there, the USDT review showed a second Amount screen while the fee was quoted (#1632), sent the user back to Amount when the fee quote expired (#1630), and had a Pay From chip that did nothing (#1631). These issues share one cause: a request with a BTC/USDT choice had no confirmation step of its own, so the asset choice lived only on Amount.

Description

  • Opens a fixed request on the confirmation step of its preselected asset: bitcoin Confirm, or the USDT review. Before, openPaykitEndpoints took any request with a USDT option to Amount, because fix: confirm fixed Paykit requests directly #1581's direct confirmation (openBitcoinRequestConfirmation) stopped whenever usdtRecipient was set. Amount remains the fallback when the terms cannot be bound, for example with too little USDT, a missing rate or a Lightning amount below the minimum. Subscription periods keep their existing Amount review.
  • Makes Pay From on both confirmation steps switch between bitcoin (Savings or Spending) and USDT for a payment request; without a request, Pay From on a USDT review stays disabled as before. The switch rebinds the request's terms and replaces the current confirmation with the other asset's, so Back never returns to a confirmation bound to the other asset. Pay From skips a source the request has no current rate for. When that source is the only other one, the confirmation stays payable and a toast shows the rate error. When a switch falls back to Amount, the confirmation it leaves is cleared. A new swipe is always required.
  • Shows a loading confirmation (amount, request summary and a loading swipe) while the USDT fee is quoted, instead of the Amount page with a spinning Continue.
  • Replaces an expired USDT fee quote on the review itself, once per swipe, and shows "The fee quote expired and was updated. Check the fee and swipe again." Before, the expired quote dropped the review back to its Amount page. Declining a payment warning keeps the reviewed quote; the warning shows again on the next swipe.

Out of Scope

Design

N/A — no design available. Reuses the existing Confirm, USDT review and Pay From chip; the USDT loading state follows the bitcoin Confirm's preparing placeholder.

Preview

Not captured: these screens need a live Paykit request with both bitcoin and USDT offered, which the local test environment cannot provide.

QA Notes

Journeys

N/A — not drivable; see Manual Tests.

Manual Tests

  • 1. Open a fixed-amount Shop order request offering bitcoin and USDT, with USDT enabled → it opens on the confirmation of the preselected asset and never moves to Amount on its own — needs a live Pubky Market order, not in Capabilities
  • 2. On that confirmation, tap Pay From → it switches between USDT and bitcoin confirmations; swipe pays with the chosen asset — same
  • 3. On the USDT review, wait past the fee quote expiry, then swipe → the quote is replaced on the same screen with the notice, and a second swipe pays — same
  • 4. Open a fixed request offering bitcoin and USDT whose pricing has a bitcoin rate only, so it opens on the bitcoin confirmation; tap Pay From → the confirmation stays with a toast "Payment rates are unavailable or expired. Try again when online.", and a swipe still pays in bitcoin — same
  • 5. Open a fixed request offering Lightning, on-chain and USDT whose pricing has a bitcoin rate only, so it opens on a bitcoin confirmation; tap Pay From twice → it moves between Spending and Savings, never to USDT, with no toast — needs a request with a Lightning endpoint, which the local fixture sender cannot make
  • 6. Pay a contact who shares bitcoin and USDT endpoints, without a payment request: on Amount pick USDT and enter 5, pick Savings, then pick USDT again → the amount is 5.00 USDT again — needs a second Bitkit wallet as the payee

Automated Checks

  • added AppViewModelSendFlowTest.kt — a fixed request switches asset on its confirmation instead of returning to Amount
  • added AppViewModelSendFlowTest.kt — a fixed USDT request without enough USDT opens Amount
  • added AppViewModelSendFlowTest.kt — a contact USDT review without a payment request keeps Pay From disabled
  • added AppViewModelSendFlowTest.kt — Pay From toward USDT with no USDT rate keeps the confirmation and shows the rate error
  • added AppViewModelSendFlowTest.kt — the Pay From cycle skips USDT when the request has no USDT rate
  • added AppViewModelSendFlowTest.kt — switching from USDT to Savings and back restores the entered USDT amount
  • added AppViewModelSendFlowTest.kt — Pay From with no rate keeps the confirmation's fee refresh running
  • added SendSheetNavigationTest.kt — the Amount fallback after a Pay From switch leaves no confirmation behind it
  • added UsdtViewModelTest.kt — an expired quote is replaced on review with the notice, at most once per swipe; other errors are left for the user
  • added UsdtViewModelTest.kt — declining a warning keeps the reviewed quote and warns again on the next swipe
  • updated AppViewModelSendFlowTest.kt — a fixed dollar request opens confirmation whether or not USDT is enabled; an automatic USDT request finishes preparation on its review

@ovitrif ovitrif self-assigned this Oct 20, 2026
@ovitrif
ovitrif requested review from a team, ben-kaufman and pwltr and removed request for a team October 20, 2026 23:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T23:28:38.007941Z 1568abb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1568abb741

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The changes reach Android's Paykit payment-request confirmation flow.

Fix Back after asset switching and the endless loader after warning cancellation before merging.

Findings

  1. P1 Back restores the wrong confirmation ▶
  2. P1 Cancel leaves an endless loader ▶

Summary

Fixed payment requests now open on the selected asset's confirmation, support switching between bitcoin and USDT, and refresh expired USDT fee quotes in place.

  • Fixed Paykit requests open on confirmation for their selected asset.
  • USDT review stays on confirmation while quotes load or refresh.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Request[Fixed payment request] --> Bind{Bind selected asset terms}
    Bind -->|Cannot bind| Amount[Amount]
    Bind -->|Bitcoin| BTC[Bitcoin confirmation]
    Bind -->|USDT| USDT[USDT review]
    BTC -->|Pay From: bind USDT| USDT
    USDT -->|Pay From: bind bitcoin| BTC
    USDT -->|Fee quote expired| Quote[Get replacement quote]
    Quote --> USDT
Loading

Reviews (1) · Last reviewed commit: "fix: keep fixed paykit requests on confi..." · Reviewed by Greptile

Comment thread app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 27adb96 (run).

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

@ovitrif

ovitrif commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a89d712: the automatic requote on an expired USDT fee now runs once per swipe instead of retrying without limit.

  • Answers the Codex finding on PaykitUsdtReview.kt, a requote loop when replacement quotes also expire.
  • A second expiry leaves the review on the expired error for the user's own retry; swiping again re-arms the requote.
  • New UsdtViewModelTest case for the repeat expiry; the USDT view model tests and detekt pass.

The two Greptile findings are being fixed next: Back after an asset switch and Cancel on the payment warning.

@ovitrif

ovitrif commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 14740ca: switching asset on a payment request now replaces the confirmation, and cancelling the payment warning keeps the reviewed quote.

  • Answers the Greptile finding Back restores the wrong confirmation: ShowPaykitConfirmation navigates with popUpTo the other asset's confirmation, so Back never lands on stale terms.
  • Answers the Greptile finding Cancel leaves an endless loader: the warning's Cancel calls dismissWarning(), which keeps the quote instead of resetting the review to its loading state.
  • New UsdtViewModelTest case for the cancel path; the USDT and send-flow unit tests and detekt pass, and the instrumented tests compile. The description is updated.

@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.

2 findings (2 P1). Keeps a fixed Paykit request on the confirmation of its preselected asset, adds a bitcoin/USDT switch on Pay From, a USDT loading confirmation and in-place requote of an expired quote. Reviewed 1568abb, full tier, reasoned from the code: per the repo's Bitkit rules the branch was not built here.

What I checked, and 4 candidates I ruled out

Read in full: PaykitUsdtReview.kt, UsdtViewModel.kt, the changed paths of UsdtSendContent.kt, SendSheet.kt and AppViewModel.kt (openPaykitEndpoints, openPaykitRequestConfirmation, bindUsdtRequestTerms, switchPaykitRequestAsset, onPaymentMethodSwitch, reviewPaykitAmount, onConfirmPay/proceedWithPayment, paymentProofPreparation, sendPaykitUsdt)
Call sites traced: SendEvent.PaymentMethodSwitch (Confirm and USDT review), SendEffect.ShowPaykitConfirmation, UsdtViewModel.edit/requote
CI: build-local green; build, lint, e2e and Greptile still pending at review time

Ruled out

  • Requote loop on a second expiry: requote only runs on usdt__error_expired with no quote, and a successful requote replaces the error with the notice, so it cannot spin.
  • Subscription periods routed to the new confirm: openPaykitRequestConfirmation returns early for billingPeriod != null with a USDT option.
  • USDT shown in fundingOptions with USDT disabled: availableFundingSources adds USDT only with a usdtRecipient, which openPaykitEndpoints sets only when USDT is enabled or a binding exists.
  • Cold reader: the stuck-loading finding stood (it rated it P2; I kept P1 because the screen has no way forward). The back-stack finding got no answer because the reader timed out, so that doubt stands.

Merge confidence: 2/5, two P1s on the new confirm paths.

Comment thread app/src/main/java/to/bitkit/ui/sheets/SendSheet.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 14740ca: one LOW, inline with a failing test. Nothing above LOW. Reviewed as a funds change.

Checked and clean (amount, fee, PIN):

  • Terms bound early: they are bound before the review opens.
  • Rechecked before paying: validateQuote and authorizeUsdtExecution check the amount, terms, quote and validity again before executing. A switch during a send clears the reviewed amount, so the payment throws instead of going out.
  • Requote limits: a requote happens at most once per swipe. It resets the confirmed warnings and the PIN state, so both are asked again, and it never sends on its own.
  • Back can't reach stale terms: popUpTo replaces the other asset's confirmation.
  • Subscriptions: periods paid in USDT keep the Amount step.

Parity: iOS synonymdev/bitkit-ios#1014 is still open and keeps the Amount step when a real BTC/USDT choice exists. The PR's Out of Scope already notes the difference.

New UI: the loading state while the USDT quote runs has no Figma frame.

Device gate (partial): 14740ca — not run yet. Pending:

  • BTC/USDT fixed request: it lands on the preselected confirmation, the amount equals the terms, switching works both ways, and Back;
  • quote expiry: swipe, see the notice, warnings and PIN asked again, one payment;
  • Cancel on a warning returns the knob to the start;
  • the chip from the inline finding.

Device results will follow in a separate review.

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

ovitrif commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 17e1b0c for @jvsena42's Pay From finding: on a USDT review without a payment request, Pay From stays disabled. Added the reviewer's test; AppViewModelSendFlowTest and detekt pass locally.

@ovitrif
ovitrif requested a review from jvsena42 October 21, 2026 13:01
…quest-asset-confirm

# Conflicts:
#	app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt
@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Merged master into this branch (bf9aaac) to resolve the conflict in UsdtSendContent.kt: master's isMerchantRequest and this PR's onSwitchAsset are both kept on UsdtQuoteDetails. No behaviour change on either side.

@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 of 14740ca..bf9aaac: no findings.

  • 17e1b0c fixes the Pay From chip on a contact USDT review without a request (AppViewModel.kt:8390-8395, with the test at AppViewModelSendFlowTest.kt:14658-14690).
  • The master merge bf9aaac resolves its one conflict in UsdtSendContent.kt correctly (UsdtQuoteDetails(quote, contact, isMerchantRequest, onSwitchAsset), :698 and :711-716).
  • Re-traced the funds path after the merge into AppViewModel:
    • terms are cleared before the rebind and re-validated in prepare / validateQuote;
    • there's no new auto-start, because a USDT-payable request always has a conversion, so shouldAutomaticallyPay is false;
    • Back and cancel from the asset switch land on Amount or close the sheet, never on stale terms.

Device gate (partial): bf9aaac — not run yet; pending a fixed BTC/USDT request switched between assets on confirmation, and l10n for the new usdt__error_quote_refreshed. Device results will follow in a separate review.

@ovitrif

ovitrif commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

@jvsena42 pushed c66819b for the l10n item in your device gate: usdt__error_quote_refreshed now has a translation in every values-*/strings.xml, worded like each locale's existing usdt__error_expired. It is also moved into alphabetical order. Resources only.

@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

@piotr-iohk this needs a device run on dd0c69f before the 3.1.0 cut, since #1713 is stacked on it. The Manual Tests section lists the cases: a fixed request with both BTC and USDT opens on its confirmation, Pay From switches between the assets, and an expired USDT quote is requoted in place. The regtest and local USDT fixture setup you used for #1635 covers them.

dd0c69f only merges master in. It had no conflicts, and the send-flow and USDT tests plus detekt pass.

@ovitrif
ovitrif removed the request for review from pwltr October 27, 2026 19:57
@ovitrif

ovitrif commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for dd0c69f:

Manual Tests 1, 2 and 3 pass. Tested on an Android 16 emulator: BTC on staging regtest, USDT on the local Arbitrum fork fixture. The request came from the Paykit fixture peer: a fixed 5 USD request (4 USD in Test 3) accepting both bitcoin and USDT, which stands in for a Shop order.

🟢 Tests 1, 2, 3
Test 1

The request opens on the USDT review of the preselected asset, after the quote loader, and never moves to Amount. With no USDT balance loaded, an earlier request opened on the bitcoin confirmation instead.

case12x.mp4

Test 2

Pay From switches USDT → bitcoin → USDT → bitcoin on the confirmations, and the swipe pays 10,000 sats in bitcoin to the request's address. The request then leaves Payment Requests.

case22x.mp4

Test 3

After 130 s on the USDT review (the quote lives 120 s), the first swipe replaces the quote on the same screen with "The fee quote expired and was updated." The second swipe pays. The recipient received exactly 4.000000 USDT, a single transfer.

case32x.mp4

@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 69e98773, at dd0c69fd. A direct Pay From switch replaces the other confirmation, and an expired USDT fee quote is refreshed once per swipe. The Amount fallback after that replacement still leaves the current confirmation on the back stack, and a later Pay From toward USDT does not use that fallback when the conversion rate is unavailable.

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

Earlier inline threads were rechecked at this head and do not reproduce: the requote loop (discussion), stacking the other confirmation on a direct switch (discussion), the warning Cancel loader (discussion), and Pay From on a contact USDT review without a request (discussion). No matching open issue was found for the Amount-fallback back stack.

GitHub CI, Lint, and local E2E succeeded on this commit. This review did not run those jobs or the new unit tests; the new test code was inspected. The author reports manual tests 1–3 passed on this commit (report); that run does not cover the fallback below. iOS source was not checked out here. The PR states the matching Amount-first route remains in iOS PaymentNavigationHelper and is out of scope.

Device testing: not performed in this review.

Suggested additional test cases

  • Android. Fixed USD request that accepts lightning, on-chain, and USDT, opened on the USDT review, then switched once to bitcoin. After the conversion quote is no longer valid, tap Pay From to move between lightning and on-chain, then use the in-sheet Back and the system Back. Amount should be the only send step; Back should not open a bitcoin confirmation whose request terms were cleared, and swipe on that confirmation should not dismiss the request.
  • Android. Fixed request with on-chain and USDT only, sitting on the bitcoin confirmation. After the conversion quote expires, tap Pay From toward USDT. The sheet should show Amount or a visible rate error, and the bitcoin confirmation should not ignore the tap.

Findings

  • [MEDIUM] Amount fallback leaves the unbound confirmation underneath — inline at app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt:124.
  • [LOW] Pay From toward USDT does nothing when the rate is unavailable — inline at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:4072.

Reviewer model: grok-4.7
Reasoning effort: high

Comment thread app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt Outdated
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@ovitrif
ovitrif requested a review from piotr-iohk October 28, 2026 07:22
@ovitrif

ovitrif commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for 56b680c:

Manual Tests 1, 2 and 3 pass on the head with both review fixes. Tested on an Android 16 emulator: BTC on staging regtest, USDT on the local Arbitrum fork fixture. The request came from the Paykit fixture peer: a fixed 5 USD request (4 USD in Test 3) accepting both bitcoin and USDT, which stands in for a Shop order.

🟢 Tests 1, 2, 3
Test 1

The request opens on the USDT review of the preselected asset, after the quote loader, and never moves to Amount.

case12x.mp4

Test 2

Pay From switches USDT → bitcoin → USDT → bitcoin on the confirmations, and the swipe pays 10,000 sats in bitcoin. The request then leaves Payment Requests.

case22x.mp4

Test 3

After 132 s on the USDT review (the quote lives 120 s), the first swipe replaces the quote on the same screen with "The fee quote expired and was updated." The second swipe pays. The recipient received exactly 4.000000 USDT.

case32x.mp4

@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: Follow-up review of 56b680c8 since dd0c69fd, using the completed baseline review. The merge base is still 69e98773. This pass inspected the amount-fallback delta and its callers. Inherited baseline coverage is the fixed-request open path, successful Pay From cycling and the confirm chip, USDT loading and quote-expiry handling, displayed-versus-executed binding, the previously reviewed unit tests, and the fee-quote copy.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

Pay From toward an asset the request cannot quote clears the reviewed confirmation and opens Amount on the asset that is still selected. Amount then requotes that asset, so the rate-unavailable notice does not stay on screen.

Validation: the new SendSheetNavigationTest and AppViewModelSendFlowTest cases were inspected and not executed. Device testing: not performed in this review. iOS was not compared; this pull request already treats the matching iOS routing as separate work.

Suggested additional test cases

  • Android. Setup: a fixed USD Paykit request with bitcoin and USDT endpoints and a conversion rate for bitcoin only; USDT enabled and enough on-chain balance to pay the bitcoin quote. Action: open the request so bitcoin Confirm is showing, then tap Pay From. Expected: the bitcoin confirmation stays swipeable, or Amount opens on USDT with “Payment rates are unavailable or expired. Try again when online.” and Continue disabled. Back must not close the sheet while that payable bitcoin quote is still available.

Earlier findings reconciled at this revision: 6 verified fixed.

Findings

  • [MEDIUM] No-rate Pay From drops the payable confirmation — inline at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:4012.

Reviewer model: grok-4.7
Reasoning effort: high

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

ovitrif commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for ee6edc3:

Manual Test 4 passes. Tested on an Android 16 emulator: BTC on staging regtest, USDT on the local Arbitrum fork fixture with a 25 USDT balance. The request came from the Paykit fixture peer: a fixed 5 USD request accepting bitcoin and USDT, priced with a bitcoin rate only. Tests 1–3 passed on 56b680c in the earlier QA comment; this commit changes only the no-rate branch of Pay From.

🟢 Test 4
Test 4

The request opens on the bitcoin confirmation (10,000 sats). Pay From toward USDT keeps that confirmation, still on Savings, and shows "Payment rates are unavailable or expired. Try again when online." A swipe then pays in bitcoin and shows Bitcoin Sent. The clip joins two recordings: the first swipe attempt by the test driver was too fast for the slider.

case4full2x.mp4

@ovitrif
ovitrif requested a review from piotr-iohk October 28, 2026 09:50

@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: Follow-up review of ee6edc3d since 56b680c8, with merge base 69e98773. Inherited baseline: pullrequestreview-5680171892. This pass covered the no-rate Pay From change and the confirmation funding-source cycle it stays on. Unchanged request open, successful Pay From rebinding, USDT quote expiry, displayed-versus-executed terms, fee-quote copy, and amount-fallback navigation remain inherited.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

A reviewed confirmation that cannot price the next Pay From source keeps that source selected, so a later priceable source is unreachable from the cycle.

Validation: the updated no-USDT-rate case in AppViewModelSendFlowTest was inspected and not executed. It selects USDT directly from an on-chain-only confirmation. Device testing: not performed in this review.

Suggested additional test cases

  • Android. Setup: a fixed payment request with Lightning, on-chain, and USDT endpoints, a BTC conversion rate, and no USDT rate, where both Spending and Savings can fund the amount. Action: open the request so it starts on the Lightning confirmation, tap Pay From once, then tap Pay From again. Expected: Pay From reaches the other bitcoin source instead of staying on Savings with only the rate toast, and swipe pays the bitcoin source left selected.

Earlier findings reconciled at this revision: 5 verified fixed.

Findings

  • [MEDIUM] Pay From stops cycling when the next asset has no rate — inline at app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:4011.

Reviewer model: grok-4.7
Reasoning effort: high

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

ovitrif commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for 17b60ca:

Manual Test 4 passes again on this head. Tested on an Android 16 emulator: BTC on staging regtest, USDT on the local Arbitrum fork fixture with a 25 USDT balance. The request came from the Paykit fixture peer: a fixed 5 USD request accepting on-chain bitcoin and USDT, priced with a bitcoin rate only. Test 5 was not run on a device: the fixture peer cannot add a Lightning endpoint to a request. pay from cycle skips USDT when the request has no USDT rate in AppViewModelSendFlowTest.kt covers it.

🟢 Test 4
Test 4

On the bitcoin confirmation (10,000 sats, Savings), Pay From has no other priced source, so the confirmation stays and the toast "Payment rates are unavailable or expired. Try again when online." shows. A swipe pays in bitcoin and shows Bitcoin Sent.

case4r2x.mp4

@ovitrif
ovitrif requested a review from piotr-iohk October 28, 2026 10:29
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ovitrif
ovitrif requested a review from ben-kaufman October 28, 2026 10:47
@ovitrif

ovitrif commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for 5e988c3:

Manual Tests 1, 2, 3, 4 and 6 pass. Tested on Android 16 emulators: BTC on staging regtest, USDT on the local Arbitrum fork fixture with a 25 USDT balance. Requests in Tests 1–4 came from the Paykit fixture peer and accepted on-chain bitcoin and USDT: a fixed 5 USD request (4 USD in Test 3), priced in USDT and bitcoin, except in Test 4, which has a bitcoin rate only. Test 6 used a second Bitkit wallet on another emulator as the payee contact. Test 5 was not run on a device: the fixture peer cannot add a Lightning endpoint to a request. pay from cycle skips USDT when the request has no USDT rate in AppViewModelSendFlowTest.kt covers it.

🟢 Tests 1, 2, 3, 4, 6
Test 1

The request opens on the USDT review at 5.00 after the quote loader and never moves to Amount.

t12x.mp4

Test 2

On the same request, Pay From switches USDT → bitcoin → USDT → bitcoin on the confirmations, and the swipe pays 10,000 sats in bitcoin (Bitcoin Sent).

t22x.mp4

Test 3

After 130 s on the USDT review (the quote lives 120 s), the first swipe replaces the quote on the same screen with "The fee quote expired and was updated." The second swipe pays. The recipient's balance went from 0 to exactly 4.000000 USDT.

t32x.mp4

Test 4

The request opens on the bitcoin confirmation (10,000 sats, Savings). Pay From has no other source with a rate, so the confirmation stays and the toast "Payment rates are unavailable or expired. Try again when online." shows. A swipe pays in bitcoin.

t42x.mp4

Test 6

Paying the second wallet's contact without a request: on Amount, USDT with 5 entered (6,141 sats), then Savings, then USDT again shows 5.00 USDT.

t62x.mp4

@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 5e988c3 (Pay From cycle commits). No HIGH or MEDIUM. One LOW inline: the confirmation's fee refresh can be left stuck loading when Pay From has no rate.

Checked:

  • hasPaymentRate matches paymentAmountForSource exactly and is pure.
  • popUpTo(graph.id) is a safe superset.
  • Unpriced sources are skipped in the cycle, and the amount fallback replaces a switched confirmation correctly.

This PR and #1725 will conflict in onPaymentMethodSwitch, in the refresh-cancel lines.

Device gate (partial): 5e988c3, not run yet. You already ran tests 1–4 and 6 on this head. Pending: tapping Pay From while the fee cell is still spinning.

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ovitrif please resolve conflicts.

…quest-asset-confirm

# Conflicts:
#	app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtViewModel.kt
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

@piotr-iohk conflicts are resolved: master is merged in 82d043f. Device tests on the new head are running now, and I'll post the results here.

@ovitrif

ovitrif commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA for 27adb96:

Manual Tests 1, 2, 3, 4 and 6 pass. Tested on Android 16 emulators: BTC on staging regtest, USDT on the local Arbitrum fork fixture with a 25 USDT balance. Requests in Tests 1–4 came from the Paykit fixture peer and accepted on-chain bitcoin and USDT: a fixed 5 USD request (4 USD in Test 3), priced in USDT and bitcoin, except in Test 4, which has a bitcoin rate only. Test 6 used a second Bitkit wallet on another emulator as the payee contact. Test 5 was not run on a device: the fixture peer cannot add a Lightning endpoint to a request. pay from cycle skips USDT when the request has no USDT rate in AppViewModelSendFlowTest.kt covers it.

🟢 Tests 1, 2, 3, 4, 6
Test 1

The request opens on the USDT review at 5.00 after the quote loader and never moves to Amount.

t12x.mp4

Test 2

On the same request, Pay From switches USDT → bitcoin → USDT → bitcoin on the confirmations, and the swipe pays 10,000 sats in bitcoin (Bitcoin Sent).

t22x.mp4

Test 3

After 130 s on the USDT review (the quote lives 120 s), the first swipe replaces the quote on the same screen with "The fee quote expired and was updated." The second swipe pays. The recipient's balance went from 0 to exactly 4.000000 USDT.

t32x.mp4

Test 4

The request opens on the bitcoin confirmation (10,000 sats, Savings). Pay From has no other source with a rate, so the confirmation stays and the toast "Payment rates are unavailable or expired. Try again when online." shows. The fee row keeps its estimate (Normal, ₿ 141) after the toast, and a swipe pays in bitcoin.

t42x.mp4

Test 6

Paying the second wallet's contact without a request: on Amount, USDT with 5 entered (6,202 sats), then Savings, then USDT again shows 5.00 USDT.

t62x.mp4

@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 27adb96. No new findings.

27adb96 fixes my LOW at AppViewModel.kt:4018. The early return for a reviewed quote with no rate now happens before ++sendStateGeneration and onchainSendRefreshJob?.cancel(), so the toast path keeps the confirmation's fee refresh running. The other branches still cancel it as before. The new test no-rate pay from toast keeps the confirmation fee refresh running in AppViewModelSendFlowTest.kt covers it.

The conflict with #1725 in onPaymentMethodSwitch still stands, so whichever lands second needs a rebase.

Device gate: 27adb96. ⚠️ Not run: tapping Pay From toward an unpriced source while the fee cell is spinning, on software and Trezor. That needs a fixed request with a btc-only rate, which only paykit-fixture-sender can send, so it is left to e2e CI. A Bitkit-made request always prices every endpoint asset, so the app can't build this precondition itself. Covered by the unit test above and your Test 4 on this head.

@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

@jvsena42 Merging this PR on your approval of head 27adb96, with the device run on that head passing, checks green and no open threads. @ben-kaufman, #1725 conflicts with this in onPaymentMethodSwitch, so it needs a rebase on master once this lands.

@ovitrif
ovitrif merged commit f9d4b4f into master Oct 28, 2026
21 checks passed
@ovitrif
ovitrif deleted the fix/paykit-fixed-request-asset-confirm branch October 28, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants