Improve fees display on send#2649
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves the resource fee display on the send flow by adding a detailed fee breakdown pane to the review transaction screen, showing inclusion fee and resource fee separately for Soroban transactions.
Changes:
- Added a new "Fee breakdown" pane in
ReviewTransactionshowing inclusion fee, resource fee, and total fee with contextual descriptions for classic vs Soroban transactions - Propagated
inclusionFeeandresourceFeefrom simulation results through both send and collectible flows - Added "Calculating..." loading state for fees and a loading indicator on the continue button during simulation
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx | New fees breakdown pane with fee detail rows and info button |
| extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss | Styles for the fees breakdown pane and info button |
| extension/src/popup/components/send/SendAmount/index.tsx | Show "Calculating..." during simulation, loading state on button |
| extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx | Pass inclusionFee and resourceFee from simulation |
| extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts | Pass inclusionFee and resourceFee from simulation |
| extension/src/popup/locales/en/translation.json | New i18n keys for fee breakdown UI |
| extension/src/popup/locales/pt/translation.json | Portuguese translations for new keys (with one regression) |
| @shared/api/internal.ts | Added network_url to simulate-tx request body |
| extension/e2e-tests/reviewTxFees.test.ts | E2E tests for fee breakdown in token, classic, and collectible sends |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
….com:stellar/freighter into feature/improve-resource-fee-display-on-send
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@leofelix077 looks nice! few notes:
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…//github.com/stellar/freighter into feature/improve-resource-fee-display-on-send
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…//github.com/stellar/freighter into feature/improve-resource-fee-display-on-send
There was a problem hiding this comment.
Found one last issue that would be worth addressing on this PR before merging it, specially since same bug is being addressed on the mobile PR so we keep parity.
| memo, | ||
| params, | ||
| networkDetails, | ||
| transactionFee, |
There was a problem hiding this comment.
This is an existing bug from before this PR but I think is worth addressing since it's related to fee handling (this is already fixed on mobile's PR): it looks like the transactionFee is being passed here but it's being discarded inside simulateTokenTransfer which means it's not being passed to the backend so the backend is always using the default fee of 100 stroops. This means the inclusion fee users are setting on transaction settings are not being effectively used.
Below is a more in-depth analysis from AI:
The user-selected inclusion fee is dropped before reaching the backend on the indexer/mainnet path.
transactionFee is correctly threaded into simulateTokenTransfer({...transactionFee}) here, but in @shared/api/internal.ts the non-custom-network branch (around line 2186) builds requestBody without including any fee field:
const requestBody = {
address,
pub_key: publicKey,
memo: memo || "",
params,
network_passphrase: networkDetails.networkPassphrase,
// fee is NOT included
};The freighter-backend /simulate-token-transfer handler accepts an optional fee and falls back to BASE_FEE when absent:
const _fee = fee || Sdk.BASE_FEE;
const builder = new Sdk.TransactionBuilder(sourceAccount, { fee: _fee, ... });
// ... assembleTransaction → returned preparedTransaction.fee = _fee + minResourceFeeSince the client submits the returned preparedTransaction XDR verbatim, the on-chain inclusion bid is always BASE_FEE (100 stroops) regardless of what the user picks in the slider. The new FeesPane shows the user's selected value as the inclusion fee, but the chain sees BASE_FEE.
Impact: invisible during normal traffic (BASE_FEE suffices), but during surge pricing — the case the slider exists for — bumping the fee has no effect; the tx loses inclusion auctions and times out.
Fix: add one line to the request body in internal.ts:
const requestBody = {
address,
pub_key: publicKey,
memo: memo || "",
fee: xlmToStroop(transactionFee).toFixed(),
params,
network_passphrase: networkDetails.networkPassphrase,
};The mobile companion PR (stellar/freighter-mobile#774) made this same change (fee: xlmToStroop(fee).toString()), and the freighter-backend already supports the field.
Code reviewFound 2 issues:
freighter/extension/src/popup/components/send/SendAmount/index.tsx Lines 558 to 567 in 95e6819
freighter/extension/src/popup/components/send/SendAmount/index.tsx Lines 509 to 532 in 95e6819 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
|
Also, I noticed a few UI bugs here: 1). If you update the fee and then click the i bubble, the modal shows the old fee Screen.Recording.2026-04-28.at.5.12.00.PM.mov |
|
@piyalbasu @CassioMG thanks for checking! I updated it now the UI + verifying that it's carrying forward the fee to the simulation Screen.Recording.2026-04-29.at.12.57.28.mov
|
There was a problem hiding this comment.
Re-reviewed at e9e5a7a. The previous concern about fee being dropped before reaching the backend is resolved — the new fee: xlmToStroop(transactionFee).toFixed() line in @shared/api/internal.ts and the test in @shared/api/__tests__/internal.test.ts confirm the wiring end-to-end.
Two Important findings, and a Small finding remain — see inline comments.
There was a problem hiding this comment.
Important — this hook misses the first-mount fee fix that was applied to the send hook.
Lines 109-110 (outside this PR's diff hunks, hence the file-level comment) still use the old pattern:
const currentTransactionFee =
currentTransactionData.transactionFee || transactionFee;The new getCurrentTransactionFee helper added in send/SendAmount/hooks/useSimulateTxData.tsx:123-132 (with the BASE_FEE fallback) was the fix for the first-mount-fee-zero issue I flagged at #2649 (comment). The send hook now uses it; this hook does not.
Why it matters here too: on first-mount of a collectible send, currentTransactionData.transactionFee = "" and transactionFee = "", so currentTransactionFee = "". Then simulateTx (line 124) receives recommendedFee: "" → simulateSendCollectible is called with transactionFee: "" → xlmToStroop("").toFixed() === "0" is set as the TransactionBuilder fee (@shared/api/internal.ts:2080). stellar-sdk's TransactionBuilder rejects fees below BASE_FEE at submit time.
Why it's mostly latent today: like the send case, handleContinue paths typically reset transactionFee before re-simulation, so the user-submitted XDR is from a re-sim with a proper fee. But the first sim still wastes a build cycle and produces an invalid XDR, and any future code path that submits the first-sim output silently breaks.
Suggested fix: export getCurrentTransactionFee from a shared location (e.g. popup/helpers/fees.ts or popup/helpers/soroban.ts) and use it here too:
import { getCurrentTransactionFee } from "popup/helpers/fees";
const currentTransactionFee = getCurrentTransactionFee({
currentTransactionFee: currentTransactionData.transactionFee,
fallbackTransactionFee: transactionFee,
});Keeping the helper inside the send/SendAmount hook directory while the collectible hook needs it would force this hook to reach across component boundaries, which is a smell — the helper lives in the wrong place even before the parity fix.
| import { NetworkDetails } from "@shared/constants/stellar"; | ||
| import { stroopToXlm } from "helpers/stellar"; | ||
| import { getBaseAccount } from "popup/helpers/account"; | ||
| import { getCurrentTransactionFee } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; |
There was a problem hiding this comment.
should we extract it to a helper as suggested here?





closes #2475
Screen.Recording.2026-03-13.at.11.42.36.mov
Screen.Recording.2026-03-13.at.11.43.27.mov