diff --git a/src/__swaps__/safe-math/SafeMath.ts b/src/__swaps__/safe-math/SafeMath.ts
index af5062e562b..74c62502fdb 100644
--- a/src/__swaps__/safe-math/SafeMath.ts
+++ b/src/__swaps__/safe-math/SafeMath.ts
@@ -391,7 +391,6 @@ export function floorWorklet(num: string | number): string {
export function roundWorklet(num: string | number): string {
'worklet';
const numStr = toStringWorklet(num);
-
if (!isNumberStringWorklet(numStr)) {
throw new Error('Argument must be a numeric string or number');
}
diff --git a/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts b/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts
new file mode 100644
index 00000000000..490be57ca41
--- /dev/null
+++ b/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts
@@ -0,0 +1,98 @@
+import { niceIncrementFormatter } from '@/__swaps__/utils/swaps';
+import { SLIDER_WIDTH } from '../constants';
+
+type TestCase = {
+ incrementDecimalPlaces: number;
+ inputAssetBalance: number | string;
+ assetBalanceDisplay: string;
+ inputAssetUsdPrice: number;
+ niceIncrement: number | string;
+ percentageToSwap: number;
+ sliderXPosition: number;
+ stripSeparators?: boolean;
+ isStablecoin?: boolean;
+} & {
+ testName: string;
+ expectedResult: string;
+};
+
+const TEST_CASES: TestCase[] = [
+ {
+ incrementDecimalPlaces: 0,
+ inputAssetBalance: 45.47364224817269,
+ assetBalanceDisplay: '45.47364225',
+ inputAssetUsdPrice: 0.9995363790000001,
+ niceIncrement: '1',
+ percentageToSwap: 0.5,
+ sliderXPosition: SLIDER_WIDTH / 2,
+ stripSeparators: true,
+ isStablecoin: true,
+ testName: 'DAI Stablecoin',
+ expectedResult: '22.74',
+ },
+ {
+ incrementDecimalPlaces: 2,
+ inputAssetBalance: 100,
+ assetBalanceDisplay: '100.00',
+ inputAssetUsdPrice: 10,
+ niceIncrement: '0.1',
+ percentageToSwap: 0,
+ sliderXPosition: 0,
+ stripSeparators: false,
+ isStablecoin: false,
+ testName: 'Zero percent swap',
+ expectedResult: '0.00',
+ },
+ {
+ incrementDecimalPlaces: 2,
+ inputAssetBalance: 100,
+ assetBalanceDisplay: '100.00',
+ inputAssetUsdPrice: 10,
+ niceIncrement: '0.1',
+ percentageToSwap: 1,
+ sliderXPosition: SLIDER_WIDTH,
+ stripSeparators: false,
+ isStablecoin: false,
+ testName: 'Full swap',
+ expectedResult: '100.00',
+ },
+ {
+ incrementDecimalPlaces: 2,
+ inputAssetBalance: 123.456,
+ assetBalanceDisplay: '123.46',
+ inputAssetUsdPrice: 1,
+ niceIncrement: '0.05',
+ percentageToSwap: 0.25,
+ sliderXPosition: SLIDER_WIDTH / 4,
+ stripSeparators: true,
+ isStablecoin: false,
+ testName: 'Quarter swap with fractional increment',
+ expectedResult: '30.86',
+ },
+ {
+ incrementDecimalPlaces: 0,
+ inputAssetBalance: '1000',
+ assetBalanceDisplay: '1,000',
+ inputAssetUsdPrice: 0.5,
+ niceIncrement: '100',
+ percentageToSwap: 0.75,
+ sliderXPosition: (3 * SLIDER_WIDTH) / 4,
+ stripSeparators: false,
+ isStablecoin: false,
+ testName: 'Large increment test',
+ expectedResult: '750',
+ },
+];
+
+describe('NiceIncrementFormatter', () => {
+ beforeAll(() => {
+ jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
+ });
+
+ TEST_CASES.forEach(({ testName, expectedResult, ...params }, index) => {
+ // eslint-disable-next-line jest/valid-title
+ test(testName || `test-${index}`, () => {
+ expect(niceIncrementFormatter({ ...params })).toBe(expectedResult);
+ });
+ });
+});
diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx
index 811a989805e..9405fd12344 100644
--- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx
+++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx
@@ -57,7 +57,7 @@ const TokenToSellListComponent = () => {
ListEmptyComponent={}
keyExtractor={uniqueId => uniqueId}
renderItem={({ item: uniqueId }) => {
- return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />;
+ return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />;
}}
/>
diff --git a/src/__swaps__/screens/Swap/constants.ts b/src/__swaps__/screens/Swap/constants.ts
index ba39be48e1a..5387d1cf794 100644
--- a/src/__swaps__/screens/Swap/constants.ts
+++ b/src/__swaps__/screens/Swap/constants.ts
@@ -44,6 +44,9 @@ export const ETH_COLOR_DARK_ACCENT = '#9CA4AD';
export const LONG_PRESS_DELAY_DURATION = 200;
export const LONG_PRESS_REPEAT_DURATION = 69;
+
+export const STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS = 2;
+export const MAXIMUM_SIGNIFICANT_DECIMALS = 6;
//
// /---- END constants ----/ //
diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
index cfb9daead9d..ad6750d1fa9 100644
--- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
+++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
@@ -33,17 +33,21 @@ import { divWorklet, equalWorklet, greaterThanWorklet, mulWorklet, toFixedWorkle
function getInitialInputValues(initialSelectedInputAsset: ExtendedAnimatedAssetWithColors | null) {
const initialBalance = Number(initialSelectedInputAsset?.balance.amount) || 0;
+ const assetBalanceDisplay = initialSelectedInputAsset?.balance.display ?? '';
const initialNiceIncrement = findNiceIncrement(initialBalance);
const initialDecimalPlaces = countDecimalPlaces(initialNiceIncrement);
+ const isStablecoin = initialSelectedInputAsset?.type === 'stablecoin';
const initialInputAmount = niceIncrementFormatter({
incrementDecimalPlaces: initialDecimalPlaces,
inputAssetBalance: initialBalance,
+ assetBalanceDisplay,
inputAssetUsdPrice: initialSelectedInputAsset?.price?.value ?? 0,
niceIncrement: initialNiceIncrement,
percentageToSwap: 0.5,
sliderXPosition: SLIDER_WIDTH / 2,
stripSeparators: true,
+ isStablecoin,
});
const initialInputNativeValue = addCommasToNumber(
@@ -119,10 +123,13 @@ export function useSwapInputsController({
return addCommasToNumber(inputValues.value.inputAmount, '0');
}
+ const assetBalanceDisplay = internalSelectedInputAsset.value.balance.display ?? '';
+
if (inputMethod.value === 'outputAmount') {
return valueBasedDecimalFormatter({
amount: inputValues.value.inputAmount,
usdTokenPrice: inputNativePrice.value,
+ assetBalanceDisplay,
roundingMode: 'up',
precisionAdjustment: -1,
isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false,
@@ -131,14 +138,17 @@ export function useSwapInputsController({
}
const balance = internalSelectedInputAsset.value?.balance.amount || 0;
+ const isStablecoin = internalSelectedInputAsset.value?.type === 'stablecoin' ?? false;
return niceIncrementFormatter({
incrementDecimalPlaces: incrementDecimalPlaces.value,
inputAssetBalance: balance,
+ assetBalanceDisplay,
inputAssetUsdPrice: inputNativePrice.value,
niceIncrement: niceIncrement.value,
percentageToSwap: percentageToSwap.value,
sliderXPosition: sliderXPosition.value,
+ isStablecoin,
});
});
@@ -166,9 +176,13 @@ export function useSwapInputsController({
if (inputMethod.value === 'outputAmount' || typeof inputValues.value.outputAmount === 'string') {
return addCommasToNumber(inputValues.value.outputAmount, '0');
}
+
+ const assetBalanceDisplay = internalSelectedOutputAsset.value.balance.display ?? '';
+
return valueBasedDecimalFormatter({
amount: inputValues.value.outputAmount,
usdTokenPrice: outputNativePrice.value,
+ assetBalanceDisplay,
roundingMode: 'down',
precisionAdjustment: -1,
isStablecoin: internalSelectedOutputAsset.value?.type === 'stablecoin' ?? false,
@@ -674,6 +688,7 @@ export function useSwapInputsController({
if (!internalSelectedInputAsset.value) return;
const balance = Number(internalSelectedInputAsset.value.balance.amount);
+
if (!balance) {
inputValues.modify(values => {
return {
@@ -687,14 +702,18 @@ export function useSwapInputsController({
return;
}
+ const assetBalanceDisplay = internalSelectedInputAsset.value.balance.display;
+
const inputAmount = niceIncrementFormatter({
incrementDecimalPlaces: incrementDecimalPlaces.value,
inputAssetBalance: balance,
+ assetBalanceDisplay,
inputAssetUsdPrice: inputNativePrice.value,
niceIncrement: niceIncrement.value,
percentageToSwap: percentageToSwap.value,
sliderXPosition: sliderXPosition.value,
stripSeparators: true,
+ isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false,
});
const inputNativeValue = mulWorklet(inputAmount, inputNativePrice.value);
inputValues.modify(values => {
@@ -844,14 +863,18 @@ export function useSwapInputsController({
return;
}
+ const assetBalanceDisplay = internalSelectedInputAsset.value?.balance.display ?? '';
+
const inputAmount = niceIncrementFormatter({
incrementDecimalPlaces: incrementDecimalPlaces.value,
inputAssetBalance: balance,
+ assetBalanceDisplay,
inputAssetUsdPrice: inputNativePrice.value,
niceIncrement: niceIncrement.value,
percentageToSwap: percentageToSwap.value,
sliderXPosition: sliderXPosition.value,
stripSeparators: true,
+ isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false,
});
const inputNativeValue = mulWorklet(inputAmount, inputNativePrice.value);
@@ -869,11 +892,14 @@ export function useSwapInputsController({
const inputNativePrice = internalSelectedInputAsset.value?.nativePrice || internalSelectedInputAsset.value?.price?.value || 0;
const outputNativePrice = internalSelectedOutputAsset.value?.nativePrice || internalSelectedOutputAsset.value?.price?.value || 0;
+ const assetBalanceDisplay = current.assetToSell?.balance.display ?? '';
+
const inputAmount = Number(
valueBasedDecimalFormatter({
amount:
inputNativePrice > 0 ? divWorklet(inputValues.value.inputNativeValue, inputNativePrice) : inputValues.value.outputAmount,
usdTokenPrice: inputNativePrice,
+ assetBalanceDisplay,
roundingMode: 'up',
precisionAdjustment: -1,
isStablecoin: current.assetToSell?.type === 'stablecoin' ?? false,
diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts
index 501c15fd7a1..3186a369bb5 100644
--- a/src/__swaps__/utils/swaps.ts
+++ b/src/__swaps__/utils/swaps.ts
@@ -3,7 +3,14 @@ import { SharedValue, convertToRGBA, isColor } from 'react-native-reanimated';
import * as i18n from '@/languages';
import { globalColors } from '@/design-system';
-import { ETH_COLOR, ETH_COLOR_DARK, SCRUBBER_WIDTH, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants';
+import {
+ ETH_COLOR,
+ ETH_COLOR_DARK,
+ MAXIMUM_SIGNIFICANT_DECIMALS,
+ SCRUBBER_WIDTH,
+ SLIDER_WIDTH,
+ STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS,
+} from '@/__swaps__/screens/Swap/constants';
import { chainNameFromChainId, chainNameFromChainIdWorklet } from '@/__swaps__/utils/chains';
import { ChainId, ChainName } from '@/__swaps__/types/chains';
import { RainbowConfig } from '@/model/remoteConfig';
@@ -221,41 +228,54 @@ export function trimTrailingZeros(value: string) {
return withTrimmedZeros.endsWith('.') ? withTrimmedZeros.slice(0, -1) : withTrimmedZeros;
}
+export function precisionBasedOffMagnitude(amount: number | string, isStablecoin = false): number {
+ 'worklet';
+
+ const magnitude = -Number(floorWorklet(sumWorklet(log10Worklet(amount), 0)));
+ // don't let stablecoins go beneath 2nd order
+ if (magnitude < -2 && isStablecoin) {
+ return -STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS;
+ }
+ return magnitude;
+}
+
export function valueBasedDecimalFormatter({
amount,
usdTokenPrice,
+ assetBalanceDisplay,
roundingMode,
precisionAdjustment,
isStablecoin,
stripSeparators = true,
+ isMaxAmount = false,
}: {
amount: number | string;
usdTokenPrice: number;
+ assetBalanceDisplay?: string;
roundingMode?: 'up' | 'down';
precisionAdjustment?: number;
isStablecoin?: boolean;
stripSeparators?: boolean;
+ isMaxAmount?: boolean;
}): string {
'worklet';
- function precisionBasedOffMagnitude(amount: number | string): number {
- const magnitude = -Number(floorWorklet(sumWorklet(log10Worklet(amount), 1)));
- return (precisionAdjustment ?? 0) + magnitude;
- }
-
function calculateDecimalPlaces(usdTokenPrice: number): number {
- const fallbackDecimalPlaces = 2;
+ const fallbackDecimalPlaces = STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS;
if (usdTokenPrice <= 0) {
return fallbackDecimalPlaces;
}
const unitsForOneCent = 0.01 / usdTokenPrice;
if (unitsForOneCent >= 1) {
- return 0;
+ return isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0;
}
- return Math.max(Math.ceil(Math.log10(1 / unitsForOneCent)) + precisionBasedOffMagnitude(amount), 0);
+ return Math.max(
+ Math.ceil(Math.log10(1 / unitsForOneCent)) + (precisionAdjustment ?? 0),
+ isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0
+ );
}
- const decimalPlaces = isStablecoin ? 2 : calculateDecimalPlaces(usdTokenPrice);
+ const decimalPlaces = calculateDecimalPlaces(usdTokenPrice);
let roundedAmount;
const factor = Math.pow(10, decimalPlaces);
@@ -270,10 +290,35 @@ export function valueBasedDecimalFormatter({
roundedAmount = divWorklet(roundWorklet(mulWorklet(amount, factor)), factor);
}
+ const maximumFractionDigits = () => {
+ // if we're selling max amount, we want to match what's displayed on the balance badge
+ // let's base the decimal places based on that (capped at 6)
+ if (isMaxAmount && assetBalanceDisplay) {
+ const decimals = assetBalanceDisplay.split('.');
+ if (decimals.length > 1) {
+ const [, decimalPlacesFromDisplay] = decimals;
+ if (decimalPlacesFromDisplay.length < STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS && isStablecoin) {
+ return STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS;
+ }
+
+ return Math.min(decimalPlacesFromDisplay.length, MAXIMUM_SIGNIFICANT_DECIMALS);
+ }
+ }
+
+ if (!isNaN(decimalPlaces)) {
+ return isStablecoin && decimalPlaces < STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS
+ ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS
+ : decimalPlaces;
+ }
+
+ // default to 6 precision if we have no calculation
+ return MAXIMUM_SIGNIFICANT_DECIMALS;
+ };
+
// Format the number to add separators and trim trailing zeros
const numberFormatter = new Intl.NumberFormat('en-US', {
- minimumFractionDigits: 0,
- maximumFractionDigits: !isNaN(decimalPlaces) ? decimalPlaces : 2, // Allow up to the required precision
+ minimumFractionDigits: isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0,
+ maximumFractionDigits: maximumFractionDigits(),
useGrouping: true,
});
@@ -285,50 +330,71 @@ export function valueBasedDecimalFormatter({
export function niceIncrementFormatter({
incrementDecimalPlaces,
inputAssetBalance,
+ assetBalanceDisplay,
inputAssetUsdPrice,
niceIncrement,
percentageToSwap,
sliderXPosition,
stripSeparators,
+ isStablecoin = false,
}: {
incrementDecimalPlaces: number;
inputAssetBalance: number | string;
+ assetBalanceDisplay: string;
inputAssetUsdPrice: number;
niceIncrement: number | string;
percentageToSwap: number;
sliderXPosition: number;
stripSeparators?: boolean;
+ isStablecoin?: boolean;
}) {
'worklet';
+
if (percentageToSwap === 0) return '0';
- if (percentageToSwap === 0.25)
+ if (percentageToSwap === 0.25) {
+ const amount = mulWorklet(inputAssetBalance, 0.25);
return valueBasedDecimalFormatter({
- amount: mulWorklet(inputAssetBalance, 0.25),
+ amount,
usdTokenPrice: inputAssetUsdPrice,
+ assetBalanceDisplay,
roundingMode: 'up',
- precisionAdjustment: -3,
+ precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin),
+ isStablecoin,
});
- if (percentageToSwap === 0.5)
+ }
+ if (percentageToSwap === 0.5) {
+ const amount = mulWorklet(inputAssetBalance, 0.5);
return valueBasedDecimalFormatter({
- amount: mulWorklet(inputAssetBalance, 0.5),
+ amount,
usdTokenPrice: inputAssetUsdPrice,
+ assetBalanceDisplay,
roundingMode: 'up',
- precisionAdjustment: -3,
+ precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin),
+ isStablecoin,
});
- if (percentageToSwap === 0.75)
+ }
+ if (percentageToSwap === 0.75) {
+ const amount = mulWorklet(inputAssetBalance, 0.75);
return valueBasedDecimalFormatter({
- amount: mulWorklet(inputAssetBalance, 0.75),
+ amount,
usdTokenPrice: inputAssetUsdPrice,
+ assetBalanceDisplay,
roundingMode: 'up',
- precisionAdjustment: -3,
+ precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin),
+ isStablecoin,
});
- if (percentageToSwap === 1)
+ }
+ if (percentageToSwap === 1) {
return valueBasedDecimalFormatter({
amount: inputAssetBalance,
usdTokenPrice: inputAssetUsdPrice,
- roundingMode: 'up',
+ assetBalanceDisplay,
+ isStablecoin,
+ isMaxAmount: true,
});
+ }
+ const decimals = isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : incrementDecimalPlaces;
const exactIncrement = divWorklet(inputAssetBalance, 100);
const isIncrementExact = equalWorklet(niceIncrement, exactIncrement);
const numberOfIncrements = divWorklet(inputAssetBalance, niceIncrement);
@@ -344,12 +410,12 @@ export function niceIncrementFormatter({
const rawAmount = mulWorklet(roundWorklet(divWorklet(mulWorklet(percentage, inputAssetBalance), niceIncrement)), niceIncrement);
- const amountToFixedDecimals = toFixedWorklet(rawAmount, incrementDecimalPlaces);
+ const amountToFixedDecimals = toFixedWorklet(rawAmount, decimals);
const formattedAmount = `${Number(amountToFixedDecimals).toLocaleString('en-US', {
useGrouping: true,
- minimumFractionDigits: 0,
- maximumFractionDigits: 8,
+ minimumFractionDigits: isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0,
+ maximumFractionDigits: MAXIMUM_SIGNIFICANT_DECIMALS,
})}`;
if (stripSeparators) return stripCommas(formattedAmount);
diff --git a/src/hooks/useSwapDerivedOutputs.ts b/src/hooks/useSwapDerivedOutputs.ts
index f175946fd4c..84d6abb7ee4 100644
--- a/src/hooks/useSwapDerivedOutputs.ts
+++ b/src/hooks/useSwapDerivedOutputs.ts
@@ -111,9 +111,7 @@ const getInputAmount = async (
// Do not deleeeet the comment below 😤
// @ts-ignore About to get quote
- console.log(JSON.stringify(quoteParams, null, 2));
const quote = await getQuote(quoteParams);
- console.log(JSON.stringify(quote, null, 2));
// if no quote, if quote is error or there's no sell amount
if (!quote || (quote as QuoteError).error || !(quote as Quote).sellAmount) {
@@ -204,8 +202,6 @@ const getOutputAmount = async (
refuel,
};
- console.log(JSON.stringify(quoteParams, null, 2));
-
const rand = Math.floor(Math.random() * 100);
logger.debug('[getOutputAmount]: Getting quote', { rand, quoteParams });
// Do not deleeeet the comment below 😤
@@ -213,8 +209,6 @@ const getOutputAmount = async (
const quote: Quote | CrosschainQuote | QuoteError | null = await (isCrosschainSwap ? getCrosschainQuote : getQuote)(quoteParams);
logger.debug('[getOutputAmount]: Got quote', { rand, quote });
- console.log(JSON.stringify(quote, null, 2));
-
if (!quote || (quote as QuoteError)?.error || !(quote as Quote)?.buyAmount) {
const quoteError = quote as QuoteError;
if (quoteError.error) {
diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts
index 50d65999d43..bd1c3af9ef0 100644
--- a/src/raps/actions/swap.ts
+++ b/src/raps/actions/swap.ts
@@ -374,8 +374,6 @@ export const swap = async ({
swapMetadataStorage.set(swap.hash.toLowerCase(), JSON.stringify({ type: 'swap', data: parameters.meta }));
}
- console.log(JSON.stringify(transaction, null, 2));
-
addNewTransaction({
address: parameters.quote.from as Address,
// chainId: parameters.chainId as ChainId,