Skip to content

fix: create order only on swipe - #1247

Merged
jvsena42 merged 1 commit into
masterfrom
fix/1243-order-on-confirm
Sep 22, 2026
Merged

jvsena42 merged 1 commit into
masterfrom
fix/1243-order-on-confirm

Conversation

@ovitrif

@ovitrif ovitrif commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1243

Description

This PR creates a Blocktank channel order only when the user confirms a transfer from savings to spending.

  • Spending Amount and Advanced request fee estimates when the user continues; they no longer create an order.
  • Software-wallet swipe confirmation and hardware-wallet Open Trezor Connect create the order for the selected amount and receiving capacity.
  • If the created order costs more than the estimate, the confirmation screen shows the new fee and requires another confirmation before funding.
  • A failed funding retry from the same confirmation reuses its unpaid order. Starting a new quote clears that order, so a later transfer creates a fresh one even when the amount is unchanged.
  • Successful software funding removes Spending Confirm from the back stack before opening Setting Up.
  • Back navigation and transfer changes remain disabled during order creation or funding so the confirmed selection cannot change in flight.

Counterpart: synonymdev/bitkit-ios#741

Out of Scope

  • Blocktank fee calculation: estimation and order creation keep using the existing backend pricing paths; this PR does not add a separate fee-reconciliation model.

Design

N/A — no design available.

Preview

android-transfer-preview.mp4

QA Notes

Journeys

  • updated: transfer-to-spending.xml creates the order from Open Trezor Connect, handles a higher final fee with a second confirmation, and retains the signed, processing, and single-activity checks.
  • regression: @transfer_1 in transfer.e2e.ts covers the existing software-wallet transfer with default and custom receiving capacity.

Manual Tests

  • 1. Hardware Wallet → Transfer To Spending → 25% → Continue → Open Trezor Connect: order is created only at confirmation; a higher final fee refreshes the screen and requires another confirmation; signing and processing create one transfer activity.
  • 2. regression: run @transfer_1 with default and custom receiving capacity → the software-wallet transfer remains user-facing equivalent to the existing flow.
  • 3. Spending Amount → Continue → return and request the same quote again → the old unpaid order is discarded; the next confirmation creates a fresh order.
  • 4. Complete a software-wallet transfer → Setting Up → back: Spending Confirm does not reopen — passed on 5620bef.

Automated Checks

  • updated: TransferViewModelTest.kt covers estimate-only navigation, higher-fee reconfirmation, same-confirmation reuse, new-quote replacement, funded-order removal, confirmation guards, and Native SegWit funding sizing.
  • updated: ContentViewTest.kt covers removing Spending Confirm from the back stack after successful funding.
  • ran: just compile, just test, and just lint; Detekt completed with 16 existing findings outside the PR files.
  • prior head: staging @transfer_1 passed with default and custom receiving capacity.
  • attempted: the hardware journey did not reach the changed flow because the test Lightning node did not finish starting.

@ovitrif ovitrif self-assigned this Sep 10, 2026
@ovitrif ovitrif changed the title fix: create the blocktank order on confirm, not on every continue fix: create channel order only on confirm Sep 10, 2026
@greptile-apps

This comment has been minimized.

Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt Outdated
@ovitrif
ovitrif marked this pull request as draft September 10, 2026 14:01
Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt Fixed
Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt Fixed
Comment thread app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt Fixed
Comment thread app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt Fixed
Comment thread app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt Fixed
Comment thread app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt Fixed
Comment thread app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt Fixed
@ovitrif
ovitrif force-pushed the fix/1243-order-on-confirm branch from f52d79c to ec9e6f9 Compare September 10, 2026 18:02
@ovitrif ovitrif changed the title fix: create channel order only on confirm fix: create order only on swipe Sep 11, 2026
@ovitrif
ovitrif force-pushed the fix/1243-order-on-confirm branch from ec9e6f9 to bdaea52 Compare September 22, 2026 00:58
Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
@ovitrif
ovitrif marked this pull request as ready for review September 22, 2026 01:06
Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 5620bef (run).

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

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

