Skip to content

Commit

Permalink
feat: holistic custom recipient address part 2 (#6072)
Browse files Browse the repository at this point in the history
* fix: smart contract sell address checks on validateTradeQuote regression

* feat: manual receive address part 2

* feat: expose manual and wallet receive address

* feat: submit on save click

* feat: handle custom/receive states

* feat: disable smart contract receive address for THOR long-tail

* feat: add tag tooltip

* feat: improve styling

* feat: display validation error

* feat: i like safety

* feat: display old manual address entry initially, then the new custom
recipient

* feat: make things less jumpy

* feat: even less jumpy

* feat: really this time less jumpy

* feat: cleanup

* fix: regression

* feat: validateTradeQuote too

* feat: cleanup

* feat: improve styling 🎨

* fix: inputrightelement pointerEvents

* fix: evm isAddress checksum

* feat: close on edit complete

* fix: disabled states

* feat: synchronize preview button disabled state on invalid states

* feat: add data-1p-ignore

* fix: ci

* feat: revert checksum checks

* fix: move outside of ReceiveSummary

* feat: confirm step receive tag

* feat: 10x css dev improve styles

* fix: ci

* feat: gm

* fix: synchronize valid checks

* feat: safety check for streaming swap trade source

* fix: synchronize buyAssetAccountId to sellAssetAccountNumber on
swapper mount

* fix: ci

* chore: update src/components/MultiHopTrade/hooks/useAccountIds.tsx

* feat: type safety for MultiHopTradeQuote and SingleHopTradeQuote

* fix: too much safety

* feat: disable while editing

* feat: revert "feat: disable while editing"

This reverts commit c0982e5.

* fix: merge issue

---------

Co-authored-by: Apotheosis <[email protected]>
Co-authored-by: Apotheosis <[email protected]>
  • Loading branch information
3 people authored Feb 5, 2024
1 parent 4d9be32 commit ef60ea7
Show file tree
Hide file tree
Showing 19 changed files with 454 additions and 194 deletions.
32 changes: 30 additions & 2 deletions packages/swapper/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ export type TradeQuoteStep = {
estimatedExecutionTimeMs: number | undefined
}

export type TradeQuote = {
type TradeQuoteBase = {
id: string
steps: TradeQuoteStep[]
rate: string // top-level rate for all steps (i.e. output amount / input amount)
receiveAddress: string
receiveAccountNumber?: number
Expand All @@ -148,6 +147,35 @@ export type TradeQuote = {
isLongtail?: boolean
}

// https://github.com/microsoft/TypeScript/pull/40002
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N
? R
: _TupleOf<T, N, [T, ...R]>
type TupleOf<T, N extends number> = N extends N
? number extends N
? T[]
: _TupleOf<T, N, []>
: never
// A trade quote can *technically* contain one or many steps, depending on the specific swap/swapper
// However, it *effectively* contains 1 or 2 steps only for now
// Whenever this changes, MultiHopTradeQuoteSteps should be updated to reflect it, with TupleOf<TradeQuoteStep, n>
// where n is a sane max number of steps between 3 and 100
export type SingleHopTradeQuoteSteps = TupleOf<TradeQuoteStep, 1>
export type MultiHopTradeQuoteSteps = TupleOf<TradeQuoteStep, 2>

export type SingleHopTradeQuote = TradeQuoteBase & {
steps: SingleHopTradeQuoteSteps
}
export type MultiHopTradeQuote = TradeQuoteBase & {
steps: MultiHopTradeQuoteSteps
}

// Note: don't try to do TradeQuote = SingleHopTradeQuote | MultiHopTradeQuote here, which would be cleaner but you'll have type errors such as
// "An interface can only extend an object type or intersection of object types with statically known members."
export type TradeQuote = TradeQuoteBase & {
steps: SingleHopTradeQuoteSteps | MultiHopTradeQuoteSteps
}

export type FromOrXpub = { from: string; xpub?: never } | { from?: never; xpub: string }

export type CowSwapOrder = {
Expand Down
1 change: 1 addition & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@
"recipientAddress": "Recipient address",
"customRecipientAddress": "Custom recipient address",
"customRecipientAddressDescription": "Enter a custom recipient address for this trade",
"thisIsYourCustomRecipientAddress": "This is your custom recipient address",
"enterCustomRecipientAddress": "Enter custom recipient address",
"tooltip": {
"rate": "This is the expected rate for this trade pair.",
Expand Down
1 change: 1 addition & 0 deletions src/components/Modals/Send/AddressInput/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const AddressInput = ({ rules, placeholder, enableQr = false }: AddressIn
value={value}
variant='filled'
data-test='send-address-input'
data-1p-ignore
// Because the InputRightElement is hover the input, we need to let this space free
pe={10}
isInvalid={!isValid}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import {
StepIndicator,
StepSeparator,
StepTitle,
Tag,
useStyleConfig,
} from '@chakra-ui/react'
import { isLedger } from '@shapeshiftoss/hdwallet-ledger'
import { useMemo } from 'react'
import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis'
import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress'
import { useWallet } from 'hooks/useWallet/useWallet'

const width = { width: '100%' }

Expand Down Expand Up @@ -43,6 +49,18 @@ export const StepperStep = ({
variant: isError ? 'error' : 'default',
}) as { indicator: SystemStyleObject }

const wallet = useWallet().state.wallet
const useReceiveAddressArgs = useMemo(
() => ({
fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)),
}),
[wallet],
)
const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress(useReceiveAddressArgs)
const receiveAddress = manualReceiveAddress ?? walletReceiveAddress

if (!receiveAddress) return null

return (
<Step style={width}>
<StepIndicator className={isPending ? 'step-pending' : undefined} sx={styles}>
Expand All @@ -56,13 +74,20 @@ export const StepperStep = ({
</SkeletonText>
</StepTitle>
{description && (
<StepDescription as={Box} {...descriptionProps}>
{isLoading ? (
<SkeletonText mt={2} noOfLines={1} skeletonHeight={3} isLoaded={!isLoading} />
) : (
description
)}
</StepDescription>
<>
<StepDescription as={Box} {...descriptionProps}>
{isLoading ? (
<SkeletonText mt={2} noOfLines={1} skeletonHeight={3} isLoaded={!isLoading} />
) : (
description
)}
</StepDescription>
{isLastStep ? (
<Tag size='md' colorScheme='blue'>
<MiddleEllipsis value={receiveAddress} />
</Tag>
) : null}
</>
)}
{content !== undefined && <Box mt={2}>{content}</Box>}
{!isLastStep && <Spacer height={6} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
import { ChevronDownIcon, ChevronUpIcon, QuestionIcon } from '@chakra-ui/icons'
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
CloseIcon,
EditIcon,
QuestionIcon,
} from '@chakra-ui/icons'
import {
Box,
Collapse,
Divider,
Flex,
IconButton,
Input,
InputGroup,
InputRightElement,
Skeleton,
Stack,
Tooltip,
useColorModeValue,
useDisclosure,
} from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { isLedger } from '@shapeshiftoss/hdwallet-ledger'
import { type AssetId } from '@shapeshiftoss/caip'
import type { AmountDisplayMeta, ProtocolFee, SwapSource } from '@shapeshiftoss/swapper'
import { SwapperName } from '@shapeshiftoss/swapper'
import type { PartialRecord } from '@shapeshiftoss/types'
import { type FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip'
import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress'
import { Row, type RowProps } from 'components/Row/Row'
import { RawText, Text } from 'components/Text'
import type { TextPropTypes } from 'components/Text/Text'
import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton'
import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag'
import { useWallet } from 'hooks/useWallet/useWallet'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { fromBaseUnit } from 'lib/math'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from 'lib/swapper/swappers/ThorchainSwapper/constants'
import { isSome, middleEllipsis } from 'lib/utils'
import { isSome } from 'lib/utils'
import {
selectActiveQuoteAffiliateBps,
selectQuoteAffiliateFeeUserCurrency,
Expand All @@ -70,23 +53,13 @@ type ReceiveSummaryProps = {
swapSource?: SwapSource
} & RowProps

const editIcon = <EditIcon />
const checkIcon = <CheckIcon />
const closeIcon = <CloseIcon />

const shapeShiftFeeModalRowHover = { textDecoration: 'underline', cursor: 'pointer' }

const tradeFeeSourceTranslation: TextPropTypes['translation'] = [
'trade.tradeFeeSource',
{ tradeFeeSource: 'ShapeShift' },
]

// TODO(gomes): implement me
const isCustomRecipientAddress = false
const recipientAddressTranslation: TextPropTypes['translation'] = isCustomRecipientAddress
? 'trade.customRecipientAddress'
: 'trade.recipientAddress'

export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
({
symbol,
Expand Down Expand Up @@ -162,46 +135,11 @@ export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
setShowFeeModal(!showFeeModal)
}, [showFeeModal])

// Recipient address state and handlers
const [isRecipientAddressEditing, setIsRecipientAddressEditing] = useState(false)
const handleEditRecipientAddressClick = useCallback(() => {
setIsRecipientAddressEditing(true)
}, [])

const handleCancelClick = useCallback(() => {
setIsRecipientAddressEditing(false)
}, [])

const handleSaveClick = useCallback(() => {
setIsRecipientAddressEditing(false)
}, [])

const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(event => {
// TODO(gomes): dispatch here and make the input controlled with a local state value and form context so that
// typing actually does something
// dispatch(tradeInput.actions.setManualReceiveAddress(undefined))
console.log(event.target.value)
}, [])

const minAmountAfterSlippageTranslation: TextPropTypes['translation'] = useMemo(
() => ['trade.minAmountAfterSlippage', { slippage: slippageAsPercentageString }],
[slippageAsPercentageString],
)

const wallet = useWallet().state.wallet
const useReceiveAddressArgs = useMemo(
() => ({
fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)),
}),
[wallet],
)
const isHolisticRecipientAddressEnabled = useFeatureFlag('HolisticRecipientAddress')

const receiveAddress = useReceiveAddress(useReceiveAddressArgs)

// This should never happen but it may
if (isHolisticRecipientAddressEnabled && !receiveAddress) return null

return (
<>
<Row fontSize='sm' fontWeight='medium' alignItems='flex-start' {...rest}>
Expand Down Expand Up @@ -351,77 +289,6 @@ export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
</Row>
</>
)}

{/* TODO(gomes): This should probably be made its own component and <ManualAddressEntry /> removed */}
{/* TODO(gomes): we can safely remove this condition when this feature goes live */}
{isHolisticRecipientAddressEnabled &&
receiveAddress &&
(isRecipientAddressEditing ? (
<InputGroup size='sm'>
<Input
value={''} // TODO: Controlled input value
onChange={handleInputChange}
autoFocus
placeholder={translate('trade.customRecipientAddressDescription')}
/>
<InputRightElement width='4.5rem'>
<Box
display='flex'
alignItems='center'
justifyContent='space-between'
width='full'
px='2'
>
<Box
as='button'
display='flex'
alignItems='center'
justifyContent='center'
borderRadius='md'
onClick={handleSaveClick}
>
{checkIcon}
</Box>
<Box
as='button'
display='flex'
alignItems='center'
justifyContent='center'
borderRadius='md'
onClick={handleCancelClick}
>
{closeIcon}
</Box>
</Box>
</InputRightElement>
</InputGroup>
) : (
<>
<Divider borderColor='border.base' />
<Row>
<Row.Label>
<Text translation={recipientAddressTranslation} />
</Row.Label>
<Row.Value whiteSpace='nowrap'>
<Stack direction='row' spacing={1} alignItems='center'>
<RawText>{middleEllipsis(receiveAddress)}</RawText>
<Tooltip
label={translate('trade.customRecipientAddressDescription')}
placement='top'
hasArrow
>
<IconButton
aria-label='Edit recipient address'
icon={editIcon}
variant='ghost'
onClick={handleEditRecipientAddressClick}
/>
</Tooltip>
</Stack>
</Row.Value>
</Row>
</>
))}
</Stack>
</Collapse>
<FeeModal isOpen={showFeeModal} onClose={handleFeeModal} />
Expand Down
Loading

0 comments on commit ef60ea7

Please sign in to comment.