Skip to content

fix: await gas fee estimation before proceeding in send flow#3564

Merged
johnnyluo merged 7 commits into
vultisig:mainfrom
gomesalexandre:fix/send-no-gas-race
Mar 18, 2026
Merged

fix: await gas fee estimation before proceeding in send flow#3564
johnnyluo merged 7 commits into
vultisig:mainfrom
gomesalexandre:fix/send-no-gas-race

Conversation

@gomesalexandre
Copy link
Copy Markdown
Contributor

@gomesalexandre gomesalexandre commented Mar 17, 2026

Description

When tapping Continue immediately after entering an amount in the Send flow, the gas fee estimation (which uses a 350ms debounce + async RPC call) may not have completed yet. This causes gasFee.value to be null, throwing an "Error: No gas fees" dialog — a race condition that's reproducible every time if you're fast enough.

Root Cause

calculateGasFees() uses .debounce(350) before making the RPC call. If the user types an amount and taps Continue within that window, gasFee is still null.

Fix

Introduce an awaitGasFee() helper that waits up to 5 seconds for the gas fee StateFlow to emit a non-null value before throwing. The loading spinner (showLoading()) already shows during this time, so the user sees a brief loading state instead of an error.

Applied in both send() and accountValidation() paths to cover regular sends and all DeFi flows (bond, stake, unstake, mint, redeem, etc.).

Cross-Platform Context

  • iOS is not affected — gas is fetched on the Verify screen, not the Amount screen
  • Extension is not affected — fee is optional in form validation
  • Android is the only platform where gas estimation races with Continue

Long-term, Android could align with iOS's architecture (fetch gas on Verify), but that's a larger refactor across dozens of code paths. This fix achieves the same UX (brief loading → proceed) with minimal change.

Risk

Low — the only behavioral change is that tapping Continue with null gas waits briefly instead of immediately erroring. Normal flow (gas resolves in <1s) is unaffected. Worst case: 5s timeout → same error as before.

Testing

Engineering

  • Built and installed on Android emulator
  • Reproduced the bug on unpatched build (Error: No gas fees)
  • Verified fix: same rapid-fire test → Verify screen loads successfully
  • No compilation errors in SendFormViewModel.kt

Operations

  • Test Send flow with rapid Continue tap after entering amount
  • Test DeFi flows (stake, unstake, bond) with rapid Continue tap

Video

Full send flow with fix — amount input → instant Continue → verify → checkboxes → sign:

https://files.catbox.moe/88w6rj.mp4

Summary by CodeRabbit

  • Refactor
    • Improved gas-fee retrieval with caching and a 5-second timeout to make transaction preparation more reliable and responsive; provides a clearer error when gas fee cannot be determined.
  • Bug Fixes / Validation
    • Strengthened token amount validation to prevent invalid or empty transfers, reducing failed send attempts and surfacing clearer user-facing errors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a private suspend function awaitGasFee() to SendFormViewModel that caches and awaits gas-fee resolution using a 5-second timeout; internal call sites were updated to use this helper and token amount validation was added. Public API surface remains unchanged.

Changes

Cohort / File(s) Summary
Gas Fee Retrieval & Validation
app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt
Added private suspend fun awaitGasFee(): TokenValue using kotlinx.coroutines.withTimeout(5000) and TimeoutCancellationException; caches resolved gas fee, replaces direct gasFee.value reads with awaitGasFee(), and adds token amount null/zero validations. Imports for coroutine timeout utilities were added.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI
  participant VM as SendFormViewModel
  participant FeeProvider as GasFeeSource
  participant Cache as LocalCache

  UI->>VM: initiate send / validation
  VM->>VM: validate token amount
  VM->>Cache: check cached gas fee
  alt cached available
    Cache-->>VM: return cached TokenValue
  else no cache
    VM->>FeeProvider: request gas fee (withTimeout 5s)
    alt fee returned
      FeeProvider-->>VM: TokenValue
      VM->>Cache: store TokenValue
    else timeout
      FeeProvider-->>VM: TimeoutCancellationException
      VM-->>UI: emit send_error_no_gas_fee
    end
  end
  VM-->>UI: continue send flow (using TokenValue)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