One HIGH and one MEDIUM, both reachable in release, both checked by a second pass and reproduced with unit tests (posted in a separate comment).

On greptile's fee thread: master's Confirm rendered order.feeSat - clientBalanceSat and order.feeSat + miningFeeSats from the order it then paid. The displayed-vs-paid LSP fee gap is therefore new in this PR, not pre-existing as the PR body says. The same point is raised on synonymdev/bitkit-ios#741.

Checked and clean:

  • Double swipe or tap. confirmPayJob / hwTransferSignJob and isBusy are checked, and the flags are set synchronously before launch.
  • Order creation failing after swipe. order stays null and the next swipe creates a new one.
  • Back while busy. BackHandler(enabled = isBusy), the gated top-bar back and the hidden drawer on both Confirm and HW Sign.
  • HW path. The passphrase check runs before the order is created. The PSBT uses the order's address and feeSat, PendingHwFundingBroadcast.matches pins order, address and fee, and a new quote clears the pending broadcast.
  • Sizing address. P2WPKH index 0, the same output type as the LSP funding address.
  • Journey. The edited actions match the code and are identical to iOS.

Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt Outdated
Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt Outdated
@jvsena42

Copy link
Copy Markdown
Member

Regression tests for the two findings in my review. They are added to TransferViewModelTest.kt after onTransferToSpendingHwConfirm signs the funding send and records the paid order.

Tests
@Test
fun `a paid hardware order is not paid again by the next hardware transfer of the same amount`() = test {
    val paidOrder = previewBtOrder()
    val nextOrder = nextOrderAfter(paidOrder)
    stubHwFundingSuccess(paidOrder)
    quoteOrder(paidOrder)
    whenever(blocktankRepo.createOrder(any(), any(), any()))
        .thenReturn(Result.success(paidOrder), Result.success(nextOrder))

    sut.onTransferToSpendingHwConfirm(HARDWARE_WALLET_ID)
    advanceUntilIdle()
    verify(cacheStore).addPaidOrder(eq(paidOrder.id), eq(TXID))

    sut.onConfirmAmount(paidOrder.clientBalanceSat.toLong())
    advanceUntilIdle()
    sut.onTransferToSpendingHwConfirm(HARDWARE_WALLET_ID)
    advanceUntilIdle()

    verify(hwWalletRepo, times(1)).composeFundingTransaction(
        eq(HARDWARE_WALLET_ID), eq(paidOrder.payment?.onchain?.address.orEmpty()), any(), any(),
    )
    verify(blocktankRepo, times(2)).createOrder(any(), any(), any())
}

@Test
fun `a paid hardware order is not paid again by the next savings transfer of the same amount`() = test {
    val paidOrder = previewBtOrder()
    val nextOrder = nextOrderAfter(paidOrder)
    stubHwFundingSuccess(paidOrder)
    stubSpendableBalances(spendable = ON_CHAIN_BALANCE)
    whenever(lightningRepo.calculateTotalFee(any(), any(), any(), anyOrNull(), anyOrNull()))
        .thenReturn(Result.success(1_000uL))
    stubSendOnChainSuccess()
    quoteOrder(paidOrder)
    whenever(blocktankRepo.createOrder(any(), any(), any()))
        .thenReturn(Result.success(paidOrder), Result.success(nextOrder))

    sut.onTransferToSpendingHwConfirm(HARDWARE_WALLET_ID)
    advanceUntilIdle()
    sut.onConfirmAmount(paidOrder.clientBalanceSat.toLong())
    advanceUntilIdle()
    sut.onTransferToSpendingConfirm()
    advanceUntilIdle()

    verify(lightningRepo, never()).sendOnChain(
        eq(paidOrder.payment?.onchain?.address.orEmpty()),
        any(), any(), anyOrNull(), anyOrNull(), any(), anyOrNull(), any(), any(), any(), any(),
    )
    verify(blocktankRepo, times(2)).createOrder(any(), any(), any())
}

