Skip to content

Commit

Permalink
feat:[CM-638][Sale Widget] Add funding balances fee utils (#1803)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhesgodi authored May 20, 2024
1 parent 8d3c503 commit 22afe96
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { BigNumber } from 'ethers';
import { FeeType } from '@imtbl/checkout-sdk';
import { FundingBalance, FundingBalanceType } from '../types';
import {
FeesBySymbol,
getFundingBalanceFeeBreakDown,
getFundingBalanceTotalFees,
} from './fundingBalanceFees';

export const SwapFundingBalanceMock = {
type: 'SWAP',
chainId: 13473,
fundingItem: {
type: 'ERC20',
fundsRequired: {
amount: BigNumber.from('500000000000000000'),
formattedAmount: '5',
},
userBalance: {
balance: BigNumber.from('100000000000000000'),
formattedBalance: '10',
},
token: {
address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E',
circulating_market_cap: null,
decimals: 18,
exchange_rate: null,
holders: '46',
icon_url: null,
name: 'Core',
symbol: 'zkCORE',
total_supply: '1000000000000000000000000000',
type: 'ERC-20',
volume_24h: null,
},
},
fees: {
approvalGasFee: {
type: 'GAS',
amount: BigNumber.from('10000000000000'),
formattedAmount: '0.0001',
token: {
name: 'Immutable Testnet Token',
symbol: 'tIMX',
address: 'native',
decimals: 18,
},
},
swapGasFee: {
type: 'GAS',
amount: BigNumber.from('200000000000000'),
formattedAmount: '0.002',
token: {
name: 'Immutable Testnet Token',
symbol: 'tIMX',
address: 'native',
decimals: 18,
},
},
swapFees: [
{
type: 'SWAP_FEE',
amount: BigNumber.from('500000000000000000'),
formattedAmount: '0.5',
token: {
name: 'Core',
symbol: 'zkCORE',
address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E',
decimals: 18,
},
},
{
type: 'SWAP_FEE',
amount: BigNumber.from('100000000000000000'),
formattedAmount: '0.1',
token: {
name: 'Core',
symbol: 'zkCORE',
address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E',
decimals: 18,
},
},
],
},
} as FundingBalance;

describe('getFundingBalanceTotalFees', () => {
it('should return empty when no fees', () => {
expect(
getFundingBalanceTotalFees({
type: FundingBalanceType.SUFFICIENT,
} as FundingBalance),
).toEqual({});
});

it('should get total fee amount by symbol', () => {
const expected: FeesBySymbol = {
tIMX: {
type: FeeType.GAS,
amount: expect.any(Object),
formattedAmount: '0.00021', // sum amount of all tIMX
token: {
name: 'Immutable Testnet Token',
symbol: 'tIMX',
address: 'native',
decimals: 18,
},
},
zkCORE: {
type: FeeType.SWAP_FEE,
amount: expect.any(Object),
formattedAmount: '0.6', // sum amount of all zkCORE
token: {
name: 'Core',
symbol: 'zkCORE',
address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E',
decimals: 18,
},
},
};

const result = getFundingBalanceTotalFees(SwapFundingBalanceMock);
expect(result).toEqual(expected);
});
});

describe('getFundingBalanceFeeBreakDown', () => {
const conversionsMock: Map<string, number> = new Map([
['tIMX', 2.3],
['zkCORE', 0.7],
]);

const t = ((v: unknown) => v) as any;

it('should return empty when no fees', () => {
const sufficientFundingBalanceMock = {
type: FundingBalanceType.SUFFICIENT,
} as unknown as FundingBalance;

expect(
getFundingBalanceFeeBreakDown(
sufficientFundingBalanceMock,
conversionsMock,
t,
),
).toEqual([]);
});

it('should return fee breakdowns', () => {
const expected = [
{
amount: '0.000200',
fiatAmount: '≈ drawers.feesBreakdown.fees.fiatPricePrefix-.--',
label: 'drawers.feesBreakdown.fees.swapGasFee.label',
prefix: '~ ',
token: {
address: 'native',
decimals: 18,
name: 'Immutable Testnet Token',
symbol: 'tIMX',
},
},
{
amount: '0.000010',
fiatAmount: '≈ drawers.feesBreakdown.fees.fiatPricePrefix-.--',
label: 'drawers.feesBreakdown.fees.approvalFee.label',
prefix: '~ ',
token: {
address: 'native',
decimals: 18,
name: 'Immutable Testnet Token',
symbol: 'tIMX',
},
},
{
amount: '0.600000',
fiatAmount: '≈ drawers.feesBreakdown.fees.fiatPricePrefix-.--',
label: 'drawers.feesBreakdown.fees.swapSecondaryFee.label',
prefix: '',
token: {
address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E',
decimals: 18,
name: 'Core',
symbol: 'zkCORE',
},
},
];

const result = getFundingBalanceFeeBreakDown(
SwapFundingBalanceMock,
conversionsMock,
t,
);

expect(result).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { utils } from 'ethers';
import { Fee, FundingStepType, TokenInfo } from '@imtbl/checkout-sdk';
import {
calculateCryptoToFiat,
abbreviateWalletAddress,
tokenValueFormat,
} from 'lib/utils';
import { FormattedFee } from 'widgets/swap/functions/swapFees';
import { TFunction } from 'i18next';
import { FundingBalance } from '../types';

export type FeesBySymbol = Record<string, Fee>;

const getTotalFeesBySymbol = (
fees: Fee[],
tokenInfo?: TokenInfo,
): FeesBySymbol => fees
.filter((fee) => fee.amount.gt(0) && fee.token)
.reduce((acc, fee) => {
if (!fee.token) return acc;

const token: TokenInfo = {
...fee.token,
address: fee.token.address || '',
symbol: fee.token.symbol || tokenInfo?.symbol || '',
};

const address = abbreviateWalletAddress(
token.address!,
'...',
).toLowerCase();
const key = token.symbol || address;
if (!key) return acc;

if (acc[key]) {
const newAmount = acc[key].amount.add(fee.amount);
return {
...acc,
[key]: {
...acc[key],
amount: newAmount,
formattedAmount: utils.formatUnits(newAmount, token.decimals),
},
};
}

if (key) {
return {
...acc,
[key]: {
...fee,
token,
formattedAmount: utils.formatUnits(fee.amount, token.decimals),
},
};
}

return acc;
}, {} as FeesBySymbol);

export const getFundingBalanceTotalFees = (
balance: FundingBalance,
): FeesBySymbol => {
if (balance.type !== FundingStepType.SWAP) {
return {};
}

const fees = [
balance.fees.approvalGasFee,
balance.fees.swapGasFee,
...balance.fees.swapFees,
];
const totalFees = getTotalFeesBySymbol(fees, balance.fundingItem.token);

return totalFees;
};

export const getFundingBalanceFeeBreakDown = (
balance: FundingBalance,
conversions: Map<string, number>,
t: TFunction,
): FormattedFee[] => {
const feesBreakdown: FormattedFee[] = [];

if (balance.type !== FundingStepType.SWAP) {
return [];
}

const addFee = (fee: Fee, label: string, prefix: string = '~ ') => {
if (fee.amount.gt(0)) {
const formattedFee = utils.formatUnits(fee.amount, fee?.token?.decimals);

feesBreakdown.push({
label,
fiatAmount: `≈ ${t(
'drawers.feesBreakdown.fees.fiatPricePrefix',
)}${calculateCryptoToFiat(
formattedFee,
fee.token?.symbol || '',
conversions,
'-.--',
4,
)}`,
amount: `${tokenValueFormat(formattedFee)}`,
prefix,
token: fee?.token!,
});
}
};

// Format gas fee
addFee(
balance.fees.swapGasFee,
t('drawers.feesBreakdown.fees.swapGasFee.label'),
);

// Format gas fee approval
addFee(
balance.fees.approvalGasFee,
t('drawers.feesBreakdown.fees.approvalFee.label'),
);

// Format the secondary fees
const totalSwapFeesBySymbol = Object.entries(
getTotalFeesBySymbol(balance.fees.swapFees, balance.fundingItem.token),
);

totalSwapFeesBySymbol.forEach(([, swapFee]) => {
const basisPoints: number = swapFee?.basisPoints ?? 0;
addFee(
swapFee,
t('drawers.feesBreakdown.fees.swapSecondaryFee.label', {
amount: basisPoints ? `${basisPoints / 100}%` : '',
}),
'',
);
});

return feesBreakdown;
};

0 comments on commit 22afe96

Please sign in to comment.