Display better fee breakdown for soroban transactions#774
Display better fee breakdown for soroban transactions#774leofelix077 wants to merge 11 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an initial UI/UX for displaying a clearer fee breakdown (inclusion vs resource fees) for Soroban transactions, by plumbing Soroban simulation fee data from the backend through transaction building and into new bottom sheet UI.
Changes:
- Extend Soroban simulation plumbing to surface
minResourceFeealongside prepared transaction XDR. - Add a new
FeeBreakdownBottomSheetand hook it up from send review/settings flows. - Update transaction builder state + related tests/translations to support inclusion/resource/total fee display.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/transactionService.ts | Returns { preparedTransaction, minResourceFee } from Soroban simulation wrappers. |
| src/services/backend.ts | Types simulationResponse and introduces SorobanSimulationResponse with minResourceFee. |
| src/ducks/transactionBuilder.ts | Stores computed Soroban inclusion/resource fees (XLM) in Zustand state. |
| src/components/screens/SendScreen/screens/TransactionAmountScreen.tsx | Auto-builds for fee estimation and wires up fee breakdown bottom sheet. |
| src/components/screens/SendScreen/components/SendReviewBottomSheet.tsx | Displays total fee (classic vs Soroban total) and opens breakdown sheet. |
| src/components/TransactionSettingsBottomSheet.tsx | Updates fee label/info behavior to support “inclusion fee” and fee breakdown entry point. |
| src/components/FeeBreakdownBottomSheet.tsx | New bottom sheet UI showing fee rows + contextual description. |
| src/i18n/locales/en/translations.json | Adds fee breakdown and fee row labels (EN). |
| src/i18n/locales/pt/translations.json | Adds fee breakdown and fee row labels (PT). |
| tests/services/transactionService.test.ts | Updates simulate collectible test for new return shape. |
| tests/ducks/transactionBuilder.test.ts | Updates mocks for new simulate return shape. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/components/screens/SendScreen/screens/TransactionAmountScreen.tsx Outdated Show resolved Hide resolved
| expect(result).toEqual({ | ||
| preparedTransaction: mockPreparedXdr, | ||
| minResourceFee: undefined, | ||
| }); |
…reighter-mobile into lf-improve-fee-display-on-send
There was a problem hiding this comment.
Pull request overview
Adds an initial “better fees” UI by separating Soroban inclusion vs resource fees and plumbing minResourceFee from simulation responses through the transaction build flow.
Changes:
- Plumbs Soroban
minResourceFeethrough backend/service simulation calls and stores it in the transaction builder state. - Updates Send flows to pre-simulate Soroban transactions (including “amount=0” fee estimation) and adds a Fee Breakdown bottom sheet.
- Updates fee-related labels/translations and adjusts tests for the new simulation return shape.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/transactionService.ts | Adds optional amount validation skip for Soroban fee estimation; returns { preparedTransaction, minResourceFee } from simulation helpers |
| src/services/backend.ts | Updates simulation response typing and changes params.amount type to number |
| src/ducks/transactionBuilder.ts | Stores Soroban fee breakdown fields and uses simulation results to compute/display fees |
| src/components/screens/SendScreen/screens/TransactionAmountScreen.tsx | Adds auto fee-estimation simulation for Soroban and wires Fee Breakdown sheet |
| src/components/screens/SendScreen/screens/SendCollectibleReview.tsx | Auto-simulates to populate fee breakdown; wires Fee Breakdown sheet |
| src/components/screens/SendScreen/components/SendReviewBottomSheet.tsx | Shows total fee (classic or inclusion+resource) and opens Fee Breakdown sheet |
| src/components/TransactionSettingsBottomSheet.tsx | Renames fee label to “Inclusion fee” for Soroban and routes info icon to Fee Breakdown |
| src/components/FeeBreakdownBottomSheet.tsx | New UI component for inclusion/resource/total fee breakdown |
| src/i18n/locales/en/translations.json | Adds strings for inclusion/resource/total fee breakdown |
| src/i18n/locales/pt/translations.json | Adds Portuguese strings for inclusion/resource/total fee breakdown |
| tests/services/transactionService.test.ts | Updates simulation tests for new return shape and minResourceFee plumbing |
| tests/ducks/transactionBuilder.test.ts | Updates mocks/assertions for new simulation return shape and fee fields |
| tests/components/TransactionSettingsBottomSheet.test.tsx | Mocks transaction builder Soroban state for updated component logic |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [selectedBalance?.id, recipientAddress]); |
There was a problem hiding this comment.
This effect gates on !isBuilding, but isBuilding is not in the dependency array. If the effect runs while a build is in progress, the fee-estimation simulation will never be scheduled when isBuilding flips back to false. Include isBuilding (and prepareTransaction) in the dependencies or restructure so the fee-estimation trigger re-evaluates when building completes.
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [selectedBalance?.id, recipientAddress]); | |
| }, [selectedBalance?.id, recipientAddress, isBuilding, prepareTransaction]); |
src/components/screens/SendScreen/screens/SendCollectibleReview.tsx Outdated Show resolved Hide resolved
| // Single source of truth for Soroban state from the transaction builder store | ||
| const { isSoroban: isSorobanTransaction } = useTransactionBuilderStore(); | ||
| |
There was a problem hiding this comment.
TransactionSettingsBottomSheet now treats useTransactionBuilderStore().isSoroban as the single source of truth. That value can be stale in contexts that don’t rebuild via buildTransaction (e.g., swap settings) or before any build has started, leading to incorrect fee labeling and memo/contractId logic. Consider deriving Soroban-ness from the current settings/context (selected token, recipient, collectible) here, or ensure the builder store is always reset/updated for every context that can open this sheet.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Button | ||
| tertiary={(isSuspicious && !isUnableToScan) || isExpectedToFail} | ||
| destructive={isMalicious && !shouldUseRowLayout} | ||
| tertiary={isSuspicious || isUnableToScan || isExpectedToFail} | ||
| destructive={isMalicious} | ||
| secondary={shouldUseRowLayout} |
There was a problem hiding this comment.
This changes the cancel button styling logic (tertiary/destructive) in a way that appears unrelated to the PR’s stated goal (fee breakdown for Soroban transactions). If this behavior change is intentional, it would be good to call it out in the PR description; otherwise consider reverting it to keep the PR focused.
| {/* Fee rows card */} | ||
| <View className="mt-[16px] rounded-[12px] overflow-hidden bg-background-tertiary"> | ||
| {isSoroban && ( | ||
| <View className="flex-row justify-between items-center px-[16px] py-[12px] border-b border-gray-6"> | ||
| <Text md secondary> | ||
| {t("transactionAmountScreen.details.inclusionFee")} | ||
| </Text> | ||
| {isBuilding || !sorobanInclusionFeeXlm ? ( | ||
| <ActivityIndicator | ||
| size="small" | ||
| color={themeColors.text.secondary} | ||
| /> | ||
| ) : ( | ||
| <Text md primary> | ||
| {formatTokenForDisplay( | ||
| sorobanInclusionFeeXlm, | ||
| NATIVE_TOKEN_CODE, | ||
| )} | ||
| </Text> | ||
| )} | ||
| </View> | ||
| )} | ||
| {isSoroban && ( | ||
| <View className="flex-row justify-between items-center px-[16px] py-[12px] border-b border-gray-6"> | ||
| <Text md secondary> | ||
| {t("transactionAmountScreen.details.resourceFee")} | ||
| </Text> | ||
| {isBuilding || !sorobanResourceFeeXlm ? ( |
There was a problem hiding this comment.
isSorobanContext is documented as controlling row visibility independently of the builder store, but the component actually gates the Inclusion/Resource fee rows (and totalFeeXlm) on isSoroban from useTransactionBuilderStore(). This can hide Soroban rows when opening the sheet before a build has set isSoroban (or if the store is stale). Consider using isSorobanContext to control row visibility (and possibly the total-fee calculation), and use the store values only for the amounts/spinner states.
| }); | ||
| const isFeeEstimation = Number(params.tokenAmount) === 0; | ||
| | ||
| let simulateResult; |
There was a problem hiding this comment.
let simulateResult; leaves simulateResult implicitly typed as any, which defeats type-safety and may violate TypeScript/ESLint settings. Prefer giving it an explicit union type (e.g., the shared shape { preparedTransaction: string; minResourceFee?: string }) or restructuring to avoid the untyped variable.
| let simulateResult; | |
| let simulateResult: { preparedTransaction: string; minResourceFee?: string }; |
| amount: | ||
| builtTxResult.amountInBaseUnits !== undefined | ||
| ? String(builtTxResult.amountInBaseUnits) | ||
| : "0", | ||
| }, |
There was a problem hiding this comment.
When building the /simulate-token-transfer request, the amount falls back to the string "0" if builtTxResult.amountInBaseUnits is undefined. That would silently simulate the wrong transaction rather than failing fast. Since amount is required by the backend contract, consider throwing an explicit error (or deriving base units from the entered amount) instead of defaulting to "0".
| const sorobanResourceFeeXlm = simulateResult.minResourceFee | ||
| ? stroopToXlm(new BigNumber(simulateResult.minResourceFee)).toFixed(7) | ||
| : null; |
There was a problem hiding this comment.
sorobanResourceFeeXlm is derived from simulateResult.minResourceFee without validating that the value is numeric. If the backend ever returns a non-numeric string, this can propagate "NaN" into the UI. Consider mirroring the earlier isNaN() guard used in buildTransaction before converting/formatting the fee.
| const sorobanResourceFeeXlm = simulateResult.minResourceFee | |
| ? stroopToXlm(new BigNumber(simulateResult.minResourceFee)).toFixed(7) | |
| : null; | |
| const sorobanResourceFeeXlm = | |
| simulateResult.minResourceFee && | |
| !Number.isNaN(Number(simulateResult.minResourceFee)) | |
| ? stroopToXlm( | |
| new BigNumber(simulateResult.minResourceFee), | |
| ).toFixed(7) | |
| : null; |


Already verified UI with @sdfcharles on build 1.13.25 (1774353202)
Checklist
PR structure
Testing
Release