@Test
fun `a paid savings transfer does not block quoting the next transfer`() = test {
    val paidOrder = spendingOrder(feeSat = 98_000uL)
    stubSpendableBalances(spendable = 110_000u)
    whenever(lightningRepo.calculateTotalFee(any(), any(), any(), anyOrNull(), anyOrNull()))
        .thenReturn(Result.success(1_000uL))
    stubSendOnChainSuccess()
    quoteOrder(paidOrder)

    sut.onTransferToSpendingConfirm()
    advanceUntilIdle()
    verify(cacheStore).addPaidOrder(eq(paidOrder.id), eq(TXID))

    val nextAmount = paidOrder.clientBalanceSat.toLong() - 1_000
    sut.onConfirmAmount(nextAmount)
    advanceUntilIdle()

    assertEquals(nextAmount.toULong(), sut.spendingUiState.value.clientBalanceSat)
}

private fun nextOrderAfter(order: IBtOrder): IBtOrder = order.copy(
    id = "order_next",
    payment = order.payment?.copy(onchain = order.payment?.onchain?.copy(address = "bcrt1qnextorderaddress")),
)

stubHwFundingSuccess(order) reuses the funding, sign and broadcast stubs from the existing HW signs test, with broadcastFunding(any()) returning HwFundingBroadcastResult(TXID, MINING_FEE, FEE_RATE, order.feeSat + MINING_FEE).

Run
./gradlew :app:testDevDebugUnitTest --tests '*TransferViewModelTest*'

At bdaea521: 85 tests, 3 failed. All 82 existing tests pass.

TooManyActualInvocations: hwWalletRepo.composeFundingTransaction("hardware-wallet", "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", ...)
Wanted 1 time ... But was 2 times
NeverWantedButInvoked: lightningRepo.sendOnChain("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", ...)
AssertionError: expected:<94602> but was:<95602>

With _spendingUiState.update { it.copy(order = null) } at the end of fundPaidOrder: 85 tests, 1 failed. Both double-pay tests pass, and only the stuck-quote test still fails, because it needs the separate reset/guard fix. No existing test breaks.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

QA reviewed on bdaea52.

Ran on Pixel_6: Transfer to spending estimated the fee, then created one order on confirm. The transfer was recorded and setup mined a block. A later hardware attempt estimated again and created the order when signing started. Trezor Bridge had already dropped the session, so the device never signed. The estimates before that sign did not create orders.

Greptile's created-fee note describes the code. The confirm screen shows the estimate and the swipe pays the order fee. On the completed transfers those amounts matched. A quote that moves between the estimate and the order would be paid without a second look. This PR leaves that guard out.

iPhone 17: the first transfer did the same thing. Confirm and the payment were the same: 226 853 to spending, 1 477 service fee, 281 network fee, 228 611 total.

Twin synonymdev/bitkit-ios#741: same behavior.

No findings.

Checked and clean

  • Continue and Advanced ask for a fee estimate and do not create an order
  • A failed funding retry reuses the unpaid order; changing the amount or receiving capacity drops it
  • Back does nothing while the confirm payment or hardware signing is running
  • The fee shown before the order exists is sized with a native SegWit address

QA LGTM

piotr-iohk
piotr-iohk previously approved these changes Sep 22, 2026
@ovitrif

ovitrif commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in ca6b62c to address the latest review:

  • If the created order has a higher fee than the estimate, confirmation refreshes to the final fee and funding waits for another confirmation.
  • A funded order is removed from reusable state, so a later same-amount transfer creates a new order.
  • Starting a new quote after confirmation is no longer blocked by a stale paying marker.
  • The higher-fee step in the hardware journey is synchronized with iOS.

Validation passed: just compile, just test, and just lint. Detekt reported 16 existing findings outside the PR files. The hardware journey was attempted, but the test Lightning node did not finish starting, so it did not reach the changed flow.

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

Delta since bdaea521 (rebased, now ca6b62c80). The replies are posted on my two threads. Still blocking:

  • MEDIUM: system Back from SettingUp still traps the user on SpendingConfirm after a successful pay.
  • MEDIUM: an unpaid order (from the fee refresh or a failed pay) survives a same-amount re-quote and is funded without an expiry check.