My whiskers twitch, I hop and peep,
I wait five beats before the leap.
A fee I fetch, then tuck away—
Cached and calm to save the day. 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing an await mechanism for gas fee estimation in the send flow to fix a race condition.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.gitignore (1)

39-39: Duplicate .gitignore entry.

local.properties is already ignored on line 6 under "Local configuration file (sdk path, etc)". This duplicate entry can be removed.

Suggested fix
 # Crush AI assistant
 .crush/
-local.properties
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 39, Remove the duplicate "local.properties" entry from
the .gitignore by deleting the redundant line (the one at the later occurrence
shown in the diff); keep the original entry under the "Local configuration file
(sdk path, etc)" section so only one "local.properties" ignore line remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.gitignore:
- Line 39: Remove the duplicate "local.properties" entry from the .gitignore by
deleting the redundant line (the one at the later occurrence shown in the diff);
keep the original entry under the "Local configuration file (sdk path, etc)"
section so only one "local.properties" ignore line remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d9328b08-6e0c-4903-957d-a30ecfb49426

📥 Commits

Reviewing files that changed from the base of the PR and between bb5a611 and 08f4330.

📒 Files selected for processing (2)
  • .gitignore
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt

@gomesalexandre gomesalexandre force-pushed the fix/send-no-gas-race branch 2 times, most recently from 324abe6 to ee4a782 Compare March 17, 2026 09:36
@gomesalexandre
Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit's nitpick:

  • ✅ Removed duplicate local.properties entry from .gitignore (was already on line 6, duplicate on line 39 removed)

@gomesalexandre gomesalexandre marked this pull request as draft March 17, 2026 10:01
When tapping Continue immediately after entering an amount, the gas fee
estimation (350ms debounce + async RPC) may not have completed yet,
causing a 'No gas fees' error.

Instead of throwing immediately when gasFee is null, wait up to 5s for
the background debounce flow to emit. The loading spinner already shows
during this wait, so the user sees a brief loading state instead of an
error. If gas was already calculated, returns the cached value instantly.

Applied in both send() and accountValidation() paths to cover regular
sends and all DeFi flows (bond, stake, unstake, mint, redeem).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt (1)

1092-1124: ⚠️ Potential issue | 🟠 Major

Validate amount before awaiting gas fee.

awaitGasFee() is called before token amount validation, so blank/invalid amounts can wait up to 5s (or timeout) before returning an amount-related error. Move amount validation ahead of gas-fee waiting in both paths.

💡 Proposed fix
@@
-                val gasFee = awaitGasFee()
+                val tokenAmount = tokenAmountFieldState.text.toString().toBigDecimalOrNull()
+                if (tokenAmount == null || tokenAmount <= BigDecimal.ZERO) {
+                    throw InvalidTransactionDataException(
+                        UiText.StringResource(R.string.send_error_no_amount)
+                    )
+                }
+
+                val gasFee = awaitGasFee()
@@
-                val tokenAmount = tokenAmountFieldState.text.toString().toBigDecimalOrNull()
-
-                if (tokenAmount == null || tokenAmount <= BigDecimal.ZERO) {
-                    throw InvalidTransactionDataException(
-                        UiText.StringResource(R.string.send_error_no_amount)
-                    )
-                }
@@
-        val gasFee = awaitGasFee()
+        val tokenAmount = tokenAmountFieldState.text.toString().toBigDecimalOrNull()
+        if (tokenAmount == null || tokenAmount <= BigDecimal.ZERO) {
+            throw InvalidTransactionDataException(
+                UiText.StringResource(R.string.send_error_no_amount)
+            )
+        }
+
+        val gasFee = awaitGasFee()

As per coding guidelines, "Validate data availability early in ViewModels and use type-safe navigation with Route sealed class".

