diff --git a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapConfirmationBottomSheet.tsx b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapConfirmationBottomSheet.tsx index 3b5b22fea..e16a80537 100644 --- a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapConfirmationBottomSheet.tsx +++ b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapConfirmationBottomSheet.tsx @@ -59,6 +59,7 @@ export const SwapConfirmationBottomSheet = ({ rateDisplay, minimumReceivedDisplay, peraFeeDisplay, + slippageDisplay, hasHighPriceImpact, priceImpactDisplay, priceImpactStyle, @@ -136,6 +137,7 @@ export const SwapConfirmationBottomSheet = ({ rateDisplay={rateDisplay} minimumReceivedDisplay={minimumReceivedDisplay} peraFeeDisplay={peraFeeDisplay} + slippageDisplay={slippageDisplay} priceImpactDisplay={priceImpactDisplay} priceImpactStyle={priceImpactStyle} /> diff --git a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapDetailsSection.tsx b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapDetailsSection.tsx index 0f98bcd89..646feb79f 100644 --- a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapDetailsSection.tsx +++ b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/SwapDetailsSection.tsx @@ -22,6 +22,7 @@ type SwapDetailsSectionProps = { rateDisplay: string minimumReceivedDisplay: string peraFeeDisplay: string + slippageDisplay: string priceImpactDisplay: string priceImpactStyle: object } @@ -31,6 +32,7 @@ export const SwapDetailsSection = ({ rateDisplay, minimumReceivedDisplay, peraFeeDisplay, + slippageDisplay, priceImpactDisplay, priceImpactStyle, }: SwapDetailsSectionProps) => { @@ -58,7 +60,7 @@ export const SwapDetailsSection = ({ diff --git a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/__tests__/SwapConfirmationBottomSheet.spec.tsx b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/__tests__/SwapConfirmationBottomSheet.spec.tsx index 48768bbad..3cb1dc61b 100644 --- a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/__tests__/SwapConfirmationBottomSheet.spec.tsx +++ b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/__tests__/SwapConfirmationBottomSheet.spec.tsx @@ -34,6 +34,7 @@ vi.mock('@perawallet/wallet-core-shared', () => ({ vi.mock('@perawallet/wallet-core-swaps', () => ({ useProvidersQuery: () => ({ data: undefined }), + apiSlippageToPercent: (slippage: Decimal) => slippage.mul(100).toString(), })) vi.mock('@perawallet/wallet-core-assets', () => ({ @@ -233,6 +234,54 @@ describe('SwapConfirmationBottomSheet', () => { expect(screen.getByText('swap.quote.pera_fee')).toBeDefined() }) + it('renders slippage as a percent (5% from API fraction 0.05)', () => { + const quote = createQuote({ slippage: new Decimal('0.05') }) + render( + , + ) + + expect(screen.getByText('5%')).toBeDefined() + }) + + it('renders pera fee using peraFeeAsset when it differs from assetIn', () => { + const quote = createQuote({ + peraFeeAmount: new Decimal('1000'), + peraFeeAsset: { + assetId: '31566704', + name: 'USDC', + unitName: 'USDC', + decimals: 6, + verificationTier: 'verified', + }, + }) + render( + , + ) + + expect(screen.getByText('1000 USDC')).toBeDefined() + }) + + it('falls back to assetIn for pera fee when peraFeeAsset is absent', () => { + const quote = createQuote({ + peraFeeAmount: new Decimal('1000'), + peraFeeAsset: undefined, + }) + render( + , + ) + + expect(screen.getByText('1000 ALGO')).toBeDefined() + }) + it('shows high price impact warning when priceImpact >= 5', () => { const quote = createQuote({ priceImpact: new Decimal('5.5') }) render( diff --git a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/useSwapConfirmation.ts b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/useSwapConfirmation.ts index b11f15c1a..9674d0e44 100644 --- a/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/useSwapConfirmation.ts +++ b/apps/mobile/src/modules/swap/components/SwapConfirmationBottomSheet/useSwapConfirmation.ts @@ -24,7 +24,10 @@ import { type PeraAsset, } from '@perawallet/wallet-core-assets' import { useCurrency } from '@perawallet/wallet-core-currencies' -import type { SwapQuote } from '@perawallet/wallet-core-swaps' +import { + apiSlippageToPercent, + type SwapQuote, +} from '@perawallet/wallet-core-swaps' import type { SwapExecutionStatus } from '../../hooks/useSwapExecution' import { useStyles } from './styles' @@ -47,6 +50,7 @@ type UseSwapConfirmationResult = { rateDisplay: string minimumReceivedDisplay: string peraFeeDisplay: string + slippageDisplay: string hasHighPriceImpact: boolean priceImpactDisplay: string priceImpactStyle: object @@ -118,8 +122,16 @@ export const useSwapConfirmation = ({ const peraFeeDisplay = useMemo(() => { if (!quote?.peraFeeAmount) return '-' - return formatAssetAmount(quote.peraFeeAmount, quote.assetIn) - }, [quote?.peraFeeAmount, quote?.assetIn]) + return formatAssetAmount( + quote.peraFeeAmount, + quote.peraFeeAsset ?? quote.assetIn, + ) + }, [quote?.peraFeeAmount, quote?.peraFeeAsset, quote?.assetIn]) + + const slippageDisplay = useMemo(() => { + if (!quote?.slippage) return '-' + return `${apiSlippageToPercent(quote.slippage)}%` + }, [quote?.slippage]) const hasHighPriceImpact = useMemo( () => @@ -154,6 +166,7 @@ export const useSwapConfirmation = ({ rateDisplay, minimumReceivedDisplay, peraFeeDisplay, + slippageDisplay, hasHighPriceImpact, priceImpactDisplay, priceImpactStyle, diff --git a/apps/mobile/src/modules/swap/components/SwapForm/__tests__/useSwapForm.spec.ts b/apps/mobile/src/modules/swap/components/SwapForm/__tests__/useSwapForm.spec.ts index 953380434..157f44286 100644 --- a/apps/mobile/src/modules/swap/components/SwapForm/__tests__/useSwapForm.spec.ts +++ b/apps/mobile/src/modules/swap/components/SwapForm/__tests__/useSwapForm.spec.ts @@ -387,7 +387,7 @@ describe('useSwapForm', () => { expect(mockSetPreferredCurrency).toHaveBeenCalledWith('ALGO') }) - it('converts stored slippage percent to decimal fraction when fetching quotes', async () => { + it('converts stored slippage percent to decimal fraction', async () => { mockSlippage = '1' mockCreateQuotes.mockResolvedValueOnce([]) diff --git a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts index c457d2967..f0f36a81e 100644 --- a/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts +++ b/apps/mobile/src/modules/swap/components/SwapQuoteDetails/useSwapQuoteDetails.ts @@ -17,7 +17,10 @@ import { RemoteConfigKeys, useRemoteConfig, } from '@perawallet/wallet-core-remote-config' -import type { SwapQuote } from '@perawallet/wallet-core-swaps' +import { + apiSlippageToPercent, + type SwapQuote, +} from '@perawallet/wallet-core-swaps' import { formatSwapRate } from '../../hooks/swapQuoteHelpers' import type { Maybe } from '@perawallet/wallet-core-shared' @@ -71,10 +74,13 @@ export const useSwapQuoteDetails = ( highThreshold, ), slippageDisplay: quote.slippage - ? `${quote.slippage.toString()}%` + ? `${apiSlippageToPercent(quote.slippage)}%` : '-', peraFeeDisplay: quote.peraFeeAmount - ? formatAssetAmount(quote.peraFeeAmount, quote.assetIn) + ? formatAssetAmount( + quote.peraFeeAmount, + quote.peraFeeAsset ?? quote.assetIn, + ) : '-', providerDisplay: quote.providerDisplayName ?? quote.provider ?? '-', }), diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts index 53677721c..86de54caa 100644 --- a/apps/mobile/vitest.setup.ts +++ b/apps/mobile/vitest.setup.ts @@ -1938,10 +1938,15 @@ vi.mock('@perawallet/wallet-core-walletconnect', () => ({ }, })) -vi.mock('@perawallet/wallet-core-swaps', () => ({ - useSwaps: vi.fn(), - isSwappableAsset: vi.fn(() => true), -})) +vi.mock('@perawallet/wallet-core-swaps', async () => { + const { Decimal } = await import('decimal.js') + return { + useSwaps: vi.fn(), + isSwappableAsset: vi.fn(() => true), + apiSlippageToPercent: (slippage: InstanceType) => + slippage.mul(100).toString(), + } +}) vi.mock('@perawallet/wallet-core-polling', () => ({ usePolling: vi.fn(), diff --git a/packages/swaps/src/api/quotes/endpoints.ts b/packages/swaps/src/api/quotes/endpoints.ts index c802441ba..a54bd9490 100644 --- a/packages/swaps/src/api/quotes/endpoints.ts +++ b/packages/swaps/src/api/quotes/endpoints.ts @@ -127,6 +127,9 @@ export const createQuotes = async ( price: toOptionalDecimal(quote.price), priceImpact: toOptionalDecimal(quote.price_impact), peraFeeAmount: toOptionalDecimal(quote.pera_fee_amount), + peraFeeAsset: quote.pera_fee_asset + ? transformDexSwapAsset(quote.pera_fee_asset) + : undefined, transactionFees: toNullableDecimal(quote.transaction_fees), })) } diff --git a/packages/swaps/src/api/quotes/schema.ts b/packages/swaps/src/api/quotes/schema.ts index 0a74e31d8..04e22ada9 100644 --- a/packages/swaps/src/api/quotes/schema.ts +++ b/packages/swaps/src/api/quotes/schema.ts @@ -71,6 +71,7 @@ export const quoteSchema = z.object({ price: z.string().optional(), price_impact: z.string().optional(), pera_fee_amount: z.string().optional(), + pera_fee_asset: dexSwapAssetSchema.optional(), transaction_fees: z.string().nullable().optional(), }) diff --git a/packages/swaps/src/models/index.ts b/packages/swaps/src/models/index.ts index 6c1a5cad5..813afad1a 100644 --- a/packages/swaps/src/models/index.ts +++ b/packages/swaps/src/models/index.ts @@ -106,6 +106,7 @@ export interface SwapQuote { price?: Decimal priceImpact?: Decimal peraFeeAmount?: Decimal + peraFeeAsset?: DexSwapAsset transactionFees?: Nullable } diff --git a/packages/swaps/src/utils/__tests__/slippage.spec.ts b/packages/swaps/src/utils/__tests__/slippage.spec.ts index 6a5c3fe45..4e355826f 100644 --- a/packages/swaps/src/utils/__tests__/slippage.spec.ts +++ b/packages/swaps/src/utils/__tests__/slippage.spec.ts @@ -11,30 +11,69 @@ */ import { describe, it, expect } from 'vitest' -import { percentToApiSlippage } from '../slippage' +import { Decimal } from 'decimal.js' +import { apiSlippageToPercent, percentToApiSlippage } from '../slippage' describe('percentToApiSlippage', () => { - it('converts 1% to 0.01', () => { + it('converts "1" to "0.01"', () => { expect(percentToApiSlippage('1')).toBe('0.01') }) - it('converts 0.5% to 0.005', () => { + it('converts "0.5" to "0.005"', () => { expect(percentToApiSlippage('0.5')).toBe('0.005') }) - it('converts 100% to 1', () => { - expect(percentToApiSlippage('100')).toBe('1') + it('converts "2" to "0.02"', () => { + expect(percentToApiSlippage('2')).toBe('0.02') + }) + + it('converts "5" to "0.05"', () => { + expect(percentToApiSlippage('5')).toBe('0.05') }) - it('converts 0% to 0', () => { + it('converts "0" to "0"', () => { expect(percentToApiSlippage('0')).toBe('0') }) - it('converts 0.1% to 0.001', () => { + it('converts "0.1" to "0.001"', () => { expect(percentToApiSlippage('0.1')).toBe('0.001') }) - it('preserves precision for values like 2.5%', () => { + it('converts "100" to "1"', () => { + expect(percentToApiSlippage('100')).toBe('1') + }) + + it('preserves precision for values like "2.5"', () => { expect(percentToApiSlippage('2.5')).toBe('0.025') }) + + it('normalises trailing zeros via Decimal', () => { + expect(percentToApiSlippage('2.50')).toBe('0.025') + }) +}) + +describe('apiSlippageToPercent', () => { + it('converts 0.05 to 5', () => { + expect(apiSlippageToPercent(new Decimal('0.05'))).toBe('5') + }) + + it('converts 0.01 to 1', () => { + expect(apiSlippageToPercent(new Decimal('0.01'))).toBe('1') + }) + + it('converts 0.005 to 0.5', () => { + expect(apiSlippageToPercent(new Decimal('0.005'))).toBe('0.5') + }) + + it('converts 0 to 0', () => { + expect(apiSlippageToPercent(new Decimal('0'))).toBe('0') + }) + + it('converts 1 to 100', () => { + expect(apiSlippageToPercent(new Decimal('1'))).toBe('100') + }) + + it('converts the API decimal-fraction response back to a percent', () => { + expect(apiSlippageToPercent(new Decimal('0.025'))).toBe('2.5') + }) }) diff --git a/packages/swaps/src/utils/slippage.ts b/packages/swaps/src/utils/slippage.ts index 943d5747a..6e042a73d 100644 --- a/packages/swaps/src/utils/slippage.ts +++ b/packages/swaps/src/utils/slippage.ts @@ -13,6 +13,12 @@ import { Decimal } from 'decimal.js' // The swap configuration UI collects slippage as a percent (e.g. "1" for 1%), -// but the Pera Swap quotes API expects a decimal fraction (1% = "0.01"). +// but the Pera Swap quotes API expects a decimal fraction on the request +// (e.g. "0.01" for 1%, validated as <= 0.9999). export const percentToApiSlippage = (percent: string): string => new Decimal(percent).div(100).toString() + +// Quotes return slippage as a decimal fraction (e.g. "0.01" for 1%) and +// the UI needs to render it as a percent. +export const apiSlippageToPercent = (slippage: Decimal): string => + slippage.mul(100).toString()