Commit 8ffe4e99 authored by cartcrom's avatar cartcrom Committed by GitHub

fix: properly display FOT swaps (#7146)

parent 2b85852a
import { t } from '@lingui/macro'
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core'
import { ChainId, Currency, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import moonpayLogoSrc from 'assets/svg/moonpay.svg'
import { nativeOnChain } from 'constants/tokens'
import {
ActivityType,
AssetActivityPartsFragment,
Currency,
Currency as GQLCurrency,
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
......@@ -17,7 +17,7 @@ import {
TokenTransferPartsFragment,
TransactionDetailsPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks'
import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import ms from 'ms'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
......@@ -142,7 +142,7 @@ function getSwapDescriptor({
*/
function formatTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): string {
if (!transactedValue) return '-'
const price = transactedValue?.currency === Currency.Usd ? transactedValue.value ?? undefined : undefined
const price = transactedValue?.currency === GQLCurrency.Usd ? transactedValue.value ?? undefined : undefined
return formatFiatPrice(price)
}
......@@ -156,7 +156,9 @@ function parseSwap(changes: TransactionChanges) {
.join()
return { title, descriptor }
} else if (changes.TokenTransfer.length === 2) {
}
// Some swaps may have more than 2 transfers, e.g. swaps with fees on tranfer
if (changes.TokenTransfer.length >= 2) {
const sent = changes.TokenTransfer.find((t) => t?.__typename === 'TokenTransfer' && t.direction === 'OUT')
const received = changes.TokenTransfer.find((t) => t?.__typename === 'TokenTransfer' && t.direction === 'IN')
if (sent && received) {
......@@ -165,6 +167,7 @@ function parseSwap(changes: TransactionChanges) {
return {
title: getSwapTitle(sent, received),
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
currencies: [gqlToCurrency(sent.asset), gqlToCurrency(received.asset)],
}
}
}
......@@ -179,7 +182,8 @@ function parseApprove(changes: TransactionChanges) {
if (changes.TokenApproval.length === 1) {
const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved`
const descriptor = `${changes.TokenApproval[0].asset.symbol}`
return { title, descriptor }
const currencies = [gqlToCurrency(changes.TokenApproval[0].asset)]
return { title, descriptor, currencies }
}
return { title: t`Unknown Approval` }
}
......@@ -194,6 +198,7 @@ function parseLPTransfers(changes: TransactionChanges) {
return {
descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`,
logos: [poolTokenA.asset.project?.logo?.url, poolTokenB.asset.project?.logo?.url],
currencies: [gqlToCurrency(poolTokenA.asset), gqlToCurrency(poolTokenB.asset)],
}
}
......@@ -210,6 +215,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined
let assetName: string | undefined
let amount: string | undefined
let currencies: (Currency | undefined)[] | undefined
if (changes.NftTransfer.length === 1) {
transfer = changes.NftTransfer[0]
......@@ -219,6 +225,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
transfer = changes.TokenTransfer[0]
assetName = transfer.asset.symbol
amount = formatNumberOrString(transfer.quantity, NumberType.TokenNonTx)
currencies = [gqlToCurrency(transfer.asset)]
}
if (transfer && assetName && amount) {
......@@ -230,17 +237,20 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
title: t`Purchased`,
descriptor: `${amount} ${assetName} ${t`for`} ${formatTransactedValue(transfer.transactedValue)}`,
logos: [moonpayLogoSrc],
currencies,
}
: {
title: t`Received`,
descriptor: `${amount} ${assetName} ${t`from`} `,
otherAccount: isAddress(transfer.sender) || undefined,
currencies,
}
} else {
return {
title: t`Sent`,
descriptor: `${amount} ${assetName} ${t`to`} `,
otherAccount: isAddress(transfer.recipient) || undefined,
currencies,
}
}
}
......@@ -276,7 +286,7 @@ const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } =
[ActivityType.Unknown]: parseUnknown,
}
function getLogoSrcs(changes: TransactionChanges): string[] {
function getLogoSrcs(changes: TransactionChanges): Array<string | undefined> {
// Uses set to avoid duplicate logos (e.g. nft's w/ same image url)
const logoSet = new Set<string | undefined>()
// Uses only NFT logos if they are present (will not combine nft image w/ token image)
......@@ -286,7 +296,7 @@ function getLogoSrcs(changes: TransactionChanges): string[] {
changes.TokenTransfer.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url))
changes.TokenApproval.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url))
}
return Array.from(logoSet).filter(Boolean) as string[]
return Array.from(logoSet)
}
function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined {
......@@ -321,6 +331,7 @@ function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activ
offchainOrderStatus: uniswapXOrderStatus,
timestamp,
logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url],
currencies: [gqlToCurrency(inputToken), gqlToCurrency(outputToken)],
title,
descriptor,
from: details.offerer,
......
......@@ -2,14 +2,14 @@ import { ChainId, Currency } from '@uniswap/sdk-core'
import blankTokenUrl from 'assets/svg/blank_token.svg'
import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction.svg'
import { MissingImageLogo } from 'components/Logo/AssetLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { Unicon } from 'components/Unicon'
import { getChainInfo } from 'constants/chainInfo'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import useENSAvatar from 'hooks/useENSAvatar'
import React from 'react'
import { Loader } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import styled from 'styled-components'
const UnknownContract = styled(UnknownStatus)`
color: ${({ theme }) => theme.textSecondary};
`
......@@ -36,15 +36,6 @@ const DoubleLogoContainer = styled.div`
}
`
type MultiLogoProps = {
chainId: ChainId
accountAddress?: string
currencies?: Array<Currency | undefined>
images?: (string | undefined)[]
size?: string
style?: React.CSSProperties
}
const StyledLogoParentContainer = styled.div`
position: relative;
top: 0;
......@@ -73,8 +64,8 @@ const CircleLogoImage = styled.img<{ size: string }>`
border-radius: 50%;
`
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
background-color: ${({ $backgroundColor }) => $backgroundColor};
const L2LogoContainer = styled.div<{ hasSquareLogo?: boolean }>`
background-color: ${({ theme, hasSquareLogo }) => (hasSquareLogo ? theme.backgroundSurface : theme.textPrimary)};
border-radius: 2px;
height: 16px;
left: 60%;
......@@ -87,81 +78,119 @@ const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
justify-content: center;
`
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
export function PortfolioLogo({
chainId = ChainId.MAINNET,
accountAddress,
currencies,
images,
size = '40px',
style,
}: MultiLogoProps) {
const chainInfo = getChainInfo(chainId)
const squareLogoUrl = chainInfo?.squareLogoUrl
const logoUrl = chainInfo?.logoUrl
const chainLogo = squareLogoUrl ?? logoUrl
const { avatar, loading } = useENSAvatar(accountAddress, false)
const theme = useTheme()
interface DoubleLogoProps {
logo1?: string
logo2?: string
size: string
onError1?: () => void
onError2?: () => void
}
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
function DoubleLogo({ logo1, onError1, logo2, onError2, size }: DoubleLogoProps) {
return (
<DoubleLogoContainer>
<CircleLogoImage size={size} src={logo1 ?? blankTokenUrl} onError={onError1} />
<CircleLogoImage size={size} src={logo2 ?? blankTokenUrl} onError={onError2} />
</DoubleLogoContainer>
)
}
let component
if (accountAddress) {
component = loading ? (
<Loader size={size} />
) : avatar ? (
<ENSAvatarImg src={avatar} alt="avatar" />
) : (
<Unicon size={40} address={accountAddress} />
)
} else if (currencies && currencies.length) {
const logo1 = <CircleLogoImage size={size} src={src ?? blankTokenUrl} onError={nextSrc} />
const logo2 = <CircleLogoImage size={size} src={src2 ?? blankTokenUrl} onError={nextSrc2} />
component =
currencies.length > 1 ? (
<DoubleLogoContainer style={style}>
{logo1}
{logo2}
</DoubleLogoContainer>
) : currencies.length === 1 ? (
<CurrencyLogo currency={currencies[0]} size={size} />
) : (
<MissingImageLogo size={size}>
{currencies[0]?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
} else if (images && images.length) {
component =
images.length > 1 ? (
<DoubleLogoContainer style={style}>
<CircleLogoImage size={size} src={images[0]} />
<CircleLogoImage size={size} src={images[images.length - 1]} />
</DoubleLogoContainer>
) : (
<CircleLogoImage size={size} src={images[0]} />
)
} else {
return <UnknownContract width={size} height={size} />
interface DoubleCurrencyLogoProps {
chainId: ChainId
currencies: Array<Currency | undefined>
backupImages?: Array<string | undefined>
size: string
}
function DoubleCurrencyLogo({ chainId, currencies, backupImages, size }: DoubleCurrencyLogoProps) {
const [src, nextSrc] = useTokenLogoSource(
currencies?.[0]?.wrapped.address,
chainId,
currencies?.[0]?.isNative,
backupImages?.[0]
)
const [src2, nextSrc2] = useTokenLogoSource(
currencies?.[1]?.wrapped.address,
chainId,
currencies?.[1]?.isNative,
backupImages?.[1]
)
if (currencies.length === 1 && src) {
return <CircleLogoImage size={size} src={src} onError={nextSrc} />
}
if (currencies.length > 1) {
return <DoubleLogo logo1={src} onError1={nextSrc} logo2={src2} onError2={nextSrc2} size={size} />
}
return (
<MissingImageLogo size={size}>
{currencies[0]?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}
function PortfolioAvatar({ accountAddress, size }: { accountAddress: string; size: string }) {
const { avatar, loading } = useENSAvatar(accountAddress, false)
if (loading) {
return <Loader size={size} />
}
if (avatar) {
return <ENSAvatarImg src={avatar} alt="avatar" />
}
return <Unicon size={40} address={accountAddress} />
}
const L2Logo =
chainId !== ChainId.MAINNET && chainLogo ? (
<L2LogoContainer $backgroundColor={squareLogoUrl ? theme.backgroundSurface : theme.textPrimary}>
{squareLogoUrl ? (
<SquareChainLogo src={chainLogo} alt="chainLogo" />
) : (
<StyledChainLogo src={chainLogo} alt="chainLogo" />
)}
</L2LogoContainer>
) : null
interface PortfolioLogoProps {
chainId: ChainId
accountAddress?: string
currencies?: Array<Currency | undefined>
images?: Array<string | undefined>
size?: string
style?: React.CSSProperties
}
function SquareL2Logo({ chainId }: { chainId: ChainId }) {
if (chainId === ChainId.MAINNET) return null
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
const chainLogo = squareLogoUrl ?? logoUrl
return (
<L2LogoContainer hasSquareLogo={!!squareLogoUrl}>
{squareLogoUrl ? (
<SquareChainLogo src={chainLogo} alt="chainLogo" />
) : (
<StyledChainLogo src={chainLogo} alt="chainLogo" />
)}
</L2LogoContainer>
)
}
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback contract icon
*/
export function PortfolioLogo(props: PortfolioLogoProps) {
return (
<StyledLogoParentContainer>
{component}
{L2Logo}
{getLogo(props)}
<SquareL2Logo chainId={props.chainId} />
</StyledLogoParentContainer>
)
}
function getLogo({ chainId, accountAddress, currencies, images, size = '40px' }: PortfolioLogoProps) {
if (accountAddress) {
return <PortfolioAvatar accountAddress={accountAddress} size={size} />
}
if (currencies && currencies.length) {
return <DoubleCurrencyLogo chainId={chainId} currencies={currencies} backupImages={images} size={size} />
}
if (images?.length === 1) {
return <CircleLogoImage size={size} src={images[0] ?? blankTokenUrl} />
}
if (images && images?.length >= 2) {
return <DoubleLogo logo1={images[0]} logo2={images[images.length - 1]} size={size} />
}
return <UnknownContract width={size} height={size} />
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment