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