Skip to content

Commit

Permalink
feat(earn): show cross chain fees on enter amount screen (#6395)
Browse files Browse the repository at this point in the history
### Description

Similar to swap screen

### Test plan

<img
src="https://github.com/user-attachments/assets/35d62391-639b-42af-8794-e43836aa1ebc"
width="250" />


### Related issues

- Part of ACT-1507

### Backwards compatibility

Yes

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
satish-ravi authored Jan 7, 2025
1 parent 6c5af6f commit 93a1b8d
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 48 deletions.
10 changes: 7 additions & 3 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2601,9 +2601,13 @@
"moreInformation": "More information",
"estNetworkFee": "Est. Network Fee",
"maxNetworkFee": "Max Network Fee",
"networkFeeDescription": "The network fee is required by the network to process the deposit transaction.",
"networkFeeDescriptionWithdrawal": "The network fee is required by the network to process the withdrawal transaction.",
"networkSwapFeeDescription": "The network fee is required by the network to process the deposit transactions. The {{appName}} fee of {{appFeePercentage}}% is charged for your use of our product.",
"estCrossChainFee": "Est. Cross-chain Fee",
"maxCrossChainFee": "Max Cross-chain Fee",
"description_deposit": "The network fee is required by the network to process the deposit transaction.",
"description_withdraw": "The network fee is required by the network to process the withdrawal transaction.",
"description_depositSwapFee": "The network fee is required by the network to process the deposit transactions. The {{appName}} fee of {{appFeePercentage}}% is charged for your use of our product.",
"description_depositCrossChain": "The network fee is required by the network to process the deposit transaction. The cross-chain fee is charged by the cross-chain provider.",
"description_depositCrossChainWithSwapFee": "The network fee is required by the network to process the deposit transactions. The {{appName}} fee of {{appFeePercentage}}% is charged for your use of our product. The cross-chain fee is charged by the cross-chain provider.",
"appSwapFee": "{{appName}} Fee"
},
"swapBottomSheet": {
Expand Down
77 changes: 67 additions & 10 deletions src/earn/EarnEnterAmount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ const mockCrossChainSwapTransaction: SwapTransaction = {
...mockSwapTransaction,
swapType: 'cross-chain',
estimatedDuration: 300,
maxCrossChainFee: '0.1',
estimatedCrossChainFee: '0.05',
maxCrossChainFee: '1000000000000000',
estimatedCrossChainFee: '500000000000000',
sellTokenAddress: mockCeloAddress,
price: '4',
guaranteedPrice: '4',
Expand Down Expand Up @@ -876,6 +876,8 @@ describe('EarnEnterAmount', () => {
const replaceSeparators = (value: string) =>
value.replace(/\./g, '|').replace(/,/g, group).replace(/\|/g, decimal)

const defaultFormat = BigNumber.config().FORMAT

beforeEach(() => {
jest
.mocked(getNumberFormatSettings)
Expand All @@ -889,6 +891,10 @@ describe('EarnEnterAmount', () => {
})
})

afterEach(() => {
BigNumber.config({ FORMAT: defaultFormat })
})

const mockStore = createMockStore({
tokens: {
tokenBalances: {
Expand Down Expand Up @@ -971,7 +977,7 @@ describe('EarnEnterAmount', () => {
isPreparingTransactions: false,
})

const { getByTestId, getByText } = render(
const { getByTestId, getByText, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator component={EarnEnterAmount} params={params} />
</Provider>
Expand All @@ -980,9 +986,16 @@ describe('EarnEnterAmount', () => {
fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1')
fireEvent.press(getByTestId('LabelWithInfo/FeeLabel'))
expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible()
expect(getByTestId('EstNetworkFee/Value')).toBeTruthy()
expect(getByTestId('MaxNetworkFee/Value')).toBeTruthy()
expect(getByText('earnFlow.enterAmount.feeBottomSheet.networkFeeDescription')).toBeVisible()
expect(getByTestId('EstNetworkFee')).toBeTruthy()
expect(getByTestId('MaxNetworkFee')).toBeTruthy()
expect(queryByTestId('SwapFee')).toBeFalsy()
expect(queryByTestId('EstCrossChainFee')).toBeFalsy()
expect(queryByTestId('MaxCrossChainFee')).toBeFalsy()
expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(
getByText('earnFlow.enterAmount.feeBottomSheet.description, {"context":"deposit"}')
).toBeVisible()
})

it('should show swap fees on the FeeDetailsBottomSheet when swap transaction is present', async () => {
Expand All @@ -997,6 +1010,43 @@ describe('EarnEnterAmount', () => {
isPreparingTransactions: false,
})

const { getByTestId, getByText, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator component={EarnEnterAmount} params={params} />
</Provider>
)

fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1')
fireEvent.press(getByTestId('LabelWithInfo/FeeLabel'))
expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible()
expect(getByTestId('EstNetworkFee')).toBeTruthy()
expect(getByTestId('MaxNetworkFee')).toBeTruthy()
expect(getByTestId('SwapFee')).toBeTruthy()
expect(queryByTestId('EstCrossChainFee')).toBeFalsy()
expect(queryByTestId('MaxCrossChainFee')).toBeFalsy()
expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(getByTestId('SwapFee/Value')).toHaveTextContent('₱0.008 (0.006 USDC)')
expect(
getByText(
'earnFlow.enterAmount.feeBottomSheet.description, {"context":"depositSwapFee","appFeePercentage":"0.6"}'
)
).toBeVisible()
expect(getByTestId('FeeDetailsBottomSheet/GotIt')).toBeVisible()
})

it('should show swap and cross chain fees on the FeeDetailsBottomSheet when cross chain swap transaction is present', async () => {
jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({
prepareTransactionsResult: {
prepareTransactionsResult: mockPreparedTransaction,
swapTransaction: mockCrossChainSwapTransaction,
},
refreshPreparedTransactions: jest.fn(),
clearPreparedTransactions: jest.fn(),
prepareTransactionError: undefined,
isPreparingTransactions: false,
})

const { getByTestId, getByText } = render(
<Provider store={store}>
<MockedNavigator component={EarnEnterAmount} params={params} />
Expand All @@ -1006,12 +1056,19 @@ describe('EarnEnterAmount', () => {
fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1')
fireEvent.press(getByTestId('LabelWithInfo/FeeLabel'))
expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible()
expect(getByTestId('EstNetworkFee/Value')).toBeTruthy()
expect(getByTestId('MaxNetworkFee/Value')).toBeTruthy()
expect(getByTestId('SwapFee/Value')).toBeTruthy()
expect(getByTestId('EstNetworkFee')).toBeTruthy()
expect(getByTestId('MaxNetworkFee')).toBeTruthy()
expect(getByTestId('SwapFee')).toBeTruthy()
expect(getByTestId('EstCrossChainFee')).toBeTruthy()
expect(getByTestId('MaxCrossChainFee')).toBeTruthy()
expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)')
expect(getByTestId('SwapFee/Value')).toHaveTextContent('₱0.008 (0.006 USDC)')
expect(getByTestId('EstCrossChainFee/Value')).toHaveTextContent('₱1.00 (0.0005 ETH)')
expect(getByTestId('MaxCrossChainFee/Value')).toHaveTextContent('₱2.00 (0.001 ETH)')
expect(
getByText(
'earnFlow.enterAmount.feeBottomSheet.networkSwapFeeDescription, {"appFeePercentage":"0.6"}'
'earnFlow.enterAmount.feeBottomSheet.description, {"context":"depositCrossChainWithSwapFee","appFeePercentage":"0.6"}'
)
).toBeVisible()
expect(getByTestId('FeeDetailsBottomSheet/GotIt')).toBeVisible()
Expand Down
97 changes: 82 additions & 15 deletions src/earn/EarnEnterAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import { StatsigFeatureGates } from 'src/statsig/types'
import Colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { SwapTransaction } from 'src/swap/types'
import getCrossChainFee from 'src/swap/getCrossChainFee'
import { SwapFeeAmount, SwapTransaction } from 'src/swap/types'
import { useSwappableTokens, useTokenInfo } from 'src/tokens/hooks'
import { feeCurrenciesSelector } from 'src/tokens/selectors'
import { TokenBalance } from 'src/tokens/slice'
Expand Down Expand Up @@ -226,6 +227,21 @@ export default function EarnEnterAmount({ route }: Props) {
})
}

const crossChainFeeCurrency = useSelector((state) =>
feeCurrenciesSelector(state, inputToken.networkId)
).find((token) => token.isNative)
const crossChainFee =
swapTransaction?.swapType === 'cross-chain' && prepareTransactionsResult
? getCrossChainFee({
feeCurrency: crossChainFeeCurrency,
preparedTransactions: prepareTransactionsResult,
estimatedCrossChainFee: swapTransaction.estimatedCrossChainFee,
maxCrossChainFee: swapTransaction.maxCrossChainFee,
fromTokenId: inputToken.tokenId,
sellAmount: swapTransaction.sellAmount,
})
: undefined

// This is for withdrawals as we want the user to be able to input the amounts in the deposit token
const { transactionToken, transactionTokenAmount } = useMemo(() => {
const transactionToken = isWithdrawal ? withdrawToken : inputToken
Expand Down Expand Up @@ -500,6 +516,7 @@ export default function EarnEnterAmount({ route }: Props) {
token={inputToken}
tokenAmount={processedAmounts.token.bignum}
isWithdrawal={isWithdrawal}
crossChainFee={crossChainFee}
/>
)}
{swapTransaction && processedAmounts.token.bignum && (
Expand Down Expand Up @@ -745,7 +762,7 @@ function TransactionDepositDetails({
)
}

// Might be sharable with src/swap/FeeInfoBottomSheet.tsx
// TODO(ACT-1534) src/swap/FeeInfoBottomSheet.tsx
function FeeDetailsBottomSheet({
forwardedRef,
testID,
Expand All @@ -757,6 +774,7 @@ function FeeDetailsBottomSheet({
token,
tokenAmount,
isWithdrawal,
crossChainFee,
}: {
forwardedRef: React.RefObject<BottomSheetModalRefType>
testID: string
Expand All @@ -768,6 +786,7 @@ function FeeDetailsBottomSheet({
token: TokenBalance
tokenAmount: BigNumber
isWithdrawal: boolean
crossChainFee?: SwapFeeAmount
}) {
const { t } = useTranslation()
const inputToken = useTokenInfo(pool.dataProps.depositTokenId)
Expand Down Expand Up @@ -858,23 +877,71 @@ function FeeDetailsBottomSheet({
</Text>
</View>
)}
{crossChainFee && crossChainFee.token && (
<>
<RowDivider key="divider" />
<View style={styles.gap8}>
<View style={styles.bottomSheetLineItem} testID="EstCrossChainFee">
<Text style={styles.bottomSheetLineLabel}>
{t('earnFlow.enterAmount.feeBottomSheet.estCrossChainFee')}
</Text>
<Text style={styles.bottomSheetLineLabelText} testID="EstCrossChainFee/Value">
{'≈ '}
<TokenDisplay
tokenId={crossChainFee.token.tokenId}
amount={crossChainFee.amount.toString()}
/>
{' ('}
<TokenDisplay
tokenId={crossChainFee.token.tokenId}
showLocalAmount={false}
amount={crossChainFee.amount.toString()}
/>
{')'}
</Text>
</View>
{crossChainFee.maxAmount && (
<View style={styles.bottomSheetLineItem} testID="MaxCrossChainFee">
<Text style={styles.bottomSheetLineLabel}>
{t('earnFlow.enterAmount.feeBottomSheet.maxCrossChainFee')}
</Text>
<Text style={styles.bottomSheetLineLabelText} testID="MaxCrossChainFee/Value">
{'≈ '}
<TokenDisplay
tokenId={crossChainFee.token.tokenId}
amount={crossChainFee.maxAmount.toString()}
/>
{' ('}
<TokenDisplay
tokenId={crossChainFee.token.tokenId}
showLocalAmount={false}
amount={crossChainFee.maxAmount.toString()}
/>
{')'}
</Text>
</View>
)}
</View>
</>
)}
<View style={descriptionContainerStyle}>
<Text style={styles.bottomSheetDescriptionTitle}>
{t('earnFlow.enterAmount.feeBottomSheet.moreInformation')}
</Text>
{swapFeeAmount ? (
<Text style={styles.bottomSheetDescriptionText}>
{t('earnFlow.enterAmount.feeBottomSheet.networkSwapFeeDescription', {
appFeePercentage: swapTransaction?.appFeePercentageIncludedInPrice,
})}
</Text>
) : (
<Text style={styles.bottomSheetDescriptionText}>
{isWithdrawal
? t('earnFlow.enterAmount.feeBottomSheet.networkFeeDescriptionWithdrawal')
: t('earnFlow.enterAmount.feeBottomSheet.networkFeeDescription')}
</Text>
)}
<Text style={styles.bottomSheetDescriptionText}>
{t('earnFlow.enterAmount.feeBottomSheet.description', {
context: isWithdrawal
? 'withdraw'
: swapFeeAmount
? crossChainFee
? 'depositCrossChainWithSwapFee'
: 'depositSwapFee'
: crossChainFee
? 'depositCrossChain'
: 'deposit',
appFeePercentage: swapTransaction?.appFeePercentageIncludedInPrice,
})}
</Text>
</View>
</View>
<Button
Expand Down
12 changes: 11 additions & 1 deletion src/swap/SwapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,17 @@ export function SwapScreen({ route }: Props) {
const crossChainFeeCurrency = useSelector((state) =>
feeCurrenciesSelector(state, fromToken?.networkId || networkConfig.defaultNetworkId)
).find((token) => token.isNative)
const crossChainFee = getCrossChainFee(quote, crossChainFeeCurrency)
const crossChainFee =
quote?.swapType === 'cross-chain'
? getCrossChainFee({
feeCurrency: crossChainFeeCurrency,
preparedTransactions: quote.preparedTransactions,
fromTokenId: quote.fromTokenId,
sellAmount: quote.sellAmount,
estimatedCrossChainFee: quote.estimatedCrossChainFee,
maxCrossChainFee: quote.maxCrossChainFee,
})
: undefined

const getWarningStatuses = () => {
// NOTE: If a new condition is added here, make sure to update `allowSwap` below if
Expand Down
12 changes: 11 additions & 1 deletion src/swap/SwapScreenV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,17 @@ export default function SwapScreenV2({ route }: Props) {
const filterChipsFrom = useFilterChips(Field.FROM)
const filterChipsTo = useFilterChips(Field.TO, route.params?.toTokenNetworkId)
const parsedSlippagePercentage = new BigNumber(maxSlippagePercentage).toFormat()
const crossChainFee = getCrossChainFee(quote, crossChainFeeCurrency)
const crossChainFee =
quote?.swapType === 'cross-chain'
? getCrossChainFee({
feeCurrency: crossChainFeeCurrency,
preparedTransactions: quote.preparedTransactions,
fromTokenId: quote.fromTokenId,
sellAmount: quote.sellAmount,
estimatedCrossChainFee: quote.estimatedCrossChainFee,
maxCrossChainFee: quote.maxCrossChainFee,
})
: undefined
const swapStatus = startedSwapId === currentSwap?.id ? currentSwap?.status : null
const confirmSwapIsLoading = swapStatus === 'started'
const confirmSwapFailed = swapStatus === 'error'
Expand Down
Loading

0 comments on commit 93a1b8d

Please sign in to comment.