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: less confusing permit2 flow #8508

Merged
merged 12 commits into from
Jan 9, 2025
13 changes: 8 additions & 5 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -923,12 +923,16 @@
"cta": "Ok, got it"
}
},
"permit2": {
"title": "Permit token transfer:",
"permit2Allowance": {
"title": "Permit2 contract allowance",
"tooltip": "This trade uses Permit2, which requires a one-time approval to grant unlimited allowance to the Permit2 contract. After this initial setup, you can authorize trades on any Permit2-supported protocol by signing gas-free messages that grant temporary 5-minute allowances."
},
"permit2Eip712": {
"title": "Permit token transfer",
"description": "Grant the smart contract permission to use %{symbol} on your behalf.",
"tooltip": "This trade has an added security feature called 'Permit2' which requires you to grant permission for the application to complete the trade on your behalf.",
"error": "A problem occurred signing Permit2 message",
"signMessage": "Sign message"
"signMessage": "Sign message",
"tooltip": "This will grant %{swapperName} a 5-minute allowance to complete the trade on your behalf."
},
"errors": {
"title": "Something went wrong",
Expand Down Expand Up @@ -1013,7 +1017,6 @@
"swap": "Swap on %{sellChainName} via %{swapperName}"
},
"approvalTitle": "Token allowance approval",
"permit2Title": "Permit token transfer",
"resetTitle": "Token allowance reset",
"fiatAmountOnChain": "%{amountFiatFormatted} on %{chainName}",
"quote": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const ApprovalStep = ({
allowanceApproval.state === TransactionExecutionState.Complete
? TransactionExecutionState.AwaitingConfirmation
: allowanceApproval.state
case HopExecutionState.AwaitingPermit2:
case HopExecutionState.AwaitingPermit2Eip712Sign:
return permit2.state
case HopExecutionState.AwaitingSwap:
case HopExecutionState.Complete:
Expand Down Expand Up @@ -118,7 +118,7 @@ export const ApprovalStep = ({
return allowanceResetContent
case HopExecutionState.AwaitingAllowanceApproval:
return allowanceApprovalContent
case HopExecutionState.AwaitingPermit2:
case HopExecutionState.AwaitingPermit2Eip712Sign:
return permit2Content
case HopExecutionState.AwaitingSwap:
case HopExecutionState.Complete:
Expand Down Expand Up @@ -172,7 +172,7 @@ export const ApprovalStep = ({
return allowanceResetDescription
case HopExecutionState.AwaitingAllowanceApproval:
return allowanceApprovalDescription
case HopExecutionState.AwaitingPermit2:
case HopExecutionState.AwaitingPermit2Eip712Sign:
return permit2Description
case HopExecutionState.AwaitingSwap:
case HopExecutionState.Complete:
Expand Down Expand Up @@ -213,7 +213,7 @@ export const ApprovalStep = ({
allowanceReset.state === TransactionExecutionState.AwaitingConfirmation) ||
(hopExecutionState === HopExecutionState.AwaitingAllowanceApproval &&
allowanceApproval.state === TransactionExecutionState.AwaitingConfirmation) ||
(hopExecutionState === HopExecutionState.AwaitingPermit2 &&
(hopExecutionState === HopExecutionState.AwaitingPermit2Eip712Sign &&
permit2.state === TransactionExecutionState.AwaitingConfirmation)
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Box, Button, Card, CircularProgress, Icon, Tooltip, VStack } from '@chakra-ui/react'
import {
Box,
Button,
Card,
CircularProgress,
Icon,
Text as CText,
Tooltip,
VStack,
} from '@chakra-ui/react'
import type { InterpolationOptions } from 'node-polyglot'
import { FaInfoCircle } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
Expand All @@ -12,7 +21,7 @@ export type ApprovalContentProps = {
isLoading: boolean
subHeadingTranslation?: string | [string, InterpolationOptions]
titleTranslation: string
tooltipTranslation: string
tooltipTranslation: string | [string, InterpolationOptions]
topRightContent?: JSX.Element
transactionExecutionState: TransactionExecutionState
onSubmit: () => void
Expand All @@ -39,7 +48,13 @@ export const ApprovalContent = ({
<Row px={2}>
<Row.Label display='flex' alignItems='center'>
<Text color='text.subtle' translation={titleTranslation} fontWeight='bold' />
<Tooltip label={translate(tooltipTranslation)}>
<Tooltip
label={
typeof tooltipTranslation === 'string'
? translate(tooltipTranslation)
: translate(...tooltipTranslation)
}
>
<Box ml={1}>
<Icon as={FaInfoCircle} color='text.subtle' fontSize='0.7em' />
</Box>
Expand All @@ -54,7 +69,11 @@ export const ApprovalContent = ({
{subHeadingTranslation && (
<Row px={2}>
<Row.Label textAlign='left' display='flex'>
<Text color='text.subtle' translation={subHeadingTranslation} />
<CText color='text.subtle'>
{typeof subHeadingTranslation === 'string'
? translate(subHeadingTranslation)
: translate(...subHeadingTranslation)}
</CText>
</Row.Label>
</Row>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const useAllowanceApprovalContent = ({
isDisabled={isApprovalButtonDisabled}
isLoading={isAllowanceApprovalLoading}
titleTranslation='trade.allowance'
tooltipTranslation={isPermit2 ? 'trade.permit2.tooltip' : 'trade.allowanceTooltip'}
tooltipTranslation={isPermit2 ? 'trade.permit2Allowance.tooltip' : 'trade.allowanceTooltip'}
transactionExecutionState={transactionExecutionState}
onSubmit={handleSignAllowanceApproval}
topRightContent={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { isSome } from '@shapeshiftoss/utils'
import type { InterpolationOptions } from 'node-polyglot'
import { useMemo } from 'react'
import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes'
import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors'
import {
selectActiveSwapperName,
selectHopExecutionMetadata,
} from 'state/slices/tradeQuoteSlice/selectors'
import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types'
import { useAppSelector } from 'state/store'

Expand Down Expand Up @@ -37,12 +40,14 @@ export const usePermit2Content = ({
allowanceApproval,
} = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter))

const swapperName = useAppSelector(selectActiveSwapperName)

const { signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId)

const { isLoading: isTradeQuotesLoading } = useGetTradeQuotes()

const isButtonDisabled = useMemo(() => {
const isAwaitingPermit2 = hopExecutionState === HopExecutionState.AwaitingPermit2
const isAwaitingPermit2 = hopExecutionState === HopExecutionState.AwaitingPermit2Eip712Sign
const isError = permit2.state === TransactionExecutionState.Failed
const isAwaitingConfirmation = permit2.state === TransactionExecutionState.AwaitingConfirmation
const isDisabled =
Expand All @@ -52,22 +57,26 @@ export const usePermit2Content = ({
}, [hopExecutionState, permit2.state, isTradeQuotesLoading])

const subHeadingTranslation: [string, InterpolationOptions] = useMemo(() => {
return ['trade.permit2.description', { symbol: tradeQuoteStep.sellAsset.symbol }]
return ['trade.permit2Eip712.description', { symbol: tradeQuoteStep.sellAsset.symbol }]
}, [tradeQuoteStep])

const tooltipTranslation: [string, InterpolationOptions] = useMemo(() => {
return ['trade.permit2Eip712.tooltip', { swapperName }]
}, [swapperName])

const content = useMemo(() => {
if (hopExecutionState !== HopExecutionState.AwaitingPermit2) return
if (hopExecutionState !== HopExecutionState.AwaitingPermit2Eip712Sign) return
return (
<ApprovalContent
buttonTranslation='trade.permit2.signMessage'
buttonTranslation='trade.permit2Eip712.signMessage'
isDisabled={isButtonDisabled}
isLoading={
/* NOTE: No loading state when signature in progress because it's instant */
isTradeQuotesLoading
}
subHeadingTranslation={subHeadingTranslation}
titleTranslation='trade.permit2.title'
tooltipTranslation='trade.permit2.tooltip'
titleTranslation='trade.permit2Eip712.title'
tooltipTranslation={tooltipTranslation}
transactionExecutionState={permit2.state}
onSubmit={signPermit2}
/>
Expand All @@ -79,6 +88,7 @@ export const usePermit2Content = ({
permit2.state,
signPermit2,
subHeadingTranslation,
tooltipTranslation,
])

const description = useMemo(() => {
Expand All @@ -98,7 +108,7 @@ export const usePermit2Content = ({
tradeQuoteStep={tradeQuoteStep}
txLines={txLines}
isError={permit2.state === TransactionExecutionState.Failed}
errorTranslation='trade.permit2.error'
errorTranslation='trade.permit2Eip712.error'
/>
)
}, [allowanceApproval.txHash, allowanceReset.txHash, permit2.state, tradeQuoteStep])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const Hop = ({
// fallthrough
case HopExecutionState.AwaitingAllowanceApproval:
// fallthrough
case HopExecutionState.AwaitingPermit2:
case HopExecutionState.AwaitingPermit2Eip712Sign:
return hopIndex === 0 ? 1 : 0
case HopExecutionState.AwaitingSwap:
return hopIndex === 0 ? 2 : 1
Expand Down Expand Up @@ -226,7 +226,7 @@ export const Hop = ({
)
case HopExecutionState.AwaitingAllowanceReset:
case HopExecutionState.AwaitingAllowanceApproval:
case HopExecutionState.AwaitingPermit2:
case HopExecutionState.AwaitingPermit2Eip712Sign:
case HopExecutionState.AwaitingSwap:
return (
<Circle size={8} bg='background.surface.raised.base'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
selectActiveQuoteErrors,
selectHopExecutionMetadata,
} from 'state/slices/tradeQuoteSlice/selectors'
import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types'
import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types'
import { useAppSelector, useSelectorWithArgs } from 'state/store'

import { StepperStep as StepperStepComponent } from '../MultiHopTradeConfirm/components/StepperStep'
Expand Down Expand Up @@ -120,6 +120,7 @@ export const ExpandedStepperSteps = ({ activeTradeQuote }: ExpandedStepperStepsP
}, [activeTradeId])

const {
state: hopExecutionState,
allowanceApproval: firstHopAllowanceApproval,
permit2: firstHopPermit2,
allowanceReset: firstHopAllowanceReset,
Expand Down Expand Up @@ -194,40 +195,65 @@ export const ExpandedStepperSteps = ({ activeTradeQuote }: ExpandedStepperStepsP
}, [firstHopAllowanceReset.txHash, firstHopSellAccountId, tradeQuoteFirstHop])

const firstHopAllowanceApprovalTitle = useMemo(() => {
return (
<Flex alignItems='center' justifyContent='space-between' flex={1}>
{firstHopPermit2.isRequired === true ? (
const content = (() => {
// Awaiting Permit2 contract allowance grant
if (
firstHopPermit2.isRequired &&
hopExecutionState === HopExecutionState.AwaitingAllowanceApproval
)
return (
<>
<Text translation='trade.permit2Title' />
<Tooltip label={translate('trade.permit2.tooltip')}>
<Text translation='trade.permit2Allowance.title' />
<Tooltip label={translate('trade.permit2Allowance.tooltip')}>
<Box ml={1}>
<Icon as={FaInfoCircle} color='text.subtle' fontSize='0.8em' />
</Box>
</Tooltip>
</>
) : (
<>
<Text translation='trade.approvalTitle' />
{firstHopAllowanceApproval.txHash && tradeQuoteFirstHop && firstHopSellAccountId && (
<TxLabel
txHash={firstHopAllowanceApproval.txHash}
explorerBaseUrl={tradeQuoteFirstHop.sellAsset.explorerTxLink}
accountId={firstHopSellAccountId}
swapperName={undefined} // no swapper base URL here, this is an allowance Tx
/>
)}
</>
)}
)

// Good ol' allowances
return (
<>
<Text translation='trade.approvalTitle' />
{firstHopAllowanceApproval.txHash && tradeQuoteFirstHop && firstHopSellAccountId && (
<TxLabel
txHash={firstHopAllowanceApproval.txHash}
explorerBaseUrl={tradeQuoteFirstHop.sellAsset.explorerTxLink}
accountId={firstHopSellAccountId}
swapperName={undefined} // no swapper base URL here, this is an allowance Tx
/>
)}
</>
)
})()
return (
<Flex alignItems='center' justifyContent='space-between' flex={1}>
{content}
</Flex>
)
}, [
firstHopAllowanceApproval.txHash,
firstHopPermit2.isRequired,
firstHopSellAccountId,
hopExecutionState,
tradeQuoteFirstHop,
translate,
])

const firstHopPermit2SignTitle = useMemo(() => {
return (
<Flex alignItems='center' justifyContent='space-between' flex={1}>
<Text translation='trade.permit2Eip712.title' />
<Tooltip label={translate('trade.permit2Eip712.tooltip', { swapperName })}>
<Box ml={1}>
<Icon as={FaInfoCircle} color='text.subtle' fontSize='0.8em' />
</Box>
</Tooltip>
</Flex>
)
}, [swapperName, translate])

const firstHopActionTitle = useMemo(() => {
return (
<Flex alignItems='center' justifyContent='space-between' flex={1} gap={2}>
Expand Down Expand Up @@ -296,8 +322,8 @@ export const ExpandedStepperSteps = ({ activeTradeQuote }: ExpandedStepperStepsP
<Flex alignItems='center' justifyContent='space-between' flex={1}>
{lastHopPermit2.isRequired === true ? (
<>
<Text translation='trade.permit2Title' />
<Tooltip label={translate('trade.permit2.tooltip')}>
<Text translation='trade.permit2Allowance.title' />
<Tooltip label={translate('trade.permit2Allowance.tooltip')}>
<Box ml={1}>
<Icon as={FaInfoCircle} color='text.subtle' fontSize='0.8em' />
</Box>
Expand Down Expand Up @@ -426,6 +452,16 @@ export const ExpandedStepperSteps = ({ activeTradeQuote }: ExpandedStepperStepsP
stepIndicatorVariant='innerSteps'
/>
) : null}
{tradeSteps[StepperStep.FirstHopPermit2Eip712Sign] ? (
<StepperStepComponent
title={firstHopPermit2SignTitle}
stepIndicator={stepIndicator}
stepProps={stepProps}
useSpacer={false}
isError={isError && currentTradeStep === StepperStep.FirstHopApproval}
stepIndicatorVariant='innerSteps'
/>
) : null}
<StepperStepComponent
title={firstHopActionTitle}
stepIndicator={stepIndicator}
Expand Down
Loading