From 53038619f60fcf46f88e8eb6d1ac804290d87c9b Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 11 Mar 2026 15:00:15 -0300 Subject: [PATCH 01/21] add initial version of displaying better minResourceFee --- @shared/api/internal.ts | 1 + .../ReviewTransaction/index.tsx | 67 +++++++++- .../ReviewTransaction/styles.scss | 116 ++++++++++++++++++ .../SendAmount/hooks/useSimulateTxData.tsx | 11 ++ .../components/send/SendAmount/index.tsx | 11 +- .../src/popup/locales/en/translation.json | 7 ++ .../src/popup/locales/pt/translation.json | 7 ++ 7 files changed, 214 insertions(+), 6 deletions(-) diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 981b71eb82..3ddea53dc6 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -2208,6 +2208,7 @@ export const simulateTransaction = async (args: { body: JSON.stringify({ xdr, network_passphrase: networkDetails.networkPassphrase, + network_url: networkDetails.networkUrl, }), }; const res = await fetch(`${INDEXER_URL}/simulate-tx`, options); diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 18713a7711..099b3165d3 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -190,11 +190,13 @@ export const ReviewTx = ({ blockaidIndex: null, reviewIndex: 0, memoIndex: 1, + feesIndex: 2, } : { blockaidIndex: 2, reviewIndex: 0, memoIndex: 1, + feesIndex: 3, }, [shouldShowTxWarning], ); @@ -364,6 +366,13 @@ export const ReviewTx = ({
{t("Fee")} +
); + const feesPane = ( +
+
+
+ +
+
setActivePaneIndex(paneConfig.reviewIndex)} + > + +
+
+
+ {t("Fees")} +
+
+ {simulationState.data?.inclusionFee && ( +
+ + {t("Inclusion Fee")} + + + {simulationState.data.inclusionFee} XLM + +
+ )} + {simulationState.data?.resourceFee && ( +
+ + {t("Resource Fee")} + + + {simulationState.data.resourceFee} XLM + +
+ )} +
+ + {t("Total Fee")} + + + {fee} XLM + +
+
+
+ {simulationState.data?.resourceFee + ? t("Fees description soroban") + : t("Fees description classic")} +
+
+ ); + // Build panes in order (no hooks on JSX) const panes: React.ReactNode[] = []; if (shouldShowTxWarning) { - panes.push(reviewPane, memoPane, blockaidPane); + panes.push(reviewPane, memoPane, blockaidPane, feesPane); } else { - panes.push(reviewPane, memoPane); + panes.push(reviewPane, memoPane, feesPane); } return ( diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index 5b81a19162..bd811b9bb5 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -255,4 +255,120 @@ line-height: 1.5rem; } } + + &__FeesDetails { + display: flex; + flex-direction: column; + width: 100%; + padding: 0 1rem; + + &__Header { + display: flex; + justify-content: space-between; + margin: pxToRem(12px) 0; + + &__Icon { + display: flex; + width: 40px; + height: 40px; + padding: 7.5px; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + border-radius: 8px; + background: var(--sds-clr-purple-03); + + svg { + width: 25px; + height: 25px; + color: var(--sds-clr-purple-11); + } + } + + &__Close { + display: flex; + width: 32px; + height: 32px; + justify-content: center; + align-items: center; + border-radius: 1000px; + background: var(--sds-clr-gray-03); + color: var(--sds-clr-gray-09); + cursor: pointer; + } + } + + &__Title { + display: flex; + align-items: center; + width: 100%; + margin: pxToRem(12px) 0; + font-size: pxToRem(20px); + font-weight: var(--font-weight-medium); + } + + &__Card { + display: flex; + flex-direction: column; + border-radius: pxToRem(16px); + background-color: var(--sds-clr-gray-03); + padding: pxToRem(12px); + + &__Row { + display: flex; + justify-content: space-between; + align-items: center; + padding: pxToRem(12px) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--sds-clr-gray-06); + } + + &--total { + padding-top: pxToRem(12px); + } + + &__Label { + font-size: pxToRem(14px); + color: var(--sds-clr-gray-11); + } + + &__Value { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + + &--total { + color: var(--sds-clr-purple-11); + } + } + } + } + + &__Description { + margin-top: pxToRem(16px); + color: var(--sds-clr-gray-11); + font-size: pxToRem(14px); + line-height: 1.5rem; + } + } + + &__Details__Row__FeesInfoBtn { + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-left: pxToRem(4px); + display: inline-flex; + align-items: center; + color: var(--sds-clr-gray-09); + + svg { + width: pxToRem(14px); + height: pxToRem(14px); + } + + &:hover { + color: var(--sds-clr-gray-12); + } + } } diff --git a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx index 52482bc9d0..0933b07263 100644 --- a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx +++ b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx @@ -72,6 +72,8 @@ interface SimSoroban { export interface SimulateTxData { transactionXdr: string; scanResult?: BlockAidScanTxResult | null; + inclusionFee?: string; + resourceFee?: string; } const CREATE_ACCOUNT_MIN_XLM = new BigNumber(1); @@ -341,6 +343,8 @@ const simulateTx = async ({ return { payload: response, recommendedFee: baseFee.plus(new BigNumber(minResourceFee)).toString(), + inclusionFee: baseFee.toString(), + resourceFee: minResourceFee, }; } @@ -545,6 +549,13 @@ function useSimulateTxData({ }), ); + if (simResponse.inclusionFee !== undefined) { + payload.inclusionFee = simResponse.inclusionFee; + } + if (simResponse.resourceFee !== undefined) { + payload.resourceFee = simResponse.resourceFee; + } + const scanUrlstub = "internal"; if (simParams.type === "classic") { const { diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 60d81a1253..83c2bc1790 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -429,9 +429,12 @@ export const SendAmount = ({ {t("Fee")}: - {inputType === "crypto" - ? `${fee} ${t("XLM")}` - : recommendedFeeUsd} + {(isToken || isCollectible) && + simulationState.state === RequestState.LOADING + ? t("Calculating...") + : inputType === "crypto" + ? `${fee} ${t("XLM")}` + : recommendedFeeUsd}
@@ -464,7 +467,7 @@ export const SendAmount = ({ + )} + + ); + return (
{title &&

{title}

} - + {({ errors, setFieldValue }) => ( <>
@@ -57,7 +82,7 @@ export const EditSettings = ({ autoComplete="off" id="fee" placeholder={t("Fee")} - label={t("Transaction Fee")} + label={feeLabel} {...field} error={errors.fee} onChange={(e) => { diff --git a/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss b/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss index eaa3055e74..8dc587b683 100644 --- a/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss +++ b/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss @@ -28,6 +28,31 @@ } } + &__fee-label { + display: inline-flex; + align-items: center; + gap: pxToRem(4px); + } + + &__fee-info-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + color: var(--sds-clr-gray-09); + + svg { + width: pxToRem(14px); + height: pxToRem(14px); + } + + &:hover { + color: var(--sds-clr-gray-12); + } + } + &__actions { display: flex; margin-top: pxToRem(32px); diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx new file mode 100644 index 0000000000..03ae754a60 --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { Icon } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { RequestState, State } from "constants/request"; +import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; + +import "./styles.scss"; + +export interface FeesPaneProps { + fee: string; + simulationState: State; + onClose: () => void; +} + +export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => { + const { t } = useTranslation(); + + const isLoading = + simulationState.state === RequestState.IDLE || + simulationState.state === RequestState.LOADING; + + return ( +
+
+
+ +
+
+ +
+
+
+ {t("Fees")} +
+
+ {!isLoading && simulationState.data?.inclusionFee && ( +
+ + {t("Inclusion Fee")} + + + {simulationState.data.inclusionFee} XLM + +
+ )} + {!isLoading && simulationState.data?.resourceFee && ( +
+ + {t("Resource Fee")} + + + {simulationState.data.resourceFee} XLM + +
+ )} +
+ + {t("Total Fee")} + + + {isLoading ? t("Calculating...") : `${fee} XLM`} + +
+
+
+ {simulationState.data?.resourceFee + ? t("Fees description soroban") + : t("Fees description classic")} +
+
+ ); +}; diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss new file mode 100644 index 0000000000..29481df9ff --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -0,0 +1,97 @@ +@use "../../../styles/utils.scss" as *; + +.FeesPane { + display: flex; + flex-direction: column; + width: 100%; + padding: pxToRem(16px) 0; + gap: pxToRem(16px); + + &__Header { + display: flex; + justify-content: space-between; + align-items: center; + + &__Icon { + display: flex; + width: pxToRem(32px); + height: pxToRem(32px); + justify-content: center; + align-items: center; + border-radius: pxToRem(8px); + background: var(--sds-clr-lilac-03); + border: 1px solid var(--sds-clr-lilac-06); + + svg { + width: pxToRem(20px); + height: pxToRem(20px); + color: var(--sds-clr-lilac-09); + } + } + + &__Close { + display: flex; + width: pxToRem(32px); + height: pxToRem(32px); + justify-content: center; + align-items: center; + border-radius: 1000px; + background: var(--sds-clr-gray-03); + color: var(--sds-clr-gray-09); + cursor: pointer; + } + } + + &__Title { + display: flex; + align-items: center; + width: 100%; + font-size: pxToRem(18px); + font-weight: var(--font-weight-medium); + } + + &__Card { + display: flex; + flex-direction: column; + border-radius: pxToRem(16px); + background-color: var(--sds-clr-gray-03); + padding: pxToRem(12px) pxToRem(16px); + + &__Row { + display: flex; + justify-content: space-between; + align-items: center; + padding: pxToRem(12px) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--sds-clr-gray-06); + } + + &__Label { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + color: var(--sds-clr-gray-11); + + &--total { + color: var(--sds-clr-lilac-11); + } + } + + &__Value { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + color: var(--sds-clr-gray-12); + + &--total { + color: var(--sds-clr-lilac-11); + } + } + } + } + + &__Description { + color: var(--sds-clr-gray-11); + font-size: pxToRem(14px); + line-height: pxToRem(20px); + } +} diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index fd4d3a00c8..0c6ead32d2 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -38,6 +38,7 @@ import { MemoRequiredLabel, } from "popup/components/WarningMessages"; import { CopyValue } from "popup/components/CopyValue"; +import { FeesPane } from "popup/components/InternalTransaction/FeesPane"; import { ActionButtons } from "./components/ActionButtons"; import { SendAsset, SendDestination } from "./components"; @@ -439,70 +440,11 @@ export const ReviewTx = ({ ); const feesPane = ( -
-
-
- -
-
setActivePaneIndex(paneConfig.reviewIndex)} - > - -
-
-
- {t("Fees")} -
-
- {simulationState.data?.inclusionFee && ( -
- - {t("Inclusion Fee")} - - - {simulationState.data.inclusionFee} XLM - -
- )} - {simulationState.data?.resourceFee && ( -
- - {t("Resource Fee")} - - - {simulationState.data.resourceFee} XLM - -
- )} -
- - {t("Total Fee")} - - - {fee} XLM - -
-
-
- {simulationState.data?.resourceFee - ? t("Fees description soroban") - : t("Fees description classic")} -
-
+ setActivePaneIndex(paneConfig.reviewIndex)} + /> ); // Build panes in order (no hooks on JSX) diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index b784baf169..c651fdb1e6 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -58,6 +58,7 @@ flex: 2; display: flex; color: var(--sds-clr-gray-09); + align-items: center; svg { margin-right: pxToRem(6px); @@ -256,102 +257,6 @@ } } - &__FeesDetails { - display: flex; - flex-direction: column; - width: 100%; - padding: 0 pxToRem(24px); - gap: pxToRem(16px); - - &__Header { - display: flex; - justify-content: space-between; - align-items: center; - - &__Icon { - display: flex; - width: pxToRem(32px); - height: pxToRem(32px); - justify-content: center; - align-items: center; - border-radius: pxToRem(8px); - background: var(--sds-clr-lilac-03); - border: 1px solid var(--sds-clr-lilac-06); - - svg { - width: pxToRem(20px); - height: pxToRem(20px); - color: var(--sds-clr-lilac-09); - } - } - - &__Close { - display: flex; - width: pxToRem(32px); - height: pxToRem(32px); - justify-content: center; - align-items: center; - border-radius: 1000px; - background: var(--sds-clr-gray-03); - color: var(--sds-clr-gray-09); - cursor: pointer; - } - } - - &__Title { - display: flex; - align-items: center; - width: 100%; - font-size: pxToRem(18px); - font-weight: var(--font-weight-medium); - } - - &__Card { - display: flex; - flex-direction: column; - border-radius: pxToRem(16px); - background-color: var(--sds-clr-gray-03); - padding: pxToRem(12px) pxToRem(16px); - - &__Row { - display: flex; - justify-content: space-between; - align-items: center; - padding: pxToRem(12px) 0; - - &:not(:last-child) { - border-bottom: 1px solid var(--sds-clr-gray-06); - } - - &__Label { - font-size: pxToRem(14px); - font-weight: var(--font-weight-medium); - color: var(--sds-clr-gray-11); - - &--total { - color: var(--sds-clr-lilac-11); - } - } - - &__Value { - font-size: pxToRem(14px); - font-weight: var(--font-weight-medium); - color: var(--sds-clr-gray-12); - - &--total { - color: var(--sds-clr-lilac-11); - } - } - } - } - - &__Description { - color: var(--sds-clr-gray-11); - font-size: pxToRem(14px); - line-height: pxToRem(20px); - } - } - &__Details__Row__FeesInfoBtn { background: none; border: none; diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 83c2bc1790..78a2b464b9 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -51,6 +51,7 @@ import { AMOUNT_ERROR, InputType } from "helpers/transaction"; import { reRouteOnboarding } from "popup/helpers/route"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { EditSettings } from "popup/components/InternalTransaction/EditSettings"; +import { FeesPane } from "popup/components/InternalTransaction/FeesPane"; import { EditMemo } from "popup/components/InternalTransaction/EditMemo"; import { ReviewTx } from "popup/components/InternalTransaction/ReviewTransaction"; import { AddressTile } from "popup/components/send/AddressTile"; @@ -112,6 +113,24 @@ export const SendAmount = ({ } = transactionData; const fee = transactionFee || recommendedFee; + // Persist the last-known inclusion fee across re-simulations so the + // EditSettings input never jumps back to the total fee while LOADING + // (the request reducer sets data: null on FETCH_DATA_START). + const lastInclusionFeeRef = useRef(null); + if ( + simulationState.state === RequestState.SUCCESS && + simulationState.data?.inclusionFee + ) { + lastInclusionFeeRef.current = simulationState.data.inclusionFee; + } + + // For Soroban: show the stable inclusion fee (base fee only). + // For classic: show the current total fee (may be user-customised). + const editSettingsFee = + isToken || isCollectible + ? (lastInclusionFeeRef.current ?? recommendedFee) + : fee; + const { state: sendAmountData, fetchData } = useGetSendAmountData( { showHidden: false, @@ -131,10 +150,14 @@ export const SendAmount = ({ const cryptoInputRef = useRef(null); const usdInputRef = useRef(null); + // Tracks the dest+asset pair that simulation was last triggered for, so we + // can detect changes and re-simulate without watching simulationState.data. + const simulationDataRef = useRef({ destination: "", asset: "" }); const [inputType, setInputType] = useState("crypto"); const [isEditingMemo, setIsEditingMemo] = React.useState(false); const [isEditingSettings, setIsEditingSettings] = React.useState(false); + const [isShowingFeesPane, setIsShowingFeesPane] = React.useState(false); const [isReviewingTx, setIsReviewingTx] = React.useState(false); const [contractSupportsMuxed, setContractSupportsMuxed] = React.useState< boolean | null @@ -235,7 +258,15 @@ export const SendAmount = ({ }; const handleContinue = async () => { - if (!transactionFee) { + if (isToken || isCollectible) { + // Reset to the inclusion fee before re-simulating. After a prior + // simulation, saveTransactionFee stored the TOTAL (inclusion + resource). + // Without this reset that total would be used as baseFee on the next run, + // inflating both inclusionFee and recommendedFee. + dispatch( + saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + ); + } else if (!transactionFee) { dispatch(saveTransactionFee(fee)); } await fetchSimulationData(); @@ -300,6 +331,34 @@ export const SendAmount = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Soroban: re-simulate whenever destination or asset changes (and on first + // mount if both are ready). simulationDataRef tracks what was last simulated so + // we detect genuine changes without watching simulationState.data. + // simulationState.state is included so that if a change arrives while a + // simulation is already in-flight, we retry once it finishes. + useEffect(() => { + if (!(isToken || isCollectible)) return; + if (!destination) return; + // Don't stack concurrent simulations. + if (simulationState.state === RequestState.LOADING) return; + + const destChanged = simulationDataRef.current.destination !== destination; + const assetChanged = simulationDataRef.current.asset !== asset; + + if (destChanged || assetChanged) { + // Reset to inclusion fee before re-simulating so total fee from a prior + // simulation isn't used as baseFee (which would inflate the result). + if (isToken || isCollectible) { + dispatch( + saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + ); + } + simulationDataRef.current = { destination, asset }; + fetchSimulationData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [destination, asset, isToken, isCollectible, simulationState.state]); + const getAmountFontSize = () => { const length = formik.values.amount.length; if (length <= 9) { @@ -457,7 +516,18 @@ export const SendAmount = ({ size="md" isRounded variant="tertiary" - onClick={() => setIsEditingSettings(true)} + onClick={() => { + setIsEditingSettings(true); + // For Soroban tokens, trigger simulation immediately so fee + // input and FeesPane both reflect the correct simulated amounts + if ( + (isToken || isCollectible) && + simulationState.state !== RequestState.SUCCESS && + simulationState.state !== RequestState.LOADING + ) { + fetchSimulationData(); + } + }} > @@ -788,15 +858,17 @@ export const SendAmount = ({ /> ) : null} - {isEditingSettings ? ( + {isEditingSettings && !isShowingFeesPane ? ( <>
setIsEditingSettings(false)} + onShowFeesInfo={() => setIsShowingFeesPane(true)} onSubmit={async ({ fee, timeout, @@ -818,6 +890,26 @@ export const SendAmount = ({ /> ) : null} + {isShowingFeesPane ? ( + { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + isModalOpen={isShowingFeesPane} + > + + { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + /> + + + ) : null} setIsReviewingTx(false)} isModalOpen={isReviewingTx} diff --git a/extension/src/popup/components/send/styles.scss b/extension/src/popup/components/send/styles.scss index 868f2bef7b..c74deeef6a 100644 --- a/extension/src/popup/components/send/styles.scss +++ b/extension/src/popup/components/send/styles.scss @@ -395,6 +395,19 @@ align-items: center; } +.FeesPaneOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow-y: auto; + z-index: var(--z-index--banner); + background: var(--sds-clr-gray-01); + padding: pxToRem(24px); + box-sizing: border-box; +} + .ReviewTxWrapper { position: absolute; width: 100%; From fcaa0be9118cb25778cddee9b9ebac0473884f67 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 09:30:21 -0300 Subject: [PATCH 05/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../InternalTransaction/ReviewTransaction/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 0c6ead32d2..f26a8af8e4 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -181,8 +181,8 @@ export const ReviewTx = ({ /** * Pane state machine: - * - With Blockaid warning: [Review, Memo, Blockaid] - Blockaid accessible via banner click - * - No warning: [Review, Memo] + * - No warning: [Review (0), Memo (1), Fees (2)] + * - With Blockaid warning: [Review (0), Memo (1), Blockaid (2), Fees (3)] - Blockaid accessible via banner click */ const paneConfig = React.useMemo( () => From 3642b9d9b88ff2a992c7610db7549cea45b075d9 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 09:30:33 -0300 Subject: [PATCH 06/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../popup/components/InternalTransaction/FeesPane/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx index 03ae754a60..4a194df758 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -26,13 +26,15 @@ export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => {
-
-
+
{t("Fees")} From a77b6f5e091fd213659b2db32f2e8108605e4ab3 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 10:34:18 -0300 Subject: [PATCH 07/21] fix ui styles and padding --- .../InternalTransaction/FeesPane/styles.scss | 4 +- .../ReviewTransaction/index.tsx | 52 ++++++++++--------- .../ReviewTransaction/styles.scss | 7 +-- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index 29481df9ff..e297fa7d03 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; width: 100%; - padding: pxToRem(16px) 0; + padding: pxToRem(32px) 0; gap: pxToRem(16px); &__Header { @@ -55,7 +55,7 @@ flex-direction: column; border-radius: pxToRem(16px); background-color: var(--sds-clr-gray-03); - padding: pxToRem(12px) pxToRem(16px); + padding: pxToRem(4px) pxToRem(16px); &__Row { display: flex; diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index f26a8af8e4..535a9f8233 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -206,6 +206,8 @@ export const ReviewTx = ({ paneConfig.blockaidIndex !== null && activePaneIndex === paneConfig.blockaidIndex; + const isOnFeesPane = activePaneIndex === paneConfig.feesIndex; + // Extract contract ID for custom tokens or collectibles const contractId = React.useMemo( () => @@ -367,6 +369,11 @@ export const ReviewTx = ({
{t("Fee")} +
+
-
-
{fee} XLM
@@ -466,25 +468,27 @@ export const ReviewTx = ({ ) : (
-
- -
+ {!isOnFeesPane && ( +
+ +
+ )}
)} diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index c651fdb1e6..4c3ab2b6df 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -68,8 +68,8 @@ &__Value { flex: 2; display: flex; - flex-direction: column; - align-items: end; + flex-direction: row; + justify-content: flex-end; .CopyValue { max-width: pxToRem(120px); @@ -262,10 +262,11 @@ border: none; cursor: pointer; padding: 0; - margin-left: pxToRem(4px); + margin-right: pxToRem(6px); display: inline-flex; align-items: center; color: var(--sds-clr-gray-09); + margin-bottom: pxToRem(2px); svg { width: pxToRem(14px); From 2e3e0a6d22a048f1c99f68e75e5c33b816aa09b6 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 10:57:55 -0300 Subject: [PATCH 08/21] extract types to shard and update fees pane padding --- .../InternalTransaction/FeesPane/index.tsx | 2 +- .../InternalTransaction/FeesPane/styles.scss | 2 +- .../ReviewTransaction/index.tsx | 2 +- .../SendAmount/hooks/useSimulateTxData.tsx | 10 +++------- .../popup/components/send/SendAmount/index.tsx | 18 ++++++++++-------- .../src/popup/components/send/styles.scss | 4 ++++ .../hooks/useSimulateTxData.ts | 9 ++------- extension/src/types/transactions.ts | 9 +++++++++ 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx index 4a194df758..0c09c710f4 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -3,7 +3,7 @@ import { Icon } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { RequestState, State } from "constants/request"; -import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; +import { SimulateTxData } from "types/transactions"; import "./styles.scss"; diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index e297fa7d03..1d621ac33b 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; width: 100%; - padding: pxToRem(32px) 0; + padding: 0 0; gap: pxToRem(16px); &__Header { diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 535a9f8233..3d224d3b96 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -21,7 +21,7 @@ import { checkIsMuxedSupported, getMemoDisabledState, } from "helpers/muxedAddress"; -import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; +import { SimulateTxData } from "types/transactions"; import { View } from "popup/basics/layout/View"; import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; import { hardwareWalletTypeSelector } from "popup/ducks/accountServices"; diff --git a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx index 0933b07263..4a25f8137e 100644 --- a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx +++ b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx @@ -48,6 +48,9 @@ import { checkIsMuxedSupported, determineMuxedDestination, } from "helpers/muxedAddress"; +import { SimulateTxData } from "types/transactions"; + +export type { SimulateTxData }; interface SimClassic { type: "classic"; @@ -69,13 +72,6 @@ interface SimSoroban { xdr: string; } -export interface SimulateTxData { - transactionXdr: string; - scanResult?: BlockAidScanTxResult | null; - inclusionFee?: string; - resourceFee?: string; -} - const CREATE_ACCOUNT_MIN_XLM = new BigNumber(1); /** diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 78a2b464b9..6bceb8d502 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -899,14 +899,16 @@ export const SendAmount = ({ isModalOpen={isShowingFeesPane} > - { - setIsShowingFeesPane(false); - setIsEditingSettings(true); - }} - /> +
+ { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + /> +
) : null} diff --git a/extension/src/popup/components/send/styles.scss b/extension/src/popup/components/send/styles.scss index c74deeef6a..8f95c709ea 100644 --- a/extension/src/popup/components/send/styles.scss +++ b/extension/src/popup/components/send/styles.scss @@ -243,6 +243,10 @@ margin-right: pxToRem(6px); } } + + &__FeesPane { + padding: pxToRem(32px) 0; + } } .SendTo { diff --git a/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts b/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts index 023dae9ce9..d932b2d5d9 100644 --- a/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts +++ b/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts @@ -13,7 +13,6 @@ import { formatTokenAmount, } from "popup/helpers/soroban"; import { simulateSendCollectible } from "@shared/api/internal"; -import { BlockAidScanTxResult } from "@shared/api/types"; import { saveSimulation, saveTransactionFee, @@ -21,13 +20,9 @@ import { } from "popup/ducks/transactionSubmission"; import { AppDispatch, AppState } from "popup/App"; import { useScanTx } from "popup/helpers/blockaid"; +import { SimulateTxData } from "types/transactions"; -export interface SimulateTxData { - transactionXdr: string; - scanResult?: BlockAidScanTxResult | null; - inclusionFee?: string; - resourceFee?: string; -} +export type { SimulateTxData }; const simulateTx = async ({ options, diff --git a/extension/src/types/transactions.ts b/extension/src/types/transactions.ts index ff73b98bda..d391155e7c 100644 --- a/extension/src/types/transactions.ts +++ b/extension/src/types/transactions.ts @@ -1,9 +1,18 @@ +import { BlockAidScanTxResult } from "@shared/api/types"; + export interface FlaggedKeys { [address: string]: { tags: Array; }; } +export interface SimulateTxData { + transactionXdr: string; + scanResult?: BlockAidScanTxResult | null; + inclusionFee?: string; + resourceFee?: string; +} + export interface TransactionInfo { url: string; tab: any; From 0e156b29cfffbc6f437c3eaa946ee5ff71a55cd0 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Tue, 17 Mar 2026 19:55:33 -0300 Subject: [PATCH 09/21] move SimulateTxData to shared types and fix FeesPane padding - Extract SimulateTxData interface to types/transactions.ts to remove duplicate definitions in send and collectible hooks - Both hooks now re-export the shared type - FeesPane and ReviewTransaction import directly from types/transactions - Fix FeesPane top/bottom padding and reset padding inside multi-pane-slider Co-Authored-By: Claude Opus 4.6 --- .../popup/components/InternalTransaction/FeesPane/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index 1d621ac33b..9114009b33 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -7,6 +7,10 @@ padding: 0 0; gap: pxToRem(16px); + .multi-pane-slider & { + padding: 0; + } + &__Header { display: flex; justify-content: space-between; From cc18c3a57dd68fb5f394b13c42d5d37b8328ccfc Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 20 Mar 2026 14:15:25 -0300 Subject: [PATCH 10/21] fix soroban rpc url for simulate transaction --- @shared/api/internal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 3ddea53dc6..5a876f957d 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -2208,7 +2208,7 @@ export const simulateTransaction = async (args: { body: JSON.stringify({ xdr, network_passphrase: networkDetails.networkPassphrase, - network_url: networkDetails.networkUrl, + network_url: networkDetails.sorobanRpcUrl, }), }; const res = await fetch(`${INDEXER_URL}/simulate-tx`, options); From 5ea2d52db65f69e29826fb5fa6e70d84624ff92d Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 11 Mar 2026 15:00:15 -0300 Subject: [PATCH 11/21] add initial version of displaying better minResourceFee --- @shared/api/internal.ts | 1 + .../ReviewTransaction/index.tsx | 67 +++++++++- .../ReviewTransaction/styles.scss | 116 ++++++++++++++++++ .../SendAmount/hooks/useSimulateTxData.tsx | 11 ++ .../components/send/SendAmount/index.tsx | 11 +- .../src/popup/locales/en/translation.json | 7 ++ .../src/popup/locales/pt/translation.json | 7 ++ 7 files changed, 214 insertions(+), 6 deletions(-) diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 981b71eb82..3ddea53dc6 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -2208,6 +2208,7 @@ export const simulateTransaction = async (args: { body: JSON.stringify({ xdr, network_passphrase: networkDetails.networkPassphrase, + network_url: networkDetails.networkUrl, }), }; const res = await fetch(`${INDEXER_URL}/simulate-tx`, options); diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 18713a7711..099b3165d3 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -190,11 +190,13 @@ export const ReviewTx = ({ blockaidIndex: null, reviewIndex: 0, memoIndex: 1, + feesIndex: 2, } : { blockaidIndex: 2, reviewIndex: 0, memoIndex: 1, + feesIndex: 3, }, [shouldShowTxWarning], ); @@ -364,6 +366,13 @@ export const ReviewTx = ({
{t("Fee")} +
); + const feesPane = ( +
+
+
+ +
+
setActivePaneIndex(paneConfig.reviewIndex)} + > + +
+
+
+ {t("Fees")} +
+
+ {simulationState.data?.inclusionFee && ( +
+ + {t("Inclusion Fee")} + + + {simulationState.data.inclusionFee} XLM + +
+ )} + {simulationState.data?.resourceFee && ( +
+ + {t("Resource Fee")} + + + {simulationState.data.resourceFee} XLM + +
+ )} +
+ + {t("Total Fee")} + + + {fee} XLM + +
+
+
+ {simulationState.data?.resourceFee + ? t("Fees description soroban") + : t("Fees description classic")} +
+
+ ); + // Build panes in order (no hooks on JSX) const panes: React.ReactNode[] = []; if (shouldShowTxWarning) { - panes.push(reviewPane, memoPane, blockaidPane); + panes.push(reviewPane, memoPane, blockaidPane, feesPane); } else { - panes.push(reviewPane, memoPane); + panes.push(reviewPane, memoPane, feesPane); } return ( diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index 5b81a19162..bd811b9bb5 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -255,4 +255,120 @@ line-height: 1.5rem; } } + + &__FeesDetails { + display: flex; + flex-direction: column; + width: 100%; + padding: 0 1rem; + + &__Header { + display: flex; + justify-content: space-between; + margin: pxToRem(12px) 0; + + &__Icon { + display: flex; + width: 40px; + height: 40px; + padding: 7.5px; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + border-radius: 8px; + background: var(--sds-clr-purple-03); + + svg { + width: 25px; + height: 25px; + color: var(--sds-clr-purple-11); + } + } + + &__Close { + display: flex; + width: 32px; + height: 32px; + justify-content: center; + align-items: center; + border-radius: 1000px; + background: var(--sds-clr-gray-03); + color: var(--sds-clr-gray-09); + cursor: pointer; + } + } + + &__Title { + display: flex; + align-items: center; + width: 100%; + margin: pxToRem(12px) 0; + font-size: pxToRem(20px); + font-weight: var(--font-weight-medium); + } + + &__Card { + display: flex; + flex-direction: column; + border-radius: pxToRem(16px); + background-color: var(--sds-clr-gray-03); + padding: pxToRem(12px); + + &__Row { + display: flex; + justify-content: space-between; + align-items: center; + padding: pxToRem(12px) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--sds-clr-gray-06); + } + + &--total { + padding-top: pxToRem(12px); + } + + &__Label { + font-size: pxToRem(14px); + color: var(--sds-clr-gray-11); + } + + &__Value { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + + &--total { + color: var(--sds-clr-purple-11); + } + } + } + } + + &__Description { + margin-top: pxToRem(16px); + color: var(--sds-clr-gray-11); + font-size: pxToRem(14px); + line-height: 1.5rem; + } + } + + &__Details__Row__FeesInfoBtn { + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-left: pxToRem(4px); + display: inline-flex; + align-items: center; + color: var(--sds-clr-gray-09); + + svg { + width: pxToRem(14px); + height: pxToRem(14px); + } + + &:hover { + color: var(--sds-clr-gray-12); + } + } } diff --git a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx index 52482bc9d0..0933b07263 100644 --- a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx +++ b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx @@ -72,6 +72,8 @@ interface SimSoroban { export interface SimulateTxData { transactionXdr: string; scanResult?: BlockAidScanTxResult | null; + inclusionFee?: string; + resourceFee?: string; } const CREATE_ACCOUNT_MIN_XLM = new BigNumber(1); @@ -341,6 +343,8 @@ const simulateTx = async ({ return { payload: response, recommendedFee: baseFee.plus(new BigNumber(minResourceFee)).toString(), + inclusionFee: baseFee.toString(), + resourceFee: minResourceFee, }; } @@ -545,6 +549,13 @@ function useSimulateTxData({ }), ); + if (simResponse.inclusionFee !== undefined) { + payload.inclusionFee = simResponse.inclusionFee; + } + if (simResponse.resourceFee !== undefined) { + payload.resourceFee = simResponse.resourceFee; + } + const scanUrlstub = "internal"; if (simParams.type === "classic") { const { diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 60d81a1253..83c2bc1790 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -429,9 +429,12 @@ export const SendAmount = ({ {t("Fee")}: - {inputType === "crypto" - ? `${fee} ${t("XLM")}` - : recommendedFeeUsd} + {(isToken || isCollectible) && + simulationState.state === RequestState.LOADING + ? t("Calculating...") + : inputType === "crypto" + ? `${fee} ${t("XLM")}` + : recommendedFeeUsd}
@@ -464,7 +467,7 @@ export const SendAmount = ({ + )} + + ); + return (
{title &&

{title}

} - + {({ errors, setFieldValue }) => ( <> @@ -57,7 +82,7 @@ export const EditSettings = ({ autoComplete="off" id="fee" placeholder={t("Fee")} - label={t("Transaction Fee")} + label={feeLabel} {...field} error={errors.fee} onChange={(e) => { diff --git a/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss b/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss index eaa3055e74..8dc587b683 100644 --- a/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss +++ b/extension/src/popup/components/InternalTransaction/EditSettings/styles.scss @@ -28,6 +28,31 @@ } } + &__fee-label { + display: inline-flex; + align-items: center; + gap: pxToRem(4px); + } + + &__fee-info-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + color: var(--sds-clr-gray-09); + + svg { + width: pxToRem(14px); + height: pxToRem(14px); + } + + &:hover { + color: var(--sds-clr-gray-12); + } + } + &__actions { display: flex; margin-top: pxToRem(32px); diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx new file mode 100644 index 0000000000..03ae754a60 --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { Icon } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { RequestState, State } from "constants/request"; +import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; + +import "./styles.scss"; + +export interface FeesPaneProps { + fee: string; + simulationState: State; + onClose: () => void; +} + +export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => { + const { t } = useTranslation(); + + const isLoading = + simulationState.state === RequestState.IDLE || + simulationState.state === RequestState.LOADING; + + return ( +
+
+
+ +
+
+ +
+
+
+ {t("Fees")} +
+
+ {!isLoading && simulationState.data?.inclusionFee && ( +
+ + {t("Inclusion Fee")} + + + {simulationState.data.inclusionFee} XLM + +
+ )} + {!isLoading && simulationState.data?.resourceFee && ( +
+ + {t("Resource Fee")} + + + {simulationState.data.resourceFee} XLM + +
+ )} +
+ + {t("Total Fee")} + + + {isLoading ? t("Calculating...") : `${fee} XLM`} + +
+
+
+ {simulationState.data?.resourceFee + ? t("Fees description soroban") + : t("Fees description classic")} +
+
+ ); +}; diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss new file mode 100644 index 0000000000..29481df9ff --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -0,0 +1,97 @@ +@use "../../../styles/utils.scss" as *; + +.FeesPane { + display: flex; + flex-direction: column; + width: 100%; + padding: pxToRem(16px) 0; + gap: pxToRem(16px); + + &__Header { + display: flex; + justify-content: space-between; + align-items: center; + + &__Icon { + display: flex; + width: pxToRem(32px); + height: pxToRem(32px); + justify-content: center; + align-items: center; + border-radius: pxToRem(8px); + background: var(--sds-clr-lilac-03); + border: 1px solid var(--sds-clr-lilac-06); + + svg { + width: pxToRem(20px); + height: pxToRem(20px); + color: var(--sds-clr-lilac-09); + } + } + + &__Close { + display: flex; + width: pxToRem(32px); + height: pxToRem(32px); + justify-content: center; + align-items: center; + border-radius: 1000px; + background: var(--sds-clr-gray-03); + color: var(--sds-clr-gray-09); + cursor: pointer; + } + } + + &__Title { + display: flex; + align-items: center; + width: 100%; + font-size: pxToRem(18px); + font-weight: var(--font-weight-medium); + } + + &__Card { + display: flex; + flex-direction: column; + border-radius: pxToRem(16px); + background-color: var(--sds-clr-gray-03); + padding: pxToRem(12px) pxToRem(16px); + + &__Row { + display: flex; + justify-content: space-between; + align-items: center; + padding: pxToRem(12px) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--sds-clr-gray-06); + } + + &__Label { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + color: var(--sds-clr-gray-11); + + &--total { + color: var(--sds-clr-lilac-11); + } + } + + &__Value { + font-size: pxToRem(14px); + font-weight: var(--font-weight-medium); + color: var(--sds-clr-gray-12); + + &--total { + color: var(--sds-clr-lilac-11); + } + } + } + } + + &__Description { + color: var(--sds-clr-gray-11); + font-size: pxToRem(14px); + line-height: pxToRem(20px); + } +} diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index fd4d3a00c8..0c6ead32d2 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -38,6 +38,7 @@ import { MemoRequiredLabel, } from "popup/components/WarningMessages"; import { CopyValue } from "popup/components/CopyValue"; +import { FeesPane } from "popup/components/InternalTransaction/FeesPane"; import { ActionButtons } from "./components/ActionButtons"; import { SendAsset, SendDestination } from "./components"; @@ -439,70 +440,11 @@ export const ReviewTx = ({ ); const feesPane = ( -
-
-
- -
-
setActivePaneIndex(paneConfig.reviewIndex)} - > - -
-
-
- {t("Fees")} -
-
- {simulationState.data?.inclusionFee && ( -
- - {t("Inclusion Fee")} - - - {simulationState.data.inclusionFee} XLM - -
- )} - {simulationState.data?.resourceFee && ( -
- - {t("Resource Fee")} - - - {simulationState.data.resourceFee} XLM - -
- )} -
- - {t("Total Fee")} - - - {fee} XLM - -
-
-
- {simulationState.data?.resourceFee - ? t("Fees description soroban") - : t("Fees description classic")} -
-
+ setActivePaneIndex(paneConfig.reviewIndex)} + /> ); // Build panes in order (no hooks on JSX) diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index b784baf169..c651fdb1e6 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -58,6 +58,7 @@ flex: 2; display: flex; color: var(--sds-clr-gray-09); + align-items: center; svg { margin-right: pxToRem(6px); @@ -256,102 +257,6 @@ } } - &__FeesDetails { - display: flex; - flex-direction: column; - width: 100%; - padding: 0 pxToRem(24px); - gap: pxToRem(16px); - - &__Header { - display: flex; - justify-content: space-between; - align-items: center; - - &__Icon { - display: flex; - width: pxToRem(32px); - height: pxToRem(32px); - justify-content: center; - align-items: center; - border-radius: pxToRem(8px); - background: var(--sds-clr-lilac-03); - border: 1px solid var(--sds-clr-lilac-06); - - svg { - width: pxToRem(20px); - height: pxToRem(20px); - color: var(--sds-clr-lilac-09); - } - } - - &__Close { - display: flex; - width: pxToRem(32px); - height: pxToRem(32px); - justify-content: center; - align-items: center; - border-radius: 1000px; - background: var(--sds-clr-gray-03); - color: var(--sds-clr-gray-09); - cursor: pointer; - } - } - - &__Title { - display: flex; - align-items: center; - width: 100%; - font-size: pxToRem(18px); - font-weight: var(--font-weight-medium); - } - - &__Card { - display: flex; - flex-direction: column; - border-radius: pxToRem(16px); - background-color: var(--sds-clr-gray-03); - padding: pxToRem(12px) pxToRem(16px); - - &__Row { - display: flex; - justify-content: space-between; - align-items: center; - padding: pxToRem(12px) 0; - - &:not(:last-child) { - border-bottom: 1px solid var(--sds-clr-gray-06); - } - - &__Label { - font-size: pxToRem(14px); - font-weight: var(--font-weight-medium); - color: var(--sds-clr-gray-11); - - &--total { - color: var(--sds-clr-lilac-11); - } - } - - &__Value { - font-size: pxToRem(14px); - font-weight: var(--font-weight-medium); - color: var(--sds-clr-gray-12); - - &--total { - color: var(--sds-clr-lilac-11); - } - } - } - } - - &__Description { - color: var(--sds-clr-gray-11); - font-size: pxToRem(14px); - line-height: pxToRem(20px); - } - } - &__Details__Row__FeesInfoBtn { background: none; border: none; diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 83c2bc1790..78a2b464b9 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -51,6 +51,7 @@ import { AMOUNT_ERROR, InputType } from "helpers/transaction"; import { reRouteOnboarding } from "popup/helpers/route"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { EditSettings } from "popup/components/InternalTransaction/EditSettings"; +import { FeesPane } from "popup/components/InternalTransaction/FeesPane"; import { EditMemo } from "popup/components/InternalTransaction/EditMemo"; import { ReviewTx } from "popup/components/InternalTransaction/ReviewTransaction"; import { AddressTile } from "popup/components/send/AddressTile"; @@ -112,6 +113,24 @@ export const SendAmount = ({ } = transactionData; const fee = transactionFee || recommendedFee; + // Persist the last-known inclusion fee across re-simulations so the + // EditSettings input never jumps back to the total fee while LOADING + // (the request reducer sets data: null on FETCH_DATA_START). + const lastInclusionFeeRef = useRef(null); + if ( + simulationState.state === RequestState.SUCCESS && + simulationState.data?.inclusionFee + ) { + lastInclusionFeeRef.current = simulationState.data.inclusionFee; + } + + // For Soroban: show the stable inclusion fee (base fee only). + // For classic: show the current total fee (may be user-customised). + const editSettingsFee = + isToken || isCollectible + ? (lastInclusionFeeRef.current ?? recommendedFee) + : fee; + const { state: sendAmountData, fetchData } = useGetSendAmountData( { showHidden: false, @@ -131,10 +150,14 @@ export const SendAmount = ({ const cryptoInputRef = useRef(null); const usdInputRef = useRef(null); + // Tracks the dest+asset pair that simulation was last triggered for, so we + // can detect changes and re-simulate without watching simulationState.data. + const simulationDataRef = useRef({ destination: "", asset: "" }); const [inputType, setInputType] = useState("crypto"); const [isEditingMemo, setIsEditingMemo] = React.useState(false); const [isEditingSettings, setIsEditingSettings] = React.useState(false); + const [isShowingFeesPane, setIsShowingFeesPane] = React.useState(false); const [isReviewingTx, setIsReviewingTx] = React.useState(false); const [contractSupportsMuxed, setContractSupportsMuxed] = React.useState< boolean | null @@ -235,7 +258,15 @@ export const SendAmount = ({ }; const handleContinue = async () => { - if (!transactionFee) { + if (isToken || isCollectible) { + // Reset to the inclusion fee before re-simulating. After a prior + // simulation, saveTransactionFee stored the TOTAL (inclusion + resource). + // Without this reset that total would be used as baseFee on the next run, + // inflating both inclusionFee and recommendedFee. + dispatch( + saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + ); + } else if (!transactionFee) { dispatch(saveTransactionFee(fee)); } await fetchSimulationData(); @@ -300,6 +331,34 @@ export const SendAmount = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Soroban: re-simulate whenever destination or asset changes (and on first + // mount if both are ready). simulationDataRef tracks what was last simulated so + // we detect genuine changes without watching simulationState.data. + // simulationState.state is included so that if a change arrives while a + // simulation is already in-flight, we retry once it finishes. + useEffect(() => { + if (!(isToken || isCollectible)) return; + if (!destination) return; + // Don't stack concurrent simulations. + if (simulationState.state === RequestState.LOADING) return; + + const destChanged = simulationDataRef.current.destination !== destination; + const assetChanged = simulationDataRef.current.asset !== asset; + + if (destChanged || assetChanged) { + // Reset to inclusion fee before re-simulating so total fee from a prior + // simulation isn't used as baseFee (which would inflate the result). + if (isToken || isCollectible) { + dispatch( + saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + ); + } + simulationDataRef.current = { destination, asset }; + fetchSimulationData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [destination, asset, isToken, isCollectible, simulationState.state]); + const getAmountFontSize = () => { const length = formik.values.amount.length; if (length <= 9) { @@ -457,7 +516,18 @@ export const SendAmount = ({ size="md" isRounded variant="tertiary" - onClick={() => setIsEditingSettings(true)} + onClick={() => { + setIsEditingSettings(true); + // For Soroban tokens, trigger simulation immediately so fee + // input and FeesPane both reflect the correct simulated amounts + if ( + (isToken || isCollectible) && + simulationState.state !== RequestState.SUCCESS && + simulationState.state !== RequestState.LOADING + ) { + fetchSimulationData(); + } + }} > @@ -788,15 +858,17 @@ export const SendAmount = ({ /> ) : null} - {isEditingSettings ? ( + {isEditingSettings && !isShowingFeesPane ? ( <>
setIsEditingSettings(false)} + onShowFeesInfo={() => setIsShowingFeesPane(true)} onSubmit={async ({ fee, timeout, @@ -818,6 +890,26 @@ export const SendAmount = ({ /> ) : null} + {isShowingFeesPane ? ( + { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + isModalOpen={isShowingFeesPane} + > + + { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + /> + + + ) : null} setIsReviewingTx(false)} isModalOpen={isReviewingTx} diff --git a/extension/src/popup/components/send/styles.scss b/extension/src/popup/components/send/styles.scss index 868f2bef7b..c74deeef6a 100644 --- a/extension/src/popup/components/send/styles.scss +++ b/extension/src/popup/components/send/styles.scss @@ -395,6 +395,19 @@ align-items: center; } +.FeesPaneOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow-y: auto; + z-index: var(--z-index--banner); + background: var(--sds-clr-gray-01); + padding: pxToRem(24px); + box-sizing: border-box; +} + .ReviewTxWrapper { position: absolute; width: 100%; From 5b553481cea729fe2d05107106fd27965ca8270b Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Thu, 12 Mar 2026 17:45:51 -0300 Subject: [PATCH 14/21] Update extension/src/popup/locales/pt/translation.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/popup/locales/pt/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index b3f0e7061c..12e4cc9d55 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -309,7 +309,7 @@ "Learn more about account reserves": "Saiba mais sobre reservas de conta", "Learn more about assets domains": "Saiba mais sobre domínios de ativos", "Learn more about conversion rates": "Saiba mais sobre taxas de conversão", - "Learn more about fees": "Learn more about fees", + "Learn more about fees": "Saiba mais sobre fees", "Learn more about transaction fees": "Saiba mais sobre taxas de transação", "Learn more about trustlines": "Saiba mais sobre trustlines", "Learn more about using Ledger": "Saiba mais sobre como usar o Ledger", From 38e025370b5c27419bcbb8c8a7b13c8cd3a33485 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 09:30:21 -0300 Subject: [PATCH 15/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../InternalTransaction/ReviewTransaction/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 0c6ead32d2..f26a8af8e4 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -181,8 +181,8 @@ export const ReviewTx = ({ /** * Pane state machine: - * - With Blockaid warning: [Review, Memo, Blockaid] - Blockaid accessible via banner click - * - No warning: [Review, Memo] + * - No warning: [Review (0), Memo (1), Fees (2)] + * - With Blockaid warning: [Review (0), Memo (1), Blockaid (2), Fees (3)] - Blockaid accessible via banner click */ const paneConfig = React.useMemo( () => From 92c8981722a2dbe3fbda8b74b987aacac8da9bbd Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 09:30:33 -0300 Subject: [PATCH 16/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../popup/components/InternalTransaction/FeesPane/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx index 03ae754a60..4a194df758 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -26,13 +26,15 @@ export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => {
-
-
+
{t("Fees")} From 604a4200e06fbdaac72ecd7b48dbc9aac71364a6 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 10:34:18 -0300 Subject: [PATCH 17/21] fix ui styles and padding --- .../InternalTransaction/FeesPane/styles.scss | 4 +- .../ReviewTransaction/index.tsx | 52 ++++++++++--------- .../ReviewTransaction/styles.scss | 7 +-- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index 29481df9ff..e297fa7d03 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; width: 100%; - padding: pxToRem(16px) 0; + padding: pxToRem(32px) 0; gap: pxToRem(16px); &__Header { @@ -55,7 +55,7 @@ flex-direction: column; border-radius: pxToRem(16px); background-color: var(--sds-clr-gray-03); - padding: pxToRem(12px) pxToRem(16px); + padding: pxToRem(4px) pxToRem(16px); &__Row { display: flex; diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index f26a8af8e4..535a9f8233 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -206,6 +206,8 @@ export const ReviewTx = ({ paneConfig.blockaidIndex !== null && activePaneIndex === paneConfig.blockaidIndex; + const isOnFeesPane = activePaneIndex === paneConfig.feesIndex; + // Extract contract ID for custom tokens or collectibles const contractId = React.useMemo( () => @@ -367,6 +369,11 @@ export const ReviewTx = ({
{t("Fee")} +
+
-
-
{fee} XLM
@@ -466,25 +468,27 @@ export const ReviewTx = ({ ) : (
-
- -
+ {!isOnFeesPane && ( +
+ +
+ )}
)} diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss index c651fdb1e6..4c3ab2b6df 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss @@ -68,8 +68,8 @@ &__Value { flex: 2; display: flex; - flex-direction: column; - align-items: end; + flex-direction: row; + justify-content: flex-end; .CopyValue { max-width: pxToRem(120px); @@ -262,10 +262,11 @@ border: none; cursor: pointer; padding: 0; - margin-left: pxToRem(4px); + margin-right: pxToRem(6px); display: inline-flex; align-items: center; color: var(--sds-clr-gray-09); + margin-bottom: pxToRem(2px); svg { width: pxToRem(14px); From 50ab2606e23ef400c9006a9a6fd950a0c62238e6 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Mon, 16 Mar 2026 10:57:55 -0300 Subject: [PATCH 18/21] extract types to shard and update fees pane padding --- .../InternalTransaction/FeesPane/index.tsx | 2 +- .../InternalTransaction/FeesPane/styles.scss | 2 +- .../ReviewTransaction/index.tsx | 2 +- .../SendAmount/hooks/useSimulateTxData.tsx | 10 +++------- .../popup/components/send/SendAmount/index.tsx | 18 ++++++++++-------- .../src/popup/components/send/styles.scss | 4 ++++ .../hooks/useSimulateTxData.ts | 9 ++------- extension/src/types/transactions.ts | 9 +++++++++ 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx index 4a194df758..0c09c710f4 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -3,7 +3,7 @@ import { Icon } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { RequestState, State } from "constants/request"; -import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; +import { SimulateTxData } from "types/transactions"; import "./styles.scss"; diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index e297fa7d03..1d621ac33b 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; width: 100%; - padding: pxToRem(32px) 0; + padding: 0 0; gap: pxToRem(16px); &__Header { diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 535a9f8233..3d224d3b96 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -21,7 +21,7 @@ import { checkIsMuxedSupported, getMemoDisabledState, } from "helpers/muxedAddress"; -import { SimulateTxData } from "popup/components/send/SendAmount/hooks/useSimulateTxData"; +import { SimulateTxData } from "types/transactions"; import { View } from "popup/basics/layout/View"; import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; import { hardwareWalletTypeSelector } from "popup/ducks/accountServices"; diff --git a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx index 0933b07263..4a25f8137e 100644 --- a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx +++ b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx @@ -48,6 +48,9 @@ import { checkIsMuxedSupported, determineMuxedDestination, } from "helpers/muxedAddress"; +import { SimulateTxData } from "types/transactions"; + +export type { SimulateTxData }; interface SimClassic { type: "classic"; @@ -69,13 +72,6 @@ interface SimSoroban { xdr: string; } -export interface SimulateTxData { - transactionXdr: string; - scanResult?: BlockAidScanTxResult | null; - inclusionFee?: string; - resourceFee?: string; -} - const CREATE_ACCOUNT_MIN_XLM = new BigNumber(1); /** diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 78a2b464b9..6bceb8d502 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -899,14 +899,16 @@ export const SendAmount = ({ isModalOpen={isShowingFeesPane} > - { - setIsShowingFeesPane(false); - setIsEditingSettings(true); - }} - /> +
+ { + setIsShowingFeesPane(false); + setIsEditingSettings(true); + }} + /> +
) : null} diff --git a/extension/src/popup/components/send/styles.scss b/extension/src/popup/components/send/styles.scss index c74deeef6a..8f95c709ea 100644 --- a/extension/src/popup/components/send/styles.scss +++ b/extension/src/popup/components/send/styles.scss @@ -243,6 +243,10 @@ margin-right: pxToRem(6px); } } + + &__FeesPane { + padding: pxToRem(32px) 0; + } } .SendTo { diff --git a/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts b/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts index 023dae9ce9..d932b2d5d9 100644 --- a/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts +++ b/extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts @@ -13,7 +13,6 @@ import { formatTokenAmount, } from "popup/helpers/soroban"; import { simulateSendCollectible } from "@shared/api/internal"; -import { BlockAidScanTxResult } from "@shared/api/types"; import { saveSimulation, saveTransactionFee, @@ -21,13 +20,9 @@ import { } from "popup/ducks/transactionSubmission"; import { AppDispatch, AppState } from "popup/App"; import { useScanTx } from "popup/helpers/blockaid"; +import { SimulateTxData } from "types/transactions"; -export interface SimulateTxData { - transactionXdr: string; - scanResult?: BlockAidScanTxResult | null; - inclusionFee?: string; - resourceFee?: string; -} +export type { SimulateTxData }; const simulateTx = async ({ options, diff --git a/extension/src/types/transactions.ts b/extension/src/types/transactions.ts index ff73b98bda..d391155e7c 100644 --- a/extension/src/types/transactions.ts +++ b/extension/src/types/transactions.ts @@ -1,9 +1,18 @@ +import { BlockAidScanTxResult } from "@shared/api/types"; + export interface FlaggedKeys { [address: string]: { tags: Array; }; } +export interface SimulateTxData { + transactionXdr: string; + scanResult?: BlockAidScanTxResult | null; + inclusionFee?: string; + resourceFee?: string; +} + export interface TransactionInfo { url: string; tab: any; From e433ac0e4ba2522a93b58cc058e392618197ebc6 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 20 Mar 2026 14:15:25 -0300 Subject: [PATCH 19/21] fix soroban rpc url for simulate transaction --- @shared/api/internal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 3ddea53dc6..5a876f957d 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -2208,7 +2208,7 @@ export const simulateTransaction = async (args: { body: JSON.stringify({ xdr, network_passphrase: networkDetails.networkPassphrase, - network_url: networkDetails.networkUrl, + network_url: networkDetails.sorobanRpcUrl, }), }; const res = await fetch(`${INDEXER_URL}/simulate-tx`, options); From 012ea774b303b128c8d4cabf77e135a6a9db8f46 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Tue, 17 Mar 2026 19:55:33 -0300 Subject: [PATCH 20/21] move SimulateTxData to shared types and fix FeesPane padding - Extract SimulateTxData interface to types/transactions.ts to remove duplicate definitions in send and collectible hooks - Both hooks now re-export the shared type - FeesPane and ReviewTransaction import directly from types/transactions - Fix FeesPane top/bottom padding and reset padding inside multi-pane-slider Co-Authored-By: Claude Opus 4.6 --- .../popup/components/InternalTransaction/FeesPane/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss index 1d621ac33b..9114009b33 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss +++ b/extension/src/popup/components/InternalTransaction/FeesPane/styles.scss @@ -7,6 +7,10 @@ padding: 0 0; gap: pxToRem(16px); + .multi-pane-slider & { + padding: 0; + } + &__Header { display: flex; justify-content: space-between; From 32a4eeab16a98e9f8776d4bae5ab662d30ba266a Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Thu, 2 Apr 2026 13:48:26 -0300 Subject: [PATCH 21/21] fix manually set fee persistence inside flow and add e2e tests --- extension/e2e-tests/reviewTxFees.test.ts | 505 +++++++++++++++++- .../EditSettings/index.tsx | 27 +- .../InternalTransaction/FeesPane/index.tsx | 10 +- .../ReviewTransaction/index.tsx | 1 + .../components/send/SendAmount/index.tsx | 100 +++- .../src/popup/ducks/transactionSubmission.ts | 9 + 6 files changed, 610 insertions(+), 42 deletions(-) diff --git a/extension/e2e-tests/reviewTxFees.test.ts b/extension/e2e-tests/reviewTxFees.test.ts index 0dcb243a0c..6f20e5fb3c 100644 --- a/extension/e2e-tests/reviewTxFees.test.ts +++ b/extension/e2e-tests/reviewTxFees.test.ts @@ -201,8 +201,11 @@ test("Fee breakdown pane shows Soroban fees for collectible send", async ({ await expect(page.getByText("You are sending").first()).toBeVisible(); }); -// Soroban token send: opening fees pane from EditSettings shows "Inclusion Fee" label + fee breakdown -test("Fee breakdown pane shows Soroban fees from Edit Settings", async ({ +// ── Comprehensive scenario 1: custom token only (no destination) ───────────── +// Full fee lifecycle: open settings → open FeesPane (no simulation data) → +// go back → change draft → open FeesPane again (draft reflected) → Save → +// all places updated → reopen settings shows saved → reopen FeesPane shows saved. +test("Custom token without destination — full fee lifecycle in EditSettings and FeesPane", async ({ page, extensionId, context, @@ -216,55 +219,303 @@ test("Fee breakdown pane shows Soroban fees from Edit Settings", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); - // Navigate to token send via Asset Detail + // Navigate to token send (no destination set yet) + await page.getByText("E2E").click(); + await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Without a destination, no auto-simulation fires — fee display stays at base fee + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00001 XLM", + ); + + // ── Open Edit Settings ────────────────────────────────────────────────────── + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + // No auto-simulation yet — shows recommended base fee + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); + + // ── Open FeesPane — simulation is triggered by the gear click ───────────── + // The stub responds even without a destination; wait for it to settle + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( + "0.00001 XLM", + { timeout: 10000 }, + ); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", + ); + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093338 XLM", + ); + await expect(page.getByTestId("review-tx-fees-description")).toContainText( + "Soroban", + ); + + // ── Close FeesPane → back to Edit Settings ──────────────────────────────── + await page.getByTestId("review-tx-fees-close-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).not.toBeVisible(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + + // ── Change draft fee ────────────────────────────────────────────────────── + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Open FeesPane again — must reflect the draft fee + resource ─────────── + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + // Total = draft(0.00005) + resource(0.0093238) = 0.0093738 + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093738 XLM", + { timeout: 5000 }, + ); + + // ── Close FeesPane → draft still in the input ───────────────────────────── + await page.getByTestId("review-tx-fees-close-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).not.toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Save the custom fee ─────────────────────────────────────────────────── + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); + // Re-simulation with baseFee=0.00005 → total = 0.0093738 + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093738 XLM", + { timeout: 10000 }, + ); + + // ── Reopen Edit Settings — must show saved fee, not the base default ─────── + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Open FeesPane from re-opened settings — must still show saved fee ────── + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( + "0.00005 XLM", + { timeout: 10000 }, + ); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", + ); + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093738 XLM", + ); + await expect(page.getByTestId("review-tx-fees-description")).toContainText( + "Soroban", + ); +}); + +// ── Comprehensive scenario 2: custom token + recipient (Soroban) ────────────── +// Same lifecycle as scenario 1, but with destination set so simulation data is +// available: full inclusion + resource breakdown, draft total = inclusion + resource, +// Save triggers re-simulation, re-opened settings shows saved fee, FeesPane updated. +test("Custom token with recipient — full fee lifecycle in EditSettings and FeesPane", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); + + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + // Navigate to token send and set destination await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-amount-input").fill("0.1"); - // Set destination await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); - // Wait for simulation to finish - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + // Wait for auto-simulation: total = baseFee(0.00001) + resource(0.0093238) + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093338 XLM", + { timeout: 10000 }, + ); - // Open Edit Settings + // ── Open Edit Settings ────────────────────────────────────────────────────── await page.getByTestId("send-amount-btn-fee").click(); - - // EditSettings should show "Inclusion Fee" label for Soroban await expect(page.getByText("Inclusion Fee")).toBeVisible(); + // Shows the inclusion fee from simulation (base fee only, not the total) + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); - // Open fees breakdown pane from EditSettings + // ── Open FeesPane (full simulation data) ───────────────────────────────── await page.getByTestId("edit-settings-fees-info-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); - - // Inclusion fee = baseFee = 0.00001 XLM await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( "0.00001 XLM", ); - - // Resource fee = 0.0093238 XLM (93238 stroops / 1e7) await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( "0.0093238 XLM", ); - - // Total fee = inclusion + resource await expect(page.getByTestId("review-tx-total-fee")).toHaveText( "0.0093338 XLM", ); - - // Description explains Soroban simulation adjustments await expect(page.getByTestId("review-tx-fees-description")).toContainText( "Soroban", ); - // Closing the fees pane returns to Edit Settings + // ── Close FeesPane → back to Edit Settings ──────────────────────────────── await page.getByTestId("review-tx-fees-close-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).not.toBeVisible(); await expect(page.getByText("Inclusion Fee")).toBeVisible(); + + // ── Change draft fee ────────────────────────────────────────────────────── + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Open FeesPane again — total must use draft + resource ───────────────── + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + // Total = draft(0.00005) + resource(0.0093238) = 0.0093738 + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093738 XLM", + ); + + // ── Close FeesPane → draft still in the input ───────────────────────────── + await page.getByTestId("review-tx-fees-close-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).not.toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Save the custom fee ─────────────────────────────────────────────────── + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); + + // Re-simulation runs with new baseFee = 0.00005 → total = 0.0093738 + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093738 XLM", + { timeout: 10000 }, + ); + + // ── Reopen Edit Settings — saved fee must survive re-simulation ──────────── + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + + // ── Open FeesPane from re-opened settings ───────────────────────────────── + // Re-simulation used baseFee=0.00005 → inclusionFee=0.00005, resource unchanged + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( + "0.00005 XLM", + ); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", + ); + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093738 XLM", + ); + await expect(page.getByTestId("review-tx-fees-description")).toContainText( + "Soroban", + ); +}); + +// ── Comprehensive scenario 3: fee override is session-scoped ────────────────── +// After saving a custom fee, navigating back to the home screen and re-entering +// the send flow must reset to the default simulated fee. The manual override +// is intentionally not persisted across navigation sessions. +test("Custom fee resets to default when re-entering send flow from home screen", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); + + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + // ── First session: set custom fee ───────────────────────────────────────── + await page.getByText("E2E").click(); + await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); + + await page.getByTestId("address-tile").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); + + // Wait for simulation + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093338 XLM", + { timeout: 10000 }, + ); + + // Save a custom fee + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); + + // Confirm the override is active: re-simulation total = 0.0093738 + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093738 XLM", + { timeout: 10000 }, + ); + + // ── Navigate back to home screen ────────────────────────────────────────── + await page.getByTestId("BackButton").click(); + // goBack() dispatches resetSubmission() (clears destination / fees / state) + // and navigates to ROUTES.account (the main AccountView) + await expect(page.getByTestId("account-view")).toBeVisible({ + timeout: 10000, + }); + + // ── Second session: re-enter the same send flow ─────────────────────────── + await page.getByText("E2E").click(); + await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // resetSubmission cleared destination → no auto-simulation fires on mount. + // Fee display shows the base fee, NOT the previous override total (0.0093738). + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00001 XLM", + ); + + // ── EditSettings must show the default inclusion fee, not the saved "0.00005" ─ + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); + + // ── FeesPane (simulation triggered by gear click) shows default total ───── + await page.getByTestId("edit-settings-fees-info-btn").click(); + await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); + // Once simulation settles, total = base(0.00001) + resource(0.0093238) = 0.0093338 + await expect(page.getByTestId("review-tx-total-fee")).toHaveText( + "0.0093338 XLM", + { timeout: 10000 }, + ); }); // Auto-simulation: after setting destination for a Soroban token send the fee @@ -303,6 +554,220 @@ test("Auto-simulation updates fee display on SendAmount before Review Send", asy ); }); +test("Soroban token — manually set fee is preserved when recipient is selected after saving", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + await page.getByText("E2E").click(); + await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); + + // Set custom fee before picking a recipient + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); + + // Select recipient — SendAmount remounts; fee must survive via Redux persistence + await page.getByTestId("address-tile").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); + + // Re-simulation uses saved baseFee=0.00005 → total = 0.00005 + 0.0093238 + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.0093738 XLM", + { timeout: 10000 }, + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Inclusion Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); +}); + +test("Classic send — manually set fee is applied and shown in settings", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00001 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); + + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00005 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); +}); + +test("Classic send — manually set fee carries through to Review Send", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00001 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00005 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + + await page.getByText("Review Send").click({ force: true }); + await expect(page.getByText("You are sending").first()).toBeVisible(); + await expect(page.getByTestId("review-tx-fee")).toHaveText("0.00005 XLM"); +}); + +test("Classic send — manually set fee resets when re-entering send flow from home", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00005 XLM", + ); + + await page.getByTestId("BackButton").click(); + await expect(page.getByTestId("account-view")).toBeVisible({ + timeout: 10000, + }); + + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00001 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00001", + ); +}); + +test("Classic send — manually set fee is preserved across change of recipient", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Set custom fee before picking a recipient + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00005 XLM", + ); + + // Select recipient — SendAmount remounts; no simulation runs for classic + await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + + await expect(page.getByTestId("send-amount-fee-display")).toHaveText( + "0.00005 XLM", + ); + + await page.getByTestId("send-amount-btn-fee").click(); + await expect(page.getByText("Transaction Fee")).toBeVisible(); + await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( + "0.00005", + ); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("Transaction Fee")).not.toBeVisible(); + + await page.getByTestId("send-amount-amount-input").fill("1"); + await page.getByText("Review Send").click({ force: true }); + await expect(page.getByText("You are sending").first()).toBeVisible(); + await expect(page.getByTestId("review-tx-fee")).toHaveText("0.00005 XLM"); +}); + // Re-simulation: changing the destination triggers a new simulation. The // baseFee reset ensures the second simulation still uses the original inclusion // fee (0.00001) as its base — EditSettings must show that value, not the inflated total. diff --git a/extension/src/popup/components/InternalTransaction/EditSettings/index.tsx b/extension/src/popup/components/InternalTransaction/EditSettings/index.tsx index e0ebb54fb8..681d66df6f 100644 --- a/extension/src/popup/components/InternalTransaction/EditSettings/index.tsx +++ b/extension/src/popup/components/InternalTransaction/EditSettings/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import { Button, Card, Icon, Input } from "@stellar/design-system"; import { Field, FieldProps, Formik, Form } from "formik"; import { useTranslation } from "react-i18next"; @@ -19,7 +19,8 @@ interface EditSettingsProps { title: string; isSoroban?: boolean; onClose: () => void; - onShowFeesInfo?: () => void; + onFeeChange?: (fee: string) => void; + onShowFeesInfo?: (currentDraftFee: string) => void; onSubmit: (args: EditSettingsFormValue) => void; } @@ -30,10 +31,14 @@ export const EditSettings = ({ title, isSoroban = false, onClose, + onFeeChange, onShowFeesInfo, onSubmit, }: EditSettingsProps) => { const { t } = useTranslation(); + // Tracks the current draft fee so onShowFeesInfo can pass it to the parent + // for an accurate FeesPane total without requiring a Redux save first. + const draftFeeRef = useRef(fee); const initialValues: EditSettingsFormValue = { fee, timeout, @@ -49,7 +54,7 @@ export const EditSettings = ({ diff --git a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx index 0c09c710f4..70e8facf26 100644 --- a/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx +++ b/extension/src/popup/components/InternalTransaction/FeesPane/index.tsx @@ -10,10 +10,16 @@ import "./styles.scss"; export interface FeesPaneProps { fee: string; simulationState: State; + isSoroban?: boolean; onClose: () => void; } -export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => { +export const FeesPane = ({ + fee, + simulationState, + isSoroban = false, + onClose, +}: FeesPaneProps) => { const { t } = useTranslation(); const isLoading = @@ -82,7 +88,7 @@ export const FeesPane = ({ fee, simulationState, onClose }: FeesPaneProps) => { className="FeesPane__Description" data-testid="review-tx-fees-description" > - {simulationState.data?.resourceFee + {isSoroban ? t("Fees description soroban") : t("Fees description classic")}
diff --git a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx index 3d224d3b96..2c98e1500e 100644 --- a/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx +++ b/extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx @@ -445,6 +445,7 @@ export const ReviewTx = ({ setActivePaneIndex(paneConfig.reviewIndex)} /> ); diff --git a/extension/src/popup/components/send/SendAmount/index.tsx b/extension/src/popup/components/send/SendAmount/index.tsx index 6bceb8d502..2288b21d78 100644 --- a/extension/src/popup/components/send/SendAmount/index.tsx +++ b/extension/src/popup/components/send/SendAmount/index.tsx @@ -37,6 +37,7 @@ import { saveIsToken, saveMemo, saveTransactionFee, + saveManualTransactionFee, saveTransactionTimeout, saveAmountUsd, } from "popup/ducks/transactionSubmission"; @@ -74,6 +75,19 @@ import "../styles.scss"; const DEFAULT_INPUT_WIDTH = 25; +// Returns the value to show in FeesPane's total row given the user's current +// draft inclusion fee and the simulated resource fee. For classic (no +// resource fee) the inclusion fee IS the total. +function buildFeesPaneTotal( + inclusionFee: string, + resourceFee: string | undefined, +): string { + if (!resourceFee) { + return inclusionFee; + } + return new BigNumber(inclusionFee).plus(resourceFee).toFixed(); +} + export const SendAmount = ({ goBack, goToNext, @@ -110,6 +124,7 @@ export const SendAmount = ({ transactionFee, isCollectible, collectibleData, + manualTransactionFee, } = transactionData; const fee = transactionFee || recommendedFee; @@ -124,13 +139,29 @@ export const SendAmount = ({ lastInclusionFeeRef.current = simulationState.data.inclusionFee; } - // For Soroban: show the stable inclusion fee (base fee only). - // For classic: show the current total fee (may be user-customised). + // Tracks the fee the user explicitly saved via EditSettings this session. + // Once set, re-simulations no longer overwrite the displayed inclusion fee, + // mirroring the mobile hasManuallyChanged pattern. + // Initialized from Redux so the value survives SendAmount unmount/remount + // (e.g. when the user navigates to pick a recipient address and returns). + const hasManuallySetFeeRef = useRef(manualTransactionFee); + + // For Soroban: prefer the user's manually-saved fee, then the last simulated + // inclusion fee (base fee only). For classic: use the current total fee. const editSettingsFee = isToken || isCollectible - ? (lastInclusionFeeRef.current ?? recommendedFee) + ? (hasManuallySetFeeRef.current ?? + lastInclusionFeeRef.current ?? + recommendedFee) : fee; + // Holds the fee the user has typed but not yet saved. Survives the + // EditSettings unmount that occurs when the fees pane opens so that the + // input re-initialises to the draft value on return. + const [draftFeeForDisplay, setDraftFeeForDisplay] = React.useState< + string | null + >(null); + const { state: sendAmountData, fetchData } = useGetSendAmountData( { showHidden: false, @@ -158,6 +189,9 @@ export const SendAmount = ({ const [isEditingMemo, setIsEditingMemo] = React.useState(false); const [isEditingSettings, setIsEditingSettings] = React.useState(false); const [isShowingFeesPane, setIsShowingFeesPane] = React.useState(false); + // Holds the fee value shown in FeesPane's total row. Updated to reflect the + // user's current draft inclusion fee before the pane opens. + const [feesPaneTotal, setFeesPaneTotal] = React.useState(fee); const [isReviewingTx, setIsReviewingTx] = React.useState(false); const [contractSupportsMuxed, setContractSupportsMuxed] = React.useState< boolean | null @@ -262,9 +296,14 @@ export const SendAmount = ({ // Reset to the inclusion fee before re-simulating. After a prior // simulation, saveTransactionFee stored the TOTAL (inclusion + resource). // Without this reset that total would be used as baseFee on the next run, - // inflating both inclusionFee and recommendedFee. + // inflating both inclusionFee and recommendedFee. Prefer any fee the + // user explicitly saved via EditSettings over the simulated base fee. dispatch( - saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + saveTransactionFee( + hasManuallySetFeeRef.current ?? + lastInclusionFeeRef.current ?? + recommendedFee, + ), ); } else if (!transactionFee) { dispatch(saveTransactionFee(fee)); @@ -348,9 +387,14 @@ export const SendAmount = ({ if (destChanged || assetChanged) { // Reset to inclusion fee before re-simulating so total fee from a prior // simulation isn't used as baseFee (which would inflate the result). + // Prefer the user's manually-saved fee if present. if (isToken || isCollectible) { dispatch( - saveTransactionFee(lastInclusionFeeRef.current ?? recommendedFee), + saveTransactionFee( + hasManuallySetFeeRef.current ?? + lastInclusionFeeRef.current ?? + recommendedFee, + ), ); } simulationDataRef.current = { destination, asset }; @@ -452,12 +496,22 @@ export const SendAmount = ({ dispatch(saveIsToken(false)); dispatch(saveAmount("0")); dispatch(saveAmountUsd("0.00")); + // Clear any manually-saved fee so the next send session always starts from + // the simulated base fee rather than a stale override. + dispatch(saveTransactionFee("")); + dispatch(saveManualTransactionFee(null)); goBack(); if (isCollectible) { goToChooseAssetAction(); } }; const goToChooseAssetAction = () => { + // Changing the asset may switch between Soroban and classic (or a different + // token), so any manually-saved fee from the prior asset should not carry + // over. Clear it here before navigating so post-remount the fee is derived + // freshly from the new asset's simulation. + dispatch(saveManualTransactionFee(null)); + hasManuallySetFeeRef.current = null; goToChooseAsset(); }; @@ -862,13 +916,30 @@ export const SendAmount = ({ <>
setIsEditingSettings(false)} - onShowFeesInfo={() => setIsShowingFeesPane(true)} + onFeeChange={(v) => { + setDraftFeeForDisplay(v); + }} + onClose={() => { + setIsEditingSettings(false); + setDraftFeeForDisplay(null); + }} + onShowFeesInfo={(currentDraftFee) => { + const inclusionFee = + currentDraftFee || + (lastInclusionFeeRef.current ?? recommendedFee); + setFeesPaneTotal( + buildFeesPaneTotal( + inclusionFee, + simulationState.data?.resourceFee, + ), + ); + setIsShowingFeesPane(true); + }} onSubmit={async ({ fee, timeout, @@ -878,14 +949,20 @@ export const SendAmount = ({ }) => { dispatch(saveTransactionFee(fee)); dispatch(saveTransactionTimeout(timeout)); + hasManuallySetFeeRef.current = fee; + dispatch(saveManualTransactionFee(fee)); setIsEditingSettings(false); + setDraftFeeForDisplay(null); // Regenerate transaction XDR with new fee (now reads fee from Redux state inside fetchData) await fetchSimulationData(); }} />
setIsEditingSettings(false)} + onClick={() => { + setIsEditingSettings(false); + setDraftFeeForDisplay(null); + }} isActive={isEditingSettings} /> @@ -901,8 +978,9 @@ export const SendAmount = ({
{ setIsShowingFeesPane(false); setIsEditingSettings(true); diff --git a/extension/src/popup/ducks/transactionSubmission.ts b/extension/src/popup/ducks/transactionSubmission.ts index 578413ca01..6d3254b930 100644 --- a/extension/src/popup/ducks/transactionSubmission.ts +++ b/extension/src/popup/ducks/transactionSubmission.ts @@ -443,6 +443,10 @@ interface TransactionData { name: string; image: string; }; + // Fee the user explicitly saved via EditSettings this session. + // Persisted in Redux so it survives SendAmount unmount/remount (e.g. when + // the user navigates to pick a recipient and returns). + manualTransactionFee: string | null; } interface HardwareWalletData { @@ -508,6 +512,7 @@ export const initialState: InitialState = { isMergeSelected: false, balancesToMigrate: [] as BalanceToMigrate[], isSoroswap: false, + manualTransactionFee: null, }, transactionSimulation: { response: null, @@ -552,6 +557,9 @@ const transactionSubmissionSlice = createSlice({ saveTransactionFee: (state, action) => { state.transactionData.transactionFee = action.payload; }, + saveManualTransactionFee: (state, action) => { + state.transactionData.manualTransactionFee = action.payload; + }, saveTransactionTimeout: (state, action) => { state.transactionData.transactionTimeout = action.payload; }, @@ -746,6 +754,7 @@ export const { saveAmountUsd, saveAsset, saveTransactionFee, + saveManualTransactionFee, saveTransactionTimeout, saveMemo, saveDestinationAsset,