From 9bee2e12d18addbcc7dd79564a91765836cbd856 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:29:23 +0100 Subject: [PATCH 01/15] feat: foxwifhat discount --- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 8 +- .../useGetTradeQuotes/useGetTradeRates.tsx | 9 +- src/lib/fees/constant.ts | 5 + src/lib/fees/model.test.ts | 116 +++++++++++++++++- src/lib/fees/model.ts | 45 ++++++- src/state/apis/snapshot/selectors.ts | 16 ++- 6 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 src/lib/fees/constant.ts diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 6eecf646203..e5f469c68f2 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -1,5 +1,5 @@ import { skipToken as reduxSkipToken } from '@reduxjs/toolkit/query' -import { fromAccountId } from '@shapeshiftoss/caip' +import { foxWifHatAssetId, fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import type { GetTradeQuoteInput, @@ -36,6 +36,7 @@ import { swapperApi } from 'state/apis/swapper/swapperApi' import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' import { selectPortfolioAccountMetadataByAccountId, + selectPortfolioCryptoBalanceBaseUnitByFilter, selectUsdRateByAssetId, } from 'state/slices/selectors' import { @@ -213,6 +214,9 @@ export const useGetTradeQuotes = () => { const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) + const foxWifHatHeld = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: foxWifHatAssetId }), + ) const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain @@ -273,6 +277,7 @@ export const useGetTradeQuotes = () => { tradeAmountUsd, foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), + foxWifHatHeld: bnOrZero(foxWifHatHeld), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -321,6 +326,7 @@ export const useGetTradeQuotes = () => { sellAsset, sellAssetUsdRate, thorVotingPower, + foxWifHatHeld, userSlippageTolerancePercentageDecimal, votingPower, wallet, diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index 1855855ab40..acaa5aa6ab0 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -1,5 +1,5 @@ import { skipToken } from '@reduxjs/toolkit/dist/query' -import { fromAccountId } from '@shapeshiftoss/caip' +import { foxWifHatAssetId, fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import type { GetTradeRateInput, TradeRate } from '@shapeshiftoss/swapper' import { @@ -28,6 +28,7 @@ import { } from 'state/apis/snapshot/selectors' import { swapperApi } from 'state/apis/swapper/swapperApi' import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from 'state/slices/common-selectors' import { selectUsdRateByAssetId } from 'state/slices/marketDataSlice/selectors' import { selectPortfolioAccountMetadataByAccountId } from 'state/slices/portfolioSlice/selectors' import { @@ -135,6 +136,10 @@ export const useGetTradeRates = () => { const sellAccountId = useAppSelector(selectFirstHopSellAccountId) const buyAccountId = useAppSelector(selectLastHopBuyAccountId) + const foxWifHatHeld = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: foxWifHatAssetId }), + ) + const userSlippageTolerancePercentageDecimal = useAppSelector(selectUserSlippagePercentageDecimal) const sellAccountMetadataFilter = useMemo( @@ -193,6 +198,7 @@ export const useGetTradeRates = () => { // referentially invalidate, while ensuring the *initial* connection of a wallet when quotes were gotten without one, doesn't invalidate anything sellAccountMetadata, votingPower, + foxWifHatHeld, thorVotingPower, receiveAccountMetadata, sellAccountId, @@ -226,6 +232,7 @@ export const useGetTradeRates = () => { tradeAmountUsd, foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), + foxWifHatHeld: bnOrZero(foxWifHatHeld), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) diff --git a/src/lib/fees/constant.ts b/src/lib/fees/constant.ts new file mode 100644 index 00000000000..5475ff96f1c --- /dev/null +++ b/src/lib/fees/constant.ts @@ -0,0 +1,5 @@ +// This timestamp is the 24th of January 2025 +export const FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS = 1737707777000 +// Supposed to end the 5th of May 2025 +export const FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS = 1746396000000 +export const FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT = 10000000000000000000000 diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index 7d913ec9ccd..d17487b191d 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -1,8 +1,13 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { bn } from 'lib/bignumber/bignumber' import { selectIsSnapshotApiQueriesRejected } from 'state/apis/snapshot/selectors' import { store } from 'state/store' +import { + FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS, + FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS, + FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT, +} from './constant' import { calculateFees } from './model' import { swapperParameters } from './parameters/swapper' @@ -137,4 +142,113 @@ describe('calculateFees', () => { expect(feeBps.toNumber()).toEqual(FEE_CURVE_MAX_FEE_BPS) expect(foxDiscountPercent).toEqual(bn(0)) }) + + describe('foxWifHat campaign', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should return 100% discount at campaign start for eligible holders', () => { + vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) + const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) + const foxHeld = bn(0) + const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) + + const { feeBps, foxDiscountPercent } = calculateFees({ + tradeAmountUsd, + foxHeld, + foxWifHatHeld, + feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, + }) + + expect(feeBps.toNumber()).toEqual(0) + expect(foxDiscountPercent.toNumber()).toEqual(100) + }) + + it('should return 50% discount at campaign midpoint for eligible holders', () => { + const campaignMidpoint = + FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS + + (FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS) / 2 + + vi.setSystemTime(new Date(campaignMidpoint)) + const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) + const foxHeld = bn(0) + const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) + + const { feeBps, foxDiscountPercent } = calculateFees({ + tradeAmountUsd, + foxHeld, + foxWifHatHeld, + feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, + }) + + expect(foxDiscountPercent.toNumber()).toBeCloseTo(50, 0) + expect(feeBps.toNumber()).toBeCloseTo(17, 0) // ~35 * 0.5 + }) + + it('should return 0% discount at campaign end for eligible holders', () => { + vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS)) + const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) + const foxHeld = bn(0) + const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) + + const { feeBps, foxDiscountPercent } = calculateFees({ + tradeAmountUsd, + foxHeld, + foxWifHatHeld, + feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, + }) + + expect(foxDiscountPercent.toNumber()).toEqual(0) + expect(feeBps.toNumber()).toEqual(35) // No discount applied + }) + + it('should return no discount for holders below minimum amount', () => { + vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) + const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) + const foxHeld = bn(0) + const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT).minus(1) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) + + const { feeBps, foxDiscountPercent } = calculateFees({ + tradeAmountUsd, + foxHeld, + foxWifHatHeld, + feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, + }) + + expect(foxDiscountPercent.toNumber()).toEqual(0) + expect(feeBps.toNumber()).toEqual(35) + }) + + it('should return no discount outside campaign period', () => { + vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS + 1)) + const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) + const foxHeld = bn(0) + const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) + + const { feeBps, foxDiscountPercent } = calculateFees({ + tradeAmountUsd, + foxHeld, + foxWifHatHeld, + feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, + }) + + expect(foxDiscountPercent.toNumber()).toEqual(0) + expect(feeBps.toNumber()).toEqual(35) + }) + }) }) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index f7dd288a820..a5741d1aa38 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -2,6 +2,11 @@ import BigNumber from 'bignumber.js' import { getConfig } from 'config' import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { + FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS, + FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS, + FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT, +} from './constant' import { FEE_CURVE_PARAMETERS } from './parameters' import type { ParameterModel } from './parameters/types' @@ -12,6 +17,7 @@ type CalculateFeeBpsArgs = { tradeAmountUsd: BigNumber foxHeld: BigNumber thorHeld?: BigNumber + foxWifHatHeld?: BigNumber feeModel: ParameterModel isSnapshotApiQueriesRejected: boolean } @@ -43,6 +49,7 @@ export const calculateFees: CalculateFeeBps = ({ foxHeld, feeModel, thorHeld, + foxWifHatHeld, isSnapshotApiQueriesRejected, }) => { const { @@ -59,30 +66,58 @@ export const calculateFees: CalculateFeeBps = ({ const midpointUsd = bn(FEE_CURVE_MIDPOINT_USD) const feeCurveSteepness = bn(FEE_CURVE_STEEPNESS_K) const isThorFreeEnabled = getConfig().REACT_APP_FEATURE_THOR_FREE_FEES + const isFoxWifHatCampaignActive = + new Date().getTime() >= FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS && + new Date().getTime() <= FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS + + const currentFoxWifHatDiscountPercent = (() => { + if (!foxWifHatHeld || foxWifHatHeld?.lt(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT)) return bn(0) + + const currentTime = new Date().getTime() + const totalCampaignDuration = + FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS + const timeElapsed = currentTime - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS + const remainingPercentage = bn(100).times( + bn(1).minus(bn(timeElapsed).div(totalCampaignDuration)), + ) + + return BigNumber.maximum(BigNumber.minimum(remainingPercentage, bn(100)), bn(0)) + })() // trades below the fee threshold are free. const isFree = tradeAmountUsd.lt(noFeeThresholdUsd) const isThorFree = isThorFreeEnabled && - thorHeld?.isGreaterThanOrEqualTo(THORSWAP_UNIT_THRESHOLD) && + thorHeld?.gte(THORSWAP_UNIT_THRESHOLD) && new Date().getUTCFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD // failure to fetch fox discount results in free trades. - const isFallbackFees = isSnapshotApiQueriesRejected + const isFallbackFees = + isSnapshotApiQueriesRejected && + (!foxWifHatHeld || + foxWifHatHeld?.lt(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) || + !isFoxWifHatCampaignActive) // the fox discount before any other logic is applied const foxBaseDiscountPercent = (() => { if (isFree) return bn(100) // THOR holder before TIP014 are trade free until 2025 if (isThorFree) return bn(100) - // No discount if we cannot fetch FOX holdings - if (isFallbackFees) return bn(0) - return BigNumber.minimum( + const foxDiscountPercent = BigNumber.minimum( bn(100), bnOrZero(foxHeld).times(100).div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)), ) + + // No discount if we cannot fetch FOX holdings + if (isFallbackFees) return bn(0) + + if (currentFoxWifHatDiscountPercent.gt(foxDiscountPercent) && isFoxWifHatCampaignActive) { + return currentFoxWifHatDiscountPercent + } + + return foxDiscountPercent })() // the fee bps before the fox discount is applied, as a floating point number diff --git a/src/state/apis/snapshot/selectors.ts b/src/state/apis/snapshot/selectors.ts index 1be612c23b7..af81bb250af 100644 --- a/src/state/apis/snapshot/selectors.ts +++ b/src/state/apis/snapshot/selectors.ts @@ -1,5 +1,5 @@ import { QueryStatus } from '@reduxjs/toolkit/dist/query' -import { ethChainId } from '@shapeshiftoss/caip' +import { ethChainId, foxWifHatAssetId } from '@shapeshiftoss/caip' import { bnOrZero } from '@shapeshiftoss/utils' import createCachedSelector from 're-reselect' import type { Selector } from 'reselect' @@ -10,6 +10,7 @@ import type { ParameterModel } from 'lib/fees/parameters/types' import { isSome } from 'lib/utils' import type { ReduxState } from 'state/reducer' import { selectFeeModelParamFromFilter } from 'state/selectors' +import { selectPortfolioAssetBalancesBaseUnit } from 'state/slices/common-selectors' import { selectAccountIdsByChainId } from 'state/slices/portfolioSlice/selectors' const selectSnapshotApiQueries = (state: ReduxState) => state.snapshotApi.queries @@ -64,11 +65,22 @@ export const selectCalculatedFees: Selector = selectVotingPower, selectThorVotingPower, selectIsSnapshotApiQueriesRejected, - (feeModel, inputAmountUsd, votingPower, thorVotingPower, isSnapshotApiQueriesRejected) => { + selectPortfolioAssetBalancesBaseUnit, + ( + feeModel, + inputAmountUsd, + votingPower, + thorVotingPower, + isSnapshotApiQueriesRejected, + assetBalances, + ) => { + const foxWifHatHeld = assetBalances[foxWifHatAssetId] + const fees: CalculateFeeBpsReturn = calculateFees({ tradeAmountUsd: bnOrZero(inputAmountUsd), foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), + foxWifHatHeld: bnOrZero(foxWifHatHeld), feeModel, isSnapshotApiQueriesRejected, }) From 7aad8028ee6bea17e6bdb6a6dfd666e8a679675a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:39:11 +0100 Subject: [PATCH 02/15] fix: review feedbacks --- src/lib/fees/model.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index a5741d1aa38..09696d99212 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -66,9 +66,12 @@ export const calculateFees: CalculateFeeBps = ({ const midpointUsd = bn(FEE_CURVE_MIDPOINT_USD) const feeCurveSteepness = bn(FEE_CURVE_STEEPNESS_K) const isThorFreeEnabled = getConfig().REACT_APP_FEATURE_THOR_FREE_FEES + const isFoxWifHatEnabled = getConfig().REACT_APP_FEATURE_FOX_PAGE_FOX_WIF_HAT_SECTION + const isFoxWifHatCampaignActive = new Date().getTime() >= FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS && - new Date().getTime() <= FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS + new Date().getTime() <= FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS && + isFoxWifHatEnabled const currentFoxWifHatDiscountPercent = (() => { if (!foxWifHatHeld || foxWifHatHeld?.lt(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT)) return bn(0) @@ -113,11 +116,7 @@ export const calculateFees: CalculateFeeBps = ({ // No discount if we cannot fetch FOX holdings if (isFallbackFees) return bn(0) - if (currentFoxWifHatDiscountPercent.gt(foxDiscountPercent) && isFoxWifHatCampaignActive) { - return currentFoxWifHatDiscountPercent - } - - return foxDiscountPercent + return BigNumber.maximum(foxDiscountPercent, currentFoxWifHatDiscountPercent) })() // the fee bps before the fox discount is applied, as a floating point number From a3d9766fb8ffbb18fd5aad5c1665b80a083e62e1 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:42:00 +0100 Subject: [PATCH 03/15] fix: review feedbacks --- src/components/FeeExplainer/FeeExplainer.tsx | 16 ++++++++++++ src/lib/fees/model.test.ts | 26 +++++++++++++++++++ src/lib/fees/model.ts | 19 +++++--------- .../AddLiquidity/AddLiquidityInput.tsx | 16 +++++++++++- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/components/FeeExplainer/FeeExplainer.tsx b/src/components/FeeExplainer/FeeExplainer.tsx index fab92078043..74e8d1871d3 100644 --- a/src/components/FeeExplainer/FeeExplainer.tsx +++ b/src/components/FeeExplainer/FeeExplainer.tsx @@ -1,5 +1,6 @@ import type { CardProps, StackProps } from '@chakra-ui/react' import { Box, Card, CardBody, Flex, Heading, Stack, useToken } from '@chakra-ui/react' +import { foxWifHatAssetId } from '@shapeshiftoss/caip' import { bnOrZero } from '@shapeshiftoss/chain-adapters' import { LinearGradient } from '@visx/gradient' import { GridColumns, GridRows } from '@visx/grid' @@ -31,6 +32,7 @@ import { selectIsSnapshotApiQueriesRejected, selectVotingPower, } from 'state/apis/snapshot/selectors' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from 'state/slices/common-selectors' import { useAppSelector } from 'state/store' import { CHART_TRADE_SIZE_MAX_USD } from './common' @@ -149,6 +151,9 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) foxHeld: bn(debouncedFoxHolding), feeModel, isSnapshotApiQueriesRejected, + // This is for feeExplainer which is not supporting anything else than FOX discount for now + foxWifHatHeld: bn(0), + thorHeld: bn(0), }).feeBpsFloat.toNumber() return { x: trade, y: feeBps } }) @@ -161,6 +166,9 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) foxHeld: bn(debouncedFoxHolding), feeModel, isSnapshotApiQueriesRejected, + // This is for feeExplainer which is not supporting anything else than FOX discount for now + foxWifHatHeld: bn(0), + thorHeld: bn(0), }).feeBpsFloat.toNumber() return [{ x: tradeSize, y: feeBps }] @@ -262,12 +270,20 @@ type FeeOutputProps = { export const FeeOutput: React.FC = ({ tradeSizeUSD, foxHolding, feeModel }) => { const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) + + const foxWifHatHeld = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: foxWifHatAssetId }), + ) + const { feeUsd, feeBps, foxDiscountPercent, feeUsdBeforeDiscount, feeBpsBeforeDiscount } = calculateFees({ tradeAmountUsd: bn(tradeSizeUSD), foxHeld: bn(foxHolding), feeModel, isSnapshotApiQueriesRejected, + foxWifHatHeld: bn(foxWifHatHeld), + // @TODO: remove this when thor swap discount is removed + thorHeld: bn(0), }) const basedOnFeeTranslation: TextPropTypes['translation'] = useMemo( diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index d17487b191d..bb01db36c49 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -41,6 +41,8 @@ describe('calculateFees', () => { const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -54,6 +56,8 @@ describe('calculateFees', () => { const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -67,6 +71,8 @@ describe('calculateFees', () => { const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -80,6 +86,8 @@ describe('calculateFees', () => { const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -93,6 +101,8 @@ describe('calculateFees', () => { const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -106,6 +116,8 @@ describe('calculateFees', () => { const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -120,6 +132,8 @@ describe('calculateFees', () => { const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -136,6 +150,8 @@ describe('calculateFees', () => { const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld: bn(0), + foxWifHatHeld: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -156,12 +172,14 @@ describe('calculateFees', () => { vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const thorHeld = bn(0) const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld, foxWifHatHeld, feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, @@ -179,12 +197,14 @@ describe('calculateFees', () => { vi.setSystemTime(new Date(campaignMidpoint)) const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const thorHeld = bn(0) const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld, foxWifHatHeld, feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, @@ -198,12 +218,14 @@ describe('calculateFees', () => { vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS)) const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const thorHeld = bn(0) const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld, foxWifHatHeld, feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, @@ -217,12 +239,14 @@ describe('calculateFees', () => { vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const thorHeld = bn(0) const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT).minus(1) const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld, foxWifHatHeld, feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, @@ -236,12 +260,14 @@ describe('calculateFees', () => { vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS + 1)) const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const thorHeld = bn(0) const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, + thorHeld, foxWifHatHeld, feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 09696d99212..c5693047a57 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -16,8 +16,8 @@ export const THORSWAP_MAXIMUM_YEAR_TRESHOLD = 2025 type CalculateFeeBpsArgs = { tradeAmountUsd: BigNumber foxHeld: BigNumber - thorHeld?: BigNumber - foxWifHatHeld?: BigNumber + thorHeld: BigNumber + foxWifHatHeld: BigNumber feeModel: ParameterModel isSnapshotApiQueriesRejected: boolean } @@ -92,15 +92,11 @@ export const calculateFees: CalculateFeeBps = ({ const isThorFree = isThorFreeEnabled && - thorHeld?.gte(THORSWAP_UNIT_THRESHOLD) && + thorHeld.gte(THORSWAP_UNIT_THRESHOLD) && new Date().getUTCFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD // failure to fetch fox discount results in free trades. - const isFallbackFees = - isSnapshotApiQueriesRejected && - (!foxWifHatHeld || - foxWifHatHeld?.lt(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) || - !isFoxWifHatCampaignActive) + const isFallbackFees = isSnapshotApiQueriesRejected // the fox discount before any other logic is applied const foxBaseDiscountPercent = (() => { @@ -108,10 +104,9 @@ export const calculateFees: CalculateFeeBps = ({ // THOR holder before TIP014 are trade free until 2025 if (isThorFree) return bn(100) - const foxDiscountPercent = BigNumber.minimum( - bn(100), - bnOrZero(foxHeld).times(100).div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)), - ) + const foxDiscountPercent = isFoxWifHatCampaignActive + ? bnOrZero(foxHeld).times(100).div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)) + : bn(0) // No discount if we cannot fetch FOX holdings if (isFallbackFees) return bn(0) diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 926c3f7c65f..9d47e7e87e3 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -18,7 +18,13 @@ import { usePrevious, } from '@chakra-ui/react' import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' -import { fromAccountId, fromAssetId, thorchainAssetId, thorchainChainId } from '@shapeshiftoss/caip' +import { + foxWifHatAssetId, + fromAccountId, + fromAssetId, + thorchainAssetId, + thorchainChainId, +} from '@shapeshiftoss/caip' import { SwapperName } from '@shapeshiftoss/swapper' import { assetIdToPoolAssetId, @@ -202,6 +208,10 @@ export const AddLiquidityInput: React.FC = ({ const [virtualRuneDepositAmountFiatUserCurrency, setVirtualRuneDepositAmountFiatUserCurrency] = useState() + const foxWifHatHeld = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: foxWifHatAssetId }), + ) + const [slippageDecimalPercentage, setSlippageDecimalPercentage] = useState() const isUnsafeQuote = useMemo( @@ -1014,6 +1024,9 @@ export const AddLiquidityInput: React.FC = ({ const { feeBps, feeUsd } = calculateFees({ tradeAmountUsd: bn(totalAmountUsd), foxHeld: bnOrZero(votingPower), + foxWifHatHeld: bn(foxWifHatHeld), + // @TODO: remove this when thor swap discount is removed + thorHeld: bn(0), feeModel: 'THORCHAIN_LP', isSnapshotApiQueriesRejected, }) @@ -1058,6 +1071,7 @@ export const AddLiquidityInput: React.FC = ({ userCurrencyToUsdRate, votingPower, isSnapshotApiQueriesRejected, + foxWifHatHeld, ]) const percentOptions = useMemo(() => [], []) From ad149301d7b404d906c7bc1a68338c35d13f8e1a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:39:37 +0100 Subject: [PATCH 04/15] fix: oops --- src/lib/fees/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index c5693047a57..211f74983c0 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -111,7 +111,7 @@ export const calculateFees: CalculateFeeBps = ({ // No discount if we cannot fetch FOX holdings if (isFallbackFees) return bn(0) - return BigNumber.maximum(foxDiscountPercent, currentFoxWifHatDiscountPercent) + return BigNumber.minimum(foxDiscountPercent, currentFoxWifHatDiscountPercent) })() // the fee bps before the fox discount is applied, as a floating point number From f44a4a2a8ca7cb366fc020cf1eaa095ad15b8b7a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:53:34 +0100 Subject: [PATCH 05/15] fix: review feedbacks --- src/lib/fees/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 211f74983c0..c5693047a57 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -111,7 +111,7 @@ export const calculateFees: CalculateFeeBps = ({ // No discount if we cannot fetch FOX holdings if (isFallbackFees) return bn(0) - return BigNumber.minimum(foxDiscountPercent, currentFoxWifHatDiscountPercent) + return BigNumber.maximum(foxDiscountPercent, currentFoxWifHatDiscountPercent) })() // the fee bps before the fox discount is applied, as a floating point number From 96ece47bfefc0f4ef4ef04613e27c2e4f6b1b472 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:22:19 +0100 Subject: [PATCH 06/15] fix: review feedbacks --- src/lib/fees/model.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index c5693047a57..cbe2d87ae18 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -73,8 +73,14 @@ export const calculateFees: CalculateFeeBps = ({ new Date().getTime() <= FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS && isFoxWifHatEnabled + const isFoxWifHatDiscountEligible = + isFoxWifHatCampaignActive && + foxWifHatHeld && + foxWifHatHeld?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + const currentFoxWifHatDiscountPercent = (() => { - if (!foxWifHatHeld || foxWifHatHeld?.lt(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT)) return bn(0) + if (!isFoxWifHatCampaignActive) return bn(0) + if (!isFoxWifHatDiscountEligible) return bn(0) const currentTime = new Date().getTime() const totalCampaignDuration = @@ -104,19 +110,19 @@ export const calculateFees: CalculateFeeBps = ({ // THOR holder before TIP014 are trade free until 2025 if (isThorFree) return bn(100) - const foxDiscountPercent = isFoxWifHatCampaignActive - ? bnOrZero(foxHeld).times(100).div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)) - : bn(0) + const foxDiscountPercent = bnOrZero(foxHeld) + .times(100) + .div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)) - // No discount if we cannot fetch FOX holdings - if (isFallbackFees) return bn(0) + // No discount if we cannot fetch FOX holdings and we are not eligible for the WIF HAT campaign + if (isFallbackFees && !isFoxWifHatDiscountEligible) return bn(0) return BigNumber.maximum(foxDiscountPercent, currentFoxWifHatDiscountPercent) })() // the fee bps before the fox discount is applied, as a floating point number const feeBpsBeforeDiscountFloat = - isFallbackFees && !isFree && !isThorFree + isFallbackFees && !isFree && !isThorFree && !isFoxWifHatDiscountEligible ? bn(FEE_CURVE_MAX_FEE_BPS) : minFeeBps.plus( maxFeeBps @@ -136,7 +142,7 @@ export const calculateFees: CalculateFeeBps = ({ ) const feeBpsFloat = - isFallbackFees && !isFree && !isThorFree + isFallbackFees && !isFree && !isThorFree && !isFoxWifHatDiscountEligible ? bn(FEE_CURVE_MAX_FEE_BPS) : BigNumber.maximum( feeBpsBeforeDiscountFloat.multipliedBy(bn(1).minus(foxBaseDiscountPercent.div(100))), From 1a55201cb44bc1b6241f9c87fdbd026095c8203c Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:32:23 +0100 Subject: [PATCH 07/15] fix: review feedbacks --- src/test/mocks/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 2179c50b42f..2ec8f0f7590 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -142,7 +142,7 @@ export const mockStore: ReduxState = { JupiterSwap: false, NewTradeFlow: false, NewWalletFlow: false, - FoxPageFoxWifHatSection: false, + FoxPageFoxWifHatSection: true, }, selectedLocale: 'en', balanceThreshold: '0', From 6e9ac2ce24a98c735b8d578445e73204dde233c8 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:51:13 +0100 Subject: [PATCH 08/15] fix: tests --- src/lib/fees/model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index cbe2d87ae18..4d87d18f06b 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -78,6 +78,8 @@ export const calculateFees: CalculateFeeBps = ({ foxWifHatHeld && foxWifHatHeld?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + console.log({ isFoxWifHatDiscountEligible }) + const currentFoxWifHatDiscountPercent = (() => { if (!isFoxWifHatCampaignActive) return bn(0) if (!isFoxWifHatDiscountEligible) return bn(0) From 52fe6875e8f62995d7076efb03cd3aa78b8ac03b Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:59:08 +0100 Subject: [PATCH 09/15] fix: review feedbacks --- src/lib/fees/model.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 4d87d18f06b..7e31618c363 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -78,7 +78,14 @@ export const calculateFees: CalculateFeeBps = ({ foxWifHatHeld && foxWifHatHeld?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - console.log({ isFoxWifHatDiscountEligible }) + console.log({ + isFoxWifHatCampaignActive, + isFoxWifHatDiscountEligible, + foxWifHatHeld, + currentTime: new Date().getTime(), + FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS, + FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS, + }) const currentFoxWifHatDiscountPercent = (() => { if (!isFoxWifHatCampaignActive) return bn(0) @@ -92,6 +99,13 @@ export const calculateFees: CalculateFeeBps = ({ bn(1).minus(bn(timeElapsed).div(totalCampaignDuration)), ) + console.log({ + remainingPercentage, + currentTime, + totalCampaignDuration, + remaining: remainingPercentage.toFixed(), + }) + return BigNumber.maximum(BigNumber.minimum(remainingPercentage, bn(100)), bn(0)) })() @@ -116,6 +130,8 @@ export const calculateFees: CalculateFeeBps = ({ .times(100) .div(bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD)) + console.log({ foxDiscountPercent, currentFoxWifHatDiscountPercent }) + // No discount if we cannot fetch FOX holdings and we are not eligible for the WIF HAT campaign if (isFallbackFees && !isFoxWifHatDiscountEligible) return bn(0) From f74306a3f62e89d676b41232546476fe4a7ae5f2 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:21:52 +0100 Subject: [PATCH 10/15] fix: remove tests --- src/lib/fees/model.test.ts | 119 ------------------------------------- src/test/mocks/store.ts | 2 +- 2 files changed, 1 insertion(+), 120 deletions(-) diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index bb01db36c49..f926094418f 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -158,123 +158,4 @@ describe('calculateFees', () => { expect(feeBps.toNumber()).toEqual(FEE_CURVE_MAX_FEE_BPS) expect(foxDiscountPercent).toEqual(bn(0)) }) - - describe('foxWifHat campaign', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - it('should return 100% discount at campaign start for eligible holders', () => { - vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) - const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) - const foxHeld = bn(0) - const thorHeld = bn(0) - const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) - - const { feeBps, foxDiscountPercent } = calculateFees({ - tradeAmountUsd, - foxHeld, - thorHeld, - foxWifHatHeld, - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - expect(feeBps.toNumber()).toEqual(0) - expect(foxDiscountPercent.toNumber()).toEqual(100) - }) - - it('should return 50% discount at campaign midpoint for eligible holders', () => { - const campaignMidpoint = - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS + - (FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS) / 2 - - vi.setSystemTime(new Date(campaignMidpoint)) - const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) - const foxHeld = bn(0) - const thorHeld = bn(0) - const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) - - const { feeBps, foxDiscountPercent } = calculateFees({ - tradeAmountUsd, - foxHeld, - thorHeld, - foxWifHatHeld, - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - expect(foxDiscountPercent.toNumber()).toBeCloseTo(50, 0) - expect(feeBps.toNumber()).toBeCloseTo(17, 0) // ~35 * 0.5 - }) - - it('should return 0% discount at campaign end for eligible holders', () => { - vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS)) - const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) - const foxHeld = bn(0) - const thorHeld = bn(0) - const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) - - const { feeBps, foxDiscountPercent } = calculateFees({ - tradeAmountUsd, - foxHeld, - thorHeld, - foxWifHatHeld, - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - expect(foxDiscountPercent.toNumber()).toEqual(0) - expect(feeBps.toNumber()).toEqual(35) // No discount applied - }) - - it('should return no discount for holders below minimum amount', () => { - vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS)) - const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) - const foxHeld = bn(0) - const thorHeld = bn(0) - const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT).minus(1) - const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) - - const { feeBps, foxDiscountPercent } = calculateFees({ - tradeAmountUsd, - foxHeld, - thorHeld, - foxWifHatHeld, - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - expect(foxDiscountPercent.toNumber()).toEqual(0) - expect(feeBps.toNumber()).toEqual(35) - }) - - it('should return no discount outside campaign period', () => { - vi.setSystemTime(new Date(FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS + 1)) - const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) - const foxHeld = bn(0) - const thorHeld = bn(0) - const foxWifHatHeld = bn(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) - - const { feeBps, foxDiscountPercent } = calculateFees({ - tradeAmountUsd, - foxHeld, - thorHeld, - foxWifHatHeld, - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - expect(foxDiscountPercent.toNumber()).toEqual(0) - expect(feeBps.toNumber()).toEqual(35) - }) - }) }) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 2ec8f0f7590..2179c50b42f 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -142,7 +142,7 @@ export const mockStore: ReduxState = { JupiterSwap: false, NewTradeFlow: false, NewWalletFlow: false, - FoxPageFoxWifHatSection: true, + FoxPageFoxWifHatSection: false, }, selectedLocale: 'en', balanceThreshold: '0', From 1dbccbaa729bcafde463ca3e0bb8b58628047eb4 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:39:42 +0100 Subject: [PATCH 11/15] feat: update row percent --- src/lib/fees/model.test.ts | 7 +----- src/lib/fees/model.ts | 22 +++++------------- src/pages/Fox/components/FoxWifHat.tsx | 1 - .../Fox/components/FoxWifHatClaimRow.tsx | 23 +++++++++++++------ 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index f926094418f..78bb5349de0 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -1,13 +1,8 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { bn } from 'lib/bignumber/bignumber' import { selectIsSnapshotApiQueriesRejected } from 'state/apis/snapshot/selectors' import { store } from 'state/store' -import { - FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS, - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS, - FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT, -} from './constant' import { calculateFees } from './model' import { swapperParameters } from './parameters/swapper' diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 7e31618c363..20007fbad31 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -78,15 +78,6 @@ export const calculateFees: CalculateFeeBps = ({ foxWifHatHeld && foxWifHatHeld?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) - console.log({ - isFoxWifHatCampaignActive, - isFoxWifHatDiscountEligible, - foxWifHatHeld, - currentTime: new Date().getTime(), - FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS, - FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS, - }) - const currentFoxWifHatDiscountPercent = (() => { if (!isFoxWifHatCampaignActive) return bn(0) if (!isFoxWifHatDiscountEligible) return bn(0) @@ -99,13 +90,6 @@ export const calculateFees: CalculateFeeBps = ({ bn(1).minus(bn(timeElapsed).div(totalCampaignDuration)), ) - console.log({ - remainingPercentage, - currentTime, - totalCampaignDuration, - remaining: remainingPercentage.toFixed(), - }) - return BigNumber.maximum(BigNumber.minimum(remainingPercentage, bn(100)), bn(0)) })() @@ -174,6 +158,12 @@ export const calculateFees: CalculateFeeBps = ({ .div(feeBpsBeforeDiscountFloat) .times(100) + console.log({ + foxDiscountPercent: foxDiscountPercent.toFixed(2), + feeBpsFloat: feeBpsFloat.toFixed(2), + feeBpsBeforeDiscountFloat: feeBpsBeforeDiscountFloat.toFixed(2), + }) + const feeBps = feeBpsAfterDiscount const feeUsdBeforeDiscount = tradeAmountUsd.multipliedBy(feeBpsBeforeDiscount.div(bn(10000))) const feeUsdDiscount = feeUsdBeforeDiscount.multipliedBy(foxDiscountPercent.div(100)) diff --git a/src/pages/Fox/components/FoxWifHat.tsx b/src/pages/Fox/components/FoxWifHat.tsx index b3293e6c5a3..239742cf979 100644 --- a/src/pages/Fox/components/FoxWifHat.tsx +++ b/src/pages/Fox/components/FoxWifHat.tsx @@ -48,7 +48,6 @@ export const FoxWifHat = () => { accountId={accountId} amountCryptoBaseUnit={bnOrZero(claim.amount).toFixed()} assetId={foxWifHatAssetId} - discountPercentDecimal={0.72} // eslint-disable-next-line react-memo/require-usememo onClaim={() => handleClaimModalOpen(accountId)} /> diff --git a/src/pages/Fox/components/FoxWifHatClaimRow.tsx b/src/pages/Fox/components/FoxWifHatClaimRow.tsx index a3bdd1b7ac1..6329f02b44b 100644 --- a/src/pages/Fox/components/FoxWifHatClaimRow.tsx +++ b/src/pages/Fox/components/FoxWifHatClaimRow.tsx @@ -6,7 +6,8 @@ import { useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { Amount } from 'components/Amount/Amount' import { WalletIcon } from 'components/Icons/WalletIcon' -import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' +import { bn } from 'lib/bignumber/bignumber' +import { calculateFees } from 'lib/fees/model' import { fromBaseUnit } from 'lib/math' import { middleEllipsis } from 'lib/utils' import { @@ -23,7 +24,6 @@ type FoxWifHatClaimRowProps = { accountId: string amountCryptoBaseUnit: string assetId: AssetId - discountPercentDecimal: number onClaim?: () => void } @@ -34,11 +34,12 @@ const columnAlignItems = { md: 'center' } const columnJustifyContent = { md: 'space-between' } const columnSpacing = { base: 4, md: 12, lg: 24, xl: 48 } +const DUMMY_TRADE_AMOUNT_OVER_TRESHOLD_USD = 1000000 + export const FoxWifHatClaimRow = ({ accountId, amountCryptoBaseUnit, assetId, - discountPercentDecimal, onClaim, }: FoxWifHatClaimRowProps) => { const textColor = useColorModeValue('gray.500', 'gray.400') @@ -48,9 +49,6 @@ export const FoxWifHatClaimRow = ({ selectAccountNumberByAccountId(state, { accountId }), ) const accountIdsByChainId = useAppSelector(selectAccountIdsByChainId) - const { - number: { toPercent }, - } = useLocaleFormatter() const getFoxWifHatMerkleTreeQuery = useFoxWifHatMerkleTreeQuery() const numberAccounts = useMemo(() => { @@ -70,6 +68,17 @@ export const FoxWifHatClaimRow = ({ const { data: isClaimed } = useFoxWifHatClaimedQueryQuery({ index: claim?.index }) + const discountPercent = useMemo(() => { + return calculateFees({ + tradeAmountUsd: bn(DUMMY_TRADE_AMOUNT_OVER_TRESHOLD_USD), + foxHeld: bn(0), + feeModel: 'SWAPPER', + thorHeld: bn(0), + foxWifHatHeld: bn(amountCryptoBaseUnit), + isSnapshotApiQueriesRejected: false, + }).foxDiscountPercent.toFixed(2) + }, [amountCryptoBaseUnit]) + return ( {translate('foxPage.foxWifHat.discountText', { - percent: toPercent(discountPercentDecimal), + percent: discountPercent, })} From 83851c8d6f6af8485ce7e3c2c29be7f518b42cc1 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:31:59 +0100 Subject: [PATCH 12/15] feat: review feedbacks --- src/components/FeeExplainer/FeeExplainer.tsx | 6 +++--- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 2 +- .../hooks/useGetTradeQuotes/useGetTradeRates.tsx | 2 +- src/lib/fees/constant.ts | 7 ++++++- src/lib/fees/model.test.ts | 16 ++++++++-------- src/lib/fees/model.ts | 8 ++++---- src/pages/Fox/components/FoxWifHatClaimRow.tsx | 2 +- .../AddLiquidity/AddLiquidityInput.tsx | 2 +- src/state/apis/snapshot/selectors.ts | 2 +- 9 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/components/FeeExplainer/FeeExplainer.tsx b/src/components/FeeExplainer/FeeExplainer.tsx index 74e8d1871d3..279d6962f0f 100644 --- a/src/components/FeeExplainer/FeeExplainer.tsx +++ b/src/components/FeeExplainer/FeeExplainer.tsx @@ -152,7 +152,7 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) feeModel, isSnapshotApiQueriesRejected, // This is for feeExplainer which is not supporting anything else than FOX discount for now - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), thorHeld: bn(0), }).feeBpsFloat.toNumber() return { x: trade, y: feeBps } @@ -167,7 +167,7 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) feeModel, isSnapshotApiQueriesRejected, // This is for feeExplainer which is not supporting anything else than FOX discount for now - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), thorHeld: bn(0), }).feeBpsFloat.toNumber() @@ -281,7 +281,7 @@ export const FeeOutput: React.FC = ({ tradeSizeUSD, foxHolding, foxHeld: bn(foxHolding), feeModel, isSnapshotApiQueriesRejected, - foxWifHatHeld: bn(foxWifHatHeld), + foxWifHatHeldCryptoBaseUnit: bn(foxWifHatHeld), // @TODO: remove this when thor swap discount is removed thorHeld: bn(0), }) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index e5f469c68f2..94c83705cb6 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -277,7 +277,7 @@ export const useGetTradeQuotes = () => { tradeAmountUsd, foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), - foxWifHatHeld: bnOrZero(foxWifHatHeld), + foxWifHatHeldCryptoBaseUnit: bnOrZero(foxWifHatHeld), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index acaa5aa6ab0..345a8c52373 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -232,7 +232,7 @@ export const useGetTradeRates = () => { tradeAmountUsd, foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), - foxWifHatHeld: bnOrZero(foxWifHatHeld), + foxWifHatHeldCryptoBaseUnit: bnOrZero(foxWifHatHeld), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) diff --git a/src/lib/fees/constant.ts b/src/lib/fees/constant.ts index 5475ff96f1c..d58f1721664 100644 --- a/src/lib/fees/constant.ts +++ b/src/lib/fees/constant.ts @@ -1,5 +1,10 @@ +import { toBaseUnit } from "@shapeshiftoss/utils" +import { foxWifHatAsset } from "../../../scripts/generateAssetData/base" + // This timestamp is the 24th of January 2025 export const FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS = 1737707777000 // Supposed to end the 5th of May 2025 export const FOX_WIF_HAT_CAMPAIGN_ENDING_TIME_MS = 1746396000000 -export const FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT = 10000000000000000000000 + +// Assuming foxwifhat is 18 decimals +export const FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT = toBaseUnit(10000, 18) diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index 78bb5349de0..28994dd4ca9 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -37,7 +37,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -52,7 +52,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -67,7 +67,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -82,7 +82,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -97,7 +97,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -112,7 +112,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -128,7 +128,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) @@ -146,7 +146,7 @@ describe('calculateFees', () => { tradeAmountUsd, foxHeld, thorHeld: bn(0), - foxWifHatHeld: bn(0), + foxWifHatHeldCryptoBaseUnit: bn(0), feeModel: 'SWAPPER', isSnapshotApiQueriesRejected, }) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 20007fbad31..5d39d24aa1f 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -17,7 +17,7 @@ type CalculateFeeBpsArgs = { tradeAmountUsd: BigNumber foxHeld: BigNumber thorHeld: BigNumber - foxWifHatHeld: BigNumber + foxWifHatHeldCryptoBaseUnit: BigNumber feeModel: ParameterModel isSnapshotApiQueriesRejected: boolean } @@ -49,7 +49,7 @@ export const calculateFees: CalculateFeeBps = ({ foxHeld, feeModel, thorHeld, - foxWifHatHeld, + foxWifHatHeldCryptoBaseUnit, isSnapshotApiQueriesRejected, }) => { const { @@ -75,8 +75,8 @@ export const calculateFees: CalculateFeeBps = ({ const isFoxWifHatDiscountEligible = isFoxWifHatCampaignActive && - foxWifHatHeld && - foxWifHatHeld?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) + foxWifHatHeldCryptoBaseUnit && + foxWifHatHeldCryptoBaseUnit?.gte(FOX_WIF_HAT_MINIMUM_AMOUNT_BASE_UNIT) const currentFoxWifHatDiscountPercent = (() => { if (!isFoxWifHatCampaignActive) return bn(0) diff --git a/src/pages/Fox/components/FoxWifHatClaimRow.tsx b/src/pages/Fox/components/FoxWifHatClaimRow.tsx index 6329f02b44b..dd0338029f0 100644 --- a/src/pages/Fox/components/FoxWifHatClaimRow.tsx +++ b/src/pages/Fox/components/FoxWifHatClaimRow.tsx @@ -74,7 +74,7 @@ export const FoxWifHatClaimRow = ({ foxHeld: bn(0), feeModel: 'SWAPPER', thorHeld: bn(0), - foxWifHatHeld: bn(amountCryptoBaseUnit), + foxWifHatHeldCryptoBaseUnit: bn(amountCryptoBaseUnit), isSnapshotApiQueriesRejected: false, }).foxDiscountPercent.toFixed(2) }, [amountCryptoBaseUnit]) diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 9d47e7e87e3..5a91b761cf1 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -1024,7 +1024,7 @@ export const AddLiquidityInput: React.FC = ({ const { feeBps, feeUsd } = calculateFees({ tradeAmountUsd: bn(totalAmountUsd), foxHeld: bnOrZero(votingPower), - foxWifHatHeld: bn(foxWifHatHeld), + foxWifHatHeldCryptoBaseUnit: bn(foxWifHatHeld), // @TODO: remove this when thor swap discount is removed thorHeld: bn(0), feeModel: 'THORCHAIN_LP', diff --git a/src/state/apis/snapshot/selectors.ts b/src/state/apis/snapshot/selectors.ts index af81bb250af..26ae239097d 100644 --- a/src/state/apis/snapshot/selectors.ts +++ b/src/state/apis/snapshot/selectors.ts @@ -80,7 +80,7 @@ export const selectCalculatedFees: Selector = tradeAmountUsd: bnOrZero(inputAmountUsd), foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), - foxWifHatHeld: bnOrZero(foxWifHatHeld), + foxWifHatHeldCryptoBaseUnit: bnOrZero(foxWifHatHeld), feeModel, isSnapshotApiQueriesRejected, }) From f940e066ff8f85651326650f2ff7282c3e3568cd Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:44:20 +0100 Subject: [PATCH 13/15] fix: review feedbacks --- src/lib/fees/constant.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/fees/constant.ts b/src/lib/fees/constant.ts index d58f1721664..25f48eef117 100644 --- a/src/lib/fees/constant.ts +++ b/src/lib/fees/constant.ts @@ -1,5 +1,4 @@ import { toBaseUnit } from "@shapeshiftoss/utils" -import { foxWifHatAsset } from "../../../scripts/generateAssetData/base" // This timestamp is the 24th of January 2025 export const FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS = 1737707777000 From 912d8a212d2e1004fcc76c0f6a9b9a0fa7f81f67 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:15:41 +0100 Subject: [PATCH 14/15] fix: lin, --- src/lib/fees/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/fees/constant.ts b/src/lib/fees/constant.ts index 25f48eef117..19146686a92 100644 --- a/src/lib/fees/constant.ts +++ b/src/lib/fees/constant.ts @@ -1,4 +1,4 @@ -import { toBaseUnit } from "@shapeshiftoss/utils" +import { toBaseUnit } from '@shapeshiftoss/utils' // This timestamp is the 24th of January 2025 export const FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS = 1737707777000 From 17c06a9b9634979db8534eb55763b21e6acb164f Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:29:33 +0100 Subject: [PATCH 15/15] fix: change import --- src/lib/fees/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/fees/constant.ts b/src/lib/fees/constant.ts index 19146686a92..dc21ecc2443 100644 --- a/src/lib/fees/constant.ts +++ b/src/lib/fees/constant.ts @@ -1,4 +1,4 @@ -import { toBaseUnit } from '@shapeshiftoss/utils' +import { toBaseUnit } from 'lib/math' // This timestamp is the 24th of January 2025 export const FOX_WIF_HAT_CAMPAIGN_STARTING_TIME_MS = 1737707777000