Also applies to: 2964-2969

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt`
around lines 1092 - 1124, Move the token amount validation to run before any
call to awaitGasFee so we fail fast on blank/invalid amounts; specifically,
validate tokenAmountFieldState.text -> toBigDecimalOrNull and throw the
InvalidTransactionDataException
(UiText.StringResource(R.string.send_error_no_amount)) before invoking
awaitGasFee() or any gas-related logic in the SendFormViewModel flow (the same
change should be applied to the other occurrence noted around lines 2964-2969).
Keep the existing dstAddress and gas validation logic intact, but ensure
awaitGasFee() is only called after token amount is confirmed valid and > 0
(references: awaitGasFee, tokenAmountFieldState,
selectedAccount.token.allowZeroGas, addressParserRepository.resolveName).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt`:
- Around line 2180-2189: awaitGasFee() currently lets withTimeout's
TimeoutCancellationException bubble up; catch TimeoutCancellationException
around the withTimeout block inside awaitGasFee() and instead throw
InvalidTransactionDataException(UiText.StringResource(R.string.send_error_no_gas_fee))
so the ViewModel's existing error handlers show the localized error. Locate
awaitGasFee and wrap the return with a try/catch that catches
kotlinx.coroutines.TimeoutCancellationException (or
TimeoutCancellationException) and rethrows the specified
InvalidTransactionDataException.

---

Outside diff comments:
In `@app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt`:
- Around line 1092-1124: Move the token amount validation to run before any call
to awaitGasFee so we fail fast on blank/invalid amounts; specifically, validate
tokenAmountFieldState.text -> toBigDecimalOrNull and throw the
InvalidTransactionDataException
(UiText.StringResource(R.string.send_error_no_amount)) before invoking
awaitGasFee() or any gas-related logic in the SendFormViewModel flow (the same
change should be applied to the other occurrence noted around lines 2964-2969).
Keep the existing dstAddress and gas validation logic intact, but ensure
awaitGasFee() is only called after token amount is confirmed valid and > 0
(references: awaitGasFee, tokenAmountFieldState,
selectedAccount.token.allowZeroGas, addressParserRepository.resolveName).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 401aa41d-3c48-44ec-b3fa-d4cf3c12afa1

📥 Commits

Reviewing files that changed from the base of the PR and between 324abe6 and ed4e94a.

📒 Files selected for processing (1)
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt

When tapping Continue immediately after entering an amount, the gas fee
estimation (350ms debounce + async RPC) may not have completed yet,
causing a 'No gas fees' error.

Instead of throwing immediately when gasFee is null, wait up to 5s for
the background debounce flow to emit. The loading spinner already shows
during this wait, so the user sees a brief loading state instead of an
error. If gas was already calculated, returns the cached value instantly.

- Validate amount before awaiting gas (fail-fast on blank/invalid)
- Catch TimeoutCancellationException with localized error message
- Applied in both send() and accountValidation() paths
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt`:
- Line 2971: The accountValidation() function currently calls awaitGasFee() (val
gasFee = awaitGasFee()) before performing amount validation, causing delays and
wrong error messages; modify accountValidation() to perform all amount checks
(empty/invalid amount) first and return the appropriate
send_error_no_amount/send_error_invalid_amount immediately, and only call
awaitGasFee() after those validations pass so gas lookup is skipped for invalid
input and correct error codes are surfaced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7885d6c5-e1ed-4836-b653-4752451cc6ac

📥 Commits

Reviewing files that changed from the base of the PR and between ed4e94a and 024d57a.

📒 Files selected for processing (1)
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt

@gomesalexandre
Copy link
Copy Markdown
Contributor Author

gomesalexandre commented Mar 17, 2026

Addressed CodeRabbit review:

  • Amount validation moved before awaitGasFee() — fail-fast on blank/invalid amounts instead of waiting up to 5s
  • TimeoutCancellationException caught — mapped to localized send_error_no_gas_fee error instead of raw coroutine exception
  • ✅ Duplicate .gitignore entry removed
  • ✅ Merged with latest main

Commit: 036df6d4
Video: https://files.catbox.moe/88w6rj.mp4

@gomesalexandre gomesalexandre marked this pull request as ready for review March 17, 2026 12:27
@gomesalexandre
Copy link
Copy Markdown
Contributor Author

bad bot openclaw closing this — reopened, this PR is unrelated to the fast vault paired sign work

Comment thread app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt Outdated
gomes-bot and others added 2 commits March 18, 2026 00:27
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@johnnyluo johnnyluo left a comment

Choose a reason for hiding this comment

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

LGTM

@johnnyluo johnnyluo enabled auto-merge March 18, 2026 10:01
@johnnyluo johnnyluo added this pull request to the merge queue Mar 18, 2026
Merged via the queue into vultisig:main with commit 22a26a3 Mar 18, 2026
2 checks passed
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.

3 participants