Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat swaps fees [SW-34] #3880

Merged
merged 15 commits into from
Jul 3, 2024
1 change: 1 addition & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const HelpCenterArticle = {
UNEXPECTED_DELEGATE_CALL: `${HELP_CENTER_URL}/en/articles/40794-why-do-i-see-an-unexpected-delegate-call-warning-in-my-transaction`,
DELEGATES: `${HELP_CENTER_URL}/en/articles/40799-what-is-a-delegate-key`,
PUSH_NOTIFICATIONS: `${HELP_CENTER_URL}/en/articles/99197-how-to-start-receiving-web-push-notifications-in-the-web-wallet`,
SWAP_WIDGET_FEES: `${HELP_CENTER_URL}/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps`,
} as const
export const HelperCenterArticleTitles = {
RECOVERY: 'Learn more about the Account recovery process',
Expand Down
25 changes: 25 additions & 0 deletions src/features/swap/components/HelpIconTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SvgIcon, Tooltip } from '@mui/material'
import InfoIcon from '@/public/images/notifications/info.svg'
import type { ReactNode } from 'react'

type Props = {
title: ReactNode
}
export const HelpIconTooltip = ({ title }: Props) => {
return (
<Tooltip title={title} arrow placement="top">
<span>
<SvgIcon
component={InfoIcon}
inheritViewBox
color="border"
fontSize="small"
sx={{
verticalAlign: 'middle',
ml: 0.5,
}}
/>
</span>
</Tooltip>
)
}
35 changes: 35 additions & 0 deletions src/features/swap/components/LegalDisclaimer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import ExternalLink from '@/components/common/ExternalLink'
import { AppRoutes } from '@/config/routes'
import { Typography } from '@mui/material'

import css from './styles.module.css'

const LegalDisclaimerContent = () => (
<div className={css.disclaimerContainer}>
<div className={css.disclaimerInner}>
<Typography mb={4} mt={4}>
You are now accessing a third party widget!
</Typography>

<Typography mb={4}>
Please note that we do not own, control, maintain or audit the CoW Swap Widget. Use of the widget is subject to
third party terms & conditions. We are not liable for any loss you may suffer in connection with interacting
with the widget, which is at your own risk.
</Typography>

<Typography mb={4}>
Our{' '}
<ExternalLink href={AppRoutes.terms} sx={{ textDecoration: 'none' }}>
terms
</ExternalLink>{' '}
contain more detailed provisions binding on you relating to such third party content.
</Typography>
<Typography>
By clicking &quot;continue&quot; you re-confirm to have read and understood our terms and this message, and
agree to them.
</Typography>
</div>
</div>
)

export default LegalDisclaimerContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.disclaimerContainer p,
.disclaimerContainer h3 {
line-height: 24px;
}

.disclaimerInner p {
text-align: justify;
}
5 changes: 4 additions & 1 deletion src/features/swap/components/SwapOrder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import Stack from '@mui/material/Stack'
import type { ReactElement } from 'react'
import type { TwapOrder as SwapTwapOrder } from '@safe-global/safe-gateway-typescript-sdk'
import {
type SwapOrder as SwapOrderType,
type Order,
type SwapOrder as SwapOrderType,
type TransactionData,
} from '@safe-global/safe-gateway-typescript-sdk'
import { DataRow } from '@/components/common/Table/DataRow'
Expand All @@ -34,6 +34,7 @@ import { EmptyRow } from '@/components/common/Table/EmptyRow'
import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration'
import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount'
import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount'
import { SurplusFee } from '@/features/swap/components/SwapOrder/rows/SurplusFee'

