diff --git a/src/components/AccountDrawer/AuthenticatedHeader.tsx b/src/components/AccountDrawer/AuthenticatedHeader.tsx index 9d05d4a5c0d..bd1bdf6fb7d 100644 --- a/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -30,7 +30,6 @@ import StatusIcon from '../Identicon/StatusIcon' import { useCachedPortfolioBalancesQuery } from '../PrefetchBalancesWrapper/PrefetchBalancesWrapper' import { useToggleAccountDrawer } from '.' import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton' -import MiniPortfolio from './MiniPortfolio' import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow' const AuthenticatedHeaderWrapper = styled.div` @@ -242,7 +241,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account View and sell NFTs )} - {isUnclaimed && ( Claim {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} reward diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx b/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx deleted file mode 100644 index e4842851594..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import Column from 'components/Column' -import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' -import { LoaderV2 } from 'components/Icons/LoadingSpinner' -import Row from 'components/Row' -import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' -import useENSName from 'hooks/useENSName' -import { useCallback } from 'react' -import styled from 'styled-components' -import { EllipsisStyle, ThemedText } from 'theme/components' -import { shortenAddress } from 'utils' -import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' - -import { PortfolioLogo } from '../PortfolioLogo' -import PortfolioRow from '../PortfolioRow' -import { useOpenOffchainActivityModal } from './OffchainActivityModal' -import { useTimeSince } from './parseRemote' -import { Activity } from './types' - -const ActivityRowDescriptor = styled(ThemedText.BodySmall)` - color: ${({ theme }) => theme.neutral2}; - ${EllipsisStyle} -` - -const StyledTimestamp = styled(ThemedText.BodySmall)` - color: ${({ theme }) => theme.neutral2}; - font-variant: small; - font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on; -` - -function StatusIndicator({ activity: { status, timestamp } }: { activity: Activity }) { - const timeSince = useTimeSince(timestamp) - - switch (status) { - case TransactionStatus.Pending: - return - case TransactionStatus.Confirmed: - return {timeSince} - case TransactionStatus.Failed: - return - } -} - -export function ActivityRow({ activity }: { activity: Activity }) { - const { chainId, title, descriptor, logos, otherAccount, currencies, hash, prefixIconSrc, offchainOrderStatus } = - activity - const openOffchainActivityModal = useOpenOffchainActivityModal() - - const { ENSName } = useENSName(otherAccount) - const onClick = useCallback(() => { - if (offchainOrderStatus) { - openOffchainActivityModal({ orderHash: hash, status: offchainOrderStatus }) - return - } - - window.open(getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION), '_blank') - }, [offchainOrderStatus, chainId, hash, openOffchainActivityModal]) - - return ( - - - - } - title={ - - {prefixIconSrc && } - {title} - - } - descriptor={ - - {descriptor} - {ENSName ?? shortenAddress(otherAccount)} - - } - right={} - onClick={onClick} - /> - ) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap b/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap deleted file mode 100644 index f5f3b5fd9fe..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap +++ /dev/null @@ -1,347 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`parseRemote parseRemoteActivities should parse NFT approval 1`] = ` -Object { - "chainId": 1, - "descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Unknown Approval", -} -`; - -exports[`parseRemote parseRemoteActivities should parse NFT approval for all 1`] = ` -Object { - "chainId": 1, - "descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Unknown Approval", -} -`; - -exports[`parseRemote parseRemoteActivities should parse NFT receive 1`] = ` -Object { - "chainId": 1, - "currencies": undefined, - "descriptor": "1 SomeCollectionName from ", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "imageUrl", - ], - "nonce": 12345, - "otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Received", -} -`; - -exports[`parseRemote parseRemoteActivities should parse NFT transfer 1`] = ` -Object { - "chainId": 1, - "descriptor": "1 SomeCollectionName", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "imageUrl", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Minted", -} -`; - -exports[`parseRemote parseRemoteActivities should parse closed UniswapX order 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - ], - "descriptor": "100 DAI for 200 WETH", - "from": "someOfferer", - "hash": "someHash", - "logos": Array [ - "someUrl", - "someUrl", - ], - "offchainOrderStatus": "expired", - "prefixIconSrc": "bolt.svg", - "status": "FAILED", - "statusMessage": "Your swap could not be fulfilled at this time. Please try again.", - "timestamp": 10000, - "title": "Swap expired", -} -`; - -exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - ExtendedEther { - "chainId": 1, - "decimals": 18, - "isNative": true, - "isToken": false, - "name": "Ether", - "symbol": "ETH", - }, - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - ], - "descriptor": "100 ETH for 100 WETH", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "https://token-icons.s3.amazonaws.com/eth.png", - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Wrapped", -} -`; - -exports[`parseRemote parseRemoteActivities should parse nft purchase 1`] = ` -Object { - "chainId": 1, - "descriptor": "1 SomeCollectionName", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "imageUrl", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Bought", -} -`; - -exports[`parseRemote parseRemoteActivities should parse receive 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - ], - "descriptor": "100 WETH from ", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - ], - "nonce": 12345, - "otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Received", -} -`; - -exports[`parseRemote parseRemoteActivities should parse remove liquidity 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - ], - "descriptor": "100 WETH and 100 DAI", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - "logoUrl", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Removed Liquidity", -} -`; - -exports[`parseRemote parseRemoteActivities should parse send 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - ], - "descriptor": "100 DAI to ", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - ], - "nonce": 12345, - "otherAccount": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Sent", -} -`; - -exports[`parseRemote parseRemoteActivities should parse swap 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - ], - "descriptor": "100 DAI for 100 WETH", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Swapped", -} -`; - -exports[`parseRemote parseRemoteActivities should parse swap order 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - Token { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "Wrapped Ether", - "symbol": "WETH", - }, - ], - "descriptor": "100 DAI for 100 WETH", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - ], - "nonce": 12345, - "prefixIconSrc": "bolt.svg", - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Swapped", -} -`; - -exports[`parseRemote parseRemoteActivities should parse token approval 1`] = ` -Object { - "chainId": 1, - "currencies": Array [ - Token { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "chainId": 1, - "decimals": 18, - "isNative": false, - "isToken": true, - "name": "DAI", - "symbol": "DAI", - }, - ], - "descriptor": "DAI", - "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", - "hash": "someHash", - "logos": Array [ - "logoUrl", - ], - "nonce": 12345, - "status": "CONFIRMED", - "timestamp": 10000, - "title": "Approved", -} -`; diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts deleted file mode 100644 index edcc22e16fb..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, WETH9 } from '@uniswap/sdk-core' -import { DAI } from 'constants/tokens' -import { - AssetActivityPartsFragment, - Chain, - Currency, - NftStandard, - SwapOrderStatus, - TokenStandard, - TransactionDirection, - TransactionStatus, - TransactionType, -} from 'graphql/data/__generated__/types-and-hooks' - -const MockOrderTimestamp = 10000 -const MockRecipientAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' -const MockSenderAddress = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' - -const mockAssetActivityPartsFragment = { - __typename: 'AssetActivity', - id: 'activityId', - timestamp: MockOrderTimestamp, - chain: Chain.Ethereum, - details: { - __typename: 'SwapOrderDetails', - id: 'detailsId', - offerer: 'offererId', - hash: 'someHash', - inputTokenQuantity: '100', - outputTokenQuantity: '200', - orderStatus: SwapOrderStatus.Open, - inputToken: { - __typename: 'Token', - id: 'tokenId', - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - }, - outputToken: { - __typename: 'Token', - id: 'tokenId', - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - }, - }, -} - -const mockSwapOrderDetailsPartsFragment = { - __typename: 'SwapOrderDetails', - id: 'someId', - offerer: 'someOfferer', - hash: 'someHash', - inputTokenQuantity: '100', - outputTokenQuantity: '200', - orderStatus: SwapOrderStatus.Open, - inputToken: { - __typename: 'Token', - id: DAI.address, - name: 'DAI', - symbol: DAI.symbol, - address: DAI.address, - decimals: 18, - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - project: { - __typename: 'TokenProject', - id: 'projectId', - isSpam: false, - logo: { - __typename: 'Image', - id: 'imageId', - url: 'someUrl', - }, - }, - }, - outputToken: { - __typename: 'Token', - id: WETH9[1].address, - name: 'Wrapped Ether', - symbol: 'WETH', - address: WETH9[1].address, - decimals: 18, - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - project: { - __typename: 'TokenProject', - id: 'projectId', - isSpam: false, - logo: { - __typename: 'Image', - id: 'imageId', - url: 'someUrl', - }, - }, - }, -} - -const mockNftApprovalPartsFragment = { - __typename: 'NftApproval', - id: 'approvalId', - nftStandard: NftStandard.Erc721, // Replace with actual enum value - approvedAddress: '0xApprovedAddress', - asset: { - __typename: 'NftAsset', - id: 'assetId', - name: 'SomeNftName', - tokenId: 'tokenId123', - nftContract: { - __typename: 'NftContract', - id: 'nftContractId', - chain: Chain.Ethereum, // Replace with actual enum value - address: '0xContractAddress', - }, - image: { - __typename: 'Image', - id: 'imageId', - url: 'imageUrl', - }, - collection: { - __typename: 'NftCollection', - id: 'collectionId', - name: 'SomeCollectionName', - }, - }, -} - -const mockNftApproveForAllPartsFragment = { - __typename: 'NftApproveForAll', - id: 'approveForAllId', - nftStandard: NftStandard.Erc721, // Replace with actual enum value - operatorAddress: '0xOperatorAddress', - approved: true, - asset: { - __typename: 'NftAsset', - id: 'assetId', - name: 'SomeNftName', - tokenId: 'tokenId123', - nftContract: { - __typename: 'NftContract', - id: 'nftContractId', - chain: Chain.Ethereum, // Replace with actual enum value - address: '0xContractAddress', - }, - image: { - __typename: 'Image', - id: 'imageId', - url: 'imageUrl', - }, - collection: { - __typename: 'NftCollection', - id: 'collectionId', - name: 'SomeCollectionName', - }, - }, -} - -const mockNftTransferPartsFragment = { - __typename: 'NftTransfer', - id: 'transferId', - nftStandard: NftStandard.Erc721, - sender: MockSenderAddress, - recipient: MockRecipientAddress, - direction: TransactionDirection.Out, - asset: { - __typename: 'NftAsset', - id: 'assetId', - name: 'SomeNftName', - tokenId: 'tokenId123', - nftContract: { - __typename: 'NftContract', - id: 'nftContractId', - chain: Chain.Ethereum, - address: '0xContractAddress', - }, - image: { - __typename: 'Image', - id: 'imageId', - url: 'imageUrl', - }, - collection: { - __typename: 'NftCollection', - id: 'collectionId', - name: 'SomeCollectionName', - }, - }, -} - -const mockTokenTransferOutPartsFragment = { - __typename: 'TokenTransfer', - id: 'tokenTransferId', - tokenStandard: TokenStandard.Erc20, - quantity: '100', - sender: MockSenderAddress, - recipient: MockRecipientAddress, - direction: TransactionDirection.Out, - asset: { - __typename: 'Token', - id: DAI.address, - name: 'DAI', - symbol: 'DAI', - address: DAI.address, - decimals: 18, - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - project: { - __typename: 'TokenProject', - id: 'projectId', - isSpam: false, - logo: { - __typename: 'Image', - id: 'logoId', - url: 'logoUrl', - }, - }, - }, - transactedValue: { - __typename: 'Amount', - id: 'amountId', - currency: Currency.Usd, - value: 100, - }, -} - -const mockNativeTokenTransferOutPartsFragment = { - __typename: 'TokenTransfer', - id: 'tokenTransferId', - asset: { - __typename: 'Token', - id: 'ETH', - name: 'Ether', - symbol: 'ETH', - address: null, - decimals: 18, - chain: 'ETHEREUM', - standard: null, - project: { - __typename: 'TokenProject', - id: 'Ethereum', - isSpam: false, - logo: { - __typename: 'Image', - id: 'ETH_logo', - url: 'https://token-icons.s3.amazonaws.com/eth.png', - }, - }, - }, - tokenStandard: 'NATIVE', - quantity: '0.25', - sender: MockSenderAddress, - recipient: MockRecipientAddress, - direction: 'OUT', - transactedValue: { - __typename: 'Amount', - id: 'ETH_amount', - currency: 'USD', - value: 399.0225, - }, -} - -const mockWrappedEthTransferInPartsFragment = { - __typename: 'TokenTransfer', - id: 'tokenTransferId', - asset: { - __typename: 'Token', - id: 'WETH', - name: 'Wrapped Ether', - symbol: 'WETH', - address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - decimals: 18, - chain: 'ETHEREUM', - standard: 'ERC20', - project: { - __typename: 'TokenProject', - id: 'weth_project_id', - isSpam: false, - logo: { - __typename: 'Image', - id: 'weth_image', - url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', - }, - }, - }, - tokenStandard: 'ERC20', - quantity: '0.25', - sender: MockSenderAddress, - recipient: MockRecipientAddress, - direction: 'IN', - transactedValue: { - __typename: 'Amount', - id: 'mockWethAmountId', - currency: 'USD', - value: 399.1334007875, - }, -} - -const mockTokenTransferInPartsFragment = { - __typename: 'TokenTransfer', - id: 'tokenTransferId', - tokenStandard: TokenStandard.Erc20, - quantity: '1', - sender: MockSenderAddress, - recipient: MockRecipientAddress, - direction: TransactionDirection.In, - asset: { - __typename: 'Token', - id: WETH9[1].address, - name: 'Wrapped Ether', - symbol: 'WETH', - address: WETH9[1].address, - decimals: 18, - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - project: { - __typename: 'TokenProject', - id: 'projectId', - isSpam: false, - logo: { - __typename: 'Image', - id: 'logoId', - url: 'logoUrl', - }, - }, - }, - transactedValue: { - __typename: 'Amount', - id: 'amountId', - currency: Currency.Usd, - value: 100, - }, -} - -const mockTokenApprovalPartsFragment = { - __typename: 'TokenApproval', - id: 'tokenApprovalId', - tokenStandard: TokenStandard.Erc20, - approvedAddress: DAI.address, - quantity: '50', - asset: { - __typename: 'Token', - id: 'tokenId', - name: 'DAI', - symbol: 'DAI', - address: DAI.address, - decimals: 18, - chain: Chain.Ethereum, - standard: TokenStandard.Erc20, - project: { - __typename: 'TokenProject', - id: 'projectId', - isSpam: false, - logo: { - __typename: 'Image', - id: 'logoId', - url: 'logoUrl', - }, - }, - }, -} - -export const MockOpenUniswapXOrder = { - ...mockAssetActivityPartsFragment, - details: mockSwapOrderDetailsPartsFragment, -} as AssetActivityPartsFragment - -export const MockClosedUniswapXOrder = { - ...mockAssetActivityPartsFragment, - details: { - ...mockSwapOrderDetailsPartsFragment, - orderStatus: SwapOrderStatus.Expired, - }, -} as AssetActivityPartsFragment - -const commonTransactionDetailsFields = { - __typename: 'TransactionDetails', - from: MockSenderAddress, - hash: 'someHash', - id: 'transactionId', - nonce: 12345, - status: TransactionStatus.Confirmed, - to: MockRecipientAddress, -} - -export const MockNFTApproval = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Approve, - assetChanges: [mockNftApprovalPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockNFTApprovalForAll = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Approve, - assetChanges: [mockNftApproveForAllPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockNFTTransfer = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Mint, - assetChanges: [mockNftTransferPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockTokenTransfer = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Swap, - assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockSwapOrder = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.SwapOrder, - assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockTokenApproval = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Approve, - assetChanges: [mockTokenApprovalPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockTokenSend = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Send, - assetChanges: [mockTokenTransferOutPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockTokenReceive = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Receive, - assetChanges: [mockTokenTransferInPartsFragment], - }, -} as AssetActivityPartsFragment - -export const MockRemoveLiquidity = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ChainId.MAINNET], - type: TransactionType.Receive, - assetChanges: [ - mockTokenTransferInPartsFragment, - { - ...mockTokenTransferOutPartsFragment, - direction: TransactionDirection.In, - }, - ], - }, -} as AssetActivityPartsFragment - -export const MockNFTReceive = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Receive, - assetChanges: [ - { - ...mockNftTransferPartsFragment, - direction: TransactionDirection.In, - }, - ], - }, -} as AssetActivityPartsFragment - -export const MockNFTPurchase = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Swap, - assetChanges: [ - mockTokenTransferOutPartsFragment, - { - ...mockNftTransferPartsFragment, - direction: TransactionDirection.In, - }, - ], - }, -} as AssetActivityPartsFragment - -export const MockWrap = { - ...mockAssetActivityPartsFragment, - details: { - ...commonTransactionDetailsFields, - type: TransactionType.Lend, - assetChanges: [mockNativeTokenTransferOutPartsFragment, mockWrappedEthTransferInPartsFragment], - }, -} as AssetActivityPartsFragment diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts index acb91b111e6..0d6e64842bc 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts +++ b/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts @@ -1,95 +1,5 @@ -import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks' -import { useEffect, useMemo } from 'react' import { usePendingOrders } from 'state/signatures/hooks' -import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks' -import { useFormatter } from 'utils/formatNumbers' - -import { useLocalActivities } from './parseLocal' -import { parseRemoteActivities } from './parseRemote' -import { Activity, ActivityMap } from './types' - -/** Detects transactions from same account with the same nonce and different hash */ -function findCancelTx(localActivity: Activity, remoteMap: ActivityMap, account: string): string | undefined { - // handles locally cached tx's that were stored before we started tracking nonces - if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return undefined - - for (const remoteTx of Object.values(remoteMap)) { - if (!remoteTx) continue - - // A pending tx is 'cancelled' when another tx with the same account & nonce but different hash makes it on chain - if ( - remoteTx.nonce === localActivity.nonce && - remoteTx.from.toLowerCase() === account.toLowerCase() && - remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase() && - remoteTx.chainId === localActivity.chainId - ) { - return remoteTx.hash - } - } - - return undefined -} - -/** Deduplicates local and remote activities */ -function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array { - const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])] - - return txHashes.reduce((acc: Array, hash) => { - const localActivity = (localMap?.[hash] ?? {}) as Activity - const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity - - if (localActivity.cancelled) { - // Hides misleading activities caused by cross-chain nonce collisions previously being incorrectly labelled as cancelled txs in redux - if (localActivity.chainId !== remoteActivity.chainId) { - acc.push(remoteActivity) - return acc - } - // Remote data only contains data of the cancel tx, rather than the original tx, so we prefer local data here - acc.push(localActivity) - } else { - // Generally prefer remote values to local value because i.e. remote swap amounts are on-chain rather than client-estimated - acc.push({ ...localActivity, ...remoteActivity } as Activity) - } - - return acc - }, []) -} - -export function useAllActivities(account: string) { - const { formatNumberOrString } = useFormatter() - const { data, loading, refetch } = useActivityQuery({ - variables: { account }, - errorPolicy: 'all', - fetchPolicy: 'cache-first', - }) - - const localMap = useLocalActivities(account) - const remoteMap = useMemo( - () => parseRemoteActivities(formatNumberOrString, data?.portfolios?.[0].assetActivities), - [data?.portfolios, formatNumberOrString] - ) - const updateCancelledTx = useTransactionCanceller() - - /* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */ - useEffect(() => { - if (!remoteMap) return - - Object.values(localMap).forEach((localActivity) => { - if (!localActivity) return - - const cancelHash = findCancelTx(localActivity, remoteMap, account) - - if (cancelHash) updateCancelledTx(localActivity.hash, localActivity.chainId, cancelHash) - }) - }, [account, localMap, remoteMap, updateCancelledTx]) - - const combinedActivities = useMemo( - () => (remoteMap ? combineActivities(localMap, remoteMap) : undefined), - [localMap, remoteMap] - ) - - return { loading, activities: combinedActivities, refetch } -} +import { usePendingTransactions } from 'state/transactions/hooks' export function usePendingActivity() { const pendingTransactions = usePendingTransactions() diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx deleted file mode 100644 index 0bf8d54d936..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useAccountDrawer } from 'components/AccountDrawer' -import Column from 'components/Column' -import { LoadingBubble } from 'components/Tokens/loading' -import { PollingInterval } from 'graphql/data/util' -import { atom, useAtom } from 'jotai' -import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' -import { useEffect, useMemo } from 'react' -import styled from 'styled-components' -import { ThemedText } from 'theme/components' - -import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' -import { ActivityRow } from './ActivityRow' -import { useAllActivities } from './hooks' -import { createGroups } from './utils' - -const ActivityGroupWrapper = styled(Column)` - margin-top: 16px; - gap: 8px; -` - -const lastFetchedAtom = atom(0) - -export function ActivityTab({ account }: { account: string }) { - const [drawerOpen, toggleWalletDrawer] = useAccountDrawer() - const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom) - - const { activities, loading, refetch } = useAllActivities(account) - - // We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer - useEffect(() => { - const currentTime = Date.now() - if (!lastFetched) { - setLastFetched(currentTime) - } else if (drawerOpen && lastFetched && currentTime - lastFetched > PollingInterval.Slow) { - refetch() - setLastFetched(currentTime) - } - }, [drawerOpen, lastFetched, refetch, setLastFetched]) - - const activityGroups = useMemo(() => createGroups(activities), [activities]) - - if (!activityGroups && loading) { - return ( - <> - - - - ) - } else if (!activityGroups || activityGroups?.length === 0) { - return - } else { - return ( - - {activityGroups.map((activityGroup) => ( - - - {activityGroup.title} - - - {activityGroup.transactions.map((activity) => ( - - ))} - - - ))} - - ) - } -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts deleted file mode 100644 index 5d5d8269d4f..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { ChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core' -import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk' -import { DAI as MockDAI, nativeOnChain, USDC_MAINNET as MockUSDC_MAINNET, USDT as MockUSDT } from 'constants/tokens' -import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks' -import { ChainTokenMap } from 'hooks/Tokens' -import { - ExactInputSwapTransactionInfo, - ExactOutputSwapTransactionInfo, - TransactionDetails, - TransactionInfo, - TransactionType as MockTxType, -} from 'state/transactions/types' -import { renderHook } from 'test-utils/render' -import { useFormatter } from 'utils/formatNumbers' - -import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types' -import { SignatureDetails, SignatureType } from '../../../../state/signatures/types' -import { signatureToActivity, transactionToActivity, useLocalActivities } from './parseLocal' - -function mockSwapInfo( - type: MockTradeType, - inputCurrency: Token, - inputCurrencyAmountRaw: string, - outputCurrency: Token, - outputCurrencyAmountRaw: string -): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { - if (type === MockTradeType.EXACT_INPUT) { - return { - type: MockTxType.SWAP, - tradeType: MockTradeType.EXACT_INPUT, - inputCurrencyId: inputCurrency.address, - inputCurrencyAmountRaw, - outputCurrencyId: outputCurrency.address, - expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw, - minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw, - isUniswapXOrder: false, - } - } else { - return { - type: MockTxType.SWAP, - tradeType: MockTradeType.EXACT_OUTPUT, - inputCurrencyId: inputCurrency.address, - expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw, - maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw, - outputCurrencyId: outputCurrency.address, - outputCurrencyAmountRaw, - isUniswapXOrder: false, - } - } -} - -const mockAccount1 = '0x000000000000000000000000000000000000000001' -const mockAccount2 = '0x000000000000000000000000000000000000000002' -const mockChainId = ChainId.MAINNET -const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId] -const mockCurrencyAmountRaw = '1000000000000000000' -const mockCurrencyAmountRawUSDC = '1000000' -const mockApprovalAmountRaw = '10000000' - -function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) { - return id + status -} - -function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) { - const hash = mockHash(id, status) - return { - hash, - from: account, - receipt: - status === MockTxStatus.Pending - ? undefined - : { - transactionHash: hash, - status: status === MockTxStatus.Confirmed ? 1 : 0, - }, - addedTime: 0, - } -} - -function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] { - // Mocks a transaction with multiple statuses - return [ - [ - { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails, - mockChainId, - ], - [ - { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails, - mockChainId, - ], - [ - { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails, - mockChainId, - ], - ] -} - -const mockTokenAddressMap: ChainTokenMap = { - [mockChainId]: { - [MockDAI.address]: MockDAI, - [MockUSDC_MAINNET.address]: MockUSDC_MAINNET, - [MockUSDT.address]: MockUSDT, - }, -} - -jest.mock('../../../../hooks/Tokens', () => ({ - useAllTokensMultichain: () => mockTokenAddressMap, -})) - -jest.mock('../../../../state/transactions/hooks', () => { - return { - useMultichainTransactions: (): [TransactionDetails, number][] => { - return [ - [ - { - info: mockSwapInfo( - MockTradeType.EXACT_INPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - ...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed), - } as TransactionDetails, - mockChainId, - ], - ...mockMultiStatus( - mockSwapInfo( - MockTradeType.EXACT_OUTPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - '0xswap_exact_input' - ), - ...mockMultiStatus( - mockSwapInfo( - MockTradeType.EXACT_INPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - '0xswap_exact_output' - ), - ...mockMultiStatus( - { - type: MockTxType.APPROVAL, - tokenAddress: MockDAI.address, - spender: mockSpenderAddress, - amount: mockApprovalAmountRaw, - }, - '0xapproval' - ), - ...mockMultiStatus( - { - type: MockTxType.APPROVAL, - tokenAddress: MockUSDT.address, - spender: mockSpenderAddress, - amount: '0', - }, - '0xrevoke_approval' - ), - ...mockMultiStatus( - { - type: MockTxType.WRAP, - unwrapped: false, - currencyAmountRaw: mockCurrencyAmountRaw, - chainId: mockChainId, - }, - '0xwrap' - ), - ...mockMultiStatus( - { - type: MockTxType.WRAP, - unwrapped: true, - currencyAmountRaw: mockCurrencyAmountRaw, - chainId: mockChainId, - }, - '0xunwrap' - ), - ...mockMultiStatus( - { - type: MockTxType.ADD_LIQUIDITY_V3_POOL, - createPool: false, - baseCurrencyId: MockUSDC_MAINNET.address, - quoteCurrencyId: MockDAI.address, - feeAmount: 500, - expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, - expectedAmountQuoteRaw: mockCurrencyAmountRaw, - }, - '0xadd_liquidity_v3' - ), - ...mockMultiStatus( - { - type: MockTxType.REMOVE_LIQUIDITY_V3, - baseCurrencyId: MockUSDC_MAINNET.address, - quoteCurrencyId: MockDAI.address, - expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, - expectedAmountQuoteRaw: mockCurrencyAmountRaw, - }, - '0xremove_liquidity_v3' - ), - ...mockMultiStatus( - { - type: MockTxType.ADD_LIQUIDITY_V2_POOL, - baseCurrencyId: MockUSDC_MAINNET.address, - quoteCurrencyId: MockDAI.address, - expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, - expectedAmountQuoteRaw: mockCurrencyAmountRaw, - }, - '0xadd_liquidity_v2' - ), - ...mockMultiStatus( - { - type: MockTxType.COLLECT_FEES, - currencyId0: MockUSDC_MAINNET.address, - currencyId1: MockDAI.address, - expectedCurrencyOwed0: mockCurrencyAmountRawUSDC, - expectedCurrencyOwed1: mockCurrencyAmountRaw, - }, - '0xcollect_fees' - ), - ...mockMultiStatus( - { - type: MockTxType.MIGRATE_LIQUIDITY_V3, - baseCurrencyId: MockUSDC_MAINNET.address, - quoteCurrencyId: MockDAI.address, - isFork: false, - }, - '0xmigrate_v3_liquidity' - ), - ] - }, - } -}) - -describe('parseLocalActivity', () => { - it('returns swap activity fields with known tokens, exact input', () => { - const { formatNumber } = renderHook(() => useFormatter()).result.current - - const details = { - info: mockSwapInfo( - MockTradeType.EXACT_INPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - receipt: { - transactionHash: '0x123', - status: 1, - }, - } as TransactionDetails - const chainId = ChainId.MAINNET - expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toEqual({ - chainId: 1, - currencies: [MockUSDC_MAINNET, MockDAI], - descriptor: '1.00 USDC for 1.00 DAI', - hash: undefined, - from: undefined, - status: 'CONFIRMED', - timestamp: NaN, - title: 'Swapped', - }) - }) - - it('returns swap activity fields with known tokens, exact output', () => { - const { formatNumber } = renderHook(() => useFormatter()).result.current - - const details = { - info: mockSwapInfo( - MockTradeType.EXACT_OUTPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - receipt: { - transactionHash: '0x123', - status: 1, - }, - } as TransactionDetails - const chainId = ChainId.MAINNET - expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toMatchObject({ - chainId: 1, - currencies: [MockUSDC_MAINNET, MockDAI], - descriptor: '1.00 USDC for 1.00 DAI', - status: 'CONFIRMED', - title: 'Swapped', - }) - }) - - it('returns swap activity fields with unknown tokens', () => { - const { formatNumber } = renderHook(() => useFormatter()).result.current - - const details = { - info: mockSwapInfo( - MockTradeType.EXACT_INPUT, - MockUSDC_MAINNET, - mockCurrencyAmountRawUSDC, - MockDAI, - mockCurrencyAmountRaw - ), - receipt: { - transactionHash: '0x123', - status: 1, - }, - } as TransactionDetails - const chainId = ChainId.MAINNET - const tokens = {} as ChainTokenMap - expect(transactionToActivity(details, chainId, tokens, formatNumber)).toMatchObject({ - chainId: 1, - currencies: [undefined, undefined], - descriptor: 'Unknown for Unknown', - status: 'CONFIRMED', - title: 'Swapped', - }) - }) - - it('only returns activity for the current account', () => { - const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current - const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current - - expect(Object.values(account1Activites)).toHaveLength(1) - expect(Object.values(account2Activites)).toHaveLength(33) - }) - - it('Properly uses correct tense of activity title based on tx status', () => { - const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current - - expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping') - expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped') - expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed') - }) - - it('Adapts Swap exact input to Activity type', () => { - const hash = mockHash('0xswap_exact_input') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Swapped', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts Swap exact output to Activity type', () => { - const hash = mockHash('0xswap_exact_output') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Swapped', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts Approval to Activity type', () => { - const hash = mockHash('0xapproval') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockDAI], - title: 'Approved', - descriptor: MockDAI.symbol, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts Revoke Approval to Activity type', () => { - const hash = mockHash('0xrevoke_approval') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDT], - title: 'Revoked approval', - descriptor: MockUSDT.symbol, - hash, - status: MockTxStatus.Confirmed, - }) - }) - - it('Adapts Wrap to Activity type', () => { - const hash = mockHash('0xwrap') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - const native = nativeOnChain(mockChainId) - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [native, native.wrapped], - title: 'Wrapped', - descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts Unwrap to Activity type', () => { - const hash = mockHash('0xunwrap') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - const native = nativeOnChain(mockChainId) - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [native.wrapped, native], - title: 'Unwrapped', - descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts AddLiquidityV3 to Activity type', () => { - const hash = mockHash('0xadd_liquidity_v3') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Added liquidity', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts RemoveLiquidityV3 to Activity type', () => { - const hash = mockHash('0xremove_liquidity_v3') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Removed liquidity', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts RemoveLiquidityV2 to Activity type', () => { - const hash = mockHash('0xadd_liquidity_v2') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Added V2 liquidity', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts CollectFees to Activity type', () => { - const hash = mockHash('0xcollect_fees') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Collected fees', - descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Adapts MigrateLiquidityV3 to Activity type', () => { - const hash = mockHash('0xmigrate_v3_liquidity') - const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] - - expect(activity).toMatchObject({ - chainId: mockChainId, - currencies: [MockUSDC_MAINNET, MockDAI], - title: 'Migrated liquidity', - descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`, - hash, - status: MockTxStatus.Confirmed, - from: mockAccount2, - }) - }) - - it('Signature to activity - returns undefined if is on chain order', () => { - const { formatNumber } = renderHook(() => useFormatter()).result.current - - expect( - signatureToActivity( - { - type: SignatureType.SIGN_UNISWAPX_ORDER, - status: UniswapXOrderStatus.FILLED, - } as SignatureDetails, - {}, - formatNumber - ) - ).toBeUndefined() - - expect( - signatureToActivity( - { - type: SignatureType.SIGN_UNISWAPX_ORDER, - status: UniswapXOrderStatus.CANCELLED, - } as SignatureDetails, - {}, - formatNumber - ) - ).toBeUndefined() - }) -}) diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts index 9d4e3d7d149..1935f6da1d9 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts +++ b/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts @@ -4,11 +4,9 @@ import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import UniswapXBolt from 'assets/svg/bolt.svg' import { nativeOnChain } from 'constants/tokens' import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' -import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens' -import { useMemo } from 'react' -import { isOnChainOrder, useAllSignatures } from 'state/signatures/hooks' +import { ChainTokenMap } from 'hooks/Tokens' +import { isOnChainOrder } from 'state/signatures/hooks' import { SignatureDetails, SignatureType } from 'state/signatures/types' -import { useMultichainTransactions } from 'state/transactions/hooks' import { AddLiquidityV2PoolTransactionInfo, AddLiquidityV3PoolTransactionInfo, @@ -26,7 +24,7 @@ import { import { NumberType, useFormatter } from 'utils/formatNumbers' import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants' -import { Activity, ActivityMap } from './types' +import { Activity } from './types' type FormatNumberFunctionType = ReturnType['formatNumber'] @@ -265,29 +263,3 @@ export function signatureToActivity( return undefined } } - -export function useLocalActivities(account: string): ActivityMap { - const allTransactions = useMultichainTransactions() - const allSignatures = useAllSignatures() - const tokens = useAllTokensMultichain() - const { formatNumber } = useFormatter() - - return useMemo(() => { - const activityMap: ActivityMap = {} - for (const [transaction, chainId] of allTransactions) { - if (transaction.from !== account) continue - - const activity = transactionToActivity(transaction, chainId, tokens, formatNumber) - if (activity) activityMap[transaction.hash] = activity - } - - for (const signature of Object.values(allSignatures)) { - if (signature.offerer !== account) continue - - const activity = signatureToActivity(signature, tokens, formatNumber) - if (activity) activityMap[signature.id] = activity - } - - return activityMap - }, [account, allSignatures, allTransactions, formatNumber, tokens]) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx b/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx deleted file mode 100644 index f07d9e20358..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import ms from 'ms' - -import { - MockClosedUniswapXOrder, - MockNFTApproval, - MockNFTApprovalForAll, - MockNFTPurchase, - MockNFTReceive, - MockNFTTransfer, - MockOpenUniswapXOrder, - MockRemoveLiquidity, - MockSwapOrder, - MockTokenApproval, - MockTokenReceive, - MockTokenSend, - MockTokenTransfer, - MockWrap, -} from './fixtures/activity' -import { parseRemoteActivities, useTimeSince } from './parseRemote' - -describe('parseRemote', () => { - beforeEach(() => { - jest.useFakeTimers() - }) - describe.skip('parseRemoteActivities', () => { - it('should not parse open UniswapX order', () => { - const result = parseRemoteActivities(jest.fn(), [MockOpenUniswapXOrder]) - expect(result).toEqual({}) - }) - it('should parse closed UniswapX order', () => { - const result = parseRemoteActivities(jest.fn(), [MockClosedUniswapXOrder]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse NFT approval', () => { - const result = parseRemoteActivities(jest.fn(), [MockNFTApproval]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse NFT approval for all', () => { - const result = parseRemoteActivities(jest.fn(), [MockNFTApprovalForAll]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse NFT transfer', () => { - const result = parseRemoteActivities(jest.fn(), [MockNFTTransfer]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse swap', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockTokenTransfer]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse nft purchase', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockNFTPurchase]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse token approval', () => { - const result = parseRemoteActivities(jest.fn(), [MockTokenApproval]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse send', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenSend]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse receive', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenReceive]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse NFT receive', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockNFTReceive]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse remove liquidity', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockRemoveLiquidity]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse swap order', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockSwapOrder]) - expect(result?.['someHash']).toMatchSnapshot() - }) - it('should parse eth wrap', () => { - const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockWrap]) - expect(result?.['someHash']).toMatchSnapshot() - }) - }) - - describe('useTimeSince', () => { - beforeEach(() => { - jest.useFakeTimers() - }) - - afterEach(() => { - jest.useRealTimers() - }) - - it('should initialize with the correct time since', () => { - const timestamp = Math.floor(Date.now() / 1000) - 60 // 60 seconds ago - const { result } = renderHook(() => useTimeSince(timestamp)) - - expect(result.current).toBe('1m') - }) - - it('should update time since every second', async () => { - const timestamp = Math.floor(Date.now() / 1000) - 50 // 50 seconds ago - const { result, rerender } = renderHook(() => useTimeSince(timestamp)) - - act(() => { - jest.advanceTimersByTime(ms('1.1s')) - }) - rerender() - - expect(result.current).toBe('51s') - }) - - it('should stop updating after 61 seconds', () => { - const timestamp = Math.floor(Date.now() / 1000) - 61 // 61 seconds ago - const { result, rerender } = renderHook(() => useTimeSince(timestamp)) - - act(() => { - jest.advanceTimersByTime(ms('121.1s')) - }) - rerender() - - // maxes out at 1m - expect(result.current).toBe('1m') - }) - }) -}) diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx b/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx deleted file mode 100644 index b9ce67ceaf8..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx +++ /dev/null @@ -1,458 +0,0 @@ -import { t } from '@lingui/macro' -import { ChainId, Currency, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core' -import UniswapXBolt from 'assets/svg/bolt.svg' -import { nativeOnChain } from 'constants/tokens' -import { - ActivityType, - AssetActivityPartsFragment, - NftApprovalPartsFragment, - NftApproveForAllPartsFragment, - NftTransferPartsFragment, - SwapOrderDetailsPartsFragment, - SwapOrderStatus, - TokenApprovalPartsFragment, - TokenAssetPartsFragment, - TokenTransferPartsFragment, - TransactionDetailsPartsFragment, -} from 'graphql/data/__generated__/types-and-hooks' -import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util' -import ms from 'ms' -import { useEffect, useState } from 'react' -import { isAddress } from 'utils' -import { isSameAddress } from 'utils/addresses' -import { NumberType, useFormatter } from 'utils/formatNumbers' - -import { OrderStatusTable, OrderTextTable } from '../constants' -import { Activity } from './types' - -type TransactionChanges = { - NftTransfer: NftTransferPartsFragment[] - TokenTransfer: TokenTransferPartsFragment[] - TokenApproval: TokenApprovalPartsFragment[] - NftApproval: NftApprovalPartsFragment[] - NftApproveForAll: NftApproveForAllPartsFragment[] -} - -type FormatNumberOrStringFunctionType = ReturnType['formatNumberOrString'] - -// TODO: Move common contract metadata to a backend service -const UNI_IMG = - 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png' - -const ENS_IMG = - 'https://464911102-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/collections%2F2TjMAeHSzwlQgcOdL48E%2Ficon%2FKWP0gk2C6bdRPliWIA6o%2Fens%20transparent%20background.png?alt=media&token=bd28b063-5a75-4971-890c-97becea09076' - -const COMMON_CONTRACTS: { [key: string]: Partial | undefined } = { - [UNI_ADDRESSES[ChainId.MAINNET].toLowerCase()]: { - title: t`UNI Governance`, - descriptor: t`Contract Interaction`, - logos: [UNI_IMG], - }, - // TODO(cartcrom): Add permit2-specific logo - '0x000000000022d473030f116ddee9f6b43ac78ba3': { - title: t`Permit2`, - descriptor: t`Uniswap Protocol`, - logos: [UNI_IMG], - }, - '0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41': { - title: t`Ethereum Name Service`, - descriptor: t`Public Resolver`, - logos: [ENS_IMG], - }, - '0x58774bb8acd458a640af0b88238369a167546ef2': { - title: t`Ethereum Name Service`, - descriptor: t`DNS Registrar`, - logos: [ENS_IMG], - }, - '0x084b1c3c81545d370f3634392de611caabff8148': { - title: t`Ethereum Name Service`, - descriptor: t`Reverse Registrar`, - logos: [ENS_IMG], - }, - '0x283af0b28c62c092c9727f1ee09c02ca627eb7f5': { - title: t`Ethereum Name Service`, - descriptor: t`ETH Registrar Controller`, - logos: [ENS_IMG], - }, -} - -function callsPositionManagerContract(assetActivity: TransactionActivity) { - const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) - if (!supportedChain) return false - return isSameAddress(assetActivity.details.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain]) -} - -// Gets counts for number of NFTs in each collection present -function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key: string]: number | undefined } { - return nftTransfers.reduce((acc, NFTChange) => { - const key = NFTChange.asset.collection?.name ?? NFTChange.asset.name - if (key) { - acc[key] = (acc?.[key] ?? 0) + 1 - } - return acc - }, {} as { [key: string]: number | undefined }) -} - -function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment): string | undefined { - const supportedSentChain = supportedChainIdFromGQLChain(sent.asset.chain) - const supportedReceivedChain = supportedChainIdFromGQLChain(received.asset.chain) - if (!supportedSentChain || !supportedReceivedChain) { - logSentryErrorForUnsupportedChain({ - extras: { sentAsset: sent.asset, receivedAsset: received.asset }, - errorMessage: 'Invalid activity from unsupported chain received from GQL', - }) - return undefined - } - if ( - sent.tokenStandard === 'NATIVE' && - isSameAddress(nativeOnChain(supportedSentChain).wrapped.address, received.asset.address) - ) - return t`Wrapped` - else if ( - received.tokenStandard === 'NATIVE' && - isSameAddress(nativeOnChain(supportedReceivedChain).wrapped.address, received.asset.address) - ) { - return t`Unwrapped` - } else { - return t`Swapped` - } -} - -function getSwapDescriptor({ - tokenIn, - inputAmount, - tokenOut, - outputAmount, -}: { - tokenIn: TokenAssetPartsFragment - outputAmount: string - tokenOut: TokenAssetPartsFragment - inputAmount: string -}) { - return `${inputAmount} ${tokenIn.symbol} for ${outputAmount} ${tokenOut.symbol}` -} - -function parseSwap(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { - if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) { - const collectionCounts = getCollectionCounts(changes.NftTransfer) - - const title = changes.NftTransfer[0].direction === 'IN' ? t`Bought` : t`Sold` - const descriptor = Object.entries(collectionCounts) - .map(([collectionName, count]) => `${count} ${collectionName}`) - .join() - - return { title, descriptor } - } - // 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.direction === 'OUT') - // Any leftover native token is refunded on exact_out swaps where the input token is native - const refund = changes.TokenTransfer.find( - (t) => t.direction === 'IN' && t.asset.id === sent?.asset.id && t.asset.standard === 'NATIVE' - ) - const received = changes.TokenTransfer.find((t) => t.direction === 'IN' && t !== refund) - - if (sent && received) { - const adjustedInput = parseFloat(sent.quantity) - parseFloat(refund?.quantity ?? '0') - const inputAmount = formatNumberOrString({ input: adjustedInput, type: NumberType.TokenNonTx }) - const outputAmount = formatNumberOrString({ input: received.quantity, type: NumberType.TokenNonTx }) - return { - title: getSwapTitle(sent, received), - descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }), - currencies: [gqlToCurrency(sent.asset), gqlToCurrency(received.asset)], - } - } - } - return { title: t`Unknown Swap` } -} - -/** - * Wrap/unwrap transactions are labelled as lend transactions on the backend. - * This function parses the transaction changes to determine if the transaction is a wrap/unwrap transaction. - */ -function parseLend(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { - const native = changes.TokenTransfer.find((t) => t.tokenStandard === 'NATIVE')?.asset - const erc20 = changes.TokenTransfer.find((t) => t.tokenStandard === 'ERC20')?.asset - if (native && erc20 && gqlToCurrency(native)?.wrapped.address === gqlToCurrency(erc20)?.wrapped.address) { - return parseSwap(changes, formatNumberOrString) - } - return { title: t`Unknown Lend` } -} - -function parseSwapOrder(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { - return { ...parseSwap(changes, formatNumberOrString), prefixIconSrc: UniswapXBolt } -} - -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}` - const currencies = [gqlToCurrency(changes.TokenApproval[0].asset)] - return { title, descriptor, currencies } - } - return { title: t`Unknown Approval` } -} - -function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { - const poolTokenA = changes.TokenTransfer[0] - const poolTokenB = changes.TokenTransfer[1] - - const tokenAQuanitity = formatNumberOrString({ input: poolTokenA.quantity, type: NumberType.TokenNonTx }) - const tokenBQuantity = formatNumberOrString({ input: poolTokenB.quantity, type: NumberType.TokenNonTx }) - - 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)], - } -} - -type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment } -type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment } - -function parseSendReceive( - changes: TransactionChanges, - formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity -) { - // TODO(cartcrom): remove edge cases after backend implements - // Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend - if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) { - return { title: t`Removed Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) } - } - - 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] - assetName = transfer.asset.collection?.name - amount = '1' - } else if (changes.TokenTransfer.length === 1) { - transfer = changes.TokenTransfer[0] - assetName = transfer.asset.symbol - amount = formatNumberOrString({ input: transfer.quantity, type: NumberType.TokenNonTx }) - currencies = [gqlToCurrency(transfer.asset)] - } - - if (transfer && assetName && amount) { - if (transfer.direction === 'IN') { - return { - 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, - } - } - } - return { title: t`Unknown Send` } -} - -function parseMint( - changes: TransactionChanges, - formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity -) { - const collectionMap = getCollectionCounts(changes.NftTransfer) - if (Object.keys(collectionMap).length === 1) { - const collectionName = Object.keys(collectionMap)[0] - - // Edge case: Minting a v3 positon represents adding liquidity - if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) { - return { title: t`Added Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) } - } - return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` } - } - return { title: t`Unknown Mint` } -} - -function parseUnknown( - _changes: TransactionChanges, - _formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity -) { - return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] } -} - -type ActivityTypeParser = ( - changes: TransactionChanges, - formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity -) => Partial -const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = { - [ActivityType.Swap]: parseSwap, - [ActivityType.Lend]: parseLend, - [ActivityType.SwapOrder]: parseSwapOrder, - [ActivityType.Approve]: parseApprove, - [ActivityType.Send]: parseSendReceive, - [ActivityType.Receive]: parseSendReceive, - [ActivityType.Mint]: parseMint, - [ActivityType.Unknown]: parseUnknown, -} - -function getLogoSrcs(changes: TransactionChanges): Array { - // Uses set to avoid duplicate logos (e.g. nft's w/ same image url) - const logoSet = new Set() - // Uses only NFT logos if they are present (will not combine nft image w/ token image) - if (changes.NftTransfer.length > 0) { - changes.NftTransfer.forEach((nftChange) => logoSet.add(nftChange.asset.image?.url)) - } else { - 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) -} - -function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined { - // We currently only have a polling mechanism for locally-sent pending orders, so we hide remote pending orders since they won't update upon completion - // TODO(WEB-2487): Add polling mechanism for remote orders to allow displaying remote pending orders - if (details.orderStatus === SwapOrderStatus.Open) return undefined - - const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity, orderStatus } = details - const uniswapXOrderStatus = OrderStatusTable[orderStatus] - const { status, statusMessage, title } = OrderTextTable[uniswapXOrderStatus] - const descriptor = getSwapDescriptor({ - tokenIn: inputToken, - inputAmount: inputTokenQuantity, - tokenOut: outputToken, - outputAmount: outputTokenQuantity, - }) - - const supportedChain = supportedChainIdFromGQLChain(chain) - if (!supportedChain) { - logSentryErrorForUnsupportedChain({ - extras: { details }, - errorMessage: 'Invalid activity from unsupported chain received from GQL', - }) - return undefined - } - - return { - hash: details.hash, - chainId: supportedChain, - status, - statusMessage, - offchainOrderStatus: uniswapXOrderStatus, - timestamp, - logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url], - currencies: [gqlToCurrency(inputToken), gqlToCurrency(outputToken)], - title, - descriptor, - from: details.offerer, - prefixIconSrc: UniswapXBolt, - } -} - -function parseRemoteActivity( - assetActivity: AssetActivityPartsFragment, - formatNumberOrString: FormatNumberOrStringFunctionType -): Activity | undefined { - try { - if (assetActivity.details.__typename === 'SwapOrderDetails') { - return parseUniswapXOrder(assetActivity as OrderActivity) - } - - const changes = assetActivity.details.assetChanges.reduce( - (acc: TransactionChanges, assetChange) => { - if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange) - else if (assetChange.__typename === 'NftApproveForAll') acc.NftApproveForAll.push(assetChange) - else if (assetChange.__typename === 'NftTransfer') acc.NftTransfer.push(assetChange) - else if (assetChange.__typename === 'TokenTransfer') acc.TokenTransfer.push(assetChange) - else if (assetChange.__typename === 'TokenApproval') acc.TokenApproval.push(assetChange) - - return acc - }, - { NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] } - ) - const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) - if (!supportedChain) { - logSentryErrorForUnsupportedChain({ - extras: { assetActivity }, - errorMessage: 'Invalid activity from unsupported chain received from GQL', - }) - return undefined - } - - const defaultFields = { - hash: assetActivity.details.hash, - chainId: supportedChain, - status: assetActivity.details.status, - timestamp: assetActivity.timestamp, - logos: getLogoSrcs(changes), - title: assetActivity.details.type, - descriptor: assetActivity.details.to, - from: assetActivity.details.from, - nonce: assetActivity.details.nonce, - } - - const parsedFields = ActivityParserByType[assetActivity.details.type]?.( - changes, - formatNumberOrString, - assetActivity as TransactionActivity - ) - return { ...defaultFields, ...parsedFields } - } catch (e) { - console.error('Failed to parse activity', e, assetActivity) - return undefined - } -} - -export function parseRemoteActivities( - formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivities?: readonly AssetActivityPartsFragment[] -) { - return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => { - const activity = parseRemoteActivity(assetActivity, formatNumberOrString) - if (activity) acc[activity.hash] = activity - return acc - }, {}) -} - -const getTimeSince = (timestamp: number) => { - const seconds = Math.floor(Date.now() - timestamp * 1000) - - let interval - // TODO(cartcrom): use locale to determine date shorthands to use for non-english - if ((interval = seconds / ms(`1y`)) > 1) return Math.floor(interval) + 'y' - if ((interval = seconds / ms(`30d`)) > 1) return Math.floor(interval) + 'mo' - if ((interval = seconds / ms(`1d`)) > 1) return Math.floor(interval) + 'd' - if ((interval = seconds / ms(`1h`)) > 1) return Math.floor(interval) + 'h' - if ((interval = seconds / ms(`1m`)) > 1) return Math.floor(interval) + 'm' - else return Math.floor(seconds / ms(`1s`)) + 's' -} - -/** - * Keeps track of the time since a given timestamp, keeping it up to date every second when necessary - * @param timestamp - * @returns - */ -export function useTimeSince(timestamp: number) { - const [timeSince, setTimeSince] = useState(getTimeSince(timestamp)) - - useEffect(() => { - const refreshTime = () => - setTimeout(() => { - if (Math.floor(Date.now() - timestamp * 1000) / ms(`61s`) <= 1) { - setTimeSince(getTimeSince(timestamp)) - timeout = refreshTime() - } - }, ms(`1s`)) - - let timeout = refreshTime() - - return () => { - timeout && clearTimeout(timeout) - } - }, [timestamp]) - - return timeSince -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts deleted file mode 100644 index 0eb4dae2fea..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' // Replace with the actual import if this is incorrect - -import { Activity } from './types' -import { createGroups } from './utils' - -describe('createGroups', () => { - it('should return undefined if activities is undefined', () => { - expect(createGroups(undefined)).toBeUndefined() - }) - - it('should return an empty array if activities is empty', () => { - expect(createGroups([])).toEqual([]) - }) - - it('should sort and group activities based on status and time', () => { - const mockActivities = [ - { timestamp: 1700000000, status: TransactionStatus.Pending }, - { timestamp: 1650000000, status: TransactionStatus.Confirmed }, - { timestamp: Date.now() / 1000 - 300, status: TransactionStatus.Confirmed }, - ] as Activity[] - - const result = createGroups(mockActivities) - - expect(result).toContainEqual( - expect.objectContaining({ - title: 'Pending', - transactions: expect.arrayContaining([ - expect.objectContaining({ timestamp: 1700000000, status: TransactionStatus.Pending }), - ]), - }) - ) - - expect(result).toContainEqual( - expect.objectContaining({ - title: 'Today', - transactions: expect.arrayContaining([ - expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }), - ]), - }) - ) - }) -}) diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts deleted file mode 100644 index 6e0ba5e5ad3..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { t } from '@lingui/macro' -import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns' -import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' - -import { Activity } from './types' - -interface ActivityGroup { - title: string - transactions: Array -} - -const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp - -export const createGroups = (activities?: Array) => { - if (!activities) return undefined - const now = Date.now() - - const pending: Array = [] - const today: Array = [] - const currentWeek: Array = [] - const last30Days: Array = [] - const currentYear: Array = [] - const yearMap: { [key: string]: Array } = {} - - // TODO(cartcrom): create different time bucket system for activities to fall in based on design wants - activities.forEach((activity) => { - if (activity.status === TransactionStatus.Pending) { - pending.push(activity) - return - } - const addedTime = activity.timestamp * 1000 - - if (isSameDay(now, addedTime)) { - today.push(activity) - } else if (isSameWeek(addedTime, now)) { - currentWeek.push(activity) - } else if (isSameMonth(addedTime, now)) { - last30Days.push(activity) - } else if (isSameYear(addedTime, now)) { - currentYear.push(activity) - } else { - const year = getYear(addedTime) - - if (!yearMap[year]) { - yearMap[year] = [activity] - } else { - yearMap[year].push(activity) - } - } - }) - const sortedYears = Object.keys(yearMap) - .sort((a, b) => parseInt(b) - parseInt(a)) - .map((year) => ({ title: year, transactions: yearMap[year] })) - - const transactionGroups: Array = [ - { title: t`Pending`, transactions: pending.sort(sortActivities) }, - { title: t`Today`, transactions: today.sort(sortActivities) }, - { title: t`This week`, transactions: currentWeek.sort(sortActivities) }, - { title: t`This month`, transactions: last30Days.sort(sortActivities) }, - { title: t`This year`, transactions: currentYear.sort(sortActivities) }, - ...sortedYears, - ] - - return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx b/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx deleted file mode 100644 index daf0598fc36..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useToggleAccountDrawer } from 'components/AccountDrawer' -import Column from 'components/Column' -import Row from 'components/Row' -import { Box } from 'nft/components/Box' -import { NftCard } from 'nft/components/card' -import { detailsHref } from 'nft/components/card/utils' -import { VerifiedIcon } from 'nft/components/icons' -import { WalletAsset } from 'nft/types' -import { floorFormatter } from 'nft/utils' -import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' -import { ThemedText } from 'theme/components' - -const FloorPrice = styled(Row)` - opacity: 0; - - // prevent empty whitespace from collapsing line height to maintain - // consistent spacing below rows - white-space: pre; -` - -const NFTContainer = styled(Column)` - gap: 8px; - min-height: 150px; - - &:hover { - ${FloorPrice} { - opacity: 1; - } - } -` -const NFTCollectionName = styled(ThemedText.BodySmall)` - white-space: pre; - text-overflow: ellipsis; - overflow: hidden; -` - -export function NFT({ - asset, - mediaShouldBePlaying, - setCurrentTokenPlayingMedia, -}: { - asset: WalletAsset - mediaShouldBePlaying: boolean - setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void -}) { - const toggleWalletDrawer = useToggleAccountDrawer() - const navigate = useNavigate() - - const navigateToNFTDetails = () => { - toggleWalletDrawer() - navigate(detailsHref(asset)) - } - - return ( - - - - - ) -} - -function NFTDetails({ asset }: { asset: WalletAsset }) { - return ( - - - {asset.asset_contract.name} - {asset.collectionIsVerified && } - - - - {asset.floorPrice ? `${floorFormatter(asset.floorPrice)} ETH` : ' '} - - - - ) -} - -const BADGE_SIZE = '18px' -function Verified() { - return ( - - - - ) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx b/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx deleted file mode 100644 index 4833df30ce3..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useNftBalance } from 'graphql/data/nft/NftBalance' -import { LoadingAssets } from 'nft/components/collection/CollectionAssetLoading' -import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' -import { useState } from 'react' -import InfiniteScroll from 'react-infinite-scroll-component' -import styled from 'styled-components' - -import { useAccountDrawer } from '../..' -import { DEFAULT_NFT_QUERY_AMOUNT } from '../constants' -import { NFT } from './NFTItem' - -export default function NFTs({ account }: { account: string }) { - const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer() - const { walletAssets, loading, hasNext, loadMore } = useNftBalance( - account, - [], - [], - DEFAULT_NFT_QUERY_AMOUNT, - undefined, - undefined, - undefined, - !walletDrawerOpen - ) - - const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState() - - if (loading && !walletAssets) - return ( - - - - ) - - if (!walletAssets || walletAssets?.length === 0) { - return - } - - return ( - - - - ) - } - dataLength={walletAssets?.length ?? 0} - style={{ overflow: 'unset' }} - scrollableTarget="wallet-dropdown-scroll-wrapper" - > - - {walletAssets?.length - ? walletAssets.map((asset, index) => { - return ( - - ) - }) - : null} - - - ) -} - -const AssetsContainer = styled.div` - display: grid; - gap: 12px; - - // use minmax to not let grid items escape the parent container - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - margin: 16px; -` diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts b/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts deleted file mode 100644 index fa7bdbf9009..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ChainId, Token } from '@uniswap/sdk-core' -import { Pool, Position } from '@uniswap/v3-sdk' -import { useAllTokensMultichain } from 'hooks/Tokens' -import { atom, useAtom } from 'jotai' -import { atomWithStorage } from 'jotai/utils' -import ms from 'ms' -import { useCallback } from 'react' -import { deserializeToken, serializeToken } from 'state/user/hooks' -import { SerializedToken } from 'state/user/types' -import { PositionDetails } from 'types/position' -import { buildCurrencyKey, currencyKey } from 'utils/currencyKey' - -import { getTokensAsync } from './getTokensAsync' -import { useInterfaceMulticallContracts } from './hooks' - -export type PositionInfo = { - owner: string - chainId: ChainId - position: Position - pool: Pool - details: PositionDetails - inRange: boolean - closed: boolean - fees?: [number?, number?] - prices?: [number?, number?] -} - -const POSITION_CACHE_EXPIRY = ms(`1m`) // 1 minute is arbitrary here -// Allows reusing recently fetched positions between component mounts -type CachedPositionsEntry = { result: PositionInfo[]; stale: boolean } -const cachedPositionsAtom = atom<{ [address: string]: CachedPositionsEntry | undefined }>({}) -type UseCachedPositionsReturnType = [CachedPositionsEntry | undefined, (positions: PositionInfo[]) => void] -/** - * Caches positions to allow reusing between component mounts - * @param account address to cache positions for - * @returns cached positions for the account, whether the cache is stale, and a function to update the positions and cache - */ -export function useCachedPositions(account: string): UseCachedPositionsReturnType { - const [cachedPositions, setCachedPositions] = useAtom(cachedPositionsAtom) - const setPositionsAndStaleTimeout = useCallback( - (positions: PositionInfo[]) => { - setCachedPositions((cache) => ({ ...cache, [account]: { result: positions, stale: false } })) - setTimeout( - () => - setCachedPositions((cache) => { - // sets stale to true if the positions haven't been updated since the timeout - if (positions === cache[account]?.result) { - return { ...cache, [account]: { result: positions, stale: true } } - } else { - return cache - } - }), - POSITION_CACHE_EXPIRY - ) - }, - [account, setCachedPositions] - ) - return [cachedPositions[account], setPositionsAndStaleTimeout] -} - -const poolAddressKey = (details: PositionDetails, chainId: ChainId) => - `${chainId}-${details.token0}-${details.token1}-${details.fee}` - -type PoolAddressMap = { [key: string]: string | undefined } -const poolAddressCacheAtom = atomWithStorage('poolCache', {}) -/** - * Caches pool addresses to prevent components from having to re-compute them - * @returns get and set functions for the cache - */ -export function usePoolAddressCache() { - const [cache, updateCache] = useAtom(poolAddressCacheAtom) - const get = useCallback( - (details: PositionDetails, chainId: ChainId) => cache[poolAddressKey(details, chainId)], - [cache] - ) - const set = useCallback( - (details: PositionDetails, chainId: ChainId, address: string) => - updateCache((c) => ({ ...c, [poolAddressKey(details, chainId)]: address })), - [updateCache] - ) - return { get, set } -} - -// These values are static, so we can persist them across sessions using `WithStorage` -const tokenCacheAtom = atomWithStorage<{ [key: string]: SerializedToken | undefined }>('cachedAsyncTokens', {}) -function useTokenCache() { - const [cache, setCache] = useAtom(tokenCacheAtom) - const get = useCallback( - (chainId: number, address: string) => { - const entry = cache[buildCurrencyKey(chainId, address)] - return entry ? deserializeToken(entry) : undefined - }, - [cache] - ) - const set = useCallback( - (token?: Token) => { - if (token) { - setCache((cache) => ({ ...cache, [currencyKey(token)]: serializeToken(token) })) - } - }, - [setCache] - ) - return { get, set } -} - -type TokenGetterFn = (addresses: string[], chainId: ChainId) => Promise<{ [key: string]: Token | undefined }> -export function useGetCachedTokens(chains: ChainId[]): TokenGetterFn { - const allTokens = useAllTokensMultichain() - const multicallContracts = useInterfaceMulticallContracts(chains) - const tokenCache = useTokenCache() - - // Used to fetch tokens not available in local state - const fetchRemoteTokens: TokenGetterFn = useCallback( - async (addresses, chainId) => { - const fetched = await getTokensAsync(addresses, chainId, multicallContracts[chainId]) - Object.values(fetched).forEach(tokenCache.set) - return fetched - }, - [multicallContracts, tokenCache] - ) - - // Uses tokens from local state if available, otherwise fetches them - const getTokens: TokenGetterFn = useCallback( - async (addresses, chainId) => { - const local: { [address: string]: Token | undefined } = {} - const missing = new Set() - addresses.forEach((address) => { - const cached = tokenCache.get(chainId, address) ?? allTokens[chainId]?.[address] - cached ? (local[address] = cached) : missing.add(address) - }) - - const fetched = await fetchRemoteTokens([...missing], chainId) - return { ...local, ...fetched } - }, - [allTokens, fetchRemoteTokens, tokenCache] - ) - - return getTokens -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts b/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts deleted file mode 100644 index eae544385e7..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ChainId, Token } from '@uniswap/sdk-core' -import ERC20_ABI from 'abis/erc20.json' -import { Erc20Interface } from 'abis/types/Erc20' -import { Erc20Bytes32Interface } from 'abis/types/Erc20Bytes32' -import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens' -import { Interface } from 'ethers/lib/utils' -import { UniswapInterfaceMulticall } from 'types/v3' -import { isAddress } from 'utils' -import { arrayToSlices } from 'utils/arrays' -import { buildCurrencyKey, CurrencyKey, currencyKey } from 'utils/currencyKey' - -type TokenMap = { [address: string]: Token | undefined } -export type Call = { target: string; callData: string; gasLimit: number } -type CallResult = { success: boolean; returnData: string } -export const DEFAULT_GAS_LIMIT = 1_000_000 - -const Erc20 = new Interface(ERC20_ABI) as Erc20Interface -const Erc20Bytes32 = new Interface(ERC20_ABI) as Erc20Bytes32Interface // Used for tokens that return bytes32 for name/symbol rather than string - -// TODO(WEB-1760): cartcrom - adapt support for multi-function multi-interface multicalls into redux-multicall to remove than this custom cache/chunking logic -// Infura rejects calls with gas costs > 10x the current block gas limit; in such case we split the call into 2 chunks -async function fetchChunk(multicall: UniswapInterfaceMulticall, chunk: Call[]): Promise { - try { - return (await multicall.callStatic.multicall(chunk)).returnData - } catch (error) { - if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) { - if (chunk.length > 1) { - const half = Math.floor(chunk.length / 2) - return Promise.all([ - fetchChunk(multicall, chunk.slice(0, half)), - fetchChunk(multicall, chunk.slice(half, chunk.length)), - ]).then(([c0, c1]) => [...c0, ...c1]) - } - } - console.error('Failed to fetch chunk', error) - throw error - } -} - -function tryParseToken(address: string, chainId: ChainId, data: CallResult[]) { - try { - const [nameData, symbolData, decimalsData, nameDataBytes32, symbolDataBytes32] = data - - const name = nameData.success - ? (Erc20.decodeFunctionResult('name', nameData.returnData)[0] as string) - : nameDataBytes32.success - ? (Erc20Bytes32.decodeFunctionResult('name', nameDataBytes32.returnData)[0] as string) - : undefined - const symbol = symbolData.success - ? (Erc20.decodeFunctionResult('symbol', symbolData.returnData)[0] as string) - : symbolDataBytes32.success - ? (Erc20Bytes32.decodeFunctionResult('symbol', symbolDataBytes32.returnData)[0] as string) - : undefined - const decimals = decimalsData.success ? parseInt(decimalsData.returnData) : DEFAULT_ERC20_DECIMALS - - return new Token(chainId, address, decimals, symbol, name) - } catch (error) { - console.error(`Failed to fetch token at address ${address} on chain ${chainId}`) - return undefined - } -} - -function parseTokens(addresses: string[], chainId: ChainId, returnData: CallResult[]) { - const tokenDataSlices = arrayToSlices(returnData, 5) - - return tokenDataSlices.reduce((acc: TokenMap, slice, index) => { - const parsedToken = tryParseToken(addresses[index], chainId, slice) - if (parsedToken) acc[parsedToken.address] = parsedToken - return acc - }, {}) -} - -const createCalls = (target: string, callData: string[]): Call[] => - callData.map((callData) => ({ target, callData, gasLimit: DEFAULT_GAS_LIMIT })) - -function createCallsForToken(address: string) { - return createCalls(address, [ - Erc20.encodeFunctionData('name'), - Erc20.encodeFunctionData('symbol'), - Erc20.encodeFunctionData('decimals'), - Erc20Bytes32.encodeFunctionData('name'), - Erc20Bytes32.encodeFunctionData('symbol'), - ]) -} - -// Prevents tokens from being fetched multiple times -const TokenPromiseCache: { [key: CurrencyKey]: Promise | undefined } = {} - -// Returns tokens using a single RPC call to the multicall contract -export async function getTokensAsync( - addresses: string[], - chainId: ChainId, - multicall: UniswapInterfaceMulticall -): Promise { - if (addresses.length === 0) return {} - const formattedAddresses: string[] = [] - const calls: Call[] = [] - const previouslyCalledTokens: Promise[] = [] - - addresses.forEach((tokenAddress) => { - const key = buildCurrencyKey(chainId, tokenAddress) - const previousCall = TokenPromiseCache[key] - if (previousCall !== undefined) { - previouslyCalledTokens.push(previousCall) - } else { - const formattedAddress = isAddress(tokenAddress) - if (!formattedAddress) return - formattedAddresses.push(formattedAddress) - calls.push(...createCallsForToken(formattedAddress)) - } - }) - - const calledTokens = fetchChunk(multicall, calls).then((returnData) => parseTokens(addresses, chainId, returnData)) - - // Caches tokens currently being fetched for further calls to use - formattedAddresses.forEach( - (address) => - (TokenPromiseCache[buildCurrencyKey(chainId, address)] = calledTokens.then((tokenMap) => tokenMap[address])) - ) - - const tokenMap = await calledTokens - // Add tokens from previous calls to the map of tokens fetched in this call - const resolvedPreviousTokens = await Promise.all(previouslyCalledTokens) - resolvedPreviousTokens.forEach((token) => token && (tokenMap[currencyKey(token)] = token)) - - return tokenMap -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts b/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts deleted file mode 100644 index 937eb1d6b31..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - ChainId, - MULTICALL_ADDRESSES, - NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES, - Token, -} from '@uniswap/sdk-core' -import type { AddressMap } from '@uniswap/smart-order-router' -import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json' -import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' -import { useWeb3React } from '@web3-react/core' -import { isSupportedChain } from 'constants/chains' -import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers' -import { BaseContract } from 'ethers/lib/ethers' -import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider' -import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks' -import { toContractInput } from 'graphql/data/util' -import useStablecoinPrice from 'hooks/useStablecoinPrice' -import { useMemo } from 'react' -import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'types/v3' -import { getContract } from 'utils' -import { CurrencyKey, currencyKey, currencyKeyFromGraphQL } from 'utils/currencyKey' - -import { PositionInfo } from './cache' - -type ContractMap = { [key: number]: T } - -// Constructs a chain-to-contract map, using the wallet's provider when available -function useContractMultichain( - addressMap: AddressMap, - ABI: any, - chainIds?: ChainId[] -): ContractMap { - const { chainId: walletChainId, provider: walletProvider } = useWeb3React() - - const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS - - return useMemo(() => { - const relevantChains = - chainIds ?? - Object.keys(addressMap) - .map((chainId) => parseInt(chainId)) - .filter((chainId) => isSupportedChain(chainId)) - - return relevantChains.reduce((acc: ContractMap, chainId) => { - const provider = - walletProvider && walletChainId === chainId - ? walletProvider - : isSupportedChain(chainId) - ? networkProviders[chainId] - : undefined - if (provider) { - acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T - } - return acc - }, {}) - }, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider]) -} - -export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap { - return useContractMultichain(V3NFT_ADDRESSES, NFTPositionManagerJSON.abi, chainIds) -} - -export function useInterfaceMulticallContracts(chainIds: ChainId[]): ContractMap { - return useContractMultichain(MULTICALL_ADDRESSES, MulticallJSON.abi, chainIds) -} - -type PriceMap = { [key: CurrencyKey]: number | undefined } -export function usePoolPriceMap(positions: PositionInfo[] | undefined) { - const contracts = useMemo(() => { - if (!positions || !positions.length) return [] - // Avoids fetching duplicate tokens by placing in map - const contractMap = positions.reduce((acc: { [key: string]: ContractInput }, { pool: { token0, token1 } }) => { - acc[currencyKey(token0)] = toContractInput(token0) - acc[currencyKey(token1)] = toContractInput(token1) - return acc - }, {}) - return Object.values(contractMap) - }, [positions]) - - const { data, loading } = useUniswapPricesQuery({ variables: { contracts }, skip: !contracts.length }) - - const priceMap = useMemo( - () => - data?.tokens?.reduce((acc: PriceMap, current) => { - if (current) acc[currencyKeyFromGraphQL(current)] = current.project?.markets?.[0]?.price?.value - return acc - }, {}) ?? {}, - [data?.tokens] - ) - - return { priceMap, pricesLoading: loading && !data } -} - -function useFeeValue(token: Token, fee: number | undefined, queriedPrice: number | undefined) { - const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined) - return useMemo(() => { - // Prefers gql price, as fetching stablecoinPrice will trigger multiple infura calls for each pool position - const price = queriedPrice ?? (stablecoinPrice ? parseFloat(stablecoinPrice.toSignificant()) : undefined) - const feeValue = fee && price ? fee * price : undefined - - return [price, feeValue] - }, [fee, queriedPrice, stablecoinPrice]) -} - -export function useFeeValues(position: PositionInfo) { - const [priceA, feeValueA] = useFeeValue(position.pool.token0, position.fees?.[0], position.prices?.[0]) - const [priceB, feeValueB] = useFeeValue(position.pool.token1, position.fees?.[1], position.prices?.[1]) - - return { priceA, priceB, fees: (feeValueA || 0) + (feeValueB || 0) } -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx b/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx deleted file mode 100644 index 324295df0d0..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { mocked } from 'test-utils/mocked' -import { owner, useMultiChainPositionsReturnValue } from 'test-utils/pools/fixtures' -import { render } from 'test-utils/render' - -import Pools from '.' -import useMultiChainPositions from './useMultiChainPositions' - -jest.mock('./useMultiChainPositions') - -jest.spyOn(console, 'warn').mockImplementation() - -beforeEach(() => { - mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) -}) -test('Pools should render LP positions', () => { - const props = { account: owner } - const { container } = render() - expect(container).not.toBeEmptyDOMElement() -}) diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx deleted file mode 100644 index bd4b37efecb..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { t } from '@lingui/macro' -import { Position } from '@uniswap/v3-sdk' -import { useWeb3React } from '@web3-react/core' -import { useToggleAccountDrawer } from 'components/AccountDrawer' -import Row from 'components/Row' -import { MouseoverTooltip } from 'components/Tooltip' -import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions' -import { useSwitchChain } from 'hooks/useSwitchChain' -import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' -import { useCallback, useMemo, useReducer } from 'react' -import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' -import { ThemedText } from 'theme/components' -import { NumberType, useFormatter } from 'utils/formatNumbers' - -import { ExpandoRow } from '../ExpandoRow' -import { PortfolioLogo } from '../PortfolioLogo' -import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' -import { PositionInfo } from './cache' -import { useFeeValues } from './hooks' -import useMultiChainPositions from './useMultiChainPositions' - -/** - * Takes an array of PositionInfo objects (format used by the Uniswap Labs gql API). - * The hook access PositionInfo.details (format used by the NFT position contract), - * filters the PositionDetails data for malicious content, - * and then returns the original data in its original format. - */ -function useFilterPossiblyMaliciousPositionInfo(positions: PositionInfo[] | undefined): PositionInfo[] { - const tokenIdsToPositionInfo: Record = useMemo( - () => - positions - ? positions.reduce((acc, position) => ({ ...acc, [position.details.tokenId.toString()]: position }), {}) - : {}, - [positions] - ) - const positionDetails = useMemo(() => positions?.map((position) => position.details) ?? [], [positions]) - const filteredPositionDetails = useFilterPossiblyMaliciousPositions(positionDetails) - - return useMemo( - () => filteredPositionDetails.map((positionDetails) => tokenIdsToPositionInfo[positionDetails.tokenId.toString()]), - [filteredPositionDetails, tokenIdsToPositionInfo] - ) -} - -export default function Pools({ account }: { account: string }) { - const { positions, loading } = useMultiChainPositions(account) - const filteredPositions = useFilterPossiblyMaliciousPositionInfo(positions) - const [showClosed, toggleShowClosed] = useReducer((showClosed) => !showClosed, false) - - const [openPositions, closedPositions] = useMemo(() => { - const openPositions: PositionInfo[] = [] - const closedPositions: PositionInfo[] = [] - for (let i = 0; i < filteredPositions.length; i++) { - const position = filteredPositions[i] - if (position.closed) { - closedPositions.push(position) - } else { - openPositions.push(position) - } - } - return [openPositions, closedPositions] - }, [filteredPositions]) - - const toggleWalletDrawer = useToggleAccountDrawer() - - if (!filteredPositions || loading) { - return - } - - if (filteredPositions.length === 0) { - return - } - - return ( - - {openPositions.map((positionInfo) => ( - - ))} - - {closedPositions.map((positionInfo) => ( - - ))} - - - ) -} - -const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>` - background-color: ${({ theme, closed, outOfRange }) => - closed ? theme.neutral2 : outOfRange ? theme.deprecated_accentWarning : theme.success}; - border-radius: 50%; - height: 8px; - width: 8px; - margin-left: 4px; - margin-top: 1px; -` - -function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) { - if (!price0 || !price1) return undefined - - const value0 = parseFloat(position.amount0.toExact()) * price0 - const value1 = parseFloat(position.amount1.toExact()) * price1 - return value0 + value1 -} - -function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) { - const { formatNumber } = useFormatter() - - const { chainId, position, pool, details, inRange, closed } = positionInfo - - const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo) - const liquidityValue = calculcateLiquidityValue(priceA, priceB, position) - - const navigate = useNavigate() - const toggleWalletDrawer = useToggleAccountDrawer() - const { chainId: walletChainId, connector } = useWeb3React() - const switchChain = useSwitchChain() - const onClick = useCallback(async () => { - if (walletChainId !== chainId) await switchChain(connector, chainId) - toggleWalletDrawer() - navigate('/pool/' + details.tokenId) - }, [walletChainId, chainId, switchChain, connector, toggleWalletDrawer, navigate, details.tokenId]) - - return ( - } - title={ - - - {pool.token0.symbol} / {pool.token1?.symbol} - - - } - descriptor={{`${pool.fee / 10000}%`}} - right={ - <> - - {`${formatNumber({ - input: liquidityValue, - type: NumberType.PortfolioBalance, - })} (liquidity) + ${formatNumber({ - input: feeValue, - type: NumberType.PortfolioBalance, - })} (fees)`} - - } - > - - {formatNumber({ - input: (liquidityValue ?? 0) + (feeValue ?? 0), - type: NumberType.PortfolioBalance, - })} - - - - - - {closed ? t`Closed` : inRange ? t`In range` : t`Out of range`} - - - - - } - /> - ) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx b/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx deleted file mode 100644 index e433848fc59..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { ChainId, CurrencyAmount, Token, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core' -import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json' -import { computePoolAddress, Pool, Position } from '@uniswap/v3-sdk' -import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens' -import { BigNumber } from 'ethers/lib/ethers' -import { Interface } from 'ethers/lib/utils' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { PositionDetails } from 'types/position' -import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'types/v3' -import { UniswapV3PoolInterface } from 'types/v3/UniswapV3Pool' -import { currencyKey } from 'utils/currencyKey' - -import { PositionInfo, useCachedPositions, useGetCachedTokens, usePoolAddressCache } from './cache' -import { Call, DEFAULT_GAS_LIMIT } from './getTokensAsync' -import { useInterfaceMulticallContracts, usePoolPriceMap, useV3ManagerContracts } from './hooks' - -function createPositionInfo( - owner: string, - chainId: ChainId, - details: PositionDetails, - slot0: any, - tokenA: Token, - tokenB: Token -): PositionInfo { - /* Instantiates a Pool with a hardcoded 0 liqudity value since the sdk only uses this value for swap state and this avoids an RPC fetch */ - const pool = new Pool(tokenA, tokenB, details.fee, slot0.sqrtPriceX96.toString(), 0, slot0.tick) - const position = new Position({ - pool, - liquidity: details.liquidity.toString(), - tickLower: details.tickLower, - tickUpper: details.tickUpper, - }) - const inRange = slot0.tick >= details.tickLower && slot0.tick < details.tickUpper - const closed = details.liquidity.eq(0) - return { owner, chainId, pool, position, details, inRange, closed } -} - -type FeeAmounts = [BigNumber, BigNumber] - -const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1) - -const DEFAULT_CHAINS = [ - ChainId.MAINNET, - ChainId.ARBITRUM_ONE, - ChainId.OPTIMISM, - ChainId.POLYGON, - ChainId.CELO, - ChainId.BNB, - ChainId.AVALANCHE, - ChainId.BASE, -] - -type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean } - -/** - * Returns all positions for a given account on multiple chains. - * - * This hook doesn't use the redux-multicall library to avoid having to manually fetching blocknumbers for each chain. - * - * @param account - account to fetch positions for - * @param chains - chains to fetch positions from - * @returns positions, fees - */ -export default function useMultiChainPositions(account: string, chains = DEFAULT_CHAINS): UseMultiChainPositionsData { - const pms = useV3ManagerContracts(chains) - const multicalls = useInterfaceMulticallContracts(chains) - - const getTokens = useGetCachedTokens(chains) - const poolAddressCache = usePoolAddressCache() - - const [cachedPositions, setPositions] = useCachedPositions(account) - const positions = cachedPositions?.result - const positionsFetching = useRef(false) - const positionsLoading = !cachedPositions?.result && positionsFetching.current - - const [feeMap, setFeeMap] = useState<{ [key: string]: FeeAmounts }>({}) - - const { priceMap, pricesLoading } = usePoolPriceMap(positions) - - const fetchPositionFees = useCallback( - async (pm: NonfungiblePositionManager, positionIds: BigNumber[], chainId: number) => { - const callData = positionIds.map((id) => - pm.interface.encodeFunctionData('collect', [ - { tokenId: id, recipient: account, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }, - ]) - ) - const fees = (await pm.callStatic.multicall(callData)).reduce((acc, feeBytes, index) => { - const key = chainId.toString() + positionIds[index] - acc[key] = pm.interface.decodeFunctionResult('collect', feeBytes) as FeeAmounts - return acc - }, {} as { [key: string]: FeeAmounts }) - - setFeeMap((prev) => ({ ...prev, ...fees })) - }, - [account] - ) - - const fetchPositionIds = useCallback( - async (pm: NonfungiblePositionManager, balance: BigNumber) => { - const callData = Array.from({ length: balance.toNumber() }, (_, i) => - pm.interface.encodeFunctionData('tokenOfOwnerByIndex', [account, i]) - ) - return (await pm.callStatic.multicall(callData)).map((idByte) => BigNumber.from(idByte)) - }, - [account] - ) - - const fetchPositionDetails = useCallback(async (pm: NonfungiblePositionManager, positionIds: BigNumber[]) => { - const callData = positionIds.map((id) => pm.interface.encodeFunctionData('positions', [id])) - return (await pm.callStatic.multicall(callData)).map( - (positionBytes, index) => - ({ - ...pm.interface.decodeFunctionResult('positions', positionBytes), - tokenId: positionIds[index], - } as unknown as PositionDetails) - ) - }, []) - - // Combines PositionDetails with Pool data to build our return type - const fetchPositionInfo = useCallback( - async (positionDetails: PositionDetails[], chainId: ChainId, multicall: UniswapInterfaceMulticall) => { - const poolInterface = new Interface(IUniswapV3PoolStateJSON.abi) as UniswapV3PoolInterface - const tokens = await getTokens( - positionDetails.flatMap((details) => [details.token0, details.token1]), - chainId - ) - - const calls: Call[] = [] - const poolPairs: [Token, Token][] = [] - positionDetails.forEach((details) => { - const tokenA = tokens[details.token0] ?? new Token(chainId, details.token0, DEFAULT_ERC20_DECIMALS) - const tokenB = tokens[details.token1] ?? new Token(chainId, details.token1, DEFAULT_ERC20_DECIMALS) - - let poolAddress = poolAddressCache.get(details, chainId) - if (!poolAddress) { - const factoryAddress = V3_CORE_FACTORY_ADDRESSES[chainId] - poolAddress = computePoolAddress({ factoryAddress, tokenA, tokenB, fee: details.fee }) - poolAddressCache.set(details, chainId, poolAddress) - } - poolPairs.push([tokenA, tokenB]) - calls.push({ - target: poolAddress, - callData: poolInterface.encodeFunctionData('slot0'), - gasLimit: DEFAULT_GAS_LIMIT, - }) - }, []) - - return (await multicall.callStatic.multicall(calls)).returnData.reduce((acc: PositionInfo[], result, i) => { - if (result.success) { - const slot0 = poolInterface.decodeFunctionResult('slot0', result.returnData) - acc.push(createPositionInfo(account, chainId, positionDetails[i], slot0, ...poolPairs[i])) - } else { - console.debug('slot0 fetch errored', result) - } - return acc - }, []) - }, - [account, poolAddressCache, getTokens] - ) - - const fetchPositionsForChain = useCallback( - async (chainId: ChainId): Promise => { - try { - const pm = pms[chainId] - const multicall = multicalls[chainId] - const balance = await pm?.balanceOf(account) - if (!pm || !multicall || balance.lt(1)) return [] - - const positionIds = await fetchPositionIds(pm, balance) - // Fetches fees in the background and stores them separetely from the results of this function - fetchPositionFees(pm, positionIds, chainId) - - const postionDetails = await fetchPositionDetails(pm, positionIds) - return fetchPositionInfo(postionDetails, chainId, multicall) - } catch (error) { - console.error(`Failed to fetch positions for chain ${chainId}`, error) - return [] - } - }, - [account, fetchPositionDetails, fetchPositionFees, fetchPositionIds, fetchPositionInfo, pms, multicalls] - ) - - const fetchAllPositions = useCallback(async () => { - positionsFetching.current = true - const positions = (await Promise.all(chains.map(fetchPositionsForChain))).flat() - positionsFetching.current = false - setPositions(positions) - }, [chains, fetchPositionsForChain, setPositions]) - - // Fetches positions when existing positions are stale and the document has focus - useEffect(() => { - if (positionsFetching.current || cachedPositions?.stale === false) return - else if (document.hasFocus()) { - fetchAllPositions() - } else { - // Avoids refetching positions until the user returns to Interface to avoid polling unnused rpc data - const onFocus = () => { - fetchAllPositions() - window.removeEventListener('focus', onFocus) - } - window.addEventListener('focus', onFocus) - return () => { - window.removeEventListener('focus', onFocus) - } - } - return - }, [fetchAllPositions, positionsFetching, cachedPositions?.stale]) - - const positionsWithFeesAndPrices: PositionInfo[] | undefined = useMemo( - () => - positions?.map((position) => { - const key = position.chainId.toString() + position.details.tokenId - const fees = feeMap[key] - ? [ - // We parse away from SDK/ethers types so fees can be multiplied by primitive number prices - parseFloat(CurrencyAmount.fromRawAmount(position.pool.token0, feeMap[key]?.[0].toString()).toExact()), - parseFloat(CurrencyAmount.fromRawAmount(position.pool.token1, feeMap[key]?.[1].toString()).toExact()), - ] - : undefined - const prices = [priceMap[currencyKey(position.pool.token0)], priceMap[currencyKey(position.pool.token1)]] - return { ...position, fees, prices } as PositionInfo - }), - [feeMap, positions, priceMap] - ) - - return { positions: positionsWithFeesAndPrices, loading: pricesLoading || positionsLoading } -} diff --git a/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx b/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx index ffc2542d491..8063ec633ab 100644 --- a/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx @@ -3,7 +3,7 @@ import Row from 'components/Row' import { LoadingBubble } from 'components/Tokens/loading' import styled, { css, keyframes } from 'styled-components' -export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>` +const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>` gap: 12px; height: 68px; padding: 0 16px; diff --git a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx deleted file mode 100644 index e20296f056c..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' -import Row from 'components/Row' -import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' -import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks' -import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util' -import { useAtomValue } from 'jotai/utils' -import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' -import { useCallback, useMemo, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' -import { EllipsisStyle, ThemedText } from 'theme/components' -import { NumberType, useFormatter } from 'utils/formatNumbers' -import { splitHiddenTokens } from 'utils/splitHiddenTokens' - -import { useToggleAccountDrawer } from '../..' -import { hideSmallBalancesAtom } from '../../SmallBalanceToggle' -import { ExpandoRow } from '../ExpandoRow' -import { PortfolioLogo } from '../PortfolioLogo' -import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' - -export default function Tokens({ account }: { account: string }) { - const toggleWalletDrawer = useToggleAccountDrawer() - const hideSmallBalances = useAtomValue(hideSmallBalancesAtom) - const [showHiddenTokens, setShowHiddenTokens] = useState(false) - - const { data } = useCachedPortfolioBalancesQuery({ account }) - - const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined - - const { visibleTokens, hiddenTokens } = useMemo( - () => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }), - [hideSmallBalances, tokenBalances] - ) - - if (!data) { - return - } - - if (tokenBalances?.length === 0) { - // TODO: consider launching moonpay here instead of just closing the drawer - return - } - - const toggleHiddenTokens = () => setShowHiddenTokens((showHiddenTokens) => !showHiddenTokens) - - return ( - - {visibleTokens.map( - (tokenBalance) => - tokenBalance.token && - )} - - {hiddenTokens.map( - (tokenBalance) => - tokenBalance.token && - )} - - - ) -} - -const TokenBalanceText = styled(ThemedText.BodySecondary)` - ${EllipsisStyle} -` -const TokenNameText = styled(ThemedText.SubHeader)` - ${EllipsisStyle} -` - -type PortfolioToken = NonNullable - -function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) { - const { formatPercent } = useFormatter() - const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0 - - const navigate = useNavigate() - const toggleWalletDrawer = useToggleAccountDrawer() - const navigateToTokenDetails = useCallback(async () => { - navigate(getTokenDetailsURL(token)) - toggleWalletDrawer() - }, [navigate, token, toggleWalletDrawer]) - const { formatNumber } = useFormatter() - - const currency = gqlToCurrency(token) - if (!currency) { - logSentryErrorForUnsupportedChain({ - extras: { token }, - errorMessage: 'Token from unsupported chain received from Mini Portfolio Token Balance Query', - }) - return null - } - return ( - } - title={{token?.name}} - descriptor={ - - {formatNumber({ - input: quantity, - type: NumberType.TokenNonTx, - })}{' '} - {token?.symbol} - - } - onClick={navigateToTokenDetails} - right={ - denominatedValue && ( - <> - - {formatNumber({ - input: denominatedValue?.value, - type: NumberType.PortfolioBalance, - })} - - - - {formatPercent(percentChange)} - - - ) - } - /> - ) -} diff --git a/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap b/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap deleted file mode 100644 index 6eb8b28d14d..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap +++ /dev/null @@ -1,164 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PortfolioLogo renders with L2 icon 1`] = ` -.c1 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 2px; - position: relative; - top: 0; - left: 0; -} - -.c1 img:nth-child(n) { - width: 19px; - height: 40px; - object-fit: cover; -} - -.c1 img:nth-child(1) { - border-radius: 20px 0 0 20px; - object-position: 0 0; -} - -.c1 img:nth-child(2) { - border-radius: 0 20px 20px 0; - object-position: 100% 0; -} - -.c0 { - position: relative; - top: 0; - left: 0; -} - -.c4 { - height: 14px; - width: 14px; -} - -.c2 { - width: 40px; - height: 40px; - border-radius: 50%; -} - -.c3 { - background-color: #222222; - border-radius: 2px; - height: 16px; - left: 60%; - position: absolute; - top: 60%; - outline: 2px solid #FFFFFF; - width: 16px; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; -} - -
-
-
- - -
-
- chainLogo -
-
-
-`; - -exports[`PortfolioLogo renders without L2 icon 1`] = ` -.c1 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 2px; - position: relative; - top: 0; - left: 0; -} - -.c1 img:nth-child(n) { - width: 19px; - height: 40px; - object-fit: cover; -} - -.c1 img:nth-child(1) { - border-radius: 20px 0 0 20px; - object-position: 0 0; -} - -.c1 img:nth-child(2) { - border-radius: 0 20px 20px 0; - object-position: 100% 0; -} - -.c0 { - position: relative; - top: 0; - left: 0; -} - -.c2 { - width: 40px; - height: 40px; - border-radius: 50%; -} - -
-
-
- - -
-
-
-`; diff --git a/src/components/AccountDrawer/MiniPortfolio/constants.tsx b/src/components/AccountDrawer/MiniPortfolio/constants.tsx index 62fb9f37e13..19fc2c8bc9e 100644 --- a/src/components/AccountDrawer/MiniPortfolio/constants.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/constants.tsx @@ -1,11 +1,8 @@ import { t } from '@lingui/macro' -import { SwapOrderStatus, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' import { UniswapXOrderStatus } from 'lib/hooks/orders/types' import { TransactionType } from 'state/transactions/types' -// use even number because rows are in groups of 2 -export const DEFAULT_NFT_QUERY_AMOUNT = 26 - const TransactionTitleTable: { [key in TransactionType]: { [state in TransactionStatus]: string } } = { [TransactionType.SWAP]: { [TransactionStatus.Pending]: t`Swapping`, @@ -220,11 +217,3 @@ export const OrderTextTable: { status: TransactionStatus.Failed, }, } - -// Converts GQL backend orderStatus enum to the enum used by the frontend and UniswapX backend -export const OrderStatusTable: { [key in SwapOrderStatus]: UniswapXOrderStatus } = { - [SwapOrderStatus.Open]: UniswapXOrderStatus.OPEN, - [SwapOrderStatus.Expired]: UniswapXOrderStatus.EXPIRED, - [SwapOrderStatus.Error]: UniswapXOrderStatus.ERROR, - [SwapOrderStatus.InsufficientFunds]: UniswapXOrderStatus.INSUFFICIENT_FUNDS, -} diff --git a/src/components/AccountDrawer/MiniPortfolio/index.tsx b/src/components/AccountDrawer/MiniPortfolio/index.tsx deleted file mode 100644 index e07d09e3e85..00000000000 --- a/src/components/AccountDrawer/MiniPortfolio/index.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Trans } from '@lingui/macro' -import Column from 'components/Column' -import { LoaderV2 } from 'components/Icons/LoadingSpinner' -import { AutoRow } from 'components/Row' -import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' -import { useIsNftPage } from 'hooks/useIsNftPage' -import { useEffect, useState } from 'react' -import styled, { useTheme } from 'styled-components' -import { BREAKPOINTS } from 'theme' -import { ThemedText } from 'theme/components' - -import { ActivityTab } from './Activity' -import { usePendingActivity } from './Activity/hooks' -import NFTs from './NFTs' -import Pools from './Pools' -import { PortfolioRowWrapper } from './PortfolioRow' -import Tokens from './Tokens' - -const Wrapper = styled(Column)` - margin-top: 28px; - display: flex; - flex-direction: column; - height: 100%; - gap: 12px; - - @media screen and (max-width: ${BREAKPOINTS.sm}px) { - margin-bottom: 48px; - } - - ${PortfolioRowWrapper} { - &:hover { - background: ${({ theme }) => theme.deprecated_hoverDefault}; - } - } -` - -const Nav = styled(AutoRow)` - gap: 20px; -` - -const NavItem = styled(ThemedText.SubHeader)<{ active?: boolean }>` - align-items: center; - color: ${({ theme, active }) => (active ? theme.neutral1 : theme.neutral2)}; - cursor: pointer; - display: flex; - justify-content: space-between; - transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} color`}; - - &:hover { - ${({ theme, active }) => !active && `color: ${theme.neutral2}`}; - } -` - -const PageWrapper = styled.div` - border-radius: 12px; - margin-right: -16px; - margin-left: -16px; - width: calc(100% + 32px); - flex: 1; -` - -interface Page { - title: React.ReactNode - key: string - component: ({ account }: { account: string }) => JSX.Element -} - -const Pages: Array = [ - { - title: Tokens, - key: 'tokens', - component: Tokens, - }, - { - title: NFTs, - key: 'nfts', - component: NFTs, - }, - { - title: Pools, - key: 'pools', - component: Pools, - }, - { - title: Activity, - key: 'activity', - component: ActivityTab, - }, -] - -export default function MiniPortfolio({ account }: { account: string }) { - const isNftPage = useIsNftPage() - const theme = useTheme() - const [currentPage, setCurrentPage] = useState(isNftPage ? 1 : 0) - const shouldDisableNFTRoutes = useDisableNFTRoutes() - const [activityUnread, setActivityUnread] = useState(false) - - const { component: Page, key: currentKey } = Pages[currentPage] - - const { hasPendingActivity } = usePendingActivity() - - useEffect(() => { - if (hasPendingActivity && currentKey !== 'activity') setActivityUnread(true) - }, [currentKey, hasPendingActivity]) - - return ( - - - - - - - ) -} diff --git a/src/graphql/data/util.tsx b/src/graphql/data/util.tsx index 65ce6f2d8ba..000ac0d0ca4 100644 --- a/src/graphql/data/util.tsx +++ b/src/graphql/data/util.tsx @@ -5,9 +5,8 @@ import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo' import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import ms from 'ms' import { useEffect } from 'react' -import { getNativeTokenDBAddress } from 'utils/nativeTokens' -import { Chain, ContractInput, HistoryDuration, TokenStandard } from './__generated__/types-and-hooks' +import { Chain, HistoryDuration, TokenStandard } from './__generated__/types-and-hooks' export enum PollingInterval { Slow = ms(`5m`), @@ -102,10 +101,6 @@ type GqlChainsType = (typeof GQL_CHAINS)[number] export function isGqlSupportedChain(chainId: number | undefined): chainId is GqlChainsType { return !!chainId && GQL_CHAINS.includes(chainId) } -export function toContractInput(currency: Currency): ContractInput { - const chain = chainIdToBackendName(currency.chainId) - return { chain, address: currency.isToken ? currency.address : getNativeTokenDBAddress(chain) } -} export function gqlToCurrency(token: { address?: string diff --git a/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx b/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx index e4e6e4afd39..1f45c5eb63e 100644 --- a/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx +++ b/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx @@ -1,13 +1,9 @@ import userEvent from '@testing-library/user-event' -import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' -import { mocked } from 'test-utils/mocked' -import { useMultiChainPositionsReturnValue, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures' +import { validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures' import { act, render, screen } from 'test-utils/render' import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons' -jest.mock('components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions') - describe('PoolDetailsStatsButton', () => { const mockProps = { chainId: 1, @@ -22,10 +18,6 @@ describe('PoolDetailsStatsButton', () => { token1: validPoolToken0, } - beforeEach(() => { - mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) - }) - it.skip('renders both buttons correctly', () => { const { asFragment } = render() expect(asFragment()).toMatchSnapshot() diff --git a/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx b/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx index 497b202725d..5b69aa5faa1 100644 --- a/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx +++ b/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx @@ -1,7 +1,5 @@ import { Trans } from '@lingui/macro' import { useWeb3React } from '@web3-react/core' -import { PositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools/cache' -import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' import Row from 'components/Row' import { Token } from 'graphql/thegraph/__generated__/types-and-hooks' diff --git a/src/utils/currencyKey.ts b/src/utils/currencyKey.ts deleted file mode 100644 index 3c9f378165c..00000000000 --- a/src/utils/currencyKey.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ChainId, Currency } from '@uniswap/sdk-core' -import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks' -import { Chain } from 'graphql/data/Token' -import { supportedChainIdFromGQLChain } from 'graphql/data/util' - -export type CurrencyKey = string - -export function buildCurrencyKey(chainId: ChainId, address: string): CurrencyKey { - // We lowercase for compatibility/indexability between gql tokens and sdk currencies - return `${chainId}-${address.toLowerCase()}` -} - -export function currencyKey(currency: Currency): CurrencyKey { - return buildCurrencyKey(currency.chainId, currency.isToken ? currency.address : NATIVE_CHAIN_ID) -} - -export function currencyKeyFromGraphQL(contract: { - address?: string - chain: Chain - standard?: TokenStandard -}): CurrencyKey { - const chainId = supportedChainIdFromGQLChain(contract.chain) - const address = contract.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : contract.address - if (!address) throw new Error('Non-native token missing address') - if (!chainId) throw new Error('Unsupported chain from pools query') - return buildCurrencyKey(chainId, address) -}