diff --git a/packages/checkout/sdk/src/env/constants.ts b/packages/checkout/sdk/src/env/constants.ts index 95b5501ee9..fc485753b9 100644 --- a/packages/checkout/sdk/src/env/constants.ts +++ b/packages/checkout/sdk/src/env/constants.ts @@ -7,6 +7,7 @@ import { NetworkDetails, NetworkMap } from '../types'; export const ENV_DEVELOPMENT = 'development' as Environment; export const DEFAULT_TOKEN_DECIMALS = 18; +export const DEFAULT_TOKEN_FORMATTING_DECIMALS = 6; export const NATIVE = 'native'; diff --git a/packages/checkout/sdk/src/smartCheckout/routing/bridge/bridgeRoute.ts b/packages/checkout/sdk/src/smartCheckout/routing/bridge/bridgeRoute.ts index 1a55f9dbd6..1a82021f5f 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/bridge/bridgeRoute.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/bridge/bridgeRoute.ts @@ -24,7 +24,7 @@ import { } from '../indexer/fetchL1Representation'; import { DEFAULT_TOKEN_DECIMALS } from '../../../env'; import { isNativeToken } from '../../../tokens'; -import { isMatchingAddress } from '../../../utils/utils'; +import { formatSmartCheckoutAmount, isMatchingAddress } from '../../../utils/utils'; export const hasSufficientL1Eth = ( tokenBalanceResult: TokenBalanceResult, @@ -92,7 +92,7 @@ const constructBridgeFundingRoute = ( type: itemType, fundsRequired: { amount: bridgeRequirement.amount, - formattedAmount: bridgeRequirement.formattedAmount, + formattedAmount: formatSmartCheckoutAmount(bridgeRequirement.formattedAmount), }, userBalance: { balance: balance.balance, diff --git a/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/constructBridgeRequirements.ts b/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/constructBridgeRequirements.ts index 9f3f68223b..6cadc46b20 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/constructBridgeRequirements.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/constructBridgeRequirements.ts @@ -4,7 +4,7 @@ import { BridgeRequirement } from '../bridge/bridgeRoute'; import { DexQuote, DexQuotes } from '../types'; import { INDEXER_ETH_ROOT_CONTRACT_ADDRESS, L1ToL2TokenAddressMapping } from '../indexer/fetchL1Representation'; import { BalanceCheckResult } from '../../balanceCheck/types'; -import { isMatchingAddress } from '../../../utils/utils'; +import { formatSmartCheckoutAmount, isMatchingAddress } from '../../../utils/utils'; // The dex will return all the fees which is in a particular token (currently always IMX) // If any of the fees are in the same token that is trying to be swapped (e.g. trying to swap IMX) @@ -145,7 +145,7 @@ export const constructBridgeRequirements = ( bridgeRequirements.push({ amount: amountToBridge, - formattedAmount: utils.formatUnits(amountToBridge, l1balance.token.decimals), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(amountToBridge, l1balance.token.decimals)), // L2 address is used for the bridge requirement as the bridge route uses the indexer to find L1 address l2address, }); diff --git a/packages/checkout/sdk/src/smartCheckout/routing/onRamp/onRampRoute.ts b/packages/checkout/sdk/src/smartCheckout/routing/onRamp/onRampRoute.ts index 0883128825..edaae2c361 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/onRamp/onRampRoute.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/onRamp/onRampRoute.ts @@ -7,7 +7,7 @@ import { import { BalanceERC20Requirement, BalanceNativeRequirement, BalanceRequirement } from '../../balanceCheck/types'; import { allowListCheckForOnRamp } from '../../allowList'; import { isNativeToken } from '../../../tokens'; -import { isMatchingAddress } from '../../../utils/utils'; +import { formatSmartCheckoutAmount, isMatchingAddress } from '../../../utils/utils'; export const onRampRoute = async ( config: CheckoutConfiguration, @@ -42,7 +42,7 @@ export const onRampRoute = async ( type: isNativeToken(required.token.address) ? ItemType.NATIVE : ItemType.ERC20, fundsRequired: { amount: delta.balance, - formattedAmount: delta.formattedBalance, + formattedAmount: formatSmartCheckoutAmount(delta.formattedBalance), }, userBalance: { balance: current.balance, diff --git a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts index a2d3237975..d0b9dffae9 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts @@ -25,6 +25,7 @@ import { } from '../../../types'; import { quoteFetcher } from './quoteFetcher'; import { HttpClient } from '../../../api/http'; +import { formatSmartCheckoutAmount } from '../../../utils/utils'; jest.mock('../../../config/remoteConfigFetcher'); jest.mock('./quoteFetcher'); @@ -236,7 +237,7 @@ describe('swapRoute', () => { type: ItemType.ERC20, fundsRequired: { amount: BigNumber.from(1), - formattedAmount: utils.formatUnits(BigNumber.from(1), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(1), 18)), }, userBalance: { balance: BigNumber.from(10), @@ -258,6 +259,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapGasFee: { @@ -268,6 +270,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapFees: [{ @@ -279,6 +282,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }], }, @@ -367,7 +371,7 @@ describe('swapRoute', () => { type: ItemType.ERC20, fundsRequired: { amount: BigNumber.from(1), - formattedAmount: utils.formatUnits(BigNumber.from(1), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(1), 18)), }, userBalance: { balance: BigNumber.from(10), @@ -389,6 +393,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapGasFee: { @@ -399,6 +404,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapFees: [{ @@ -410,6 +416,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }], }, @@ -510,7 +517,7 @@ describe('swapRoute', () => { type: ItemType.ERC20, fundsRequired: { amount: BigNumber.from(1), - formattedAmount: utils.formatUnits(BigNumber.from(1), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(1), 18)), }, userBalance: { balance: BigNumber.from(10), @@ -532,6 +539,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapGasFee: { @@ -542,6 +550,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapFees: [{ @@ -553,6 +562,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }], }, @@ -564,7 +574,7 @@ describe('swapRoute', () => { type: ItemType.ERC20, fundsRequired: { amount: BigNumber.from(1), - formattedAmount: utils.formatUnits(BigNumber.from(1), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(1), 18)), }, userBalance: { balance: BigNumber.from(10), @@ -586,6 +596,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapGasFee: { @@ -596,6 +607,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }, swapFees: [{ @@ -607,6 +619,7 @@ describe('swapRoute', () => { decimals: 18, name: 'IMX', symbol: 'IMX', + address: undefined, }, }], }, @@ -1165,7 +1178,7 @@ describe('swapRoute', () => { type: ItemType.NATIVE, fundsRequired: { amount: BigNumber.from(100), - formattedAmount: utils.formatUnits(BigNumber.from(100), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(100), 18)), }, userBalance: { balance: BigNumber.from(100), @@ -1222,7 +1235,7 @@ describe('swapRoute', () => { type: ItemType.ERC20, fundsRequired: { amount: BigNumber.from(100), - formattedAmount: utils.formatUnits(BigNumber.from(100), 18), + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits(BigNumber.from(100), 18)), }, userBalance: { balance: BigNumber.from(100), diff --git a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.ts b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.ts index 892cb4c8fb..228bd98ac0 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.ts @@ -17,7 +17,7 @@ import { BalanceCheckResult, BalanceRequirement } from '../../balanceCheck/types import { TokenBalanceResult } from '../types'; import { quoteFetcher } from './quoteFetcher'; import { isNativeToken } from '../../../tokens'; -import { isMatchingAddress } from '../../../utils/utils'; +import { formatSmartCheckoutAmount, isMatchingAddress } from '../../../utils/utils'; const constructFees = ( approvalGasFee: Amount | null | undefined, @@ -105,10 +105,10 @@ export const constructSwapRoute = ( type, fundsRequired: { amount: fundsRequired, - formattedAmount: utils.formatUnits( + formattedAmount: formatSmartCheckoutAmount(utils.formatUnits( fundsRequired, userBalance.token.decimals, - ), + )), }, userBalance: { balance: userBalance.balance, diff --git a/packages/checkout/sdk/src/utils/utils.test.ts b/packages/checkout/sdk/src/utils/utils.test.ts index e873ec7943..2846340cfe 100644 --- a/packages/checkout/sdk/src/utils/utils.test.ts +++ b/packages/checkout/sdk/src/utils/utils.test.ts @@ -1,5 +1,5 @@ import { ChainId } from '../types'; -import { isMatchingAddress, isZkEvmChainId } from './utils'; +import { formatSmartCheckoutAmount, isMatchingAddress, isZkEvmChainId } from './utils'; describe('utils', () => { it('should return true if addresses are the same', () => { @@ -38,4 +38,21 @@ describe('utils', () => { expect(chainId).toBeFalsy(); }); }); + + describe('formatSmartCheckoutAmount', () => { + const formatTokenAmountPatterns = [ + { amount: '0.1234567', expected: '0.123457' }, + { amount: '0.1234561', expected: '0.123457' }, + { amount: '0.1234560', expected: '0.123456' }, + { amount: '0.1234', expected: '0.1234' }, + { amount: '120.100001', expected: '120.100001' }, + { amount: '120.1000011', expected: '120.100002' }, + { amount: '120.1000019', expected: '120.100002' }, + { amount: '120.10000101', expected: '120.100002' }, + { amount: '0.000000000000000001', expected: '0.000001' }, + ]; + it.each(formatTokenAmountPatterns)('.formatTokenAmount($amount)', ({ amount, expected }) => { + expect(formatSmartCheckoutAmount(amount)).toEqual(expected); + }); + }); }); diff --git a/packages/checkout/sdk/src/utils/utils.ts b/packages/checkout/sdk/src/utils/utils.ts index f24d210ac4..fc823e9900 100644 --- a/packages/checkout/sdk/src/utils/utils.ts +++ b/packages/checkout/sdk/src/utils/utils.ts @@ -1,3 +1,4 @@ +import { DEFAULT_TOKEN_FORMATTING_DECIMALS } from '../env/constants'; import { ChainId } from '../types'; export const isMatchingAddress = (addressA: string = '', addressB: string = '') => ( @@ -7,3 +8,34 @@ export const isMatchingAddress = (addressA: string = '', addressB: string = '') export const isZkEvmChainId = (chainId: ChainId) => chainId === ChainId.IMTBL_ZKEVM_DEVNET || chainId === ChainId.IMTBL_ZKEVM_TESTNET || chainId === ChainId.IMTBL_ZKEVM_MAINNET; + +const trimRoundUpDecimals = (s: string, maxDecimals: number): string => { + const pointIndex = s.indexOf('.'); + const extraDecimals = s.substring(pointIndex + maxDecimals + 1); + if (extraDecimals && parseFloat(extraDecimals) >= 1) { + const trimmedDecimals = s.substring(0, pointIndex + maxDecimals + 1); + const increment = 1 / (10 ** maxDecimals); + return (parseFloat(trimmedDecimals) + increment).toString(); + } + return parseFloat(s.substring(0, pointIndex + maxDecimals + 1)).toString(); +}; + +/** + * Rounds up a token amount to a set number of decimals, so it can be handled by Swap, Bridge, OnRamp Widgets. + * Widgets can only handle formatted values of 6 (DEFAULT_TOKEN_FORMATTING_DECIMALS) decimals. + * @param amount + * @param maxDecimals + * @returns + */ +export const formatSmartCheckoutAmount = ( + amount: string, + maxDecimals: number = DEFAULT_TOKEN_FORMATTING_DECIMALS, +) => { + // Only float numbers will be handled by this function + const pointIndex = amount.indexOf('.'); + if (pointIndex === -1) return amount; + + const formattedAmount = trimRoundUpDecimals(amount, maxDecimals); + + return formattedAmount; +};