feat: implementing permit2 with pay with any token (#5926)
* feat: implementing permit2 with pay with any token * permit2 hook * usePayWithAnyTokenHook * removing ternary operators * weird export type bug * resolving merge * fixing nft test * styles * refactoring styles * reformatting * price impact warnings * forgot trans tag * responding to comments * fixes * disabling pay with any token when on the wrong chain * missing enabled flag * vertically centering button
This commit is contained in:
parent
6df2f3677e
commit
3eaeb65b07
@ -1,7 +1,24 @@
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function usePayWithAnyTokenFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.payWithAnyToken)
|
||||
}
|
||||
|
||||
export function usePayWithAnyTokenEnabled(): boolean {
|
||||
const flagEnabled = usePayWithAnyTokenFlag() === BaseVariant.Enabled
|
||||
const { chainId } = useWeb3React()
|
||||
try {
|
||||
// Detect if the Universal Router is not yet deployed to chainId.
|
||||
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
|
||||
// It will be removed once Universal Router is deployed on all supported chains.
|
||||
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
|
||||
return flagEnabled
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseVariant as PayWithAnyTokenVariant }
|
||||
|
@ -579,7 +579,6 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
nftAssets?: Maybe<NftAssetConnection>;
|
||||
nftBalances?: Maybe<NftBalanceConnection>;
|
||||
nftCollections?: Maybe<NftCollectionConnection>;
|
||||
@ -595,13 +594,6 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryAssetActivitiesArgs = {
|
||||
address: Scalars['String'];
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
pageSize?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftAssetsArgs = {
|
||||
address: Scalars['String'];
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
|
@ -1,23 +1,28 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { formatPriceImpact } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Loader'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useBestTrade } from 'hooks/useBestTrade'
|
||||
import { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
|
||||
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
|
||||
import { useTokenInput } from 'nft/hooks/useTokenInput'
|
||||
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
|
||||
import { BagStatus } from 'nft/types'
|
||||
@ -25,11 +30,16 @@ import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
|
||||
import { PropsWithChildren, useMemo, useState } from 'react'
|
||||
import { AlertTriangle, ChevronDown } from 'react-feather'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { warningSeverity } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
const LOW_SEVERITY_THRESHOLD = 1
|
||||
const MEDIUM_SEVERITY_THRESHOLD = 3
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
padding: 0px 12px;
|
||||
`
|
||||
@ -45,14 +55,12 @@ const Footer = styled.div`
|
||||
border-bottom-right-radius: 12px;
|
||||
`
|
||||
|
||||
const FooterHeader = styled(Column)<{ warningText?: boolean }>`
|
||||
const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>`
|
||||
padding-top: 8px;
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
padding-bottom: ${({ usingPayWithAnyToken }) => (usingPayWithAnyToken ? '16px' : '20px')};
|
||||
`
|
||||
|
||||
const CurrencyRow = styled(Row)<{ warningText?: boolean }>`
|
||||
padding-top: 4px;
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
const CurrencyRow = styled(Row)`
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
`
|
||||
@ -71,17 +79,26 @@ const WarningText = styled(ThemedText.BodyPrimary)`
|
||||
color: ${({ theme }) => theme.accentWarning};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 0 !important;
|
||||
margin-bottom: 10px !important;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const HelperText = styled(ThemedText.Caption)<{ $color: string }>`
|
||||
color: ${({ $color }) => $color};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 10px !important;
|
||||
`
|
||||
|
||||
const CurrencyInput = styled(Row)`
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const PayButton = styled(Row)<{ disabled?: boolean }>`
|
||||
background: ${({ theme }) => theme.accentAction};
|
||||
const PayButton = styled.button<{ $backgroundColor: string }>`
|
||||
display: flex;
|
||||
background: ${({ $backgroundColor }) => $backgroundColor};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
@ -91,18 +108,41 @@ const PayButton = styled(Row)<{ disabled?: boolean }>`
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 0px;
|
||||
opacity: ${({ disabled }) => (disabled ? 0.6 : 1)};
|
||||
cursor: ${({ disabled }) => (disabled ? 'auto' : 'pointer')};
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
const FiatLoadingBubble = styled(LoadingBubble)`
|
||||
border-radius: 4px;
|
||||
width: 4rem;
|
||||
height: 1rem;
|
||||
align-self: end;
|
||||
`
|
||||
const PriceImpactContainer = styled(Row)`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const PriceImpactRow = styled(Row)`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
interface ActionButtonProps {
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
backgroundColor: string
|
||||
}
|
||||
|
||||
const ActionButton = ({ disabled, children, onClick }: PropsWithChildren<ActionButtonProps>) => {
|
||||
const ActionButton = ({ disabled, children, onClick, backgroundColor }: PropsWithChildren<ActionButtonProps>) => {
|
||||
return (
|
||||
<PayButton disabled={disabled} onClick={onClick}>
|
||||
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor}>
|
||||
{children}
|
||||
</PayButton>
|
||||
)
|
||||
@ -120,6 +160,97 @@ const Warning = ({ children }: PropsWithChildren<unknown>) => {
|
||||
)
|
||||
}
|
||||
|
||||
interface HelperTextProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => {
|
||||
if (!children) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<HelperText lineHeight="16px" $color={color}>
|
||||
{children}
|
||||
</HelperText>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: ask design about no route found
|
||||
const InputCurrencyValue = ({
|
||||
usingPayWithAnyToken,
|
||||
totalEthPrice,
|
||||
activeCurrency,
|
||||
tradeState,
|
||||
trade,
|
||||
}: {
|
||||
usingPayWithAnyToken: boolean
|
||||
totalEthPrice: BigNumber
|
||||
activeCurrency: Currency | undefined | null
|
||||
tradeState: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
}) => {
|
||||
if (!usingPayWithAnyToken) {
|
||||
return (
|
||||
<ThemedText.BodyPrimary lineHeight="20px" fontWeight="500">
|
||||
{formatWeiToDecimal(totalEthPrice.toString())}
|
||||
{activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
if (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) {
|
||||
return (
|
||||
<ThemedText.BodyPrimary
|
||||
lineHeight="20px"
|
||||
fontWeight="500"
|
||||
color={tradeState === TradeState.VALID ? 'textPrimary' : 'textTertiary'}
|
||||
>
|
||||
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
|
||||
<Trans>Fetching price...</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
const FiatValue = ({
|
||||
usdcValue,
|
||||
priceImpact,
|
||||
priceImpactColor,
|
||||
}: {
|
||||
usdcValue: CurrencyAmount<Token> | null
|
||||
priceImpact: Percent | undefined
|
||||
priceImpactColor: string | undefined
|
||||
}) => {
|
||||
if (!usdcValue) {
|
||||
return <FiatLoadingBubble />
|
||||
}
|
||||
|
||||
return (
|
||||
<PriceImpactContainer>
|
||||
{priceImpact && priceImpactColor && (
|
||||
<>
|
||||
<MouseoverTooltip text={t`The estimated difference between the USD values of input and output amounts.`}>
|
||||
<PriceImpactRow>
|
||||
<AlertTriangle color={priceImpactColor} size="16px" />
|
||||
<ThemedText.BodySmall style={{ color: priceImpactColor }} lineHeight="20px">
|
||||
(<Trans>{formatPriceImpact(priceImpact)}</Trans>)
|
||||
</ThemedText.BodySmall>
|
||||
</PriceImpactRow>
|
||||
</MouseoverTooltip>
|
||||
</>
|
||||
)}
|
||||
<ThemedText.BodySmall color="textTertiary" lineHeight="20px">
|
||||
{`${ethNumberStandardFormatter(usdcValue?.toExact(), true)}`}
|
||||
</ThemedText.BodySmall>
|
||||
</PriceImpactContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface BagFooterProps {
|
||||
totalEthPrice: BigNumber
|
||||
bagStatus: BagStatus
|
||||
@ -139,7 +270,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenFlag() === PayWithAnyTokenVariant.Enabled
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
|
||||
const inputCurrency = useTokenInput((state) => state.inputCurrency)
|
||||
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
|
||||
const defaultCurrency = useCurrency('ETH')
|
||||
@ -155,11 +286,52 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
|
||||
return parseEther(balanceInEth).gte(totalEthPrice)
|
||||
}, [connected, chainId, balanceInEth, totalEthPrice])
|
||||
|
||||
const { buttonText, disabled, warningText, handleClick } = useMemo(() => {
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken
|
||||
|
||||
const parsedOutputAmount = useMemo(() => {
|
||||
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
|
||||
}, [defaultCurrency, totalEthPrice])
|
||||
const { state: tradeState, trade, maximumAmountIn } = usePayWithAnyTokenSwap(inputCurrency, parsedOutputAmount)
|
||||
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
|
||||
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
|
||||
maximumAmountIn,
|
||||
shouldUsePayWithAnyToken
|
||||
)
|
||||
|
||||
const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount)
|
||||
const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount)
|
||||
const usdcValue = usingPayWithAnyToken ? fiatValueTradeInput : fiatValueTradeOutput
|
||||
const stablecoinPriceImpact = useMemo(
|
||||
() =>
|
||||
tradeState === TradeState.SYNCING || !trade
|
||||
? undefined
|
||||
: computeFiatValuePriceImpact(fiatValueTradeInput, fiatValueTradeOutput),
|
||||
[fiatValueTradeInput, fiatValueTradeOutput, tradeState, trade]
|
||||
)
|
||||
const { priceImpactWarning, priceImpactColor } = useMemo(() => {
|
||||
const severity = warningSeverity(stablecoinPriceImpact)
|
||||
|
||||
if (severity < LOW_SEVERITY_THRESHOLD) {
|
||||
return { priceImpactWarning: false, priceImpactColor: undefined }
|
||||
}
|
||||
|
||||
if (severity < MEDIUM_SEVERITY_THRESHOLD) {
|
||||
return { priceImpactWarning: false, priceImpactColor: theme.accentWarning }
|
||||
}
|
||||
|
||||
return { priceImpactWarning: true, priceImpactColor: theme.accentCritical }
|
||||
}, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning])
|
||||
|
||||
const { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } = useMemo(() => {
|
||||
let handleClick = fetchAssets
|
||||
let buttonText = <Trans>Something went wrong</Trans>
|
||||
let disabled = true
|
||||
let warningText = null
|
||||
let warningText = undefined
|
||||
let helperText = undefined
|
||||
let helperTextColor = theme.textSecondary
|
||||
let buttonColor = theme.accentAction
|
||||
|
||||
if (connected && chainId !== SupportedChainId.MAINNET) {
|
||||
handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
|
||||
@ -179,34 +351,62 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
|
||||
}
|
||||
disabled = false
|
||||
buttonText = <Trans>Connect wallet</Trans>
|
||||
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Fetching Route</Trans>
|
||||
} else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
|
||||
handleClick = () => updateAllowance()
|
||||
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
|
||||
|
||||
if (allowance.state === AllowanceState.LOADING) {
|
||||
buttonText = <Trans>Loading Allowance</Trans>
|
||||
} else if (isAllowancePending) {
|
||||
buttonText = <Trans>Approve in your wallet</Trans>
|
||||
} else if (isApprovalLoading) {
|
||||
buttonText = <Trans>Approval pending</Trans>
|
||||
} else {
|
||||
helperText = <Trans>An approval is needed to use this token. </Trans>
|
||||
buttonText = <Trans>Approve</Trans>
|
||||
}
|
||||
} else if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Proceed in wallet</Trans>
|
||||
} else if (bagStatus === BagStatus.PROCESSING_TRANSACTION) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Transaction pending</Trans>
|
||||
} else if (priceImpactWarning && priceImpactColor) {
|
||||
disabled = false
|
||||
buttonColor = priceImpactColor
|
||||
helperText = <Trans>Price impact warning</Trans>
|
||||
helperTextColor = priceImpactColor
|
||||
buttonText = <Trans>Pay Anyway</Trans>
|
||||
} else if (sufficientBalance === true) {
|
||||
disabled = false
|
||||
buttonText = <Trans>Pay</Trans>
|
||||
}
|
||||
|
||||
return { buttonText, disabled, warningText, handleClick }
|
||||
}, [bagStatus, chainId, connected, connector, fetchAssets, setBagExpanded, sufficientBalance, toggleWalletModal])
|
||||
return { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor }
|
||||
}, [
|
||||
fetchAssets,
|
||||
theme.textSecondary,
|
||||
theme.accentAction,
|
||||
connected,
|
||||
chainId,
|
||||
sufficientBalance,
|
||||
bagStatus,
|
||||
usingPayWithAnyToken,
|
||||
tradeState,
|
||||
allowance.state,
|
||||
priceImpactWarning,
|
||||
priceImpactColor,
|
||||
connector,
|
||||
toggleWalletModal,
|
||||
setBagExpanded,
|
||||
isAllowancePending,
|
||||
isApprovalLoading,
|
||||
updateAllowance,
|
||||
])
|
||||
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
|
||||
const parsedAmount = useMemo(() => {
|
||||
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
|
||||
}, [defaultCurrency, totalEthPrice])
|
||||
|
||||
const { state: swapState, trade: swapTrade } = useBestTrade(
|
||||
TradeType.EXACT_OUTPUT,
|
||||
parsedAmount,
|
||||
inputCurrency ?? undefined
|
||||
)
|
||||
|
||||
const usdcValue = useStablecoinValue(inputCurrency ? swapTrade?.inputAmount : parsedAmount)
|
||||
const traceEventProperties = {
|
||||
usd_value: usdcValue?.toExact(),
|
||||
...eventProperties,
|
||||
@ -216,55 +416,50 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
|
||||
<FooterContainer>
|
||||
<Footer>
|
||||
{shouldUsePayWithAnyToken && (
|
||||
<CurrencyRow>
|
||||
<Column gap="xs">
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Pay with</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}>
|
||||
<CurrencyLogo currency={activeCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{activeCurrency?.symbol}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ChevronDown size={20} color={theme.textSecondary} />
|
||||
</CurrencyInput>
|
||||
</Column>
|
||||
<TotalColumn gap="xs">
|
||||
<ThemedText.SubHeaderSmall marginBottom="4px">
|
||||
<Trans>Total</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{inputCurrency
|
||||
? swapState !== TradeState.VALID
|
||||
? '-'
|
||||
: ethNumberStandardFormatter(swapTrade?.inputAmount.toExact())
|
||||
: formatWeiToDecimal(totalEthPrice.toString())}
|
||||
{activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
|
||||
{`${ethNumberStandardFormatter(usdcValue?.toExact(), true)}`}
|
||||
</ThemedText.BodySmall>
|
||||
</TotalColumn>
|
||||
</CurrencyRow>
|
||||
<FooterHeader gap="xs" usingPayWithAnyToken={shouldUsePayWithAnyToken}>
|
||||
<CurrencyRow>
|
||||
<Column gap="xs">
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Pay with</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}>
|
||||
<CurrencyLogo currency={activeCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{activeCurrency?.symbol}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ChevronDown size={20} color={theme.textSecondary} />
|
||||
</CurrencyInput>
|
||||
</Column>
|
||||
<TotalColumn gap="xs">
|
||||
<ThemedText.SubHeaderSmall marginBottom="4px">
|
||||
<Trans>Total</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<InputCurrencyValue
|
||||
usingPayWithAnyToken={usingPayWithAnyToken}
|
||||
totalEthPrice={totalEthPrice}
|
||||
activeCurrency={activeCurrency}
|
||||
tradeState={tradeState}
|
||||
trade={trade}
|
||||
/>
|
||||
</TotalColumn>
|
||||
</CurrencyRow>
|
||||
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
|
||||
</FooterHeader>
|
||||
)}
|
||||
{!shouldUsePayWithAnyToken && (
|
||||
<FooterHeader gap="xs" warningText={!!warningText}>
|
||||
<FooterHeader gap="xs">
|
||||
<Row justify="space-between">
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} ETH
|
||||
{formatWeiToDecimal(totalEthPrice.toString())}
|
||||
{activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
</Row>
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
usdcValue?.toExact(),
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</Row>
|
||||
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
|
||||
</FooterHeader>
|
||||
)}
|
||||
<TraceEvent
|
||||
@ -275,7 +470,8 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
|
||||
shouldLogImpression={connected && !disabled}
|
||||
>
|
||||
<Warning>{warningText}</Warning>
|
||||
<ActionButton onClick={handleClick} disabled={disabled}>
|
||||
<Helper color={helperTextColor}>{helperText}</Helper>
|
||||
<ActionButton onClick={handleClick} disabled={disabled} backgroundColor={buttonColor}>
|
||||
{isPending && <Loader size="20px" stroke="white" />}
|
||||
{buttonText}
|
||||
</ActionButton>
|
||||
|
29
src/nft/hooks/usePayWithAnyTokenSwap.ts
Normal file
29
src/nft/hooks/usePayWithAnyTokenSwap.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Currency, CurrencyAmount, NativeCurrency, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
|
||||
import { useBestTrade } from 'hooks/useBestTrade'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
export default function usePayWithAnyTokenSwap(
|
||||
inputCurrency?: Currency,
|
||||
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
maximumAmountIn: CurrencyAmount<Token> | undefined
|
||||
} {
|
||||
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
|
||||
const allowedSlippage = useAutoSlippageTolerance(trade)
|
||||
const maximumAmountIn = useMemo(() => {
|
||||
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
|
||||
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
|
||||
}, [allowedSlippage, trade])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
state,
|
||||
trade,
|
||||
maximumAmountIn,
|
||||
}
|
||||
}, [maximumAmountIn, state, trade])
|
||||
}
|
48
src/nft/hooks/usePermit2Approval.ts
Normal file
48
src/nft/hooks/usePermit2Approval.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
export default function usePermit2Approval(
|
||||
amount?: CurrencyAmount<Token>,
|
||||
maximumAmount?: CurrencyAmount<Token>,
|
||||
enabled?: boolean
|
||||
) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const allowance = usePermit2Allowance(
|
||||
enabled ? maximumAmount ?? (amount?.currency.isToken ? (amount as CurrencyAmount<Token>) : undefined) : undefined,
|
||||
enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||
)
|
||||
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
|
||||
const [isAllowancePending, setIsAllowancePending] = useState(false)
|
||||
const updateAllowance = useCallback(async () => {
|
||||
invariant(allowance.state === AllowanceState.REQUIRED)
|
||||
setIsAllowancePending(true)
|
||||
try {
|
||||
await allowance.approveAndPermit()
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||
chain_id: chainId,
|
||||
token_symbol: maximumAmount?.currency.symbol,
|
||||
token_address: maximumAmount?.currency.address,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setIsAllowancePending(false)
|
||||
}
|
||||
}, [allowance, chainId, maximumAmount?.currency.address, maximumAmount?.currency.symbol])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
allowance,
|
||||
isApprovalLoading,
|
||||
isAllowancePending,
|
||||
updateAllowance,
|
||||
}
|
||||
}, [allowance, isAllowancePending, isApprovalLoading, updateAllowance])
|
||||
}
|
Loading…
Reference in New Issue
Block a user