From 12791a2a297be82bf63584ca4c11d928d305fb76 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 18 Feb 2026 16:11:17 -0800 Subject: [PATCH 1/2] Fix pre-existing lint warnings in SwapConfirmation and ExchangeQuote --- eslint.config.mjs | 1 - src/components/scenes/SwapConfirmationScene.tsx | 4 ++-- src/components/themed/ExchangeQuoteComponent.tsx | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index dc6e9ef7eaa..c20fb1371c6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -230,7 +230,6 @@ export default [ 'src/components/progress-indicators/StepProgressBar.tsx', 'src/components/rows/CryptoFiatAmountRow.tsx', - 'src/components/rows/CurrencyRow.tsx', 'src/components/rows/EdgeRow.tsx', diff --git a/src/components/scenes/SwapConfirmationScene.tsx b/src/components/scenes/SwapConfirmationScene.tsx index a85e7974158..5b3f52dbf00 100644 --- a/src/components/scenes/SwapConfirmationScene.tsx +++ b/src/components/scenes/SwapConfirmationScene.tsx @@ -243,7 +243,7 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { expirationDate != null ? expirationDate.toISOString() : 'no expiration' } networkFee: - currencyCode ${networkFee.currencyCode} + tokenId ${String(networkFee.tokenId)} nativeAmount ${networkFee.nativeAmount} `) @@ -295,7 +295,7 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { } } - const renderTimer = (): React.ReactNode => { + const renderTimer = (): React.ReactElement | null => { const { expirationDate } = selectedQuote if (expirationDate == null) return null return ( diff --git a/src/components/themed/ExchangeQuoteComponent.tsx b/src/components/themed/ExchangeQuoteComponent.tsx index e7d4678b9ae..5418fec588b 100644 --- a/src/components/themed/ExchangeQuoteComponent.tsx +++ b/src/components/themed/ExchangeQuoteComponent.tsx @@ -130,7 +130,7 @@ export const ExchangeQuote: React.FC = props => { label: React.ReactNode, value: React.ReactNode, style: any = {} - ): React.ReactNode => { + ): React.ReactElement => { return ( {label} @@ -139,7 +139,7 @@ export const ExchangeQuote: React.FC = props => { ) } - const renderBottom = (): React.ReactNode => { + const renderBottom = (): React.ReactElement | null => { if (fromTo === 'from') { const feeTextStyle = showFeeWarning === true ? styles.bottomWarningText : styles.bottomText From 2d188a4f73fa5102f0ede362cab7c8f973a772a3 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 18 Feb 2026 15:00:13 -0800 Subject: [PATCH 2/2] Add price impact warning to swap confirmation Calculate price impact by comparing quote fiat values against market rates. Display the percentage inline next to the fiat price in the TO card, colored by severity (grey <5%, orange 5-15%, red >15%). Show warning cards based on price impact and fee warning state: - Price impact only: dedicated warning card - Both price impact and fee warning: combined card replaces fee warning - Fee warning only: existing behavior preserved --- CHANGELOG.md | 1 + .../SwapConfirmationScene.test.tsx.snap | 429 +++++++++--------- src/components/rows/CurrencyRow.tsx | 12 +- .../scenes/SwapConfirmationScene.tsx | 75 ++- .../themed/ExchangeQuoteComponent.tsx | 32 +- src/locales/en_US.ts | 6 + src/locales/strings/enUS.json | 4 + 7 files changed, 350 insertions(+), 209 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b3466f5da..92ad2654af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - added: Monad (MON) wallet support - added: Nym (NYM) wallet support - added: opBNB (BNB) support +- added: Price impact warning on swap confirmation with color-coded severity - added: Register SwapKit V3 as a separate exchange plugin - added: Warning card on send scene when Nym mixnet is active and transaction is loading - added: Verify buy tracking values via Moonpay transactions API with mismatch diagnostics diff --git a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap index 6dbf122dcf9..080b5371a76 100644 --- a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap @@ -370,7 +370,7 @@ exports[`SwapConfirmationScene should render with loading props 1`] = ` "initialValues": { "transform": [ { - "translateY": -30, + "translateY": -60, }, ], }, @@ -397,6 +397,205 @@ exports[`SwapConfirmationScene should render with loading props 1`] = ` } } nativeID="1" + > + + + + + + + +  + + + High Price Impact + + + + This swap rate is significantly less favorable than the current market rate. + + + + + €0.000000 + + (99.99%) + @@ -1408,7 +1630,7 @@ exports[`SwapConfirmationScene should render with loading props 1`] = ` "reduceMotionV": "system", } } - nativeID="6" + nativeID="7" > - - - - - - - - -  - - - Estimated Quote - - - - The amount above is an estimate. This exchange may result in less funds received than quoted. - - - - = props => { - const { marginRem, nativeAmount, hideBalance, token, tokenId, wallet } = props + const { + marginRem, + nativeAmount, + hideBalance, + rightSubTextExtended, + token, + tokenId, + wallet + } = props const { pluginId } = wallet.currencyInfo const { showTokenNames = false } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} const theme = useTheme() @@ -96,6 +105,7 @@ const CurrencyRowComponent: React.FC = props => { leftSubtext={name} rightText={cryptoText} rightSubText={fiatText} + rightSubTextExtended={rightSubTextExtended} marginRem={marginRem} /> ) diff --git a/src/components/scenes/SwapConfirmationScene.tsx b/src/components/scenes/SwapConfirmationScene.tsx index 5b3f52dbf00..04eed5b9982 100644 --- a/src/components/scenes/SwapConfirmationScene.tsx +++ b/src/components/scenes/SwapConfirmationScene.tsx @@ -1,5 +1,5 @@ import { useIsFocused } from '@react-navigation/native' -import { add, div, gt, gte, toFixed } from 'biggystring' +import { add, div, gt, gte, lte, sub, toFixed } from 'biggystring' import type { EdgeSwapQuote, EdgeSwapResult } from 'edge-core-js' import React, { useState } from 'react' import { SectionList, type ViewStyle } from 'react-native' @@ -54,6 +54,8 @@ import { ModalFooter } from '../themed/ModalParts' import { SafeSlider } from '../themed/SafeSlider' import { WalletListSectionHeader } from '../themed/WalletListSectionHeader' +const PRICE_IMPACT_WARNING_THRESHOLD = 0.05 + export interface SwapConfirmationParams { selectedQuote: EdgeSwapQuote quotes: EdgeSwapQuote[] @@ -76,6 +78,8 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { const styles = getStyles(theme) const account = useSelector(state => state.core.account) + const defaultIsoFiat = useSelector(state => state.ui.settings.defaultIsoFiat) + const exchangeRates = useSelector(state => state.exchangeRates) const feeFiat = useSelector(state => selectedQuote == null ? '0' @@ -87,6 +91,7 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { selectedQuote.networkFee.nativeAmount ) ) + const [pending, setPending] = useState(false) const swapRequestOptions = useSwapRequestOptions() @@ -118,6 +123,48 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { const { request } = selectedQuote const { quoteFor } = request + const priceImpact = React.useMemo(() => { + const { fromWallet, fromTokenId, toWallet, toTokenId } = request + + const fromExchangeDenom = getExchangeDenom( + fromWallet.currencyConfig, + fromTokenId + ) + const toExchangeDenom = getExchangeDenom(toWallet.currencyConfig, toTokenId) + + const fromExchangeAmount = convertNativeToExchange( + fromExchangeDenom.multiplier + )(selectedQuote.fromNativeAmount) + const toExchangeAmount = convertNativeToExchange( + toExchangeDenom.multiplier + )(selectedQuote.toNativeAmount) + + const fromFiatValue = convertCurrency( + exchangeRates, + fromWallet.currencyInfo.pluginId, + fromTokenId, + defaultIsoFiat, + fromExchangeAmount + ) + const toFiatValue = convertCurrency( + exchangeRates, + toWallet.currencyInfo.pluginId, + toTokenId, + defaultIsoFiat, + toExchangeAmount + ) + + if (lte(fromFiatValue, '0')) return undefined + + const impact = parseFloat( + div(sub(fromFiatValue, toFiatValue), fromFiatValue, 8) + ) + return impact > 0 ? impact : undefined + }, [selectedQuote, exchangeRates, defaultIsoFiat, request]) + + const showPriceImpact = + priceImpact != null && priceImpact >= PRICE_IMPACT_WARNING_THRESHOLD + const scrollPadding = React.useMemo( () => ({ paddingBottom: theme.rem(ModalFooter.bottomRem) @@ -383,7 +430,23 @@ export const SwapConfirmationScene: React.FC = (props: Props) => { return ( - {showFeeWarning ? ( + {showPriceImpact && showFeeWarning ? ( + + + + ) : showPriceImpact ? ( + + + + ) : showFeeWarning ? ( = (props: Props) => { - + = (props: Props) => { onPress={handlePoweredByTap} /> - {selectedQuote.isEstimate ? ( + {selectedQuote.isEstimate && !showPriceImpact ? ( = props => { - const { fromTo, quote, showFeeWarning } = props + const { fromTo, priceImpact, quote, showFeeWarning } = props const { request, fromNativeAmount, toNativeAmount, networkFee } = quote const { fromWallet, fromTokenId, toWallet, toTokenId } = request @@ -184,6 +186,21 @@ export const ExchangeQuote: React.FC = props => { } } + const priceImpactNode = + !isFrom && priceImpact != null && priceImpact > 0 ? ( + = 0.15 + ? styles.priceImpactHigh + : priceImpact >= 0.05 + ? styles.priceImpactMedium + : styles.priceImpactLow + } + > + {` (${formatNumber(priceImpact * 100, { toFixed: 2 })}%)`} + + ) : undefined + return ( = props => { marginRem={0.5} nativeAmount={nativeAmount} hideBalance={false} + rightSubTextExtended={priceImpactNode} /> {renderBottom()} @@ -223,5 +241,17 @@ const getStyles = cacheStyles((theme: Theme) => ({ bottomWarningText: { fontSize: theme.rem(0.75), color: theme.warningText + }, + priceImpactLow: { + fontSize: theme.rem(0.75), + color: theme.deactivatedText + }, + priceImpactMedium: { + fontSize: theme.rem(0.75), + color: theme.warningText + }, + priceImpactHigh: { + fontSize: theme.rem(0.75), + color: theme.dangerText } })) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 0c324de9d77..9dce7c12c5e 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -547,6 +547,12 @@ const strings = { swap_token_no_enabled_exchanges_2s: 'No enabled exchanges support %1$s (on %2$s) at this time', swap_minimum_receive_amount: 'Min Receive Amount', + swap_price_impact_warning_title: 'High Price Impact', + swap_price_impact_warning_body: + 'This swap rate is significantly less favorable than the current market rate.', + swap_price_impact_fee_warning_title: 'High Price Impact', + swap_price_impact_fee_warning_body: + 'This swap rate is significantly less favorable than the current market rate. High network fees relative to the swap amount are a contributing factor.', swap_minimum_amount_1s: 'Min %1$s', settings_button_clear_logs: 'Clear Logs', send_to_1s: 'Send to %1$s', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index b83e0fe33d2..0406d99057d 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -397,6 +397,10 @@ "swap_preferred_promo_instructions": "When multiple exchanges can fill an order, the current promotion always prefers:", "swap_token_no_enabled_exchanges_2s": "No enabled exchanges support %1$s (on %2$s) at this time", "swap_minimum_receive_amount": "Min Receive Amount", + "swap_price_impact_warning_title": "High Price Impact", + "swap_price_impact_warning_body": "This swap rate is significantly less favorable than the current market rate.", + "swap_price_impact_fee_warning_title": "High Price Impact", + "swap_price_impact_fee_warning_body": "This swap rate is significantly less favorable than the current market rate. High network fees relative to the swap amount are a contributing factor.", "swap_minimum_amount_1s": "Min %1$s", "settings_button_clear_logs": "Clear Logs", "send_to_1s": "Send to %1$s",