type SwapOrderProps = {
txData?: TransactionData
Expand Down Expand Up @@ -200,6 +201,7 @@ export const SellOrder = ({ order }: { order: SwapOrderType }) => {
<OrderUidRow order={order} key="order-uid-row" />,
<StatusRow order={order} key="status-row" />,
<RecipientRow order={order} key="recipient-row" />,
<SurplusFee order={order} key="fee-row" />,
]}
/>
)
Expand All @@ -222,6 +224,7 @@ export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => {
<PriceRow order={order} key="price-row" />,
<SurplusRow order={order} key="surplus-row" />,
<RecipientRow order={order} key="recipient-row" />,
<SurplusFee order={order} key="fee-row" />,
<EmptyRow key="spacer-0" />,
<DataRow title="No of parts" key="n_of_parts">
{numberOfParts}
Expand Down
44 changes: 44 additions & 0 deletions src/features/swap/components/SwapOrder/rows/SurplusFee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Order } from '@safe-global/safe-gateway-typescript-sdk'
import { getOrderFeeBps } from '@/features/swap/helpers/utils'
import { DataRow } from '@/components/common/Table/DataRow'
import { formatVisualAmount } from '@/utils/formatters'
import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip'

export const SurplusFee = ({
order,
}: {
order: Pick<Order, 'fullAppData' | 'sellToken' | 'buyToken' | 'status' | 'executedSurplusFee' | 'kind'>
}) => {
const bps = getOrderFeeBps(order)
const { executedSurplusFee, status, sellToken, buyToken, kind } = order
let token = sellToken

if (kind === 'buy') {
token = buyToken
}

if (executedSurplusFee === null || typeof executedSurplusFee === 'undefined' || executedSurplusFee === '0') {
return null
}

return (
<DataRow
title={
<>
Total fees
<HelpIconTooltip
title={
<>
The amount of fees paid for this order.
{bps > 0 && `This includes a Widget fee of ${bps / 100} % and network fees.`}
</>
}
/>
</>
}
key="widget_fee"
>
{formatVisualAmount(BigInt(executedSurplusFee), token.decimals)} {token.symbol}
</DataRow>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk'
import { getOrderFeeBps } from '@/features/swap/helpers/utils'
import { DataRow } from '@/components/common/Table/DataRow'
import { HelpCenterArticle } from '@/config/constants'
import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip'
import MUILink from '@mui/material/Link'

export const OrderFeeConfirmationView = ({
order,
}: {
order: Pick<OrderConfirmationView, 'fullAppData'>
hideWhenNonFulfilled?: boolean
}) => {
const bps = getOrderFeeBps(order)

const title = (
<>
Widget fee{' '}
<HelpIconTooltip
title={
<>
The tiered widget fees incurred here will contribute to a license fee that supports the Safe community.
Neither Safe Ecosystem Foundation nor {`Safe{Wallet}`}
operate the CoW Swap Widget and/or CoW Swap.{` `}
<MUILink href={HelpCenterArticle.SWAP_WIDGET_FEES} target="_blank" rel="noopener noreferrer">
Learn more
</MUILink>
</>
}
/>
</>
)

return (
<DataRow title={title} key="widget_fee">
{Number(bps) / 100} %
</DataRow>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import NamedAddress from '@/components/common/NamedAddressInfo'
import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration'
import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount'
import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount'
import { OrderFeeConfirmationView } from '@/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView'

type SwapOrderProps = {
order: OrderConfirmationView
Expand Down Expand Up @@ -92,6 +93,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd
) : (
<></>
),
<OrderFeeConfirmationView key="SurplusFee" order={order} hideWhenNonFulfilled={false} />,
<DataRow key="Interact with" title="Interact with">
<NamedAddress address={settlementContract} onlyName hasExplorer shortAddress={false} avatarSize={24} />
</DataRow>,
Expand Down
2 changes: 2 additions & 0 deletions src/features/swap/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const SWAP_TITLE = 'Safe Swap'
export const SWAP_ORDER_TITLE = 'Swap order'
export const LIMIT_ORDER_TITLE = 'Limit order'
export const TWAP_ORDER_TITLE = 'TWAP order'

export const SWAP_FEE_RECIPIENT = '0x63695Eee2c3141BDE314C5a6f89B98E62808d716'
131 changes: 131 additions & 0 deletions src/features/swap/helpers/__tests__/fee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee'
import { type OnTradeParamsPayload } from '@cowprotocol/events'
import { stableCoinAddresses } from '@/features/swap/helpers/data/stablecoins'

describe('calculateFeePercentageInBps', () => {
it('returns correct fee for non-stablecoin and sell order', () => {
let orderParams: OnTradeParamsPayload = {
sellToken: { address: 'non-stablecoin-address' },
buyToken: { address: 'non-stablecoin-address' },
buyTokenFiatAmount: '50000',
sellTokenFiatAmount: '50000',
orderKind: 'sell',
} as OnTradeParamsPayload

const result = calculateFeePercentageInBps(orderParams)
expect(result).toBe(35)

orderParams = {
...orderParams,
buyTokenFiatAmount: '100000',
sellTokenFiatAmount: '100000',
}

const result2 = calculateFeePercentageInBps(orderParams)
expect(result2).toBe(20)

orderParams = {
...orderParams,
buyTokenFiatAmount: '1000000',
sellTokenFiatAmount: '1000000',
}

const result3 = calculateFeePercentageInBps(orderParams)
expect(result3).toBe(10)
})

it('returns correct fee for non-stablecoin and buy order', () => {
let orderParams: OnTradeParamsPayload = {
sellToken: { address: 'non-stablecoin-address' },
buyToken: { address: 'non-stablecoin-address' },
buyTokenFiatAmount: '50000',
sellTokenFiatAmount: '50000',
orderKind: 'buy',
} as OnTradeParamsPayload

const result = calculateFeePercentageInBps(orderParams)
expect(result).toBe(35)

orderParams = {
...orderParams,
buyTokenFiatAmount: '100000',
sellTokenFiatAmount: '100000',
}

const result2 = calculateFeePercentageInBps(orderParams)
expect(result2).toBe(20)

orderParams = {
...orderParams,
buyTokenFiatAmount: '1000000',
sellTokenFiatAmount: '1000000',
}

const result3 = calculateFeePercentageInBps(orderParams)
expect(result3).toBe(10)
})

it('returns correct fee for stablecoin and sell order', () => {
const stableCoinAddressesKeys = Object.keys(stableCoinAddresses)
let orderParams: OnTradeParamsPayload = {
sellToken: { address: stableCoinAddressesKeys[0] },
buyToken: { address: stableCoinAddressesKeys[1] },
buyTokenFiatAmount: '50000',
sellTokenFiatAmount: '50000',
orderKind: 'sell',
} as OnTradeParamsPayload

const result = calculateFeePercentageInBps(orderParams)
expect(result).toBe(10)

orderParams = {
...orderParams,
buyTokenFiatAmount: '100000',
sellTokenFiatAmount: '100000',
}

const result2 = calculateFeePercentageInBps(orderParams)
expect(result2).toBe(7)

orderParams = {
...orderParams,
buyTokenFiatAmount: '1000000',
sellTokenFiatAmount: '1000000',
}

const result3 = calculateFeePercentageInBps(orderParams)
expect(result3).toBe(5)
})

it('returns correct fee for stablecoin and buy order', () => {
const stableCoinAddressesKeys = Object.keys(stableCoinAddresses)
let orderParams: OnTradeParamsPayload = {
sellToken: { address: stableCoinAddressesKeys[0] },
buyToken: { address: stableCoinAddressesKeys[1] },
buyTokenFiatAmount: '50000',
sellTokenFiatAmount: '50000',
orderKind: 'buy',
} as OnTradeParamsPayload

const result = calculateFeePercentageInBps(orderParams)
expect(result).toBe(10)

orderParams = {
...orderParams,
buyTokenFiatAmount: '100000',
sellTokenFiatAmount: '100000',
}

const result2 = calculateFeePercentageInBps(orderParams)
expect(result2).toBe(7)

orderParams = {
...orderParams,
buyTokenFiatAmount: '1000000',
sellTokenFiatAmount: '1000000',
}

const result3 = calculateFeePercentageInBps(orderParams)
expect(result3).toBe(5)
})
})
Loading
Loading