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(partial-approvals): partial approve v2 #5269

Merged
merged 18 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ import { ExplorerDataType, getExplorerLink, getRandomInt, isSellOrder, shortenAd
import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenLogo } from '@cowprotocol/tokens'
import { Command } from '@cowprotocol/types'
import { Confetti, ExternalLink, InfoTooltip, ProductLogo, ProductVariant, TokenAmount, UI } from '@cowprotocol/ui'
import {
Confetti,
ExternalLink,
InfoTooltip,
ProductLogo,
ProductVariant,
TokenAmount,
UI,
UnderlinedLinkStyledButton,
} from '@cowprotocol/ui'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'

import { AnimatePresence, motion } from 'framer-motion'
Expand Down Expand Up @@ -1123,14 +1132,14 @@ function ExpiredStep(props: OrderProgressBarV2Props) {
<h3>The good news</h3>
<p>
Unlike on other exchanges, you won't be charged for this! Feel free to{' '}
<styledEl.Button
<UnderlinedLinkStyledButton
onClick={() => {
props.navigateToNewOrder?.()
trackNewOrderClick()
}}
>
place a new order
</styledEl.Button>{' '}
</UnderlinedLinkStyledButton>{' '}
without worry.
</p>
</styledEl.InfoCard>
Expand Down
12 changes: 2 additions & 10 deletions apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import IMAGE_STAR_SHINE from '@cowprotocol/assets/cow-swap/star-shine.svg'
import { SingleLetterLogoWrapper } from '@cowprotocol/tokens'
import { ButtonPrimary, Font, LinkStyledButton, Media, UI } from '@cowprotocol/ui'
import { ButtonPrimary, Font, Media, UI } from '@cowprotocol/ui'

import styled, { css, keyframes } from 'styled-components/macro'

Expand Down Expand Up @@ -66,6 +66,7 @@ export const StepsContainer = styled.div<{ $height: number; $minHeight?: string;
padding: 0;

// implement a gradient to hide the bottom of the steps container using white to opacity white using pseudo element

&::after {
content: ${({ bottomGradient }) => (bottomGradient ? '""' : 'none')};
position: absolute;
Expand Down Expand Up @@ -143,15 +144,6 @@ export const CancelButton = styled(CancelButtonOriginal)`
}
`

export const Button = styled(LinkStyledButton)`
font-size: 14px;
text-decoration: underline;

&:hover {
text-decoration: none;
}
`

export const ProgressImageWrapper = styled.div<{ bgColor?: string; padding?: string; height?: string; gap?: string }>`
width: 100%;
height: ${({ height }) => height || '246px'};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
import { getQuoteTimeOffset } from 'modules/tradeQuote'
import { useTradeSlippage } from 'modules/tradeSlippage'
import { SettingsTab, TradeRateDetails, useHighFeeWarning } from 'modules/tradeWidgetAddons'
import { useOpenSettingsTab } from 'modules/tradeWidgetAddons/state/settingsTabState'
import { useTradeUsdAmounts } from 'modules/usdAmount'

import { Routes } from 'common/constants/routes'
Expand Down Expand Up @@ -92,6 +93,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) {
const deadlineState = useUserTransactionTTL()
const partialApproveState = usePartialApprove()
const isHookTradeType = useIsHooksTradeType()
const openSettings = useOpenSettingsTab()

const isTradePriceUpdating = useTradePricesUpdate()

Expand Down Expand Up @@ -207,6 +209,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) {
const showTwapSuggestionBanner = !enabledTradeTypes || enabledTradeTypes.includes(TradeType.ADVANCED)
const isNativeSellInHooksStore = swapButtonContext.swapButtonState === SwapButtonState.SellNativeInHooks

const isApprovalNeeded = !isHookTradeType && swapButtonContext.needsApproval

const swapWarningsTopProps: SwapWarningsTopProps = useMemo(
() => ({
chainId,
Expand All @@ -216,6 +220,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) {
priceImpact: priceImpactParams.priceImpact,
tradeUrlParams,
isNativeSellInHooksStore,
isApprovalNeeded,
openSettings,
}),
[
chainId,
Expand All @@ -225,6 +231,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) {
priceImpactParams.priceImpact,
tradeUrlParams,
isNativeSellInHooksStore,
isApprovalNeeded,
openSettings,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useIsWrappedOut } from 'modules/trade/hooks/useIsWrappedInOrOut'
import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken'
import { QuoteDeadlineParams } from 'modules/tradeQuote'

import { useApproveState } from 'common/hooks/useApproveState'
import { ApprovalState, useApproveState } from 'common/hooks/useApproveState'
import { useSafeMemo } from 'common/hooks/useSafeMemo'

import { useHandleSwapOrEthFlow } from './useHandleSwapOrEthFlow'
Expand Down Expand Up @@ -129,6 +129,10 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge
isHooksStore,
})

const needsApproval =
(approvalState === ApprovalState.NOT_APPROVED || approvalState === ApprovalState.PENDING) &&
swapInputError === undefined

return useSafeMemo(
() => ({
swapButtonState,
Expand All @@ -146,6 +150,7 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge
widgetStandaloneMode: standaloneMode,
quoteDeadlineParams,
isPartialApprove,
needsApproval,
}),
[
swapButtonState,
Expand All @@ -163,6 +168,7 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge
standaloneMode,
quoteDeadlineParams,
isPartialApprove,
needsApproval,
],
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const swapButtonsContext: SwapButtonsContext = {
toggleWalletModal: () => void 0,
hasEnoughWrappedBalanceForSwap: true,
isPartialApprove: false,
needsApproval: false,
quoteDeadlineParams: {
validFor: 0,
quoteValidTo: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface SwapButtonsContext {
widgetStandaloneMode?: boolean
quoteDeadlineParams: QuoteDeadlineParams
isPartialApprove: boolean
needsApproval: boolean
}

const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext) => JSX.Element } = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg'
import { Command } from '@cowprotocol/types'
import { BannerOrientation, ClosableBanner, InlineBanner, UnderlinedLinkStyledButton } from '@cowprotocol/ui'

import styled from 'styled-components/macro'
import * as timeago from 'timeago.js'

const BANNER_STORAGE_KEY = 'partialPermitBannerKey:v0'

const YEARS_SINCE_DEPLOYMENT = timeago.format(new Date('2021-06-08')).replace(/ ago/, '') // mainnet contract deployment date https://etherscan.io/tx/0xf49f90aa5a268c40001d1227b76bb4dd8247f18361fcad9fffd4a7a44f1320d3

type PartialApprovalBannerProps = {
isApprovalNeeded?: boolean
openSettings: Command
}

export function PartialApprovalBanner({ isApprovalNeeded, openSettings }: PartialApprovalBannerProps) {
if (!isApprovalNeeded) {
return null
}

return ClosableBanner(BANNER_STORAGE_KEY, (onClose) => (
<InlineBanner
bannerType="success"
orientation={BannerOrientation.Horizontal}
customIcon={ICON_TOKENS}
iconSize={32}
onClose={onClose}
>
<p>
<b>NEW: </b>You can now choose to do only minimal approvals in the <Link onClick={openSettings}>settings</Link>.
When enabled, every order placed that needs approval will request only the minimum necessary to trade. When
disabled, you can enjoy the same trusted experience CoW Swap has provided for the past {YEARS_SINCE_DEPLOYMENT}.
</p>
</InlineBanner>
))
}

const Link = styled(UnderlinedLinkStyledButton)`
padding: 0;
`
7 changes: 7 additions & 0 deletions apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'

import { genericPropsChecker } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { Command } from '@cowprotocol/types'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'

import TradeGp from 'legacy/state/swap/TradeGp'
Expand All @@ -11,6 +12,7 @@ import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssu
import { TradeUrlParams } from 'modules/trade/types/TradeRawState'
import { BundleTxWrapBanner, HighFeeWarning } from 'modules/tradeWidgetAddons'

import { PartialApprovalBanner } from './banners/PartialApprovalBanner'
import { TwapSuggestionBanner } from './banners/TwapSuggestionBanner'

export interface SwapWarningsTopProps {
Expand All @@ -21,6 +23,8 @@ export interface SwapWarningsTopProps {
priceImpact: Percent | undefined
tradeUrlParams: TradeUrlParams
isNativeSellInHooksStore: boolean
isApprovalNeeded: boolean
openSettings: Command
}

export interface SwapWarningsBottomProps {
Expand All @@ -39,6 +43,8 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps)
priceImpact,
tradeUrlParams,
isNativeSellInHooksStore,
isApprovalNeeded,
openSettings
} = props

return (
Expand All @@ -49,6 +55,7 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps)
<>
<HighFeeWarning />
<BundleTxWrapBanner />
<PartialApprovalBanner isApprovalNeeded={isApprovalNeeded} openSettings={openSettings} />

{showTwapSuggestionBanner && (
<TwapSuggestionBanner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,19 @@ export function SettingsTab({
<RowBetween>
<RowFixed>
<ThemedText.Black fontWeight={400} fontSize={14}>
<Trans>Partial Approve</Trans>
<Trans>Minimal Approvals</Trans>
</ThemedText.Black>
<HelpTooltip
text={
<Trans>
Allows you to approve a token for a specific amount, rather than the maximum amount.
By default, token approvals & permits are for an unlimited amount, which ensures you don't pay extra for subsequent trades.
<br />
<br />
When this setting is enabled, approvals & permits will be for the minimum amount instead of unlimited.
This incurs additional costs at every trade.
<br />
<br />
Existing approvals must be revoked manually before you can re-approve.
</Trans>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useSetAtom } from 'jotai'

import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { Command } from '@cowprotocol/types'
Expand All @@ -10,7 +9,7 @@ import styled from 'styled-components/macro'

import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips'

import { settingsTabStateAtom } from '../../../state/settingsTabState'
import { useOpenSettingsTab } from '../../../state/settingsTabState'
import { RowStyleProps, StyledInfoIcon, StyledRowBetween, TextWrapper, TransactionText } from '../styled'

const DefaultSlippage = styled.span`
Expand Down Expand Up @@ -65,9 +64,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) {
isSmartSlippageLoading,
} = props

const setSettingTabState = useSetAtom(settingsTabStateAtom)

const openSettings = () => setSettingTabState({ open: true })
const openSettings = useOpenSettingsTab()

const tooltipContent =
slippageTooltip ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { atom } from 'jotai'
import { atom, useSetAtom } from 'jotai'

export const settingsTabStateAtom = atom({ open: false })

export function useOpenSettingsTab() {
const setSettingTabState = useSetAtom(settingsTabStateAtom)

return () => setSettingTabState({ open: true })
}
2 changes: 1 addition & 1 deletion libs/ui/src/containers/InlineBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'
import { ReactNode } from 'react'

import { X } from 'react-feather'
import SVG from 'react-inlinesvg'
Expand Down
9 changes: 9 additions & 0 deletions libs/ui/src/pure/LinkStyledButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ export const LinkStyledButton = styled.button<{ disabled?: boolean; bg?: boolean
text-decoration: none;
}
`

export const UnderlinedLinkStyledButton = styled(LinkStyledButton)`
font-size: 14px;
text-decoration: underline;

&:hover {
text-decoration: none;
}
`
Loading