Fixed and verified:

  • A paid order is cleared in fundPaidOrder, so a same-amount follow-up creates a new order. My HW → HW regression test passes at head, alongside the author's equivalents: TransferViewModelTest has 87 tests, all passing.
  • A higher order fee refreshes the fee, the total and the mining plan, and the next swipe pays exactly what is shown. The HW grid updates too.
  • The quote entry points gate on active jobs. A new quote cannot land while a pay or sign job is running, because each setter re-checks the job before its state update with no suspension in between.
  • HW retry after a failed broadcast still reuses the same signed tx, since the order and the pending tx are kept.

@ovitrif
ovitrif force-pushed the fix/1243-order-on-confirm branch from ca6b62c to 19115be Compare September 22, 2026 14:50
@ovitrif

ovitrif commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Updated in 5620bef to address the latest exact-head review:

  • Starting a new quote now drops any unpaid order from the previous confirmation, so a later same-amount transfer creates a fresh order.
  • Successful software funding removes Spending Confirm from the back stack before opening Setting Up, so back cannot return to the completed confirmation.

This answers the stale-order and back-navigation threads. just compile, just test, and just lint passed; Detekt reported only the 16 existing findings outside the changed files.

@ovitrif
ovitrif requested a review from jvsena42 September 22, 2026 14:54

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

Delta since ca6b62c80 (19115be70): no findings. Both MEDIUMs are fixed.

  • OnSpendingFundingPaid now navigates with popUpTo<Routes.SpendingConfirm> { inclusive = true }, so Back from SettingUp lands on SpendingAmount. A new Continue there is gated on active jobs only, and the resulting quote clears isConfirmPaying.
  • onEstimateReady always drops order, so a new quote from Amount creates a fresh order. Reuse stays within one Confirm or HW Sign visit. The Advanced and Default re-quotes still keep a matching order, which is fine because they run inside that visit. HW retry still reuses the pending signed tx, since a retry goes through ensureSpendingOrder, not onEstimateReady.

@ovitrif
ovitrif force-pushed the fix/1243-order-on-confirm branch from 19115be to 5620bef Compare September 22, 2026 15:04
@ovitrif

ovitrif commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased the reviewed changes onto current master in 5620bef after the base advanced. The order-reuse and back-stack fixes are unchanged. just compile, just test, and just lint passed again; Detekt still reports only the 16 existing findings outside the changed files.

@ovitrif
ovitrif requested a review from jvsena42 September 22, 2026 15:06

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

Delta since 19115be70 (5620bef61): rebase only. The PR diff against the new merge-base is line-for-line identical to the head I reviewed. No findings.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

QA reviewed on 5620bef.

Since the review on 22 Sep, a higher created fee waits for another confirmation, a new quote drops the previous unpaid order, and Back from the in-progress screen no longer returns to the completed confirm.

Ran on Pixel_6. 25% of savings showed 11 291 to spending, 1 192 service fee, 141 network fee, 12 624 total. The swipe opened the in-progress screen. System Back landed on the amount screen, with 11 291 still filled in, not on confirm. Savings shows that transfer as 12 624. The created fee matched the estimate, so the second confirmation did not appear.

Twin bitkit-ios#741 is on 43af31d. Not run on a simulator this round.

Findings

No findings.

Checked and clean

  • A higher order fee updates the confirm amount and does not pay until the next swipe.
  • A new quote clears the stored order. A retry on the same confirm still reuses it.
  • Paid funding removes Spending Confirm from the back stack. System Back from the in-progress screen opened the amount screen.
  • The hardware journey line matches the second Open Trezor Connect tap when the fee is higher.

QA LGTM

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

tAck

Not tested: Hardware wallet

@jvsena42
jvsena42 merged commit 1ab1f03 into master Sep 22, 2026
19 checks passed
@jvsena42
jvsena42 deleted the fix/1243-order-on-confirm branch September 22, 2026 17:50
jvsena42 added a commit to guzino/bitkit-android that referenced this pull request Sep 23, 2026
Master's synonymdev#1247 removed the order this link named, so the conflicts could
only be resolved by reworking the link onto the quote flow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

fix: create order only on swipe

4 participants