(() => {
+ if (selectedProviderName === null) return bestQuote
+ const match = allQuotes.find(
+ quote => quote.provider === selectedProviderName,
+ )
+ return match ?? bestQuote
+ }, [allQuotes, selectedProviderName, bestQuote])
+
+ const providerSelectionMode: 'auto' | 'manual' = useMemo(() => {
+ if (selectedProviderName === null) return 'auto'
+ const matchExists = allQuotes.some(
+ quote => quote.provider === selectedProviderName,
+ )
+ return matchExists ? 'manual' : 'auto'
+ }, [allQuotes, selectedProviderName])
+
+ useEffect(() => {
+ if (selectedProviderName === null) return
+ const matchExists = allQuotes.some(
+ quote => quote.provider === selectedProviderName,
+ )
+ if (!matchExists) setSelectedProviderName(null)
+ }, [allQuotes, selectedProviderName])
+
+ useEffect(() => {
+ if (!selectedQuote?.amountOut) {
+ setReceiveAmount(null)
+ return
+ }
+ const receiveDecimals = selectedQuote.assetOut.decimals ?? 0
+ setReceiveAmount(
+ baseUnitsToDisplayUnits(selectedQuote.amountOut, receiveDecimals),
+ )
+ }, [selectedQuote])
+
const canSwap = useMemo(
() =>
selectedQuote !== null &&
@@ -241,7 +269,8 @@ export const useSwapForm = (): UseSwapFormResult => {
setToAsset(fromAsset)
setPayAmount(receiveAmount)
setReceiveAmount(payAmount)
- setSelectedQuote(null)
+ setAllQuotes([])
+ setSelectedProviderName(null)
resetQuoteMutation()
}, [
fromAsset,
@@ -288,7 +317,8 @@ export const useSwapForm = (): UseSwapFormResult => {
setFromAsset(asset.assetId)
setPayAmount(null)
setReceiveAmount(null)
- setSelectedQuote(null)
+ setAllQuotes([])
+ setSelectedProviderName(null)
resetQuoteMutation()
},
[setFromAsset, resetQuoteMutation],
@@ -298,12 +328,17 @@ export const useSwapForm = (): UseSwapFormResult => {
(asset: AssetWithAccountBalance) => {
setToAsset(asset.assetId)
setReceiveAmount(null)
- setSelectedQuote(null)
+ setAllQuotes([])
+ setSelectedProviderName(null)
resetQuoteMutation()
},
[setToAsset, resetQuoteMutation],
)
+ const handleProviderApply = useCallback((providerName: string | null) => {
+ setSelectedProviderName(providerName)
+ }, [])
+
const successCloseTimer = useRunAfterDelay()
const handleCloseConfirm = useCallback(() => {
@@ -329,7 +364,8 @@ export const useSwapForm = (): UseSwapFormResult => {
)
setPayAmount(null)
setReceiveAmount(null)
- setSelectedQuote(null)
+ setAllQuotes([])
+ setSelectedProviderName(null)
resetQuoteMutation()
confirmModal.close()
}, SUCCESS_DISPLAY_MS)
@@ -385,18 +421,23 @@ export const useSwapForm = (): UseSwapFormResult => {
isQuoteFetching,
isQuoteError,
selectedQuote,
+ allQuotes,
+ selectedProviderName,
+ providerSelectionMode,
canSwap,
swapStatus: swapExecution.status,
payAssetModal,
receiveAssetModal,
configModal,
confirmModal,
+ providerModal,
handlePayAmountChange,
handleSwapDirection,
handleMaxPress,
handlePayAssetSelected,
handleReceiveAssetSelected,
handleConfigApply,
+ handleProviderApply,
handleConfirmSwap,
handleOpenConfirm,
handleCloseConfirm,
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/ProviderSelectionItem.tsx b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/ProviderSelectionItem.tsx
new file mode 100644
index 000000000..f613afc26
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/ProviderSelectionItem.tsx
@@ -0,0 +1,55 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { ReactNode } from 'react'
+import { PWRadioButton, PWText, PWView } from '@components/core'
+import { useStyles } from './styles'
+
+export type ProviderSelectionItemProps = {
+ left: ReactNode
+ label: string
+ right: ReactNode
+ isSelected: boolean
+ onPress: () => void
+ testID?: string
+}
+
+export const ProviderSelectionItem = ({
+ left,
+ label,
+ right,
+ isSelected,
+ onPress,
+ testID,
+}: ProviderSelectionItemProps) => {
+ const styles = useStyles()
+
+ return (
+
+
+ {left}
+
+ {label}
+
+
+ {right}
+
+ )
+}
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/SwapProviderBottomSheet.tsx b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/SwapProviderBottomSheet.tsx
new file mode 100644
index 000000000..2ad125000
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/SwapProviderBottomSheet.tsx
@@ -0,0 +1,149 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { useTheme } from '@rneui/themed'
+import {
+ PWBottomSheet,
+ PWIcon,
+ PWImage,
+ PWText,
+ PWToolbar,
+ PWTouchableOpacity,
+ PWView,
+} from '@components/core'
+import { useLanguage } from '@hooks/useLanguage'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+import { ProviderSelectionItem } from './ProviderSelectionItem'
+import { useSwapProviderBottomSheet } from './useSwapProviderBottomSheet'
+import { useStyles } from './styles'
+
+export type SwapProviderBottomSheetProps = {
+ isVisible: boolean
+ onClose: () => void
+ quotes: SwapQuote[]
+ selectedProviderName: string | null
+ onApply: (providerName: string | null) => void
+}
+
+export const SwapProviderBottomSheet = ({
+ isVisible,
+ onClose,
+ quotes,
+ selectedProviderName,
+ onApply,
+}: SwapProviderBottomSheetProps) => {
+ const { t } = useLanguage()
+ const styles = useStyles()
+ const { theme } = useTheme()
+
+ const { userSelection, rows, handleSelect, handleApply } =
+ useSwapProviderBottomSheet({
+ isVisible,
+ quotes,
+ selectedProviderName,
+ onApply,
+ onClose,
+ })
+
+ return (
+
+
+ }
+ center={
+
+ {t('swap.provider.change_title')}
+
+ }
+ right={
+
+
+ {t('swap.provider.apply')}
+
+
+ }
+ />
+
+
+ }
+ label={t('swap.provider.auto_label')}
+ right={
+
+ {t('swap.provider.auto_description')}
+
+ }
+ isSelected={userSelection === null}
+ onPress={() => handleSelect(null)}
+ testID='swap-provider-option-auto'
+ />
+ {rows.map((row, index) => (
+
+ ) : (
+
+ )
+ }
+ label={row.displayName}
+ right={
+
+
+ {row.amountDisplay}
+
+ {row.fiatDisplay && (
+
+ {row.fiatDisplay}
+
+ )}
+
+ }
+ isSelected={userSelection === row.quote.provider}
+ onPress={() => handleSelect(row.quote.provider ?? null)}
+ testID={`swap-provider-option-${row.quote.provider ?? 'unknown'}`}
+ />
+ ))}
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/__tests__/SwapProviderBottomSheet.spec.tsx b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/__tests__/SwapProviderBottomSheet.spec.tsx
new file mode 100644
index 000000000..7dea61ca8
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/__tests__/SwapProviderBottomSheet.spec.tsx
@@ -0,0 +1,366 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import React from 'react'
+import { render, screen, fireEvent } from '@test-utils/render'
+import { describe, it, expect, vi } from 'vitest'
+import { Decimal } from 'decimal.js'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+import { SwapProviderBottomSheet } from '../SwapProviderBottomSheet'
+
+vi.mock('@hooks/useLanguage', () => ({
+ useLanguage: () => ({ t: (key: string) => key }),
+}))
+
+vi.mock('@perawallet/wallet-core-assets', () => ({
+ formatAssetAmount: (amount: Decimal, asset: { unitName?: string }) =>
+ `${amount.toString()} ${asset.unitName ?? ''}`.trim(),
+}))
+
+vi.mock('@perawallet/wallet-core-currencies', () => ({
+ useCurrency: () => ({
+ preferredCurrency: 'USD',
+ usdToPreferred: (value: Decimal) => value,
+ }),
+}))
+
+vi.mock('@perawallet/wallet-core-shared', () => ({
+ formatCurrency: (value: Decimal, _precision: number, currency: string) =>
+ `${currency} ${value.toString()}`,
+}))
+
+vi.mock('@perawallet/wallet-core-swaps', () => ({
+ useProvidersQuery: () => ({
+ data: [
+ {
+ name: 'vestige',
+ displayName: 'Vestige.fi',
+ iconUrl: 'https://example.com/vestige.png',
+ },
+ {
+ name: 'tinyman',
+ displayName: 'Tinyman',
+ iconUrl: 'https://example.com/tinyman.png',
+ },
+ ],
+ }),
+}))
+
+vi.mock('@components/core', () => ({
+ PWBottomSheet: ({
+ children,
+ isVisible,
+ }: {
+ children: React.ReactNode
+ isVisible: boolean
+ }) => (isVisible ? {children}
: null),
+ PWToolbar: ({
+ left,
+ center,
+ right,
+ }: {
+ left: React.ReactNode
+ center: React.ReactNode
+ right: React.ReactNode
+ }) => (
+
+ {left}
+ {center}
+ {right}
+
+ ),
+ PWIcon: ({ name, onPress }: { name: string; onPress?: () => void }) => (
+
+ ),
+ PWImage: () => ,
+ PWText: ({
+ children,
+ style,
+ }: {
+ children?: React.ReactNode
+ style?: unknown
+ }) => {children},
+ PWTouchableOpacity: ({
+ children,
+ onPress,
+ testID,
+ style,
+ }: {
+ children?: React.ReactNode
+ onPress?: () => void
+ testID?: string
+ style?: unknown
+ }) => (
+
+ ),
+ PWRadioButton: ({
+ children,
+ onPress,
+ testID,
+ containerStyle,
+ isSelected,
+ }: {
+ children?: React.ReactNode
+ onPress?: () => void
+ testID?: string
+ containerStyle?: unknown
+ isSelected?: boolean
+ }) => (
+
+ ),
+ PWView: ({
+ children,
+ style,
+ testID,
+ }: {
+ children?: React.ReactNode
+ style?: unknown
+ testID?: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+const createQuote = (overrides: Partial = {}): SwapQuote => ({
+ assetIn: {
+ assetId: 0,
+ unitName: 'ALGO',
+ decimals: 6,
+ verificationTier: 'verified',
+ },
+ assetOut: {
+ assetId: 31566704,
+ unitName: 'USDC',
+ decimals: 6,
+ verificationTier: 'verified',
+ },
+ amountOut: new Decimal('600.08'),
+ amountOutUsdValue: '600.08',
+ provider: 'vestige',
+ providerDisplayName: 'Vestige.fi',
+ ...overrides,
+})
+
+describe('SwapProviderBottomSheet', () => {
+ it('does not render when not visible', () => {
+ render(
+ ,
+ )
+
+ expect(screen.queryByTestId('bottom-sheet')).toBeNull()
+ })
+
+ it('renders Auto row and excludes the best-price quote from the list', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByTestId('swap-provider-option-auto')).toBeDefined()
+ expect(screen.getByTestId('swap-provider-option-tinyman')).toBeDefined()
+ expect(screen.queryByTestId('swap-provider-option-vestige')).toBeNull()
+ })
+
+ it('auto row is marked selected when selectedProviderName is null', () => {
+ render(
+ ,
+ )
+
+ expect(
+ screen.getByTestId('swap-provider-option-auto-radio').children
+ .length,
+ ).toBeGreaterThan(0)
+ })
+
+ it('calls onApply with draft and onClose when Apply is pressed', () => {
+ const onApply = vi.fn()
+ const onClose = vi.fn()
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByTestId('swap-provider-option-tinyman'))
+ fireEvent.click(screen.getByTestId('swap-provider-apply'))
+
+ expect(onApply).toHaveBeenCalledWith('tinyman')
+ expect(onClose).toHaveBeenCalled()
+ })
+
+ it('Apply with Auto selected calls onApply with null', () => {
+ const onApply = vi.fn()
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByTestId('swap-provider-option-auto'))
+ fireEvent.click(screen.getByTestId('swap-provider-apply'))
+
+ expect(onApply).toHaveBeenCalledWith(null)
+ })
+
+ it('renders quotes without a provider name and applies null when selected', () => {
+ const onApply = vi.fn()
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByTestId('swap-provider-option-unknown'))
+ fireEvent.click(screen.getByTestId('swap-provider-apply'))
+
+ expect(onApply).toHaveBeenCalledWith(null)
+ })
+
+ it('renders provider rows ordered by amountOut descending', () => {
+ render(
+ ,
+ )
+
+ const providerButtons = screen
+ .getAllByRole('button')
+ .map(button => button.getAttribute('data-testid') ?? '')
+ .filter(id => id.startsWith('swap-provider-option-'))
+ .filter(id => !id.endsWith('-radio'))
+ .filter(id => id !== 'swap-provider-option-auto')
+
+ expect(providerButtons).toEqual([
+ 'swap-provider-option-tinyman',
+ 'swap-provider-option-folks',
+ ])
+ })
+
+ it('calls onClose when the cross icon is pressed', () => {
+ const onClose = vi.fn()
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByTestId('icon-cross'))
+
+ expect(onClose).toHaveBeenCalled()
+ })
+})
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/index.ts b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/index.ts
new file mode 100644
index 000000000..05c07b139
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/index.ts
@@ -0,0 +1,14 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+export { SwapProviderBottomSheet } from './SwapProviderBottomSheet'
+export type { SwapProviderBottomSheetProps } from './SwapProviderBottomSheet'
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/styles.ts b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/styles.ts
new file mode 100644
index 000000000..ad5833d34
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/styles.ts
@@ -0,0 +1,67 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { makeStyles } from '@rneui/themed'
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ paddingBottom: theme.spacing.lg,
+ },
+ applyText: {
+ color: theme.colors.linkPrimary,
+ },
+ list: {
+ paddingHorizontal: theme.spacing.lg,
+ paddingVertical: theme.spacing.md,
+ gap: theme.spacing.lg,
+ },
+ item: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: theme.spacing.md,
+ paddingVertical: theme.spacing.sm,
+ paddingHorizontal: 0,
+ },
+ itemLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: theme.spacing.md,
+ flex: 1,
+ },
+ logo: {
+ width: theme.spacing.xl,
+ height: theme.spacing.xl,
+ borderRadius: theme.borderRadius.full,
+ overflow: 'hidden',
+ },
+ itemLabel: {
+ color: theme.colors.textMain,
+ },
+ itemRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: theme.spacing.md,
+ },
+ rightTextColumn: {
+ alignItems: 'flex-end',
+ },
+ amountText: {
+ color: theme.colors.textMain,
+ },
+ fiatText: {
+ color: theme.colors.textGray,
+ },
+ autoDescription: {
+ color: theme.colors.textGray,
+ },
+}))
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/useSwapProviderBottomSheet.ts b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/useSwapProviderBottomSheet.ts
new file mode 100644
index 000000000..a2573f702
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderBottomSheet/useSwapProviderBottomSheet.ts
@@ -0,0 +1,118 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { Decimal } from 'decimal.js'
+import { formatAssetAmount } from '@perawallet/wallet-core-assets'
+import { useCurrency } from '@perawallet/wallet-core-currencies'
+import { formatCurrency } from '@perawallet/wallet-core-shared'
+import {
+ useProvidersQuery,
+ type SwapQuote,
+} from '@perawallet/wallet-core-swaps'
+import { sortQuotesByAmountOutDesc } from '../../hooks/swapQuoteHelpers'
+
+type UseSwapProviderBottomSheetParams = {
+ isVisible: boolean
+ quotes: SwapQuote[]
+ selectedProviderName: string | null
+ onApply: (providerName: string | null) => void
+ onClose: () => void
+}
+
+export type SwapProviderRow = {
+ quote: SwapQuote
+ iconUrl: string | undefined
+ displayName: string
+ amountDisplay: string
+ fiatDisplay: string | undefined
+}
+
+type UseSwapProviderBottomSheetResult = {
+ userSelection: string | null
+ rows: SwapProviderRow[]
+ handleSelect: (providerName: string | null) => void
+ handleApply: () => void
+}
+
+export const useSwapProviderBottomSheet = ({
+ isVisible,
+ quotes,
+ selectedProviderName,
+ onApply,
+ onClose,
+}: UseSwapProviderBottomSheetParams): UseSwapProviderBottomSheetResult => {
+ const { preferredCurrency, usdToPreferred } = useCurrency()
+ const { data: providers } = useProvidersQuery()
+
+ const [userSelection, setUserSelection] = useState(
+ selectedProviderName,
+ )
+
+ useEffect(() => {
+ if (isVisible) setUserSelection(selectedProviderName)
+ }, [isVisible, selectedProviderName])
+
+ const handleSelect = useCallback((providerName: string | null) => {
+ setUserSelection(providerName)
+ }, [])
+
+ const handleApply = useCallback(() => {
+ onApply(userSelection)
+ onClose()
+ }, [userSelection, onApply, onClose])
+
+ const sortedQuotes = useMemo(
+ () => sortQuotesByAmountOutDesc(quotes),
+ [quotes],
+ )
+
+ // Auto row already represents the top quote; drop it from the explicit list.
+ const alternativeQuotes = useMemo(
+ () => sortedQuotes.slice(1),
+ [sortedQuotes],
+ )
+
+ const rows = useMemo(
+ () =>
+ alternativeQuotes.map(quote => {
+ const providerItem = providers?.find(
+ item => item.name === quote.provider,
+ )
+ const displayName =
+ quote.providerDisplayName ??
+ providerItem?.displayName ??
+ quote.provider ??
+ '-'
+ const amountDisplay = quote.amountOut
+ ? formatAssetAmount(quote.amountOut, quote.assetOut)
+ : '-'
+ const fiatDisplay = quote.amountOutUsdValue
+ ? formatCurrency(
+ usdToPreferred(new Decimal(quote.amountOutUsdValue)),
+ 2,
+ preferredCurrency,
+ )
+ : undefined
+ return {
+ quote,
+ iconUrl: providerItem?.iconUrl,
+ displayName,
+ amountDisplay,
+ fiatDisplay,
+ }
+ }),
+ [alternativeQuotes, providers, usdToPreferred, preferredCurrency],
+ )
+
+ return { userSelection, rows, handleSelect, handleApply }
+}
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderRow/SwapProviderRow.tsx b/apps/mobile/src/modules/swap/components/SwapProviderRow/SwapProviderRow.tsx
new file mode 100644
index 000000000..3a3046e8a
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderRow/SwapProviderRow.tsx
@@ -0,0 +1,75 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { useMemo } from 'react'
+import { PWIcon, PWText, PWTouchableOpacity, PWView } from '@components/core'
+import { useLanguage } from '@hooks/useLanguage'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+import { SwapProviderDisplay } from '../SwapProviderDisplay'
+import { formatSwapRate } from '../../hooks/swapQuoteHelpers'
+import { useStyles } from './styles'
+
+export type SwapProviderRowProps = {
+ quote: SwapQuote
+ selectionMode: 'auto' | 'manual'
+ onPress: () => void
+}
+
+export const SwapProviderRow = ({
+ quote,
+ selectionMode,
+ onPress,
+}: SwapProviderRowProps) => {
+ const { t } = useLanguage()
+ const styles = useStyles()
+
+ const rateDisplay = useMemo(() => formatSwapRate(quote), [quote])
+
+ const title =
+ selectionMode === 'auto'
+ ? t('swap.provider.title_auto')
+ : t('swap.provider.title_manual')
+
+ return (
+
+
+ {title}
+
+
+
+
+
+ {rateDisplay}
+
+
+
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderRow/__tests__/SwapProviderRow.spec.tsx b/apps/mobile/src/modules/swap/components/SwapProviderRow/__tests__/SwapProviderRow.spec.tsx
new file mode 100644
index 000000000..473428c5c
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderRow/__tests__/SwapProviderRow.spec.tsx
@@ -0,0 +1,160 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import React from 'react'
+import { render, screen, fireEvent } from '@test-utils/render'
+import { describe, it, expect, vi } from 'vitest'
+import { Decimal } from 'decimal.js'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+import { SwapProviderRow } from '../SwapProviderRow'
+
+vi.mock('@hooks/useLanguage', () => ({
+ useLanguage: () => ({ t: (key: string) => key }),
+}))
+
+vi.mock('@perawallet/wallet-core-shared', () => ({
+ DEFAULT_PRECISION: 2,
+ formatNumber: (value: Decimal) => ({
+ sign: '',
+ integer: value.toFixed(2).split('.')[0],
+ fraction: `.${value.toFixed(2).split('.')[1] ?? '00'}`,
+ }),
+}))
+
+vi.mock('@perawallet/wallet-core-assets', () => ({
+ ALGO_ASSET: { decimals: 6 },
+}))
+
+vi.mock('../../SwapProviderDisplay', () => ({
+ SwapProviderDisplay: ({
+ providerDisplayName,
+ }: {
+ providerDisplayName?: string
+ }) => {providerDisplayName},
+}))
+
+vi.mock('@components/core', () => ({
+ PWView: ({
+ children,
+ style,
+ }: {
+ children?: React.ReactNode
+ style?: unknown
+ }) => {children}
,
+ PWText: ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ ),
+ PWIcon: ({ name }: { name: string }) => (
+
+ ),
+ PWTouchableOpacity: ({
+ children,
+ onPress,
+ testID,
+ }: {
+ children?: React.ReactNode
+ onPress?: () => void
+ testID?: string
+ }) => (
+
+ ),
+}))
+
+const createQuote = (overrides: Partial = {}): SwapQuote => ({
+ assetIn: {
+ assetId: 0,
+ unitName: 'ALGO',
+ decimals: 6,
+ verificationTier: 'verified',
+ },
+ assetOut: {
+ assetId: 31566704,
+ unitName: 'USDC',
+ decimals: 6,
+ verificationTier: 'verified',
+ },
+ price: new Decimal('0.33'),
+ provider: 'vestige',
+ providerDisplayName: 'Vestige.fi',
+ ...overrides,
+})
+
+describe('SwapProviderRow', () => {
+ it('renders the auto title when selectionMode is auto', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText('swap.provider.title_auto')).toBeDefined()
+ })
+
+ it('renders the manual title when selectionMode is manual', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText('swap.provider.title_manual')).toBeDefined()
+ })
+
+ it('renders the quote rate', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText(/1 ALGO/)).toBeDefined()
+ expect(screen.getByText(/USDC/)).toBeDefined()
+ })
+
+ it('renders a dash rate when quote price is missing', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText('-')).toBeDefined()
+ })
+
+ it('calls onPress when the row is tapped', () => {
+ const onPress = vi.fn()
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByTestId('swap-provider-row'))
+
+ expect(onPress).toHaveBeenCalled()
+ })
+})
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderRow/index.ts b/apps/mobile/src/modules/swap/components/SwapProviderRow/index.ts
new file mode 100644
index 000000000..6e3a6f798
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderRow/index.ts
@@ -0,0 +1,14 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+export { SwapProviderRow } from './SwapProviderRow'
+export type { SwapProviderRowProps } from './SwapProviderRow'
diff --git a/apps/mobile/src/modules/swap/components/SwapProviderRow/styles.ts b/apps/mobile/src/modules/swap/components/SwapProviderRow/styles.ts
new file mode 100644
index 000000000..f18b7c706
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapProviderRow/styles.ts
@@ -0,0 +1,37 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { makeStyles } from '@rneui/themed'
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ paddingHorizontal: theme.spacing.lg,
+ gap: theme.spacing.sm,
+ },
+ title: {
+ color: theme.colors.textGray,
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ gap: theme.spacing.sm,
+ },
+ right: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: theme.spacing.sm,
+ },
+ rate: {
+ color: theme.colors.textMain,
+ },
+}))
diff --git a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/SwapQuoteDetails.tsx b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/SwapQuoteDetails.tsx
index 8eab84d89..19de2479b 100644
--- a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/SwapQuoteDetails.tsx
+++ b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/SwapQuoteDetails.tsx
@@ -10,13 +10,13 @@
limitations under the License
*/
-import { useMemo } from 'react'
-import { Decimal } from 'decimal.js'
import { PWSkeleton, PWText, PWView } from '@components/core'
import { useLanguage } from '@hooks/useLanguage'
-import { formatNumber } from '@perawallet/wallet-core-shared'
import type { SwapQuote } from '@perawallet/wallet-core-swaps'
-import { formatAssetAmount } from '@perawallet/wallet-core-assets'
+import {
+ useSwapQuoteDetails,
+ type PriceImpactLevel,
+} from './useSwapQuoteDetails'
import { useStyles } from './styles'
export type SwapQuoteDetailsProps = {
@@ -24,46 +24,27 @@ export type SwapQuoteDetailsProps = {
isLoading?: boolean
}
-const PRICE_IMPACT_LOW_THRESHOLD = new Decimal(1)
-const PRICE_IMPACT_HIGH_THRESHOLD = new Decimal(5)
-
export const SwapQuoteDetails = ({
quote,
isLoading,
}: SwapQuoteDetailsProps) => {
const { t } = useLanguage()
const styles = useStyles()
+ const {
+ rateDisplay,
+ priceImpactDisplay,
+ priceImpactLevel,
+ slippageDisplay,
+ peraFeeDisplay,
+ providerDisplay,
+ } = useSwapQuoteDetails(quote)
- const priceImpactStyle = useMemo(() => {
- if (!quote.priceImpact) return styles.value
- if (quote.priceImpact.lessThan(PRICE_IMPACT_LOW_THRESHOLD))
- return styles.priceImpactLow
- if (quote.priceImpact.lessThan(PRICE_IMPACT_HIGH_THRESHOLD))
- return styles.priceImpactMedium
- return styles.priceImpactHigh
- }, [quote.priceImpact, styles])
-
- const formattedPeraFee = useMemo(() => {
- if (!quote.peraFeeAmount) return '-'
- return formatAssetAmount(quote.peraFeeAmount, quote.assetIn)
- }, [quote.peraFeeAmount, quote.assetIn])
-
- const formattedExchangeFee = useMemo(() => {
- if (!quote.exchangeFeeAmount) return '-'
- return formatAssetAmount(quote.exchangeFeeAmount, quote.assetIn)
- }, [quote.exchangeFeeAmount, quote.assetIn])
-
- const rateDisplay = useMemo(() => {
- if (!quote.price) return '-'
- const outDecimals = quote.assetOut.decimals ?? 6
- const { sign, integer, fraction } = formatNumber(
- quote.price,
- outDecimals,
- undefined,
- 2,
- )
- return `1 ${quote.assetIn.unitName ?? ''} ≈ ${sign}${integer}${fraction} ${quote.assetOut.unitName ?? ''}`
- }, [quote.price, quote.assetIn, quote.assetOut])
+ const priceImpactStyleMap: Record = {
+ none: styles.value,
+ low: styles.priceImpactLow,
+ medium: styles.priceImpactMedium,
+ high: styles.priceImpactHigh,
+ }
return (
- {quote.priceImpact
- ? `${quote.priceImpact.toDecimalPlaces(2).toString()}%`
- : '-'}
+ {priceImpactDisplay}
)}
@@ -133,7 +112,7 @@ export const SwapQuoteDetails = ({
variant='body'
style={styles.value}
>
- {quote.slippage ? `${quote.slippage.toString()}%` : '-'}
+ {slippageDisplay}
)}
@@ -155,29 +134,7 @@ export const SwapQuoteDetails = ({
variant='body'
style={styles.value}
>
- {formattedPeraFee}
-
- )}
-
-
-
-
- {t('swap.quote.exchange_fee')}
-
- {isLoading ? (
-
- ) : (
-
- {formattedExchangeFee}
+ {peraFeeDisplay}
)}
@@ -199,7 +156,7 @@ export const SwapQuoteDetails = ({
variant='body'
style={styles.value}
>
- {quote.providerDisplayName ?? quote.provider ?? '-'}
+ {providerDisplay}
)}
diff --git a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/__tests__/SwapQuoteDetails.spec.tsx b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/__tests__/SwapQuoteDetails.spec.tsx
index 557736f2f..b19452e5b 100644
--- a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/__tests__/SwapQuoteDetails.spec.tsx
+++ b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/__tests__/SwapQuoteDetails.spec.tsx
@@ -24,6 +24,7 @@ vi.mock('@hooks/useLanguage', () => ({
}))
vi.mock('@perawallet/wallet-core-shared', () => ({
+ DEFAULT_PRECISION: 2,
formatNumber: (
value: Decimal,
maxPrecision: number,
@@ -41,10 +42,25 @@ vi.mock('@perawallet/wallet-core-shared', () => ({
}))
vi.mock('@perawallet/wallet-core-assets', () => ({
+ ALGO_ASSET: { decimals: 6 },
formatAssetAmount: (amount: Decimal, asset: { unitName?: string }) =>
`${amount.toString()} ${asset.unitName ?? ''}`.trim(),
}))
+vi.mock('@perawallet/wallet-core-remote-config', () => ({
+ RemoteConfigKeys: {
+ swap_price_impact_low_threshold: 'swap_price_impact_low_threshold',
+ swap_price_impact_high_threshold: 'swap_price_impact_high_threshold',
+ },
+ useRemoteConfig: () => ({
+ getNumberValue: (key: string) => {
+ if (key === 'swap_price_impact_low_threshold') return 1
+ if (key === 'swap_price_impact_high_threshold') return 5
+ return 0
+ },
+ }),
+}))
+
vi.mock('@components/core', () => ({
PWView: ({
children,
@@ -83,7 +99,6 @@ const createQuote = (overrides: Partial = {}): SwapQuote => ({
priceImpact: new Decimal('0.5'),
slippage: new Decimal('0.5'),
peraFeeAmount: new Decimal('1000'),
- exchangeFeeAmount: new Decimal('2000'),
provider: 'tinyman',
providerDisplayName: 'Tinyman',
...overrides,
@@ -98,7 +113,6 @@ describe('SwapQuoteDetails', () => {
expect(screen.getByText(/swap.quote.price_impact/)).toBeDefined()
expect(screen.getByText(/swap.quote.slippage_tolerance/)).toBeDefined()
expect(screen.getByText(/swap.quote.pera_fee/)).toBeDefined()
- expect(screen.getByText(/swap.quote.exchange_fee/)).toBeDefined()
expect(screen.getByText(/swap.quote.provider/)).toBeDefined()
expect(screen.getByText('Tinyman')).toBeDefined()
})
@@ -113,7 +127,7 @@ describe('SwapQuoteDetails', () => {
)
const skeletons = screen.getAllByTestId('skeleton')
- expect(skeletons.length).toBe(6)
+ expect(skeletons.length).toBe(5)
})
it('displays dash for missing optional fields', () => {
@@ -121,7 +135,6 @@ describe('SwapQuoteDetails', () => {
priceImpact: undefined,
slippage: undefined,
peraFeeAmount: undefined,
- exchangeFeeAmount: undefined,
provider: undefined,
providerDisplayName: undefined,
})
diff --git a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts
new file mode 100644
index 000000000..ccd6c9c71
--- /dev/null
+++ b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts
@@ -0,0 +1,82 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { useMemo } from 'react'
+import { Decimal } from 'decimal.js'
+import { formatAssetAmount } from '@perawallet/wallet-core-assets'
+import {
+ RemoteConfigKeys,
+ useRemoteConfig,
+} from '@perawallet/wallet-core-remote-config'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+import { formatSwapRate } from '../../hooks/swapQuoteHelpers'
+
+export type PriceImpactLevel = 'none' | 'low' | 'medium' | 'high'
+
+type UseSwapQuoteDetailsResult = {
+ rateDisplay: string
+ priceImpactDisplay: string
+ priceImpactLevel: PriceImpactLevel
+ slippageDisplay: string
+ peraFeeDisplay: string
+ providerDisplay: string
+}
+
+const getPriceImpactLevel = (
+ priceImpact: Decimal | null | undefined,
+ lowThreshold: Decimal,
+ highThreshold: Decimal,
+): PriceImpactLevel => {
+ if (!priceImpact) return 'none'
+ if (priceImpact.lessThan(lowThreshold)) return 'low'
+ if (priceImpact.lessThan(highThreshold)) return 'medium'
+ return 'high'
+}
+
+export const useSwapQuoteDetails = (
+ quote: SwapQuote,
+): UseSwapQuoteDetailsResult => {
+ const remoteConfigService = useRemoteConfig()
+
+ const lowThreshold = new Decimal(
+ remoteConfigService.getNumberValue(
+ RemoteConfigKeys.swap_price_impact_low_threshold,
+ ),
+ )
+ const highThreshold = new Decimal(
+ remoteConfigService.getNumberValue(
+ RemoteConfigKeys.swap_price_impact_high_threshold,
+ ),
+ )
+
+ return useMemo(
+ () => ({
+ rateDisplay: formatSwapRate(quote),
+ priceImpactDisplay: quote.priceImpact
+ ? `${quote.priceImpact.toDecimalPlaces(2).toString()}%`
+ : '-',
+ priceImpactLevel: getPriceImpactLevel(
+ quote.priceImpact,
+ lowThreshold,
+ highThreshold,
+ ),
+ slippageDisplay: quote.slippage
+ ? `${quote.slippage.toString()}%`
+ : '-',
+ peraFeeDisplay: quote.peraFeeAmount
+ ? formatAssetAmount(quote.peraFeeAmount, quote.assetIn)
+ : '-',
+ providerDisplay: quote.providerDisplayName ?? quote.provider ?? '-',
+ }),
+ [quote, lowThreshold, highThreshold],
+ )
+}
diff --git a/apps/mobile/src/modules/swap/hooks/swapQuoteHelpers.ts b/apps/mobile/src/modules/swap/hooks/swapQuoteHelpers.ts
new file mode 100644
index 000000000..9c01bc883
--- /dev/null
+++ b/apps/mobile/src/modules/swap/hooks/swapQuoteHelpers.ts
@@ -0,0 +1,46 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { ALGO_ASSET } from '@perawallet/wallet-core-assets'
+import { DEFAULT_PRECISION, formatNumber } from '@perawallet/wallet-core-shared'
+import type { SwapQuote } from '@perawallet/wallet-core-swaps'
+
+export const pickBestByAmountOut = (quotes: SwapQuote[]): SwapQuote | null =>
+ quotes.reduce((prev, curr) => {
+ if (!curr.amountOut) return prev
+ if (!prev?.amountOut) return curr
+ return curr.amountOut.greaterThan(prev.amountOut) ? curr : prev
+ }, null)
+
+export const sortQuotesByAmountOutDesc = (quotes: SwapQuote[]): SwapQuote[] =>
+ [...quotes].sort((a, b) => {
+ if (!a.amountOut && !b.amountOut) return 0
+ if (!a.amountOut) return 1
+ if (!b.amountOut) return -1
+ if (a.amountOut.greaterThan(b.amountOut)) return -1
+ if (b.amountOut.greaterThan(a.amountOut)) return 1
+ return 0
+ })
+
+export const formatSwapRate = (quote: SwapQuote): string => {
+ if (!quote.price) return '-'
+ const outDecimals = quote.assetOut.decimals ?? ALGO_ASSET.decimals
+ const { sign, integer, fraction } = formatNumber(
+ quote.price,
+ outDecimals,
+ undefined,
+ DEFAULT_PRECISION,
+ )
+ const inUnit = quote.assetIn.unitName ?? ''
+ const outUnit = quote.assetOut.unitName ?? ''
+ return `1 ${inUnit} ≈ ${sign}${integer}${fraction} ${outUnit}`
+}
diff --git a/apps/mobile/vitest.config.ts b/apps/mobile/vitest.config.ts
index 8b0dbd202..505db729d 100644
--- a/apps/mobile/vitest.config.ts
+++ b/apps/mobile/vitest.config.ts
@@ -294,6 +294,8 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
+ pool: 'forks',
+ fileParallelism: false,
setupFiles: ['./vitest.setup.ts'],
server: {
deps: {
diff --git a/extensions/platform/src/remote-config/models/index.ts b/extensions/platform/src/remote-config/models/index.ts
index 247f5d689..9d301d940 100644
--- a/extensions/platform/src/remote-config/models/index.ts
+++ b/extensions/platform/src/remote-config/models/index.ts
@@ -15,6 +15,8 @@ export const RemoteConfigKeys = {
fee_warning_standard_fee: 'fee_warning_standard_fee',
fee_warning_usd_threshold: 'fee_warning_usd_threshold',
staking_projects: 'staking_projects',
+ swap_price_impact_low_threshold: 'swap_price_impact_low_threshold',
+ swap_price_impact_high_threshold: 'swap_price_impact_high_threshold',
} as const
export type RemoteConfigKey =
@@ -28,6 +30,8 @@ export const RemoteConfigDefaults: Record<
fee_warning_standard_fee: 0.001,
fee_warning_usd_threshold: 0.01,
staking_projects: '',
+ swap_price_impact_low_threshold: 1,
+ swap_price_impact_high_threshold: 5,
}
export interface RemoteConfigService {
diff --git a/packages/swaps/src/api/quotes/endpoints.ts b/packages/swaps/src/api/quotes/endpoints.ts
index d42a10b65..edb678a9c 100644
--- a/packages/swaps/src/api/quotes/endpoints.ts
+++ b/packages/swaps/src/api/quotes/endpoints.ts
@@ -123,7 +123,6 @@ export const createQuotes = async (
price: toOptionalDecimal(quote.price),
priceImpact: toOptionalDecimal(quote.price_impact),
peraFeeAmount: toOptionalDecimal(quote.pera_fee_amount),
- exchangeFeeAmount: toOptionalDecimal(quote.exchange_fee_amount),
transactionFees: toNullableDecimal(quote.transaction_fees),
}))
}
diff --git a/packages/swaps/src/api/quotes/schema.ts b/packages/swaps/src/api/quotes/schema.ts
index 1b3b5b293..0a74e31d8 100644
--- a/packages/swaps/src/api/quotes/schema.ts
+++ b/packages/swaps/src/api/quotes/schema.ts
@@ -71,7 +71,6 @@ export const quoteSchema = z.object({
price: z.string().optional(),
price_impact: z.string().optional(),
pera_fee_amount: z.string().optional(),
- exchange_fee_amount: z.string().optional(),
transaction_fees: z.string().nullable().optional(),
})
diff --git a/packages/swaps/src/models/index.ts b/packages/swaps/src/models/index.ts
index 6495926df..3bd05b2b6 100644
--- a/packages/swaps/src/models/index.ts
+++ b/packages/swaps/src/models/index.ts
@@ -109,7 +109,6 @@ export interface SwapQuote {
price?: Decimal
priceImpact?: Decimal
peraFeeAmount?: Decimal
- exchangeFeeAmount?: Decimal
transactionFees?: Decimal | null
}