From 3c9068a0a5b81723643754c41d48829d38bbf4b0 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 02:00:35 +0300 Subject: [PATCH 01/74] TW-1479: [EVM] Transactions history --- src/app/atoms/EmptyState.tsx | 4 +- src/app/atoms/Identicon.tsx | 7 +- src/app/atoms/ModalInfoBlock.tsx | 2 +- src/app/atoms/NetworkLogo.tsx | 100 +++--- src/app/icons/base/documents.svg | 6 +- src/app/icons/base/outLink.svg | 5 + .../backup-options-modal.tsx | 2 +- .../components/CollectibleItem.tsx | 89 ++--- .../pages/Home/OtherComponents/AssetTab.tsx | 59 +++- src/app/root-hooks/evm/balances-loading.ts | 2 +- .../evm/collectibles-metadata-loading.ts | 2 +- .../evm/tokens-exchange-rates-loading.ts | 2 +- .../root-hooks/evm/tokens-metadata-loading.ts | 2 +- src/app/templates/AssetIcon.tsx | 55 +-- src/app/templates/activity/Activity.tsx | 18 +- src/app/templates/activity/evm/index.tsx | 263 ++++++++++++++ .../activity/interactions-connector.svg | 6 + .../temple/endpoints/evm/{api.ts => index.ts} | 10 +- .../endpoints/evm/types/transactions.ts | 329 ++++++++++++++++++ src/lib/utils/evm.utils.ts | 12 + 20 files changed, 789 insertions(+), 186 deletions(-) create mode 100644 src/app/icons/base/outLink.svg create mode 100644 src/app/templates/activity/evm/index.tsx create mode 100644 src/app/templates/activity/interactions-connector.svg rename src/lib/apis/temple/endpoints/evm/{api.ts => index.ts} (70%) create mode 100644 src/lib/apis/temple/endpoints/evm/types/transactions.ts diff --git a/src/app/atoms/EmptyState.tsx b/src/app/atoms/EmptyState.tsx index 29b88abd0d..40d824fc78 100644 --- a/src/app/atoms/EmptyState.tsx +++ b/src/app/atoms/EmptyState.tsx @@ -8,10 +8,10 @@ import { T } from 'lib/i18n'; interface EmptyStateProps { className?: string; - variant: 'tokenSearch' | 'universal' | 'searchUniversal'; + variant?: 'tokenSearch' | 'universal' | 'searchUniversal'; } -export const EmptyState = memo(({ className, variant }) => ( +export const EmptyState = memo(({ className, variant = 'universal' }) => (
{variant === 'universal' ? : } diff --git a/src/app/atoms/Identicon.tsx b/src/app/atoms/Identicon.tsx index b7c62db6fc..13175b06a9 100644 --- a/src/app/atoms/Identicon.tsx +++ b/src/app/atoms/Identicon.tsx @@ -27,13 +27,14 @@ export const Identicon = ({
- +
); }; diff --git a/src/app/atoms/ModalInfoBlock.tsx b/src/app/atoms/ModalInfoBlock.tsx index fd8d40c5c3..7ed62605bb 100644 --- a/src/app/atoms/ModalInfoBlock.tsx +++ b/src/app/atoms/ModalInfoBlock.tsx @@ -34,7 +34,7 @@ export const ModalInfoBlock = memo(({ Icon, headline, descr isHovered ? 'bg-primary-hover-low text-primary-hover' : 'bg-primary-low text-primary' )} > - +
diff --git a/src/app/atoms/NetworkLogo.tsx b/src/app/atoms/NetworkLogo.tsx index 07aeef19d3..d401662320 100644 --- a/src/app/atoms/NetworkLogo.tsx +++ b/src/app/atoms/NetworkLogo.tsx @@ -1,6 +1,7 @@ -import React, { CSSProperties, forwardRef, memo, useMemo } from 'react'; +import React, { CSSProperties, FC, memo, useMemo } from 'react'; import clsx from 'clsx'; +import type { Placement } from 'tippy.js'; import BinanceSmartChainIconSrc from 'app/icons/networks/bsc.svg?url'; import EthereumIconSrc from 'app/icons/networks/ethereum.svg?url'; @@ -8,6 +9,7 @@ import OptimismIconSrc from 'app/icons/networks/optimism.svg?url'; import PolygonIconSrc from 'app/icons/networks/polygon.svg?url'; import { getEvmNativeAssetIcon } from 'lib/images-uri'; import { TEZOS_MAINNET_CHAIN_ID } from 'lib/temple/types'; +import useTippy, { UseTippyOptions } from 'lib/ui/useTippy'; import { Identicon } from './Identicon'; import { TezNetworkLogo } from './NetworksLogos'; @@ -37,47 +39,28 @@ interface EvmNetworkLogoProps { networkName: string; chainId: number; size?: number; - className?: string; imgClassName?: string; style?: CSSProperties; } -export const EvmNetworkLogo = memo( - forwardRef( - ({ networkName, chainId, size = 24, className, imgClassName, style }, ref) => { - const source = useMemo(() => { - if (logosRecord[chainId]) return logosRecord[chainId]; - - const nativeAssetIcon = getEvmNativeAssetIcon(chainId, size * 2); - - if (nativeAssetIcon) return nativeAssetIcon; - - return undefined; - }, [chainId, size]); - - return ( -
- {source ? ( - {networkName} - ) : ( - - )} -
- ); - } - ) -); +export const EvmNetworkLogo = memo(({ networkName, chainId, size = 24, imgClassName, style }) => { + const source = useMemo(() => logosRecord[chainId] || getEvmNativeAssetIcon(chainId, size * 2), [chainId, size]); -const ICON_CONTAINER_MULTIPLIER = 0.8; -const ICON_SIZE_MULTIPLIER = 2; -const initialsOpts = { chars: 1 }; + return source ? ( + {networkName} + ) : ( + + ); +}); + +const IDENTICON_OPTS = { chars: 1 }; interface NetworkLogoFallbackProps { networkName: string; @@ -85,16 +68,45 @@ interface NetworkLogoFallbackProps { className?: string; } -export const NetworkLogoFallback = memo(({ networkName, size = 24, className }) => ( +const NetworkLogoFallback = memo(({ networkName, size = 24, className }) => (
-
- +
+
)); + +interface NetworkLogoTooltipWrapProps { + className?: string; + networkName?: string; + placement?: Placement; +} + +export const NetworkLogoTooltipWrap: FC> = ({ + className, + networkName, + placement = 'bottom-start', + children +}) => { + const tippyProps = useMemo( + () => ({ + trigger: 'mouseenter', + hideOnClick: false, + content: networkName ?? 'Unknown Network', + animation: 'shift-away-subtle', + placement + }), + [networkName, placement] + ); + + const networkIconRef = useTippy(tippyProps); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/app/icons/base/documents.svg b/src/app/icons/base/documents.svg index f6cca48335..afa80defbe 100644 --- a/src/app/icons/base/documents.svg +++ b/src/app/icons/base/documents.svg @@ -1,3 +1,5 @@ - - + + diff --git a/src/app/icons/base/outLink.svg b/src/app/icons/base/outLink.svg new file mode 100644 index 0000000000..ac671662a3 --- /dev/null +++ b/src/app/icons/base/outLink.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx b/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx index 146c02b387..c6812f32bd 100644 --- a/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx +++ b/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx @@ -31,7 +31,7 @@ export const BackupOptionsModal = memo(({ onSelect }) = onClick={onSelect} testID={BackupOptionsModalSelectors.manualBackupButton} > - + {t('backupManually')}
diff --git a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx index 54b87bf939..f28fe8ba67 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx @@ -5,8 +5,7 @@ import clsx from 'clsx'; import { IconBase, ToggleSwitch } from 'app/atoms'; import Money from 'app/atoms/Money'; -import { EvmNetworkLogo, NetworkLogoFallback } from 'app/atoms/NetworkLogo'; -import { TezNetworkLogo } from 'app/atoms/NetworksLogos'; +import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as DeleteIcon } from 'app/icons/base/delete.svg'; import { dispatch } from 'app/store'; import { setEvmCollectibleStatusAction } from 'app/store/evm/assets/actions'; @@ -29,9 +28,7 @@ import { T } from 'lib/i18n'; import { getTokenName } from 'lib/metadata'; import { getCollectibleName, getCollectionName } from 'lib/metadata/utils'; import { atomsToTokens } from 'lib/temple/helpers'; -import { TEZOS_MAINNET_CHAIN_ID } from 'lib/temple/types'; import { useBooleanState } from 'lib/ui/hooks'; -import useTippy, { UseTippyOptions } from 'lib/ui/useTippy'; import { Link } from 'lib/woozie'; import { useEvmChainByChainId, useTezosChainByChainId } from 'temple/front/chains'; import { TempleChainKind } from 'temple/types'; @@ -67,17 +64,6 @@ export const TezosCollectibleItem = memo( const network = useTezosChainByChainId(tezosChainId); - const tippyProps = useMemo( - () => ({ - trigger: 'mouseenter', - hideOnClick: false, - content: network?.name ?? 'Unknown Network', - animation: 'shift-away-subtle', - placement: 'bottom' - }), - [network] - ); - const storedToken = useStoredTezosCollectibleSelector(accountPkh, tezosChainId, assetSlug); const checked = getAssetStatus(balanceAtomic, storedToken?.status) === 'enabled'; @@ -110,8 +96,6 @@ export const TezosCollectibleItem = memo( [checked, assetSlug, tezosChainId, accountPkh] ); - const networkIconRef = useTippy(tippyProps); - const decimals = metadata?.decimals; const imgContainerStyles = useMemo( @@ -168,13 +152,17 @@ export const TezosCollectibleItem = memo( /> {network && ( -
- {network.chainId === TEZOS_MAINNET_CHAIN_ID ? ( - - ) : ( - - )} -
+ + + )}
@@ -231,13 +219,13 @@ export const TezosCollectibleItem = memo( )} {network && ( -
- {network.chainId === TEZOS_MAINNET_CHAIN_ID ? ( - - ) : ( - - )} -
+ + + )} @@ -322,19 +310,6 @@ export const EvmCollectibleItem = memo( const network = useEvmChainByChainId(evmChainId); - const tippyProps = useMemo( - () => ({ - trigger: 'mouseenter', - hideOnClick: false, - content: network?.name ?? 'Unknown Network', - animation: 'shift-away-subtle', - placement: 'bottom' - }), - [network] - ); - - const networkIconRef = useTippy(tippyProps); - const imgContainerStyles = useMemo( () => (showDetails ? ImgWithDetailsContainerStyle : ImgContainerStyle), [showDetails] @@ -359,13 +334,17 @@ export const EvmCollectibleItem = memo( {metadata && } {network && ( - + className="absolute bottom-0.5 right-0.5" + placement="bottom" + > + + )} @@ -412,13 +391,9 @@ export const EvmCollectibleItem = memo( )} {network && ( - + + + )} diff --git a/src/app/pages/Home/OtherComponents/AssetTab.tsx b/src/app/pages/Home/OtherComponents/AssetTab.tsx index c8cd53e7f8..bf1b60bff8 100644 --- a/src/app/pages/Home/OtherComponents/AssetTab.tsx +++ b/src/app/pages/Home/OtherComponents/AssetTab.tsx @@ -1,14 +1,15 @@ -import React, { memo } from 'react'; +import React, { FC, memo } from 'react'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import { ContentContainer } from 'app/layouts/containers'; -import { ActivityTab } from 'app/templates/activity/Activity'; +import { ActivityTab, EvmActivityTab } from 'app/templates/activity/Activity'; import AssetInfo from 'app/templates/AssetInfo'; import { TabsBar, TabsBarTabInterface } from 'app/templates/TabBar'; import { TEZ_TOKEN_SLUG, isTezAsset } from 'lib/assets'; +import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { t } from 'lib/i18n'; -import { UNDER_DEVELOPMENT_MSG } from 'temple/evm/under_dev_msg'; +import { isEvmNativeTokenSlug } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; import { HomeProps } from '../interfaces'; @@ -20,23 +21,22 @@ export const AssetTab = memo>>(({ chainKin {chainKind === TempleChainKind.Tezos ? ( ) : ( - + )} )); -interface AssetTabProps { +interface TezosAssetTabProps { chainId: string; assetSlug: string; } -const TezosAssetTab = memo(({ chainId, assetSlug }) => +const TezosAssetTab: FC = ({ chainId, assetSlug }) => isTezAsset(assetSlug) ? ( ) : ( - ) -); + ); const TEZOS_GAS_TABS: TabsBarTabInterface[] = [ { name: 'activity', titleI18nKey: 'activity' }, @@ -62,18 +62,18 @@ const TezosGasTab = memo<{ tezosChainId: string }>(({ tezosChainId }) => { ); }); -const TEZOS_TOKEN_TABS: TabsBarTabInterface[] = [ +const TOKEN_TABS: TabsBarTabInterface[] = [ { name: 'activity', titleI18nKey: 'activity' }, { name: 'info', titleI18nKey: 'info' } ]; -const TezosTokenTab = memo(({ chainId, assetSlug }) => { +const TezosTokenTab = memo(({ chainId, assetSlug }) => { const tabSlug = useLocationSearchParamValue('tab'); const tabName = tabSlug === 'info' ? 'info' : 'activity'; return ( <> - + {tabName === 'activity' ? ( @@ -84,23 +84,44 @@ const TezosTokenTab = memo(({ chainId, assetSlug }) => { ); }); -const EVM_TOKEN_TABS: TabsBarTabInterface[] = [ - { name: 'activity', titleI18nKey: 'activity' }, - { name: 'info', titleI18nKey: 'info' } -]; +interface EvmAssetTabProps { + chainId: number; + assetSlug: string; +} + +const EvmAssetTab: FC = ({ chainId, assetSlug }) => + isEvmNativeTokenSlug(assetSlug) ? ( + + ) : ( + + ); + +const EVM_GAS_TABS: TabsBarTabInterface[] = [{ name: 'activity', titleI18nKey: 'activity' }]; + +const EvmGasTab = memo<{ chainId: number }>(({ chainId }) => { + const tabName = 'activity'; + + return ( + <> + + + + + ); +}); -const EvmTokenTab = memo(({ chainId, assetSlug }) => { +const EvmTokenTab = memo(({ chainId, assetSlug }) => { const tabSlug = useLocationSearchParamValue('tab'); const tabName = tabSlug === 'info' ? 'info' : 'activity'; return ( <> - + {tabName === 'activity' ? ( -
{UNDER_DEVELOPMENT_MSG}
+ ) : ( - + )} ); diff --git a/src/app/root-hooks/evm/balances-loading.ts b/src/app/root-hooks/evm/balances-loading.ts index d54ca1cd30..e5d6e97119 100644 --- a/src/app/root-hooks/evm/balances-loading.ts +++ b/src/app/root-hooks/evm/balances-loading.ts @@ -5,7 +5,7 @@ import { setEvmBalancesLoadingState } from 'app/store/evm/actions'; import { processLoadedEvmAssetsAction } from 'app/store/evm/assets/actions'; import { processLoadedEvmAssetsBalancesAction } from 'app/store/evm/balances/actions'; import { useAllEvmChainsBalancesLoadingStatesSelector } from 'app/store/evm/selectors'; -import { getEvmBalances } from 'lib/apis/temple/endpoints/evm/api'; +import { getEvmBalances } from 'lib/apis/temple/endpoints/evm'; import { isSupportedChainId } from 'lib/apis/temple/endpoints/evm/api.utils'; import { EVM_BALANCES_SYNC_INTERVAL } from 'lib/fixed-times'; import { useInterval, useMemoWithCompare } from 'lib/ui/hooks'; diff --git a/src/app/root-hooks/evm/collectibles-metadata-loading.ts b/src/app/root-hooks/evm/collectibles-metadata-loading.ts index 12c81b7b50..b0e353ac1d 100644 --- a/src/app/root-hooks/evm/collectibles-metadata-loading.ts +++ b/src/app/root-hooks/evm/collectibles-metadata-loading.ts @@ -9,7 +9,7 @@ import { } from 'app/store/evm/collectibles-metadata/actions'; import { useEvmCollectiblesMetadataRecordSelector } from 'app/store/evm/collectibles-metadata/selectors'; import { useEvmCollectiblesMetadataLoadingSelector } from 'app/store/evm/selectors'; -import { getEvmCollectiblesMetadata } from 'lib/apis/temple/endpoints/evm/api'; +import { getEvmCollectiblesMetadata } from 'lib/apis/temple/endpoints/evm'; import { isSupportedChainId } from 'lib/apis/temple/endpoints/evm/api.utils'; import { fetchEvmCollectiblesMetadataFromChain } from 'lib/evm/on-chain/metadata'; import { useEnabledEvmChains } from 'temple/front'; diff --git a/src/app/root-hooks/evm/tokens-exchange-rates-loading.ts b/src/app/root-hooks/evm/tokens-exchange-rates-loading.ts index 2541191609..9bce2b71a0 100644 --- a/src/app/root-hooks/evm/tokens-exchange-rates-loading.ts +++ b/src/app/root-hooks/evm/tokens-exchange-rates-loading.ts @@ -4,7 +4,7 @@ import { dispatch } from 'app/store'; import { setEvmTokensExchangeRatesLoading } from 'app/store/evm/actions'; import { useEvmTokensExchangeRatesLoadingSelector } from 'app/store/evm/selectors'; import { processLoadedEvmExchangeRatesAction } from 'app/store/evm/tokens-exchange-rates/actions'; -import { getEvmTokensMetadata } from 'lib/apis/temple/endpoints/evm/api'; +import { getEvmTokensMetadata } from 'lib/apis/temple/endpoints/evm'; import { isSupportedChainId } from 'lib/apis/temple/endpoints/evm/api.utils'; import { RATES_SYNC_INTERVAL } from 'lib/fixed-times'; import { useInterval, useMemoWithCompare } from 'lib/ui/hooks'; diff --git a/src/app/root-hooks/evm/tokens-metadata-loading.ts b/src/app/root-hooks/evm/tokens-metadata-loading.ts index 6fa630ff10..777571c7df 100644 --- a/src/app/root-hooks/evm/tokens-metadata-loading.ts +++ b/src/app/root-hooks/evm/tokens-metadata-loading.ts @@ -13,7 +13,7 @@ import { } from 'app/store/evm/tokens-metadata/actions'; import { useEvmTokensMetadataRecordSelector } from 'app/store/evm/tokens-metadata/selectors'; import { isValidFetchedEvmMetadata } from 'app/store/evm/tokens-metadata/utils'; -import { getEvmTokensMetadata } from 'lib/apis/temple/endpoints/evm/api'; +import { getEvmTokensMetadata } from 'lib/apis/temple/endpoints/evm'; import { isSupportedChainId } from 'lib/apis/temple/endpoints/evm/api.utils'; import { toTokenSlug } from 'lib/assets'; import { fetchEvmTokensMetadataFromChain } from 'lib/evm/on-chain/metadata'; diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index f70bf9c5f2..9653f40401 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -1,16 +1,13 @@ -import React, { FC, memo, useMemo } from 'react'; +import React, { FC, memo } from 'react'; import clsx from 'clsx'; import { Identicon } from 'app/atoms'; -import { EvmNetworkLogo, NetworkLogoFallback } from 'app/atoms/NetworkLogo'; -import { TezNetworkLogo } from 'app/atoms/NetworksLogos'; +import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as CollectiblePlaceholder } from 'app/icons/collectible-placeholder.svg'; import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; import { AssetMetadataBase, getAssetSymbol, isCollectible, useTezosAssetMetadata } from 'lib/metadata'; import { EvmTokenMetadata } from 'lib/metadata/types'; -import { TEZOS_MAINNET_CHAIN_ID } from 'lib/temple/types'; -import useTippy, { UseTippyOptions } from 'lib/ui/useTippy'; import { isEvmNativeTokenSlug } from 'lib/utils/evm.utils'; import { useEvmChainByChainId, useTezosChainByChainId } from 'temple/front/chains'; @@ -75,19 +72,6 @@ export const TezosTokenIconWithNetwork = memo( const network = useTezosChainByChainId(tezosChainId); const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); - const tippyProps = useMemo( - () => ({ - trigger: 'mouseenter', - hideOnClick: false, - content: network?.name ?? 'Unknown Network', - animation: 'shift-away-subtle', - placement: 'bottom-start' - }), - [network] - ); - - const networkIconRef = useTippy(tippyProps); - return (
( loader={} fallback={} /> + {network && ( -
- {network.chainId === TEZOS_MAINNET_CHAIN_ID ? ( - - ) : ( - - )} -
+ + + )}
); @@ -128,19 +109,6 @@ export const EvmTokenIconWithNetwork = memo( const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; - const tippyProps = useMemo( - () => ({ - trigger: 'mouseenter', - hideOnClick: false, - content: network?.name ?? 'Unknown Network', - animation: 'shift-away-subtle', - placement: 'bottom-start' - }), - [network] - ); - - const networkIconRef = useTippy(tippyProps); - return (
( loader={} fallback={} /> + {network && ( - + + + )}
); diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index 892485bdac..31311c362c 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -16,12 +16,14 @@ import { PartnersPromotion, PartnersPromotionVariant } from 'app/templates/partn import { TEMPLE_TOKEN_SLUG } from 'lib/assets'; import { t, T } from 'lib/i18n/react'; import useTezosActivities from 'lib/temple/activity-new/hook'; -import { UNDER_DEVELOPMENT_MSG } from 'temple/evm/under_dev_msg'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { ActivityItem } from './ActivityItem'; +import { EvmActivityTab } from './evm'; import { ReactivateAdsBanner } from './ReactivateAdsBanner'; +export { EvmActivityTab }; + const INITIAL_NUMBER = 30; const LOAD_STEP = 30; @@ -32,7 +34,11 @@ interface Props { export const ActivityTab = memo(({ tezosChainId, assetSlug }) => ( - {tezosChainId ? : } + {tezosChainId ? ( + + ) : ( + + )} )); @@ -48,21 +54,21 @@ const ActivityWithChainSelect = memo(() => { {network.kind === 'tezos' ? ( - + ) : ( -
{UNDER_DEVELOPMENT_MSG}
+ )} ); }); -interface TezosActivityProps { +interface TezosActivityTabProps { tezosChainId: string; assetSlug?: string; } -const TezosActivity: FC = ({ tezosChainId, assetSlug }) => { +const TezosActivityTab: FC = ({ tezosChainId, assetSlug }) => { const network = useTezosChainByChainId(tezosChainId); const accountAddress = useAccountAddressForTezos(); if (!network || !accountAddress) throw new DeadEndBoundaryError(); diff --git a/src/app/templates/activity/evm/index.tsx b/src/app/templates/activity/evm/index.tsx new file mode 100644 index 0000000000..956603d226 --- /dev/null +++ b/src/app/templates/activity/evm/index.tsx @@ -0,0 +1,263 @@ +import React, { FC, memo } from 'react'; + +import clsx from 'clsx'; + +import { Anchor, HashShortView, IconBase, SyncSpinner } from 'app/atoms'; +import { EmptyState } from 'app/atoms/EmptyState'; +import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { DeadEndBoundaryError } from 'app/ErrorBoundary'; +import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { ReactComponent as DocumentsIcon } from 'app/icons/base/documents.svg'; +import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; +import { getEvmTransactions, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; +import { t } from 'lib/i18n'; +import { useTypedSWR } from 'lib/swr'; +import { useBooleanState } from 'lib/ui/hooks'; +import { getEvmAddressSafe } from 'lib/utils/evm.utils'; +import { useAccountAddressForEvm } from 'temple/front'; +import { OneOfChains, useEvmChainByChainId } from 'temple/front/chains'; +import { TempleChainKind } from 'temple/types'; + +import { ReactComponent as InteractionsConnectorSvg } from '../interactions-connector.svg'; + +interface EvmActivityTabProps { + chainId: number; + assetSlug?: string; +} + +export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { + const network = useEvmChainByChainId(chainId); + const accountAddress = useAccountAddressForEvm(); + + if (!network || !accountAddress) throw new DeadEndBoundaryError(); + + const { data, isLoading: isSyncing } = useTypedSWR(['evm-activity-history', chainId], async () => { + return await getEvmTransactions(accountAddress, chainId, 0); + }); + + console.log('Data:', data); + + console.log(1, data?.items.length); + console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); + + const activities = data?.items.map(item => parseGoldRushTransaction(item, chainId, accountAddress)) ?? []; + + return ( +
+ {activities.length ? ( + <> + {activities.map(activity => ( + + ))} + + {isSyncing && } + + ) : isSyncing ? ( + + ) : ( + + )} +
+ ); +}; + +function parseGoldRushTransaction(item: GoldRushTransaction, chainId: number, accountAddress: string): EvmActivity { + const logEvents = item.log_events ?? []; + + return { + chain: TempleChainKind.EVM, + chainId, + kind: (() => { + if (!logEvents.length) { + if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; + } else if (logEvents.length === 1) { + const logEvent = logEvents[0]!; + if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; + + if (logEvent.decoded?.name === 'Transfer') { + if (getEvmAddressSafe(logEvent.decoded.params[0].value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1].value) === accountAddress) return ActivityKindEnum.receive; + } + } + + return ActivityKindEnum.interaction; + })(), + hash: item.tx_hash!, + operations: logEvents.map(logEvent => ({ + kind: (() => { + if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; + + if (logEvent.decoded?.name === 'Transfer') { + if (getEvmAddressSafe(logEvent.decoded.params[0].value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1].value) === accountAddress) return ActivityKindEnum.receive; + } + + return ActivityKindEnum.interaction; + })() + })) + }; +} + +const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ activity, chain }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + + const hash = activity.hash; + + if (activity.kind !== ActivityKindEnum.interaction || activity.operations.length <= 1) + return ( + + ); + + const operations = activity.operations; + + return ( +
+ {operations.slice(0, 3).map((operation, i) => ( + <> + {i > 0 && } + + + + ))} + + {operations.length > 3 ? ( + <> + + + {expanded + ? operations.slice(3).map((operation, j) => ( + <> + {j > 0 && } + + + + )) + : null} + + ) : null} +
+ ); +}; + +const InteractionsConnector = memo(() => ( +
+ +
+)); + +interface ActivityItemBaseComponentProps { + chainId: string | number; + kind: ActivityKindEnum; + hash: string; + networkName: string; +} + +const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName }) => { + return ( + +
+
+ {/* asset */} + +
+ + + {typeof chainId === 'number' ? ( + + ) : ( + + )} + +
+ +
+
+
{ActivityKindTitle[kind]}
+ +
-33 USDT
+
+ +
+
+ + + +
+ +
-12.00 $
+
+
+
+ ); +}; + +enum ActivityKindEnum { + interaction, + send, + receive, + swap, + approve +} + +const ActivityKindTitle: Record = { + [ActivityKindEnum.interaction]: 'Interaction', + [ActivityKindEnum.send]: 'Send', + [ActivityKindEnum.receive]: 'Receive', + [ActivityKindEnum.swap]: 'Swap', + [ActivityKindEnum.approve]: 'Approve' +}; + +type Activity = TezosActivity | EvmActivity; + +interface ActivityBase { + kind: ActivityKindEnum; +} + +interface TezosActivity extends ActivityBase { + chain: TempleChainKind.Tezos; + chainId: string; + hash: string; + operations: TezosOperation[]; +} + +interface TezosOperation { + kind: ActivityKindEnum; +} + +interface EvmActivity extends ActivityBase { + chain: TempleChainKind.EVM; + chainId: number; + hash: string; + operations: EvmOperation[]; +} + +interface EvmOperation { + kind: ActivityKindEnum; +} diff --git a/src/app/templates/activity/interactions-connector.svg b/src/app/templates/activity/interactions-connector.svg new file mode 100644 index 0000000000..064f90a8b5 --- /dev/null +++ b/src/app/templates/activity/interactions-connector.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/lib/apis/temple/endpoints/evm/api.ts b/src/lib/apis/temple/endpoints/evm/index.ts similarity index 70% rename from src/lib/apis/temple/endpoints/evm/api.ts rename to src/lib/apis/temple/endpoints/evm/index.ts index af8bec39ef..0ffa7005ab 100644 --- a/src/lib/apis/temple/endpoints/evm/api.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -1,6 +1,9 @@ import { templeWalletApi } from '../templewallet.api'; import { BalancesResponse, ChainID, NftAddressBalanceNftResponse } from './api.interfaces'; +import { GoldRushTransaction } from './types/transactions'; + +export type { GoldRushTransaction }; export const getEvmBalances = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/balances', walletAddress, chainId); @@ -12,10 +15,13 @@ export const getEvmTokensMetadata = (walletAddress: string, chainId: ChainID) => export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/collectibles-metadata', walletAddress, chainId); -const buildEvmRequest = (url: string, walletAddress: string, chainId: ChainID) => +export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page: number) => + buildEvmRequest<{ items: GoldRushTransaction[] }>('/transactions', walletAddress, chainId, { page }); + +const buildEvmRequest = (url: string, walletAddress: string, chainId: ChainID, params?: object) => templeWalletApi .get(`evm${url}`, { - params: { walletAddress, chainId } + params: { ...params, walletAddress, chainId } }) .then( res => res.data, diff --git a/src/lib/apis/temple/endpoints/evm/types/transactions.ts b/src/lib/apis/temple/endpoints/evm/types/transactions.ts new file mode 100644 index 0000000000..0debaf8b5f --- /dev/null +++ b/src/lib/apis/temple/endpoints/evm/types/transactions.ts @@ -0,0 +1,329 @@ +type Nullable = { + [P in keyof T]: T[P] | null; +}; + +export type GoldRushTransaction = Nullable<{ + /** * The block signed timestamp in UTC. */ + block_signed_at: Date; + /** * The height of the block. */ + block_height: number; + /** * The hash of the block. Use it to remove transactions from re-org-ed blocks. */ + block_hash: string; + /** * The requested transaction hash. */ + tx_hash: string; + /** * The offset is the position of the tx in the block. */ + tx_offset: number; + /** * Indicates whether a transaction failed or succeeded. */ + successful: boolean; + /** * The sender's wallet address. */ + from_address: string; + /** * The address of the miner. */ + miner_address: string; + /** * The label of `from` address. */ + from_address_label: string; + /** * The receiver's wallet address. */ + to_address: string; + /** * The label of `to` address. */ + to_address_label: string; + /** * The value attached to this tx. */ + value: bigint; + /** * The value attached in `quote-currency` to this tx. */ + value_quote: number; + /** * A prettier version of the quote for rendering purposes. */ + pretty_value_quote: string; + /** * The requested chain native gas token metadata. */ + gas_metadata: ContractMetadata; + gas_offered: number; + /** * The gas spent for this tx. */ + gas_spent: number; + /** * The gas price at the time of this tx. */ + gas_price: number; + /** * The total transaction fees (`gas_price` * `gas_spent`) paid for this tx, denoted in wei. */ + fees_paid: bigint; + /** * The gas spent in `quote-currency` denomination. */ + gas_quote: number; + /** * A prettier version of the quote for rendering purposes. */ + pretty_gas_quote: string; + /** * The native gas exchange rate for the requested `quote-currency`. */ + gas_quote_rate: number; + /** * The explorer links for this transaction. */ + explorers: Explorer[]; + /** * The details for the dex transaction. */ + dex_details: Nullable[]; + /** * The details for the NFT sale transaction. */ + nft_sale_details: Nullable[]; + /** * The details for the lending protocol transaction. */ + lending_details: Nullable[]; + /** * The log events. */ + log_events: Nullable[]; + /** * The details related to the safe transaction. */ + safe_details: Nullable[]; +}>; + +interface NftSalesReport { + /** * The offset is the position of the log entry within an event log. */ + log_offset: number; + /** * Stores the topic event hash. All events have a unique topic event hash. */ + topic0: string; + /** * Stores the contract address of the protocol that facilitated the event. */ + protocol_contract_address: string; + /** * Stores the name of the protocol that facilitated the event. */ + protocol_name: string; + /** * The protocol logo URL. */ + protocol_logo_url: string; + /** * Stores the address of the transaction recipient. */ + to: string; + /** * Stores the address of the transaction sender. */ + from: string; + /** * Stores the address selling the NFT. */ + maker: string; + /** * Stores the address buying the NFT. */ + taker: string; + /** * Stores the NFTs token ID. All NFTs have a token ID. Within a collection, these token IDs are unique. If the NFT is transferred to another owner, the token id remains the same, as this number is its identifier within a collection. For example, if a collection has 10K NFTs then an NFT in that collection can have a token ID from 1-10K. */ + token_id: string; + /** * Stores the address of the collection. For example, [Bored Ape Yacht Club](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d) */ + collection_address: string; + /** * Stores the name of the collection. */ + collection_name: string; + /** * Stores the address of the token used to purchase the NFT. */ + token_address: string; + /** * Stores the name of the token used to purchase the NFT. */ + token_name: string; + /** * Stores the ticker symbol of the token used to purchase the NFT. */ + ticker_symbol: string; + /** * Stores the number decimal of the token used to purchase the NFT. */ + num_decimals: number; + contract_quote_rate: number; + /** * The token amount used to purchase the NFT. For example, if the user purchased an NFT for 1 ETH. The `nft_token_price` field will hold `1`. */ + nft_token_price: number; + /** * The USD amount used to purchase the NFT. */ + nft_token_price_usd: number; + pretty_nft_token_price_usd: string; + /** * The price of the NFT denominated in the chains native token. Even if a seller sells their NFT for DAI or MANA, this field denominates the price in the native token (e.g. ETH, AVAX, FTM, etc.) */ + nft_token_price_native: number; + pretty_nft_token_price_native: string; + /** * Stores the number of NFTs involved in the sale. It's quick routine to see multiple NFTs involved in a single sale. */ + token_count: number; + num_token_ids_sold_per_sale: number; + num_token_ids_sold_per_tx: number; + num_collections_sold_per_sale: number; + num_collections_sold_per_tx: number; + trade_type: string; + trade_group_type: string; +} + +interface DexReport { + /** * The offset is the position of the log entry within an event log. */ + log_offset: number; + /** * Stores the name of the protocol that facilitated the event. */ + protocol_name: string; + /** * Stores the contract address of the protocol that facilitated the event. */ + protocol_address: string; + /** * The protocol logo URL. */ + protocol_logo_url: string; + /** * Stores the aggregator responsible for the event. */ + aggregator_name: string; + /** * Stores the contract address of the aggregator responsible for the event. */ + aggregator_address: string; + /** * DEXs often have multiple version - e.g Uniswap V1, V2 and V3. The `version` field allows you to look at a specific version of the DEX. */ + version: number; + /** * Similarly to the `version` field, `fork_version` gives you the version of the forked DEX. For example, most DEXs are a fork of Uniswap V2; therefore, `fork` = `aave` & `fork_version` = `2` */ + fork_version: number; + /** * Many DEXs are a fork of an already established DEX. The fork field allows you to see which DEX has been forked. */ + fork: string; + /** * Stores the event taking place - e.g `swap`, `add_liquidity` and `remove_liquidity`. */ + event: string; + /** * Stores the address of the pair that the user interacts with. */ + pair_address: string; + pair_lp_fee_bps: number; + lp_token_address: string; + lp_token_ticker: string; + lp_token_num_decimals: number; + lp_token_name: string; + lp_token_value: string; + exchange_rate_usd: number; + /** * Stores the address of token 0 in the specific pair. */ + token_0_address: string; + /** * Stores the ticker symbol of token 0 in the specific pair. */ + token_0_ticker: string; + /** * Stores the number of contract decimals of token 0 in the specific pair. */ + token_0_num_decimals: number; + /** * Stores the contract name of token 0 in the specific pair. */ + token_0_name: string; + /** * Stores the address of token 1 in the specific pair. */ + token_1_address: string; + /** * Stores the ticker symbol of token 1 in the specific pair. */ + token_1_ticker: string; + /** * Stores the number of contract decimals of token 1 in the specific pair. */ + token_1_num_decimals: number; + /** * Stores the contract name of token 1 in the specific pair. */ + token_1_name: string; + /** * Stores the amount of token 0 used in the transaction. For example, 1 ETH, 100 USDC, 30 UNI, etc. */ + token_0_amount: string; + token_0_quote_rate: number; + token_0_usd_quote: number; + pretty_token_0_usd_quote: string; + token_0_logo_url: string; + /** * Stores the amount of token 1 used in the transaction. For example, 1 ETH, 100 USDC, 30 UNI, etc. */ + token_1_amount: string; + token_1_quote_rate: number; + token_1_usd_quote: number; + pretty_token_1_usd_quote: string; + token_1_logo_url: string; + /** * Stores the wallet address that initiated the transaction (i.e the wallet paying the gas fee). */ + sender: string; + /** * Stores the recipient of the transaction - recipients can be other wallets or smart contracts. For example, if you want to Swap tokens on Uniswap, the Uniswap router would typically be the recipient of the transaction. */ + recipient: string; +} + +interface ContractMetadata { + /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ + contract_decimals: number; + /** * The string returned by the `name()` method. */ + contract_name: string; + /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ + contract_ticker_symbol: string; + /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ + contract_address: string; + /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ + supports_erc: string[]; + /** * The contract logo URL. */ + logo_url: string; +} + +interface Explorer { + /** * The name of the explorer. */ + label: string; + /** * The URL of the explorer. */ + url: string; +} + +interface LendingReport { + /** * The offset is the position of the log entry within an event log. */ + log_offset: number; + /** * Stores the name of the lending protocol that facilitated the event. */ + protocol_name: string; + /** * Stores the contract address of the lending protocol that facilitated the event. */ + protocol_address: string; + /** * The protocol logo URL. */ + protocol_logo_url: string; + /** * Lending protocols often have multiple version (e.g. Aave V1, V2 and V3). The `version` field allows you to look at a specific version of the Lending protocol. */ + version: string; + /** * Many lending protocols are a fork of an already established protocol. The fork column allows you to see which lending protocol has been forked. */ + fork: string; + /** * Similarly to the `version` column, `fork_version` gives you the version of the forked lending protocol. For example, most lending protocols in the space are a fork of Aave V2; therefore, `fork` = `aave` & `fork_version` = `2` */ + fork_version: string; + /** * Stores the event taking place - e.g `borrow`, `deposit`, `liquidation`, 'repay' and 'withdraw'. */ + event: string; + /** * Stores the name of the LP token issued by the lending protocol. LP tokens can be debt or interest bearing tokens. */ + lp_token_name: string; + /** * Stores the number decimal of the LP token. */ + lp_decimals: number; + /** * Stores the ticker symbol of the LP token. */ + lp_ticker_symbol: string; + /** * Stores the token address of the LP token. */ + lp_token_address: string; + /** * Stores the amount of LP token used in the event (e.g. 1 aETH, 100 cUSDC, etc). */ + lp_token_amount: number; + /** * Stores the total USD amount of all the LP Token used in the event. */ + lp_token_price: number; + /** * Stores the exchange rate between the LP and underlying token. */ + exchange_rate: number; + /** * Stores the USD price of the LP Token used in the event. */ + exchange_rate_usd: number; + /** * Stores the name of the token going into the lending protocol (e.g the token getting deposited). */ + token_name_in: string; + /** * Stores the number decimal of the token going into the lending protocol. */ + token_decimal_in: number; + /** * Stores the address of the token going into the lending protocol. */ + token_address_in: string; + /** * Stores the ticker symbol of the token going into the lending protocol. */ + token_ticker_in: string; + /** * Stores the logo URL of the token going into the lending protocol. */ + token_logo_in: string; + /** * Stores the amount of tokens going into the lending protocol (e.g 1 ETH, 100 USDC, etc). */ + token_amount_in: number; + /** * Stores the total USD amount of all tokens going into the lending protocol. */ + amount_in_usd: number; + pretty_amount_in_usd: string; + /** * Stores the name of the token going out of the lending protocol (e.g the token getting deposited). */ + token_name_out: string; + /** * Stores the number decimal of the token going out of the lending protocol. */ + token_decimals_out: number; + /** * Stores the address of the token going out of the lending protocol. */ + token_address_out: string; + /** * Stores the ticker symbol of the token going out of the lending protocol. */ + token_ticker_out: string; + /** * Stores the logo URL of the token going out of the lending protocol. */ + token_logo_out: string; + /** * Stores the amount of tokens going out of the lending protocol (e.g 1 ETH, 100 USDC, etc). */ + token_amount_out: number; + /** * Stores the total USD amount of all tokens going out of the lending protocol. */ + amount_out_usd: number; + pretty_amount_out_usd: string; + /** * Stores the type of loan the user is taking out. Lending protocols enable you to take out a stable or variable loan. Only relevant to borrow events. */ + borrow_rate_mode: number; + /** * Stores the interest rate of the loan. Only relevant to borrow events. */ + borrow_rate: number; + on_behalf_of: string; + /** * Stores the wallet address liquidating the loan. Only relevant to liquidation events. */ + liquidator: string; + /** * Stores the wallet address of the user initiating the event. */ + user: string; +} + +interface LogEvent { + /** * The block signed timestamp in UTC. */ + block_signed_at: Date; + /** * The height of the block. */ + block_height: number; + /** * The offset is the position of the tx in the block. */ + tx_offset: number; + /** * The offset is the position of the log entry within an event log. */ + log_offset: number; + /** * The requested transaction hash. */ + tx_hash: string; + /** * The log topics in raw data. */ + raw_log_topics: string[]; + /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ + sender_contract_decimals: number; + /** * The name of the sender. */ + sender_name: string; + sender_contract_ticker_symbol: string; + /** * The address of the sender. */ + sender_address: string; + /** * The label of the sender address. */ + sender_address_label: string; + /** * The contract logo URL. */ + sender_logo_url: string; + /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ + supports_erc: string[]; + /** * The address of the deployed UniswapV2 like factory contract for this DEX. */ + sender_factory_address: string; + /** * The log events in raw. */ + raw_log_data: string; + /** * The decoded item. */ + decoded: DecodedItem; +} + +interface SafeDetails { + /** * The address that signed the safe transaction. */ + owner_address: string; + /** * The signature of the owner for the safe transaction. */ + signature: string; + /** * The type of safe signature used. */ + signature_type: string; +} + +interface DecodedItem { + name: string; + signature: string; + params: Param[]; +} +interface Param { + name: string; + type: string; + indexed: boolean; + decoded: boolean; + value: string; +} diff --git a/src/lib/utils/evm.utils.ts b/src/lib/utils/evm.utils.ts index e4a30fcf8e..78e3fd40d2 100644 --- a/src/lib/utils/evm.utils.ts +++ b/src/lib/utils/evm.utils.ts @@ -1,3 +1,5 @@ +import { getAddress } from 'viem'; + import { BalanceItem, BalanceNftData, NftData } from 'lib/apis/temple/endpoints/evm/api.interfaces'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; @@ -15,3 +17,13 @@ export const isProperCollectibleMetadata = ( Boolean(data.token_id && data.token_url && data.external_data); export const isEvmNativeTokenSlug = (slug: string) => slug === EVM_TOKEN_SLUG; + +export function getEvmAddressSafe(value: string | nullish): HexString | null { + if (!value) return null; + + try { + return getAddress(value); + } catch { + return null; + } +} From c95d0f62960192eac0b6bbb96c3417df8c67086c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 15:45:43 +0300 Subject: [PATCH 02/74] TW-1479: [EVM] Transactions history. Refactor --- TODO.md | 8 ++ src/app/pages/Home/Home.tsx | 4 +- .../pages/Home/OtherComponents/AssetTab.tsx | 24 ++-- .../templates/activity/{evm => }/index.tsx | 127 +++++++++++++++--- .../activity/{Activity.tsx => tezos.tsx} | 51 +------ src/app/templates/activity/utils.ts | 0 6 files changed, 131 insertions(+), 83 deletions(-) create mode 100644 TODO.md rename src/app/templates/activity/{evm => }/index.tsx (64%) rename src/app/templates/activity/{Activity.tsx => tezos.tsx} (72%) create mode 100644 src/app/templates/activity/utils.ts diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000000..2841451d58 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +- NFTs +- Asset details (name, symbol, decimals) due to user's wallet set-up +- `FAILED` // Look into `successful": false` +- Block explorers href +- Date grouping +- Infinite scroll +- New loader +- diff --git a/src/app/pages/Home/Home.tsx b/src/app/pages/Home/Home.tsx index 86977e5d9a..4293d8e10a 100644 --- a/src/app/pages/Home/Home.tsx +++ b/src/app/pages/Home/Home.tsx @@ -12,7 +12,7 @@ import { useAppEnv } from 'app/env'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import PageLayout, { PageLayoutProps } from 'app/layouts/PageLayout'; import { useMainnetTokensScamlistSelector } from 'app/store/tezos/assets/selectors'; -import { ActivityTab } from 'app/templates/activity/Activity'; +import { ActivityWithChainSelect } from 'app/templates/activity'; import { AdvertisingBanner } from 'app/templates/advertising/advertising-banner/advertising-banner'; import { AppHeader } from 'app/templates/AppHeader'; import { toastSuccess } from 'app/toaster'; @@ -110,7 +110,7 @@ const Home = memo(props => { case 'collectibles': return ; case 'activity': - return ; + return ; default: return ; } diff --git a/src/app/pages/Home/OtherComponents/AssetTab.tsx b/src/app/pages/Home/OtherComponents/AssetTab.tsx index bf1b60bff8..aad5a4e477 100644 --- a/src/app/pages/Home/OtherComponents/AssetTab.tsx +++ b/src/app/pages/Home/OtherComponents/AssetTab.tsx @@ -3,7 +3,7 @@ import React, { FC, memo } from 'react'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import { ContentContainer } from 'app/layouts/containers'; -import { ActivityTab, EvmActivityTab } from 'app/templates/activity/Activity'; +import { TezosActivityTab, EvmActivityTab } from 'app/templates/activity'; import AssetInfo from 'app/templates/AssetInfo'; import { TabsBar, TabsBarTabInterface } from 'app/templates/TabBar'; import { TEZ_TOKEN_SLUG, isTezAsset } from 'lib/assets'; @@ -52,7 +52,9 @@ const TezosGasTab = memo<{ tezosChainId: string }>(({ tezosChainId }) => { {tabName === 'activity' ? ( - + + + ) : ( @@ -76,7 +78,9 @@ const TezosTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - + + + ) : ( )} @@ -96,17 +100,11 @@ const EvmAssetTab: FC = ({ chainId, assetSlug }) => ); -const EVM_GAS_TABS: TabsBarTabInterface[] = [{ name: 'activity', titleI18nKey: 'activity' }]; - const EvmGasTab = memo<{ chainId: number }>(({ chainId }) => { - const tabName = 'activity'; - return ( - <> - - + - + ); }); @@ -119,7 +117,9 @@ const EvmTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - + + + ) : ( )} diff --git a/src/app/templates/activity/evm/index.tsx b/src/app/templates/activity/index.tsx similarity index 64% rename from src/app/templates/activity/evm/index.tsx rename to src/app/templates/activity/index.tsx index 956603d226..cf3667efc1 100644 --- a/src/app/templates/activity/evm/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -2,23 +2,54 @@ import React, { FC, memo } from 'react'; import clsx from 'clsx'; -import { Anchor, HashShortView, IconBase, SyncSpinner } from 'app/atoms'; +import { Anchor, HashShortView, IconBase, Money, SyncSpinner } from 'app/atoms'; import { EmptyState } from 'app/atoms/EmptyState'; import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { ReactComponent as DocumentsIcon } from 'app/icons/base/documents.svg'; +import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; +import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; +import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; +import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; +import { ContentContainer } from 'app/layouts/containers'; +import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; import { getEvmTransactions, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; +import { toTokenSlug } from 'lib/assets'; import { t } from 'lib/i18n'; import { useTypedSWR } from 'lib/swr'; +import { atomsToTokens } from 'lib/temple/helpers'; import { useBooleanState } from 'lib/ui/hooks'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { useAccountAddressForEvm } from 'temple/front'; import { OneOfChains, useEvmChainByChainId } from 'temple/front/chains'; import { TempleChainKind } from 'temple/types'; -import { ReactComponent as InteractionsConnectorSvg } from '../interactions-connector.svg'; +import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; +import { TezosActivityTab } from './tezos'; + +export { TezosActivityTab }; + +export const ActivityWithChainSelect = memo(() => { + const chainSelectController = useChainSelectController(); + const network = chainSelectController.value; + + return ( + <> +
+ + + + + {network.kind === 'tezos' ? ( + + ) : ( + + )} + + + ); +}); interface EvmActivityTabProps { chainId: number; @@ -76,30 +107,54 @@ function parseGoldRushTransaction(item: GoldRushTransaction, chainId: number, ac if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0].value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1].value) === accountAddress) return ActivityKindEnum.receive; + if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; } } return ActivityKindEnum.interaction; })(), hash: item.tx_hash!, - operations: logEvents.map(logEvent => ({ - kind: (() => { + operations: logEvents.map(logEvent => { + const kind: ActivityKindEnum = (() => { if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0].value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1].value) === accountAddress) return ActivityKindEnum.receive; + if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; } return ActivityKindEnum.interaction; - })() - })) + })(); + + return { + kind, + asset: (() => { + if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; + + const value: string = logEvent.decoded?.params[2]?.value ?? '0'; + const decimals = logEvent.sender_contract_decimals; + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const nft = logEvent.decoded?.params[2]?.indexed ?? false; + + if (!contractAddress || decimals == null) return; + + return { + slug: toTokenSlug(contractAddress, nft ? value : undefined), + amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${value}` : value, + decimals: nft ? 0 : decimals ?? 0, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft + }; + })() + }; + }) }; } const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ activity, chain }) => { + if (activity.chain !== TempleChainKind.EVM) throw new Error('Tezos activities in dev'); + const [expanded, , , toggleExpanded] = useBooleanState(false); const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; @@ -116,12 +171,12 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act /> ); - const operations = activity.operations; + const operations = activity.operations as EvmOperation[]; return (
{operations.slice(0, 3).map((operation, i) => ( - <> + {i > 0 && } = ({ act hash={hash} chainId={chain.chainId} networkName={networkName} + asset={operation.asset} /> - + ))} {operations.length > 3 ? ( @@ -147,17 +203,17 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act {expanded ? operations.slice(3).map((operation, j) => ( - <> + {j > 0 && } - + )) : null} @@ -177,15 +233,20 @@ interface ActivityItemBaseComponentProps { kind: ActivityKindEnum; hash: string; networkName: string; + asset?: { + amount: string; + decimals: number; + symbol?: string; + }; } -const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName }) => { +const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset }) => { return (
{/* asset */} - +
@@ -201,7 +262,13 @@ const ActivityItemBaseComponent: FC = ({ kind, h
{ActivityKindTitle[kind]}
-
-33 USDT
+ {asset && ( +
+ {asset.amount.startsWith('-') ? null : '+'} + {atomsToTokens(asset.amount, asset.decimals).toFixed(6)}{' '} + {asset.symbol || '???'} +
+ )}
@@ -234,6 +301,14 @@ const ActivityKindTitle: Record = { [ActivityKindEnum.approve]: 'Approve' }; +const ActivityKindIconSvg: Record = { + [ActivityKindEnum.interaction]: DocumentsSvg, + [ActivityKindEnum.send]: SendSvg, + [ActivityKindEnum.receive]: IncomeSvg, + [ActivityKindEnum.swap]: SwapSvg, + [ActivityKindEnum.approve]: DocumentsSvg +}; + type Activity = TezosActivity | EvmActivity; interface ActivityBase { @@ -258,6 +333,14 @@ interface EvmActivity extends ActivityBase { operations: EvmOperation[]; } -interface EvmOperation { - kind: ActivityKindEnum; +interface EvmOperation extends ActivityBase { + asset?: EvmActivityAsset; +} + +interface EvmActivityAsset { + slug: string; + amount: string; + decimals: number; + nft?: boolean; + symbol?: string; } diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/tezos.tsx similarity index 72% rename from src/app/templates/activity/Activity.tsx rename to src/app/templates/activity/tezos.tsx index 31311c362c..8fe84c0b48 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/tezos.tsx @@ -1,74 +1,31 @@ -import React, { memo, FC, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import clsx from 'clsx'; import InfiniteScroll from 'react-infinite-scroll-component'; import { SyncSpinner } from 'app/atoms'; -import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import { useAppEnv } from 'app/env'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { ReactComponent as LayersIcon } from 'app/icons/layers.svg'; -import { ContentContainer } from 'app/layouts/containers'; import { useShouldShowPartnersPromoSelector } from 'app/store/partners-promotion/selectors'; -import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; import { PartnersPromotion, PartnersPromotionVariant } from 'app/templates/partners-promotion'; import { TEMPLE_TOKEN_SLUG } from 'lib/assets'; -import { t, T } from 'lib/i18n/react'; +import { T } from 'lib/i18n/react'; import useTezosActivities from 'lib/temple/activity-new/hook'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { ActivityItem } from './ActivityItem'; -import { EvmActivityTab } from './evm'; import { ReactivateAdsBanner } from './ReactivateAdsBanner'; -export { EvmActivityTab }; - const INITIAL_NUMBER = 30; const LOAD_STEP = 30; - interface Props { - tezosChainId?: string; - assetSlug?: string; -} - -export const ActivityTab = memo(({ tezosChainId, assetSlug }) => ( - - {tezosChainId ? ( - - ) : ( - - )} - -)); - -const ActivityWithChainSelect = memo(() => { - const chainSelectController = useChainSelectController(); - const network = chainSelectController.value; - - return ( - <> -
- - - - - {network.kind === 'tezos' ? ( - - ) : ( - - )} - - - ); -}); - -interface TezosActivityTabProps { tezosChainId: string; assetSlug?: string; } -const TezosActivityTab: FC = ({ tezosChainId, assetSlug }) => { +export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { const network = useTezosChainByChainId(tezosChainId); const accountAddress = useAccountAddressForTezos(); if (!network || !accountAddress) throw new DeadEndBoundaryError(); @@ -142,7 +99,7 @@ const TezosActivityTab: FC = ({ tezosChainId, assetSlug }
); -}; +}); /** * Build onscroll listener to trigger next loading, when fetching data resulted in error. diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts new file mode 100644 index 0000000000..e69de29bb2 From e0bd98547e9c5c87f527b37ac55693a80864c880 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Sep 2024 02:15:26 +0300 Subject: [PATCH 03/74] TW-1479: [EVM] Transactions history. ++ Parsing --- TODO.md | 17 ++ .../Home/OtherComponents/AssetBanner.tsx | 4 +- src/app/templates/AssetIcon.tsx | 50 ++-- .../templates/activity/ActivityItemBase.tsx | 131 +++++++++++ src/app/templates/activity/index.tsx | 193 +--------------- src/app/templates/activity/utils.ts | 213 ++++++++++++++++++ src/lib/assets/utils.ts | 8 +- 7 files changed, 403 insertions(+), 213 deletions(-) create mode 100644 src/app/templates/activity/ActivityItemBase.tsx diff --git a/TODO.md b/TODO.md index 2841451d58..22cc77e917 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- Approve amount - NFTs - Asset details (name, symbol, decimals) due to user's wallet set-up - `FAILED` // Look into `successful": false` @@ -6,3 +7,19 @@ - Infinite scroll - New loader - + +### IDEAS +- Fees, paid in gas +- - Especially, when tx is a gas transfer +- Clickable asset symbol +- - Take user to asset page +- + +### ISSUES +- NFTs are not obvious +- Approve amount +- - Unsetting approval (false in ApprovalForAll for ERC721) +- Getting contract address & asset ID +- - Maybe make asset symbol clickable +- Classification for operations. Same? +- 'Show more/less' button logic diff --git a/src/app/pages/Home/OtherComponents/AssetBanner.tsx b/src/app/pages/Home/OtherComponents/AssetBanner.tsx index a877396e30..3aa6fa0659 100644 --- a/src/app/pages/Home/OtherComponents/AssetBanner.tsx +++ b/src/app/pages/Home/OtherComponents/AssetBanner.tsx @@ -114,7 +114,9 @@ const EvmAssetBanner = memo(({ evmChainId, assetSlug }) => return ( <>
- +
+ +
{ tezosChainId: string; assetSlug: string; + Loader?: React.ComponentType; + Fallback?: React.ComponentType; } -export const TezosAssetIcon = memo(({ tezosChainId, className, style, ...props }) => { - const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); +export const TezosAssetIcon = memo( + ({ tezosChainId, className, style, Loader, Fallback, ...props }) => { + const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); - return ( -
- } - fallback={} - /> -
- ); -}); + return ( +
+ : } + fallback={Fallback ? : } + /> +
+ ); + } +); interface EvmAssetIconProps extends Omit { evmChainId: number; assetSlug: string; + Loader?: React.ComponentType; + Fallback?: React.ComponentType; } -export const EvmTokenIcon = memo(({ evmChainId, assetSlug, className, style, ...props }) => { +export const EvmTokenIcon = memo(({ evmChainId, assetSlug, Loader, Fallback, ...props }) => { const network = useEvmChainByChainId(evmChainId); const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, assetSlug); const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; return ( -
- } - fallback={} - /> -
+ : } + fallback={Fallback ? : } + /> ); }); diff --git a/src/app/templates/activity/ActivityItemBase.tsx b/src/app/templates/activity/ActivityItemBase.tsx new file mode 100644 index 0000000000..9ac5ac984c --- /dev/null +++ b/src/app/templates/activity/ActivityItemBase.tsx @@ -0,0 +1,131 @@ +import React, { FC, useCallback, useMemo } from 'react'; + +import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; +import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; +import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; +import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; +import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; +import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; +import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; +import { atomsToTokens } from 'lib/temple/helpers'; + +import { EvmTokenIcon, TezosAssetIcon } from '../AssetIcon'; + +import { ActivityKindEnum, InfinitySymbol } from './utils'; + +interface Props { + chainId: string | number; + kind: ActivityKindEnum; + hash: string; + networkName: string; + asset?: AssetProp; +} + +interface AssetProp { + contract: string; + tokenId?: string; + amount?: string | typeof InfinitySymbol; + decimals: number; + symbol?: string; +} + +export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset }) => { + const amountJsx = useMemo(() => { + if (!asset) return null; + + return ( +
+ {asset.amount ? ( + asset.amount === InfinitySymbol ? ( + '∞ ' + ) : ( + <> + {asset.amount.startsWith('-') ? null : '+'} + {atomsToTokens(asset.amount, asset.decimals).toFixed(6)}{' '} + + ) + ) : null} + + {asset.symbol || '???'} +
+ ); + }, [asset]); + + const IconFallback = useCallback( + () => ( +
+ +
+ ), + [kind] + ); + + return ( +
+
+ {asset ? ( + typeof chainId === 'number' ? ( + + ) : ( + + ) + ) : ( + + )} + + + {typeof chainId === 'number' ? ( + + ) : ( + + )} + +
+ +
+
+
{ActivityKindTitle[kind]}
+ + {amountJsx} +
+ +
+ + + + + + +
-12.00 $
+
+
+
+ ); +}; + +const ActivityKindTitle: Record = { + [ActivityKindEnum.interaction]: 'Interaction', + [ActivityKindEnum.send]: 'Send', + [ActivityKindEnum.receive]: 'Receive', + [ActivityKindEnum.swap]: 'Swap', + [ActivityKindEnum.approve]: 'Approve' +}; + +const ActivityKindIconSvg: Record = { + [ActivityKindEnum.interaction]: DocumentsSvg, + [ActivityKindEnum.send]: SendSvg, + [ActivityKindEnum.receive]: IncomeSvg, + [ActivityKindEnum.swap]: SwapSvg, + [ActivityKindEnum.approve]: DocumentsSvg +}; diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index cf3667efc1..fbce3c2e77 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -2,31 +2,24 @@ import React, { FC, memo } from 'react'; import clsx from 'clsx'; -import { Anchor, HashShortView, IconBase, Money, SyncSpinner } from 'app/atoms'; +import { IconBase, SyncSpinner } from 'app/atoms'; import { EmptyState } from 'app/atoms/EmptyState'; -import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; -import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; -import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; -import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; -import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { ContentContainer } from 'app/layouts/containers'; import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; -import { getEvmTransactions, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; -import { toTokenSlug } from 'lib/assets'; +import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; import { t } from 'lib/i18n'; import { useTypedSWR } from 'lib/swr'; -import { atomsToTokens } from 'lib/temple/helpers'; import { useBooleanState } from 'lib/ui/hooks'; -import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { useAccountAddressForEvm } from 'temple/front'; import { OneOfChains, useEvmChainByChainId } from 'temple/front/chains'; import { TempleChainKind } from 'temple/types'; +import { ActivityItemBaseComponent } from './ActivityItemBase'; import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; import { TezosActivityTab } from './tezos'; +import { Activity, ActivityKindEnum, EvmOperation, parseGoldRushTransaction } from './utils'; export { TezosActivityTab }; @@ -92,66 +85,6 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) ); }; -function parseGoldRushTransaction(item: GoldRushTransaction, chainId: number, accountAddress: string): EvmActivity { - const logEvents = item.log_events ?? []; - - return { - chain: TempleChainKind.EVM, - chainId, - kind: (() => { - if (!logEvents.length) { - if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; - } else if (logEvents.length === 1) { - const logEvent = logEvents[0]!; - if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; - - if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; - } - } - - return ActivityKindEnum.interaction; - })(), - hash: item.tx_hash!, - operations: logEvents.map(logEvent => { - const kind: ActivityKindEnum = (() => { - if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; - - if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; - } - - return ActivityKindEnum.interaction; - })(); - - return { - kind, - asset: (() => { - if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; - - const value: string = logEvent.decoded?.params[2]?.value ?? '0'; - const decimals = logEvent.sender_contract_decimals; - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const nft = logEvent.decoded?.params[2]?.indexed ?? false; - - if (!contractAddress || decimals == null) return; - - return { - slug: toTokenSlug(contractAddress, nft ? value : undefined), - amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${value}` : value, - decimals: nft ? 0 : decimals ?? 0, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft - }; - })() - }; - }) - }; -} - const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ activity, chain }) => { if (activity.chain !== TempleChainKind.EVM) throw new Error('Tezos activities in dev'); @@ -168,6 +101,7 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act hash={activity.hash} chainId={chain.chainId} networkName={networkName} + asset={activity.asset} /> ); @@ -227,120 +161,3 @@ const InteractionsConnector = memo(() => (
)); - -interface ActivityItemBaseComponentProps { - chainId: string | number; - kind: ActivityKindEnum; - hash: string; - networkName: string; - asset?: { - amount: string; - decimals: number; - symbol?: string; - }; -} - -const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset }) => { - return ( - -
-
- {/* asset */} - -
- - - {typeof chainId === 'number' ? ( - - ) : ( - - )} - -
- -
-
-
{ActivityKindTitle[kind]}
- - {asset && ( -
- {asset.amount.startsWith('-') ? null : '+'} - {atomsToTokens(asset.amount, asset.decimals).toFixed(6)}{' '} - {asset.symbol || '???'} -
- )} -
- -
-
- - - -
- -
-12.00 $
-
-
-
- ); -}; - -enum ActivityKindEnum { - interaction, - send, - receive, - swap, - approve -} - -const ActivityKindTitle: Record = { - [ActivityKindEnum.interaction]: 'Interaction', - [ActivityKindEnum.send]: 'Send', - [ActivityKindEnum.receive]: 'Receive', - [ActivityKindEnum.swap]: 'Swap', - [ActivityKindEnum.approve]: 'Approve' -}; - -const ActivityKindIconSvg: Record = { - [ActivityKindEnum.interaction]: DocumentsSvg, - [ActivityKindEnum.send]: SendSvg, - [ActivityKindEnum.receive]: IncomeSvg, - [ActivityKindEnum.swap]: SwapSvg, - [ActivityKindEnum.approve]: DocumentsSvg -}; - -type Activity = TezosActivity | EvmActivity; - -interface ActivityBase { - kind: ActivityKindEnum; -} - -interface TezosActivity extends ActivityBase { - chain: TempleChainKind.Tezos; - chainId: string; - hash: string; - operations: TezosOperation[]; -} - -interface TezosOperation { - kind: ActivityKindEnum; -} - -interface EvmActivity extends ActivityBase { - chain: TempleChainKind.EVM; - chainId: number; - hash: string; - operations: EvmOperation[]; -} - -interface EvmOperation extends ActivityBase { - asset?: EvmActivityAsset; -} - -interface EvmActivityAsset { - slug: string; - amount: string; - decimals: number; - nft?: boolean; - symbol?: string; -} diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index e69de29bb2..e8e71ac3d6 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -0,0 +1,213 @@ +import { GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; +import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { getEvmAddressSafe } from 'lib/utils/evm.utils'; +import { TempleChainKind } from 'temple/types'; + +export enum ActivityKindEnum { + interaction, + send, + receive, + swap, + approve +} + +export type Activity = TezosActivity | EvmActivity; + +interface ActivityBase { + kind: ActivityKindEnum; +} + +interface TezosActivity extends ActivityBase { + chain: TempleChainKind.Tezos; + chainId: string; + hash: string; + operations: TezosOperation[]; +} + +interface TezosOperation { + kind: ActivityKindEnum; +} + +interface EvmActivity extends ActivityBase { + chain: TempleChainKind.EVM; + chainId: number; + hash: string; + asset?: EvmActivityAsset; + operations: EvmOperation[]; +} + +export interface EvmOperation extends ActivityBase { + asset?: EvmActivityAsset; +} + +interface EvmActivityAsset { + contract: string; + tokenId?: string; + amount?: string | typeof InfinitySymbol; + decimals: number; + nft?: boolean; + symbol?: string; +} + +export const InfinitySymbol = Symbol('Infinity'); + +export function parseGoldRushTransaction( + item: GoldRushTransaction, + chainId: number, + accountAddress: string +): EvmActivity { + const logEvents = item.log_events ?? []; + + const { kind, asset } = ((): { kind: ActivityKindEnum; asset?: EvmActivityAsset } => { + if (!logEvents.length) { + const kind = (() => { + if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; + + return ActivityKindEnum.interaction; + })(); + + if (kind === ActivityKindEnum.interaction) return { kind }; + + const decimals = item.gas_metadata?.contract_decimals; + if (decimals == null) return { kind }; + + const value: string = item.value?.toString() ?? '0'; + + const nft = false; + + const asset: EvmActivityAsset = { + contract: EVM_TOKEN_SLUG, + amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${value}` : value, + decimals: nft ? 0 : decimals ?? 0, + symbol: item.gas_metadata?.contract_ticker_symbol + }; + + return { kind, asset }; + } + + if (logEvents.length !== 1) return { kind: ActivityKindEnum.interaction }; + + const logEvent = logEvents[0]!; + + if (!logEvent.decoded) return { kind: ActivityKindEnum.interaction }; + + const fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); + const toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const decimals = logEvent.sender_contract_decimals; + + if (logEvent.decoded.name === 'Transfer') { + const kind = (() => { + if (fromAddress === accountAddress) return ActivityKindEnum.send; + if (toAddress === accountAddress) return ActivityKindEnum.receive; + + return ActivityKindEnum.interaction; + })(); + + if (kind === ActivityKindEnum.interaction) return { kind }; + + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; + const nft = logEvent.decoded.params[2]?.indexed ?? false; + + if (!contractAddress || decimals == null) return { kind }; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId: nft ? amountOrTokenId : undefined, + amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, + decimals: nft ? 0 : decimals ?? 0, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft + }; + + return { kind, asset }; + } + + if (logEvent.decoded.name === 'Approval' && fromAddress === accountAddress) { + const kind = ActivityKindEnum.approve; + + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; + const nft = logEvent.decoded.params[2]?.indexed ?? false; + + if (!contractAddress || decimals == null) return { kind }; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId: nft ? amountOrTokenId : undefined, + amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs + decimals, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft + }; + + return { kind, asset }; + } + + if ( + logEvent.decoded.name === 'ApprovalForAll' && + // @ts-expect-error // `value` is not always `:string` + logEvent.decoded.params[2]?.value === true && + fromAddress === accountAddress + ) { + const kind = ActivityKindEnum.approve; + + if (!contractAddress || decimals == null) return { kind }; + + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: InfinitySymbol, + decimals, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft: true + }; + + return { kind, asset }; + } + + return { kind: ActivityKindEnum.interaction }; + })(); + + return { + chain: TempleChainKind.EVM, + chainId, + kind, + hash: item.tx_hash!, + asset, + operations: logEvents.map(logEvent => { + const kind: ActivityKindEnum = (() => { + // if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; + + if (logEvent.decoded?.name === 'Transfer') { + if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; + } + + return ActivityKindEnum.interaction; + })(); + + return { + kind, + asset: (() => { + if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; + + const amountOrTokenId: string = logEvent.decoded?.params[2]?.value ?? '0'; + const decimals = logEvent.sender_contract_decimals; + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const nft = logEvent.decoded?.params[2]?.indexed ?? false; + + if (!contractAddress || decimals == null) return; + + return { + contract: contractAddress, + tokenId: nft ? amountOrTokenId : undefined, + amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, + decimals, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft + }; + })() + }; + }) + }; +} diff --git a/src/lib/assets/utils.ts b/src/lib/assets/utils.ts index d1f5bfb8a8..f480bc3002 100644 --- a/src/lib/assets/utils.ts +++ b/src/lib/assets/utils.ts @@ -4,7 +4,7 @@ import type { AssetMetadataBase } from 'lib/metadata'; import { isTezosDcpChainId } from 'temple/networks'; import { TempleChainKind } from 'temple/types'; -import { TEZ_TOKEN_SLUG, TEZOS_SYMBOL, TEZOS_DCP_SYMBOL } from './defaults'; +import { TEZ_TOKEN_SLUG, EVM_TOKEN_SLUG, TEZOS_SYMBOL, TEZOS_DCP_SYMBOL } from './defaults'; import type { Asset, FA2Token } from './types'; const CHAIN_SLUG_SEPARATOR = ':'; @@ -13,6 +13,12 @@ export const getTezosGasSymbol = (chainId: string) => (isTezosDcpChainId(chainId export const toTokenSlug = (contract: string, id: string | number = 0) => `${contract}_${id}`; +export const toTezosAssetSlug = (contract: string, id?: string) => + contract === TEZ_TOKEN_SLUG ? TEZ_TOKEN_SLUG : toTokenSlug(contract, id); + +export const toEvmAssetSlug = (contract: string, id?: string) => + contract === EVM_TOKEN_SLUG ? EVM_TOKEN_SLUG : toTokenSlug(contract, id); + export const fromAssetSlug = (slug: string) => slug.split('_') as [contract: T, tokenId?: string]; export const toChainAssetSlug = (chainKind: TempleChainKind, chainId: number | string, assetSlug: string) => From c82105868c3eb7c62d826f16e36f3b5986ebe4b2 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Sep 2024 22:38:09 +0300 Subject: [PATCH 04/74] TW-1479: [EVM] Transaction history. + Extra icon SRC from API --- TODO.md | 2 + src/app/templates/AssetIcon.tsx | 36 +++++++----- src/app/templates/AssetImage.tsx | 44 ++++++++------ .../templates/activity/ActivityItemBase.tsx | 57 +++++++++++++++---- src/app/templates/activity/index.tsx | 7 ++- src/app/templates/activity/utils.ts | 18 ++++-- src/lib/images-uri.ts | 4 +- 7 files changed, 115 insertions(+), 53 deletions(-) diff --git a/TODO.md b/TODO.md index 22cc77e917..434ec3cdd8 100644 --- a/TODO.md +++ b/TODO.md @@ -23,3 +23,5 @@ - - Maybe make asset symbol clickable - Classification for operations. Same? - 'Show more/less' button logic +- `TransferSingle` of ERC-1155 +- diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index 9f3d7e03b9..55f0b5db1c 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -16,12 +16,13 @@ import { TezosAssetImage, AssetImageBaseProps, EvmAssetImage } from './AssetImag interface TezosAssetIconProps extends Omit { tezosChainId: string; assetSlug: string; + extraSrc?: string; Loader?: React.ComponentType; Fallback?: React.ComponentType; } export const TezosAssetIcon = memo( - ({ tezosChainId, className, style, Loader, Fallback, ...props }) => { + ({ tezosChainId, className, style, extraSrc, Loader, Fallback, ...props }) => { const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); return ( @@ -29,6 +30,7 @@ export const TezosAssetIcon = memo( : } fallback={Fallback ? : } /> @@ -40,26 +42,30 @@ export const TezosAssetIcon = memo( interface EvmAssetIconProps extends Omit { evmChainId: number; assetSlug: string; + extraSrc?: string; Loader?: React.ComponentType; Fallback?: React.ComponentType; } -export const EvmTokenIcon = memo(({ evmChainId, assetSlug, Loader, Fallback, ...props }) => { - const network = useEvmChainByChainId(evmChainId); - const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, assetSlug); +export const EvmTokenIcon = memo( + ({ evmChainId, assetSlug, extraSrc, Loader, Fallback, ...props }) => { + const network = useEvmChainByChainId(evmChainId); + const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, assetSlug); - const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; + const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; - return ( - : } - fallback={Fallback ? : } - /> - ); -}); + return ( + : } + fallback={Fallback ? : } + /> + ); + } +); const ICON_DEFAULT_SIZE = 40; const ASSET_IMAGE_DEFAULT_SIZE = 30; diff --git a/src/app/templates/AssetImage.tsx b/src/app/templates/AssetImage.tsx index 6f43c24a24..28eab0a33e 100644 --- a/src/app/templates/AssetImage.tsx +++ b/src/app/templates/AssetImage.tsx @@ -6,18 +6,20 @@ import { EvmAssetMetadataBase } from 'lib/metadata/types'; import { ImageStacked, ImageStackedProps } from 'lib/ui/ImageStacked'; export interface AssetImageBaseProps - extends Pick { + extends Pick< + ImageStackedProps, + 'loader' | 'fallback' | 'className' | 'style' | 'onStackLoaded' | 'onStackFailed' | 'alt' + > { sources: string[]; - metadata?: EvmAssetMetadataBase | AssetMetadataBase; size?: number; } const AssetImageBase: FC = ({ sources, - metadata, className, size, style, + alt, loader, fallback, onStackLoaded, @@ -38,7 +40,7 @@ const AssetImageBase: FC = ({ sources={sources} loader={loader} fallback={fallback} - alt={metadata?.name} + alt={alt} className={className} style={styleMemo} height={size} @@ -52,29 +54,37 @@ const AssetImageBase: FC = ({ interface TezosAssetImageProps extends Omit { metadata?: AssetMetadataBase; fullViewCollectible?: boolean; + extraSrc?: string; } -export const TezosAssetImage: FC = ({ metadata, fullViewCollectible, ...rest }) => { +export const TezosAssetImage: FC = ({ metadata, fullViewCollectible, extraSrc, ...rest }) => { const sources = useMemo(() => { - if (metadata && isCollectibleTokenMetadata(metadata)) - return buildCollectibleImagesStack(metadata, fullViewCollectible); + const sources = + metadata && isCollectibleTokenMetadata(metadata) + ? buildCollectibleImagesStack(metadata, fullViewCollectible) + : buildTokenImagesStack(metadata?.thumbnailUri); - return buildTokenImagesStack(metadata?.thumbnailUri); - }, [metadata, fullViewCollectible]); + if (extraSrc) sources.push(extraSrc); - return ; + return sources; + }, [metadata, fullViewCollectible, extraSrc]); + + return ; }; interface EvmAssetImageProps extends Omit { metadata?: EvmAssetMetadataBase; - evmChainId?: number; + evmChainId: number; + extraSrc?: string; } -export const EvmAssetImage: FC = ({ evmChainId, metadata, ...rest }) => { - const sources = useMemo( - () => (metadata ? buildEvmTokenIconSources(metadata, evmChainId) : []), - [evmChainId, metadata] - ); +export const EvmAssetImage: FC = ({ evmChainId, metadata, extraSrc, ...rest }) => { + const sources = useMemo(() => { + const sources = metadata ? buildEvmTokenIconSources(metadata, evmChainId) : []; + if (extraSrc) sources.push(extraSrc); + + return sources; + }, [evmChainId, metadata, extraSrc]); - return ; + return ; }; diff --git a/src/app/templates/activity/ActivityItemBase.tsx b/src/app/templates/activity/ActivityItemBase.tsx index 9ac5ac984c..b589bae43d 100644 --- a/src/app/templates/activity/ActivityItemBase.tsx +++ b/src/app/templates/activity/ActivityItemBase.tsx @@ -7,6 +7,7 @@ import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; +import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -20,6 +21,7 @@ interface Props { hash: string; networkName: string; asset?: AssetProp; + blockExplorerUrl?: string; } interface AssetProp { @@ -28,13 +30,25 @@ interface AssetProp { amount?: string | typeof InfinitySymbol; decimals: number; symbol?: string; + iconURL?: string; } -export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset }) => { - const amountJsx = useMemo(() => { - if (!asset) return null; +export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset, blockExplorerUrl }) => { + const assetSlug = asset + ? typeof chainId === 'number' + ? toEvmAssetSlug(asset.contract, asset.tokenId) + : toTezosAssetSlug(asset.contract, asset.tokenId) + : null; - return ( + const { amountForFiat, amountJsx } = useMemo(() => { + if (!asset) return {}; + + const amountForFiat = + typeof asset.amount === 'string' && (kind === ActivityKindEnum.receive || kind === ActivityKindEnum.send) + ? atomsToTokens(asset.amount, asset.decimals) + : null; + + const amountJsx = (
{asset.amount ? ( asset.amount === InfinitySymbol ? ( @@ -42,7 +56,7 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw ) : ( <> {asset.amount.startsWith('-') ? null : '+'} - {atomsToTokens(asset.amount, asset.decimals).toFixed(6)}{' '} + {atomsToTokens(asset.amount, asset.decimals)}{' '} ) ) : null} @@ -50,7 +64,9 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw {asset.symbol || '???'}
); - }, [asset]); + + return { amountForFiat, amountJsx }; + }, [asset, kind]); const IconFallback = useCallback( () => ( @@ -63,20 +79,22 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw return (
-
- {asset ? ( +
+ {assetSlug ? ( typeof chainId === 'number' ? ( ) : ( ) @@ -101,13 +119,28 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw
- + -
-12.00 $
+ {amountForFiat && assetSlug ? ( +
+ {amountForFiat.isPositive() && '+'} + + +
+ ) : null}
diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index fbce3c2e77..a371918f73 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -92,16 +92,17 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - const hash = activity.hash; + const { hash, blockExplorerUrl } = activity; if (activity.kind !== ActivityKindEnum.interaction || activity.operations.length <= 1) return ( ); @@ -120,6 +121,7 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act chainId={chain.chainId} networkName={networkName} asset={operation.asset} + blockExplorerUrl={blockExplorerUrl} /> ))} @@ -146,6 +148,7 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act chainId={chain.chainId} networkName={networkName} asset={operation.asset} + blockExplorerUrl={blockExplorerUrl} /> )) diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index e8e71ac3d6..82b9d5d5fd 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -32,6 +32,7 @@ interface EvmActivity extends ActivityBase { chain: TempleChainKind.EVM; chainId: number; hash: string; + blockExplorerUrl?: string; asset?: EvmActivityAsset; operations: EvmOperation[]; } @@ -47,6 +48,7 @@ interface EvmActivityAsset { decimals: number; nft?: boolean; symbol?: string; + iconURL?: string; } export const InfinitySymbol = Symbol('Infinity'); @@ -96,6 +98,7 @@ export function parseGoldRushTransaction( const toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); const contractAddress = getEvmAddressSafe(logEvent.sender_address); const decimals = logEvent.sender_contract_decimals; + const iconURL = logEvent.sender_logo_url ?? undefined; if (logEvent.decoded.name === 'Transfer') { const kind = (() => { @@ -118,7 +121,8 @@ export function parseGoldRushTransaction( amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, decimals: nft ? 0 : decimals ?? 0, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft + nft, + iconURL }; return { kind, asset }; @@ -138,7 +142,8 @@ export function parseGoldRushTransaction( amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs decimals, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft + nft, + iconURL }; return { kind, asset }; @@ -159,7 +164,8 @@ export function parseGoldRushTransaction( amount: InfinitySymbol, decimals, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft: true + nft: true, + iconURL }; return { kind, asset }; @@ -173,6 +179,7 @@ export function parseGoldRushTransaction( chainId, kind, hash: item.tx_hash!, + blockExplorerUrl: item.explorers?.[0]?.url, asset, operations: logEvents.map(logEvent => { const kind: ActivityKindEnum = (() => { @@ -186,6 +193,8 @@ export function parseGoldRushTransaction( return ActivityKindEnum.interaction; })(); + const iconURL = logEvent.sender_logo_url ?? undefined; + return { kind, asset: (() => { @@ -204,7 +213,8 @@ export function parseGoldRushTransaction( amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, decimals, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft + nft, + iconURL }; })() }; diff --git a/src/lib/images-uri.ts b/src/lib/images-uri.ts index dede2b8432..5261a4a56d 100644 --- a/src/lib/images-uri.ts +++ b/src/lib/images-uri.ts @@ -240,9 +240,7 @@ const getEvmCustomChainIconUrl = (chainId: number, metadata: EvmAssetMetadataBas : `${baseUrl}${chainName}/assets/${metadata.address}/logo.png`; }; -export const buildEvmTokenIconSources = (metadata: EvmAssetMetadataBase, chainId?: number) => { - if (!chainId) return []; - +export const buildEvmTokenIconSources = (metadata: EvmAssetMetadataBase, chainId: number) => { const mainFallback = getEvmCustomChainIconUrl(chainId, metadata); return mainFallback ? [getCompressedImageUrl(mainFallback, COMPRESSES_TOKEN_ICON_SIZE)] : []; From d2ab39bd1ebd5506c26ae2e5a4be12ae4967b030 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Sep 2024 01:05:39 +0300 Subject: [PATCH 05/74] TW-1479: [EVM] Transaction history. Refactor. Assets Images Components --- .../CollectiblePage/CollectiblePageImage.tsx | 4 +- .../components/AudioCollectible.tsx | 4 +- .../Home/OtherComponents/AssetBanner.tsx | 4 +- src/app/templates/AssetIcon.tsx | 217 +++++++----------- .../AssetImageStacked.tsx} | 104 +++++---- src/app/templates/AssetImage/index.tsx | 67 ++++++ .../SwapForm/SwapRoute/lb-pool-part.tsx | 4 +- .../templates/activity/ActivityItemBase.tsx | 8 +- src/lib/metadata/index.ts | 16 +- src/lib/metadata/utils.ts | 11 +- 10 files changed, 239 insertions(+), 200 deletions(-) rename src/app/templates/{AssetImage.tsx => AssetImage/AssetImageStacked.tsx} (54%) create mode 100644 src/app/templates/AssetImage/index.tsx diff --git a/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx b/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx index b80aa8d0ce..533f1047a0 100644 --- a/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx +++ b/src/app/pages/Collectibles/CollectiblePage/CollectiblePageImage.tsx @@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Model3DViewer } from 'app/atoms/Model3DViewer'; import { useCollectiblesListOptionsSelector } from 'app/store/assets-filter-options/selectors'; -import { TezosAssetImage } from 'app/templates/AssetImage'; +import { TezosAssetImageStacked } from 'app/templates/AssetImage'; import { isSvgDataUriInUtf8Encoding, buildObjktCollectibleArtifactUri } from 'lib/images-uri'; import { TokenMetadata } from 'lib/metadata'; import { EvmCollectibleMetadata } from 'lib/metadata/types'; @@ -86,7 +86,7 @@ export const TezosCollectiblePageImage = memo( } return ( - } diff --git a/src/app/pages/Collectibles/components/AudioCollectible.tsx b/src/app/pages/Collectibles/components/AudioCollectible.tsx index a6987abbf2..17229dcab3 100644 --- a/src/app/pages/Collectibles/components/AudioCollectible.tsx +++ b/src/app/pages/Collectibles/components/AudioCollectible.tsx @@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { emptyFn } from '@rnw-community/shared'; -import { TezosAssetImage } from 'app/templates/AssetImage'; +import { TezosAssetImageStacked } from 'app/templates/AssetImage'; import { AssetMetadataBase } from 'lib/metadata'; import { CollectibleImageFallback } from './CollectibleImageFallback'; @@ -43,7 +43,7 @@ export const AudioCollectible = memo(({ uri, metadata, className, style, loop hidden={!ready} audioPoster={ - } diff --git a/src/app/pages/Home/OtherComponents/AssetBanner.tsx b/src/app/pages/Home/OtherComponents/AssetBanner.tsx index 3aa6fa0659..85e16b5f9c 100644 --- a/src/app/pages/Home/OtherComponents/AssetBanner.tsx +++ b/src/app/pages/Home/OtherComponents/AssetBanner.tsx @@ -4,7 +4,7 @@ import Money from 'app/atoms/Money'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; import AddressChip from 'app/templates/AddressChip'; -import { TezosAssetIcon, EvmTokenIcon } from 'app/templates/AssetIcon'; +import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { EvmBalance, TezosBalance } from 'app/templates/Balance'; import InFiat from 'app/templates/InFiat'; import { setAnotherSelector, setTestID } from 'lib/analytics'; @@ -115,7 +115,7 @@ const EvmAssetBanner = memo(({ evmChainId, assetSlug }) => <>
- +
{ - tezosChainId: string; - assetSlug: string; - extraSrc?: string; - Loader?: React.ComponentType; - Fallback?: React.ComponentType; -} +export const TezosAssetIcon = memo(({ className, style, ...props }) => ( +
+ +
+)); -export const TezosAssetIcon = memo( - ({ tezosChainId, className, style, extraSrc, Loader, Fallback, ...props }) => { - const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); - - return ( -
- : } - fallback={Fallback ? : } - /> -
- ); - } -); - -interface EvmAssetIconProps extends Omit { - evmChainId: number; - assetSlug: string; - extraSrc?: string; - Loader?: React.ComponentType; - Fallback?: React.ComponentType; -} - -export const EvmTokenIcon = memo( - ({ evmChainId, assetSlug, extraSrc, Loader, Fallback, ...props }) => { - const network = useEvmChainByChainId(evmChainId); - const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, assetSlug); - - const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; - - return ( - : } - fallback={Fallback ? : } - /> - ); - } -); +const TezosAssetIconPlaceholder: TezosAssetImageProps['Fallback'] = memo(({ metadata, size }) => ( + +)); const ICON_DEFAULT_SIZE = 40; const ASSET_IMAGE_DEFAULT_SIZE = 30; const NETWORK_IMAGE_DEFAULT_SIZE = 16; -interface TezosTokenIconWithNetworkProps - extends Omit { - tezosChainId: string; - assetSlug: string; -} - -export const TezosTokenIconWithNetwork = memo( - ({ tezosChainId, className, style, ...props }) => { - const network = useTezosChainByChainId(tezosChainId); - const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); - - return ( -
- } - fallback={} - /> - - {network && ( - - - - )} -
- ); - } -); - -interface EvmTokenIconWithNetworkProps - extends Omit { - evmChainId: number; - assetSlug: string; -} - -export const EvmTokenIconWithNetwork = memo( - ({ evmChainId, assetSlug, className, style, ...props }) => { - const network = useEvmChainByChainId(evmChainId); - const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, assetSlug); - - const metadata = isEvmNativeTokenSlug(assetSlug) ? network?.currency : tokenMetadata; - - return ( -
- } - fallback={} - /> - - {network && ( - - - - )} -
- ); - } -); +export const TezosTokenIconWithNetwork = memo(({ tezosChainId, className, style, ...props }) => { + const network = useTezosChainByChainId(tezosChainId); + + return ( +
+ + + {network && ( + + + + )} +
+ ); +}); + +export const EvmAssetIcon = memo(({ className, style, ...props }) => ( +
+ +
+)); + +const EvmAssetIconPlaceholder: EvmAssetImageProps['Fallback'] = memo(({ metadata, size }) => ( + +)); + +export const EvmTokenIconWithNetwork = memo(({ evmChainId, className, style, ...props }) => { + const network = useEvmChainByChainId(evmChainId); + + return ( +
+ + + {network && ( + + + + )} +
+ ); +}); -interface PlaceholderProps { - metadata: EvmTokenMetadata | AssetMetadataBase | nullish; +const AssetIconPlaceholder = memo<{ + isCollectible?: boolean; + metadata: AssetMetadataBase | EvmAssetMetadataBase | nullish; size?: number; -} - -const AssetIconPlaceholder: FC = ({ metadata, size }) => { - return metadata && isCollectible(metadata) ? ( - +}>(({ isCollectible, metadata, size }) => + isCollectible ? ( + ) : ( - ); -}; + ) +); diff --git a/src/app/templates/AssetImage.tsx b/src/app/templates/AssetImage/AssetImageStacked.tsx similarity index 54% rename from src/app/templates/AssetImage.tsx rename to src/app/templates/AssetImage/AssetImageStacked.tsx index 28eab0a33e..15c107ede2 100644 --- a/src/app/templates/AssetImage.tsx +++ b/src/app/templates/AssetImage/AssetImageStacked.tsx @@ -1,11 +1,65 @@ import React, { FC, useMemo } from 'react'; -import { buildTokenImagesStack, buildCollectibleImagesStack, buildEvmTokenIconSources } from 'lib/images-uri'; -import { AssetMetadataBase, isCollectibleTokenMetadata } from 'lib/metadata'; +import { + buildTokenImagesStack, + buildCollectibleImagesStack, + buildEvmTokenIconSources, + buildEvmCollectibleIconSources +} from 'lib/images-uri'; +import { AssetMetadataBase, isTezosCollectibleMetadata } from 'lib/metadata'; import { EvmAssetMetadataBase } from 'lib/metadata/types'; +import { isEvmCollectibleMetadata } from 'lib/metadata/utils'; import { ImageStacked, ImageStackedProps } from 'lib/ui/ImageStacked'; -export interface AssetImageBaseProps +export interface TezosAssetImageStackedProps extends Omit { + metadata?: AssetMetadataBase; + fullViewCollectible?: boolean; + extraSrc?: string; +} + +export const TezosAssetImageStacked: FC = ({ + metadata, + fullViewCollectible, + extraSrc, + ...rest +}) => { + const sources = useMemo(() => { + const sources = + metadata && isTezosCollectibleMetadata(metadata) + ? buildCollectibleImagesStack(metadata, fullViewCollectible) + : buildTokenImagesStack(metadata?.thumbnailUri); + + if (extraSrc) sources.push(extraSrc); + + return sources; + }, [metadata, fullViewCollectible, extraSrc]); + + return ; +}; + +export interface EvmAssetImageStackedProps extends Omit { + metadata?: EvmAssetMetadataBase; + evmChainId: number; + extraSrc?: string; +} + +export const EvmAssetImageStacked: FC = ({ evmChainId, metadata, extraSrc, ...rest }) => { + const sources = useMemo(() => { + const sources = metadata + ? isEvmCollectibleMetadata(metadata) + ? buildEvmCollectibleIconSources(metadata) + : buildEvmTokenIconSources(metadata, evmChainId) + : []; + + if (extraSrc) sources.push(extraSrc); + + return sources; + }, [evmChainId, metadata, extraSrc]); + + return ; +}; + +export interface AssetImageStackedProps extends Pick< ImageStackedProps, 'loader' | 'fallback' | 'className' | 'style' | 'onStackLoaded' | 'onStackFailed' | 'alt' @@ -14,7 +68,7 @@ export interface AssetImageBaseProps size?: number; } -const AssetImageBase: FC = ({ +export const AssetImageStacked: FC = ({ sources, className, size, @@ -23,7 +77,8 @@ const AssetImageBase: FC = ({ loader, fallback, onStackLoaded, - onStackFailed + onStackFailed, + ...rest }) => { const styleMemo: React.CSSProperties = useMemo( () => ({ @@ -47,44 +102,7 @@ const AssetImageBase: FC = ({ width={size} onStackLoaded={onStackLoaded} onStackFailed={onStackFailed} + {...rest} /> ); }; - -interface TezosAssetImageProps extends Omit { - metadata?: AssetMetadataBase; - fullViewCollectible?: boolean; - extraSrc?: string; -} - -export const TezosAssetImage: FC = ({ metadata, fullViewCollectible, extraSrc, ...rest }) => { - const sources = useMemo(() => { - const sources = - metadata && isCollectibleTokenMetadata(metadata) - ? buildCollectibleImagesStack(metadata, fullViewCollectible) - : buildTokenImagesStack(metadata?.thumbnailUri); - - if (extraSrc) sources.push(extraSrc); - - return sources; - }, [metadata, fullViewCollectible, extraSrc]); - - return ; -}; - -interface EvmAssetImageProps extends Omit { - metadata?: EvmAssetMetadataBase; - evmChainId: number; - extraSrc?: string; -} - -export const EvmAssetImage: FC = ({ evmChainId, metadata, extraSrc, ...rest }) => { - const sources = useMemo(() => { - const sources = metadata ? buildEvmTokenIconSources(metadata, evmChainId) : []; - if (extraSrc) sources.push(extraSrc); - - return sources; - }, [evmChainId, metadata, extraSrc]); - - return ; -}; diff --git a/src/app/templates/AssetImage/index.tsx b/src/app/templates/AssetImage/index.tsx new file mode 100644 index 0000000000..15972eb571 --- /dev/null +++ b/src/app/templates/AssetImage/index.tsx @@ -0,0 +1,67 @@ +import React, { memo } from 'react'; + +import { AssetMetadataBase, useEvmAssetMetadata, useTezosAssetMetadata } from 'lib/metadata'; +import { EvmAssetMetadataBase } from 'lib/metadata/types'; + +import { + TezosAssetImageStackedProps, + TezosAssetImageStacked, + EvmAssetImageStackedProps, + EvmAssetImageStacked +} from './AssetImageStacked'; + +export type { TezosAssetImageStackedProps, EvmAssetImageStackedProps }; +export { TezosAssetImageStacked }; + +export interface TezosAssetImageProps + extends Omit { + tezosChainId: string; + assetSlug: string; + extraSrc?: string; + Loader?: Placeholder; + Fallback?: Placeholder; +} + +export const TezosAssetImage = memo(({ Loader, Fallback, ...props }) => { + const { tezosChainId, className, style, extraSrc, ...rest } = props; + + const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); + + return ( + : undefined} + fallback={Fallback ? : undefined} + {...rest} + /> + ); +}); + +export interface EvmAssetImageProps + extends Omit { + evmChainId: number; + assetSlug: string; + extraSrc?: string; + Loader?: Placeholder; + Fallback?: Placeholder; +} + +export const EvmAssetImage = memo(({ Loader, Fallback, ...props }) => { + const { evmChainId, assetSlug, extraSrc, ...rest } = props; + + const metadata = useEvmAssetMetadata(assetSlug, evmChainId); + + return ( + : undefined} + fallback={Fallback ? : undefined} + {...rest} + /> + ); +}); + +type Placeholder = React.ComponentType & { metadata?: M }>; diff --git a/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx b/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx index 4efbb0539d..1ed6a3db6c 100644 --- a/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx +++ b/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx @@ -3,7 +3,7 @@ import React, { FC, useMemo } from 'react'; import classNames from 'clsx'; import { ReactComponent as Separator } from 'app/icons/separator.svg'; -import { TezosAssetImage } from 'app/templates/AssetImage'; +import { TezosAssetImageStacked } from 'app/templates/AssetImage'; import { SIRS_TOKEN_METADATA } from 'lib/assets/known-tokens'; import useTippy from 'lib/ui/useTippy'; @@ -53,7 +53,7 @@ export const LbPoolPart: FC = ({ amount, isLbOutput, totalChains }) => { style={advancedLbPoolItemStyles} >
- +
diff --git a/src/app/templates/activity/ActivityItemBase.tsx b/src/app/templates/activity/ActivityItemBase.tsx index b589bae43d..78d1e1f9f6 100644 --- a/src/app/templates/activity/ActivityItemBase.tsx +++ b/src/app/templates/activity/ActivityItemBase.tsx @@ -11,7 +11,7 @@ import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Ba import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; -import { EvmTokenIcon, TezosAssetIcon } from '../AssetIcon'; +import { EvmAssetIcon, TezosAssetIcon } from '../AssetIcon'; import { ActivityKindEnum, InfinitySymbol } from './utils'; @@ -82,10 +82,10 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw
{assetSlug ? ( typeof chainId === 'number' ? ( - @@ -93,7 +93,7 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw diff --git a/src/lib/metadata/index.ts b/src/lib/metadata/index.ts index 1c309ab43b..2563c8e19e 100644 --- a/src/lib/metadata/index.ts +++ b/src/lib/metadata/index.ts @@ -1,6 +1,8 @@ import { useCallback, useEffect, useRef } from 'react'; import { dispatch } from 'app/store'; +import { useEvmCollectibleMetadataSelector } from 'app/store/evm/collectibles-metadata/selectors'; +import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; import { loadCollectiblesMetadataAction } from 'app/store/tezos/collectibles-metadata/actions'; import { useAllCollectiblesMetadataSelector, @@ -17,14 +19,16 @@ import { METADATA_API_LOAD_CHUNK_SIZE } from 'lib/apis/temple'; import { isTezAsset } from 'lib/assets'; import { fromChainAssetSlug } from 'lib/assets/utils'; import { isTruthy } from 'lib/utils'; +import { isEvmNativeTokenSlug } from 'lib/utils/evm.utils'; import { useAllTezosChains } from 'temple/front'; +import { useEvmChainByChainId } from 'temple/front/chains'; import { isTezosDcpChainId } from 'temple/networks'; import { TEZOS_METADATA, FILM_METADATA } from './defaults'; -import { AssetMetadataBase, TokenMetadata } from './types'; +import { AssetMetadataBase, EvmAssetMetadataBase, TokenMetadata } from './types'; export type { AssetMetadataBase, TokenMetadata } from './types'; -export { isCollectible, isCollectibleTokenMetadata, getAssetSymbol, getTokenName } from './utils'; +export { isCollectible, isTezosCollectibleMetadata, getAssetSymbol, getTokenName } from './utils'; export { TEZOS_METADATA }; @@ -37,6 +41,14 @@ export const useTezosAssetMetadata = (slug: string, tezosChainId: string): Asset return isTezAsset(slug) ? getTezosGasMetadata(tezosChainId) : tokenMetadata || collectibleMetadata; }; +export const useEvmAssetMetadata = (slug: string, evmChainId: number): EvmAssetMetadataBase | undefined => { + const network = useEvmChainByChainId(evmChainId); + const tokenMetadata = useEvmTokenMetadataSelector(evmChainId, slug); + const collectibleMetadata = useEvmCollectibleMetadataSelector(evmChainId, slug); + + return isEvmNativeTokenSlug(slug) ? network?.currency : tokenMetadata || collectibleMetadata; +}; + type TokenMetadataGetter = (slug: string) => TokenMetadata | undefined; export const useGetTokenMetadata = () => { diff --git a/src/lib/metadata/utils.ts b/src/lib/metadata/utils.ts index ca2f7df529..d50d064048 100644 --- a/src/lib/metadata/utils.ts +++ b/src/lib/metadata/utils.ts @@ -8,10 +8,11 @@ import { TokenMetadata, TezosTokenStandardsEnum, EvmTokenMetadata, - EvmCollectibleMetadata + EvmCollectibleMetadata, + EvmAssetMetadataBase } from './types'; -export function getAssetSymbol(metadata: EvmTokenMetadata | AssetMetadataBase | nullish, short = false) { +export function getAssetSymbol(metadata: EvmAssetMetadataBase | AssetMetadataBase | nullish, short = false) { if (!metadata || !metadata.symbol) return '???'; if (!short) return metadata.symbol; return metadata.symbol === 'tez' ? TEZOS_SYMBOL : metadata.symbol.substring(0, 5); @@ -36,9 +37,13 @@ export const isCollectible = (metadata: StringRecord) => /** * @deprecated // Assertion here is not safe! */ -export const isCollectibleTokenMetadata = (metadata: AssetMetadataBase): metadata is TokenMetadata => +export const isTezosCollectibleMetadata = (metadata: AssetMetadataBase): metadata is TokenMetadata => isCollectible(metadata); +/** TODO: Better way */ +export const isEvmCollectibleMetadata = (metadata: EvmAssetMetadataBase): metadata is EvmCollectibleMetadata => + 'image' in metadata; + export const buildTokenMetadataFromFetched = ( token: TokenMetadataResponse, address: string, From 65a605c4fdd3865ea2b015c5bd27b97493e9ab64 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Sep 2024 23:19:01 +0300 Subject: [PATCH 06/74] TW-1479: [EVM] Transactions history. Minor fixes --- .../components/AddTokenModal/AddTokenForm.tsx | 7 +- .../templates/activity/ActivityItemBase.tsx | 8 +- src/app/templates/activity/index.tsx | 4 +- src/app/templates/activity/utils.ts | 80 ++++++++++--------- src/lib/assets/utils.ts | 2 +- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/AddTokenForm.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/AddTokenForm.tsx index fd286a74f6..6b64ee5fa8 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/AddTokenForm.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/AddTokenForm.tsx @@ -36,7 +36,7 @@ import { fetchOneTokenMetadata } from 'lib/metadata/fetch'; import { TokenMetadataNotFoundError } from 'lib/metadata/on-chain'; import { EvmTokenMetadata } from 'lib/metadata/types'; import { loadContract } from 'lib/temple/contract'; -import { useSafeState } from 'lib/ui/hooks'; +import { useSafeState, useUpdatableRef } from 'lib/ui/hooks'; import { navigate } from 'lib/woozie'; import { OneOfChains, useAccountAddressForEvm, useAccountAddressForTezos, useAllTezosChains } from 'temple/front'; import { validateEvmContractAddress } from 'temple/front/evm/helpers'; @@ -206,10 +206,7 @@ export const AddTokenForm = memo( const loadMetadata = useDebouncedCallback(loadMetadataPure, 500); - const loadMetadataRef = useRef(loadMetadata); - useEffect(() => { - loadMetadataRef.current = loadMetadata; - }, [loadMetadata]); + const loadMetadataRef = useUpdatableRef(loadMetadata); useEffect(() => { if (formValid) { diff --git a/src/app/templates/activity/ActivityItemBase.tsx b/src/app/templates/activity/ActivityItemBase.tsx index 78d1e1f9f6..0f30d6c462 100644 --- a/src/app/templates/activity/ActivityItemBase.tsx +++ b/src/app/templates/activity/ActivityItemBase.tsx @@ -49,7 +49,7 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw : null; const amountJsx = ( -
+
{asset.amount ? ( asset.amount === InfinitySymbol ? ( '∞ ' @@ -78,8 +78,8 @@ export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, netw ); return ( -
-
+
+
{assetSlug ? ( typeof chainId === 'number' ? ( = ({ kind, hash, chainId, netw
-
+
{ActivityKindTitle[kind]}
diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index a371918f73..ca65723211 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -55,7 +55,7 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) if (!network || !accountAddress) throw new DeadEndBoundaryError(); - const { data, isLoading: isSyncing } = useTypedSWR(['evm-activity-history', chainId], async () => { + const { data, isLoading: isSyncing } = useTypedSWR(['evm-activity-history', chainId, accountAddress], async () => { return await getEvmTransactions(accountAddress, chainId, 0); }); @@ -160,7 +160,7 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act }; const InteractionsConnector = memo(() => ( -
+
)); diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index 82b9d5d5fd..e74d223d84 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -174,6 +174,47 @@ export function parseGoldRushTransaction( return { kind: ActivityKindEnum.interaction }; })(); + const operations = logEvents.map(logEvent => { + const kind: ActivityKindEnum = (() => { + // if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; + + if (logEvent.decoded?.name === 'Transfer') { + if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; + } + + return ActivityKindEnum.interaction; + })(); + + const iconURL = logEvent.sender_logo_url ?? undefined; + + return { + kind, + asset: (() => { + if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; + + const amountOrTokenId: string = logEvent.decoded?.params[2]?.value ?? '0'; + const decimals = logEvent.sender_contract_decimals; + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const nft = logEvent.decoded?.params[2]?.indexed ?? false; + + if (!contractAddress || decimals == null) return; + + const amount = nft ? '1' : amountOrTokenId; + + return { + contract: contractAddress, + tokenId: nft ? amountOrTokenId : undefined, + amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, + decimals, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft, + iconURL + }; + })() + }; + }); + return { chain: TempleChainKind.EVM, chainId, @@ -181,43 +222,6 @@ export function parseGoldRushTransaction( hash: item.tx_hash!, blockExplorerUrl: item.explorers?.[0]?.url, asset, - operations: logEvents.map(logEvent => { - const kind: ActivityKindEnum = (() => { - // if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; - - if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; - } - - return ActivityKindEnum.interaction; - })(); - - const iconURL = logEvent.sender_logo_url ?? undefined; - - return { - kind, - asset: (() => { - if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; - - const amountOrTokenId: string = logEvent.decoded?.params[2]?.value ?? '0'; - const decimals = logEvent.sender_contract_decimals; - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const nft = logEvent.decoded?.params[2]?.indexed ?? false; - - if (!contractAddress || decimals == null) return; - - return { - contract: contractAddress, - tokenId: nft ? amountOrTokenId : undefined, - amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, - decimals, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft, - iconURL - }; - })() - }; - }) + operations }; } diff --git a/src/lib/assets/utils.ts b/src/lib/assets/utils.ts index f480bc3002..325ca73db7 100644 --- a/src/lib/assets/utils.ts +++ b/src/lib/assets/utils.ts @@ -11,7 +11,7 @@ const CHAIN_SLUG_SEPARATOR = ':'; export const getTezosGasSymbol = (chainId: string) => (isTezosDcpChainId(chainId) ? TEZOS_DCP_SYMBOL : TEZOS_SYMBOL); -export const toTokenSlug = (contract: string, id: string | number = 0) => `${contract}_${id}`; +export const toTokenSlug = (contract: string, id?: string | number) => `${contract}_${id || '0'}`; export const toTezosAssetSlug = (contract: string, id?: string) => contract === TEZ_TOKEN_SLUG ? TEZ_TOKEN_SLUG : toTokenSlug(contract, id); From 3f587ce78848f4fda7c786aebc84ec130c756d6a Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 15 Sep 2024 21:17:05 +0300 Subject: [PATCH 07/74] TW-1479: [EVM] Transaction history. + Gas operation --- src/app/templates/activity/index.tsx | 14 +--- src/app/templates/activity/utils.ts | 113 ++++++++------------------- 2 files changed, 33 insertions(+), 94 deletions(-) diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index ca65723211..19ab340cff 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -19,7 +19,7 @@ import { TempleChainKind } from 'temple/types'; import { ActivityItemBaseComponent } from './ActivityItemBase'; import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; import { TezosActivityTab } from './tezos'; -import { Activity, ActivityKindEnum, EvmOperation, parseGoldRushTransaction } from './utils'; +import { Activity, EvmOperation, parseGoldRushTransaction } from './utils'; export { TezosActivityTab }; @@ -94,18 +94,6 @@ const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ act const { hash, blockExplorerUrl } = activity; - if (activity.kind !== ActivityKindEnum.interaction || activity.operations.length <= 1) - return ( - - ); - const operations = activity.operations as EvmOperation[]; return ( diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index e74d223d84..b855b55f39 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -13,11 +13,7 @@ export enum ActivityKindEnum { export type Activity = TezosActivity | EvmActivity; -interface ActivityBase { - kind: ActivityKindEnum; -} - -interface TezosActivity extends ActivityBase { +interface TezosActivity { chain: TempleChainKind.Tezos; chainId: string; hash: string; @@ -28,16 +24,16 @@ interface TezosOperation { kind: ActivityKindEnum; } -interface EvmActivity extends ActivityBase { +interface EvmActivity { chain: TempleChainKind.EVM; chainId: number; hash: string; blockExplorerUrl?: string; - asset?: EvmActivityAsset; operations: EvmOperation[]; } -export interface EvmOperation extends ActivityBase { +export interface EvmOperation { + kind: ActivityKindEnum; asset?: EvmActivityAsset; } @@ -60,39 +56,8 @@ export function parseGoldRushTransaction( ): EvmActivity { const logEvents = item.log_events ?? []; - const { kind, asset } = ((): { kind: ActivityKindEnum; asset?: EvmActivityAsset } => { - if (!logEvents.length) { - const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; - - return ActivityKindEnum.interaction; - })(); - - if (kind === ActivityKindEnum.interaction) return { kind }; - - const decimals = item.gas_metadata?.contract_decimals; - if (decimals == null) return { kind }; - - const value: string = item.value?.toString() ?? '0'; - - const nft = false; - - const asset: EvmActivityAsset = { - contract: EVM_TOKEN_SLUG, - amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${value}` : value, - decimals: nft ? 0 : decimals ?? 0, - symbol: item.gas_metadata?.contract_ticker_symbol - }; - - return { kind, asset }; - } - - if (logEvents.length !== 1) return { kind: ActivityKindEnum.interaction }; - - const logEvent = logEvents[0]!; - - if (!logEvent.decoded) return { kind: ActivityKindEnum.interaction }; + const operations = logEvents.map(logEvent => { + if (!logEvent.decoded?.params) return { kind: ActivityKindEnum.interaction }; const fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); const toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); @@ -110,15 +75,17 @@ export function parseGoldRushTransaction( if (kind === ActivityKindEnum.interaction) return { kind }; + if (!contractAddress || decimals == null) return { kind }; + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; const nft = logEvent.decoded.params[2]?.indexed ?? false; - if (!contractAddress || decimals == null) return { kind }; + const amount = nft ? '1' : amountOrTokenId; const asset: EvmActivityAsset = { contract: contractAddress, tokenId: nft ? amountOrTokenId : undefined, - amount: nft ? '1' : kind === ActivityKindEnum.send ? `-${amountOrTokenId}` : amountOrTokenId, + amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, decimals: nft ? 0 : decimals ?? 0, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, nft, @@ -172,56 +139,40 @@ export function parseGoldRushTransaction( } return { kind: ActivityKindEnum.interaction }; - })(); - - const operations = logEvents.map(logEvent => { - const kind: ActivityKindEnum = (() => { - // if (logEvent.decoded?.name === 'Approval') return ActivityKindEnum.approve; + }); - if (logEvent.decoded?.name === 'Transfer') { - if (getEvmAddressSafe(logEvent.decoded.params[0]?.value) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(logEvent.decoded.params[1]?.value) === accountAddress) return ActivityKindEnum.receive; - } + const gasOperation: EvmOperation | null = (() => { + const kind = (() => { + if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; - return ActivityKindEnum.interaction; + return null; })(); - const iconURL = logEvent.sender_logo_url ?? undefined; + if (!kind) return null; + + const decimals = item.gas_metadata?.contract_decimals; + if (decimals == null) return null; + + const value: string = item.value?.toString() ?? '0'; - return { - kind, - asset: (() => { - if (kind !== ActivityKindEnum.send && kind !== ActivityKindEnum.receive) return; - - const amountOrTokenId: string = logEvent.decoded?.params[2]?.value ?? '0'; - const decimals = logEvent.sender_contract_decimals; - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const nft = logEvent.decoded?.params[2]?.indexed ?? false; - - if (!contractAddress || decimals == null) return; - - const amount = nft ? '1' : amountOrTokenId; - - return { - contract: contractAddress, - tokenId: nft ? amountOrTokenId : undefined, - amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, - decimals, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft, - iconURL - }; - })() + const asset: EvmActivityAsset = { + contract: EVM_TOKEN_SLUG, + amount: kind === ActivityKindEnum.send ? `-${value}` : value, + decimals: decimals ?? 0, + symbol: item.gas_metadata?.contract_ticker_symbol }; - }); + + return { kind, asset }; + })(); + + if (gasOperation) operations.unshift(gasOperation); return { chain: TempleChainKind.EVM, chainId, - kind, hash: item.tx_hash!, blockExplorerUrl: item.explorers?.[0]?.url, - asset, operations }; } From eacd106207e34557582a184f74f51d5045229f73 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Sep 2024 22:30:02 +0300 Subject: [PATCH 08/74] TW-1479: [EVM] Transactions history. [Tezos] Formatting activities. [EVM] Applying metadata. --- TODO.md | 1 + src/app/atoms/EmptyState.tsx | 1 + src/app/pages/Home/Home.tsx | 4 +- .../pages/Home/OtherComponents/AssetTab.tsx | 27 +- .../evm/collectibles-metadata/selectors.ts | 3 + .../store/evm/tokens-metadata/selectors.ts | 3 + src/app/templates/AssetImage/index.tsx | 10 +- src/app/templates/activity/Activity.tsx | 159 +++++++++++ ...ItemBase.tsx => ActivityOperationBase.tsx} | 12 +- .../activity/ActivityTabContainer.tsx | 43 +++ .../activity/EvmActivityOperation.tsx | 29 ++ .../activity/TezosActivityOperation.tsx | 64 +++++ src/app/templates/activity/index.tsx | 217 +++++++-------- src/app/templates/activity/tezos.tsx | 115 -------- .../templates/partners-promotion/index.tsx | 248 +++++++++--------- .../activity/utils.ts => lib/activity/evm.ts} | 102 +++---- src/lib/activity/index.ts | 7 + src/lib/activity/tezos.ts | 59 +++++ src/lib/activity/types.ts | 62 +++++ src/lib/metadata/index.ts | 38 ++- src/lib/temple/activity-new/helpers.ts | 3 +- src/lib/temple/activity-new/index.ts | 2 +- 22 files changed, 771 insertions(+), 438 deletions(-) create mode 100644 src/app/templates/activity/Activity.tsx rename src/app/templates/activity/{ActivityItemBase.tsx => ActivityOperationBase.tsx} (96%) create mode 100644 src/app/templates/activity/ActivityTabContainer.tsx create mode 100644 src/app/templates/activity/EvmActivityOperation.tsx create mode 100644 src/app/templates/activity/TezosActivityOperation.tsx delete mode 100644 src/app/templates/activity/tezos.tsx rename src/{app/templates/activity/utils.ts => lib/activity/evm.ts} (69%) create mode 100644 src/lib/activity/index.ts create mode 100644 src/lib/activity/tezos.ts create mode 100644 src/lib/activity/types.ts diff --git a/TODO.md b/TODO.md index 434ec3cdd8..bfe9d0a187 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- Prepare activities from TZKT in one parse - Approve amount - NFTs - Asset details (name, symbol, decimals) due to user's wallet set-up diff --git a/src/app/atoms/EmptyState.tsx b/src/app/atoms/EmptyState.tsx index 40d824fc78..6670da7c35 100644 --- a/src/app/atoms/EmptyState.tsx +++ b/src/app/atoms/EmptyState.tsx @@ -14,6 +14,7 @@ interface EmptyStateProps { export const EmptyState = memo(({ className, variant = 'universal' }) => (
{variant === 'universal' ? : } + diff --git a/src/app/pages/Home/Home.tsx b/src/app/pages/Home/Home.tsx index 4293d8e10a..e9a41253f9 100644 --- a/src/app/pages/Home/Home.tsx +++ b/src/app/pages/Home/Home.tsx @@ -12,7 +12,7 @@ import { useAppEnv } from 'app/env'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import PageLayout, { PageLayoutProps } from 'app/layouts/PageLayout'; import { useMainnetTokensScamlistSelector } from 'app/store/tezos/assets/selectors'; -import { ActivityWithChainSelect } from 'app/templates/activity'; +import { MultichainActivityTab } from 'app/templates/activity'; import { AdvertisingBanner } from 'app/templates/advertising/advertising-banner/advertising-banner'; import { AppHeader } from 'app/templates/AppHeader'; import { toastSuccess } from 'app/toaster'; @@ -110,7 +110,7 @@ const Home = memo(props => { case 'collectibles': return ; case 'activity': - return ; + return ; default: return ; } diff --git a/src/app/pages/Home/OtherComponents/AssetTab.tsx b/src/app/pages/Home/OtherComponents/AssetTab.tsx index aad5a4e477..db032e0e07 100644 --- a/src/app/pages/Home/OtherComponents/AssetTab.tsx +++ b/src/app/pages/Home/OtherComponents/AssetTab.tsx @@ -4,6 +4,7 @@ import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import { ContentContainer } from 'app/layouts/containers'; import { TezosActivityTab, EvmActivityTab } from 'app/templates/activity'; +import { ActivityTabContainer } from 'app/templates/activity/ActivityTabContainer'; import AssetInfo from 'app/templates/AssetInfo'; import { TabsBar, TabsBarTabInterface } from 'app/templates/TabBar'; import { TEZ_TOKEN_SLUG, isTezAsset } from 'lib/assets'; @@ -52,9 +53,9 @@ const TezosGasTab = memo<{ tezosChainId: string }>(({ tezosChainId }) => { {tabName === 'activity' ? ( - + - + ) : ( @@ -78,9 +79,9 @@ const TezosTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - + - + ) : ( )} @@ -95,19 +96,13 @@ interface EvmAssetTabProps { const EvmAssetTab: FC = ({ chainId, assetSlug }) => isEvmNativeTokenSlug(assetSlug) ? ( - + + + ) : ( ); -const EvmGasTab = memo<{ chainId: number }>(({ chainId }) => { - return ( - - - - ); -}); - const EvmTokenTab = memo(({ chainId, assetSlug }) => { const tabSlug = useLocationSearchParamValue('tab'); const tabName = tabSlug === 'info' ? 'info' : 'activity'; @@ -117,9 +112,9 @@ const EvmTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - - - + + + ) : ( )} diff --git a/src/app/store/evm/collectibles-metadata/selectors.ts b/src/app/store/evm/collectibles-metadata/selectors.ts index 703e8bafce..2f6a18d121 100644 --- a/src/app/store/evm/collectibles-metadata/selectors.ts +++ b/src/app/store/evm/collectibles-metadata/selectors.ts @@ -4,6 +4,9 @@ import { EvmCollectibleMetadata } from 'lib/metadata/types'; export const useEvmCollectiblesMetadataRecordSelector = () => useSelector(({ evmCollectiblesMetadata }) => evmCollectiblesMetadata.metadataRecord); +export const useEvmChainCollectiblesMetadataRecordSelector = (chainId: number) => + useSelector(({ evmCollectiblesMetadata }) => evmCollectiblesMetadata.metadataRecord[chainId]); + export const useEvmCollectibleMetadataSelector = ( chainId: number, collectibleSlug: string diff --git a/src/app/store/evm/tokens-metadata/selectors.ts b/src/app/store/evm/tokens-metadata/selectors.ts index f44468d5f3..2c8a7694da 100644 --- a/src/app/store/evm/tokens-metadata/selectors.ts +++ b/src/app/store/evm/tokens-metadata/selectors.ts @@ -4,5 +4,8 @@ import { EvmTokenMetadata } from 'lib/metadata/types'; export const useEvmTokensMetadataRecordSelector = () => useSelector(({ evmTokensMetadata }) => evmTokensMetadata.metadataRecord); +export const useEvmChainTokensMetadataRecordSelector = (chainId: number) => + useSelector(({ evmTokensMetadata }) => evmTokensMetadata.metadataRecord[chainId]); + export const useEvmTokenMetadataSelector = (chainId: number, tokenSlug: string): EvmTokenMetadata | undefined => useSelector(({ evmTokensMetadata }) => evmTokensMetadata.metadataRecord[chainId]?.[tokenSlug]); diff --git a/src/app/templates/AssetImage/index.tsx b/src/app/templates/AssetImage/index.tsx index 15972eb571..15dbd55cc4 100644 --- a/src/app/templates/AssetImage/index.tsx +++ b/src/app/templates/AssetImage/index.tsx @@ -17,20 +17,18 @@ export interface TezosAssetImageProps extends Omit { tezosChainId: string; assetSlug: string; - extraSrc?: string; Loader?: Placeholder; Fallback?: Placeholder; } export const TezosAssetImage = memo(({ Loader, Fallback, ...props }) => { - const { tezosChainId, className, style, extraSrc, ...rest } = props; + const { tezosChainId, assetSlug, ...rest } = props; - const metadata = useTezosAssetMetadata(props.assetSlug, tezosChainId); + const metadata = useTezosAssetMetadata(assetSlug, tezosChainId); return ( : undefined} fallback={Fallback ? : undefined} {...rest} @@ -42,13 +40,12 @@ export interface EvmAssetImageProps extends Omit { evmChainId: number; assetSlug: string; - extraSrc?: string; Loader?: Placeholder; Fallback?: Placeholder; } export const EvmAssetImage = memo(({ Loader, Fallback, ...props }) => { - const { evmChainId, assetSlug, extraSrc, ...rest } = props; + const { evmChainId, assetSlug, ...rest } = props; const metadata = useEvmAssetMetadata(assetSlug, evmChainId); @@ -56,7 +53,6 @@ export const EvmAssetImage = memo(({ Loader, Fallback, ...pr : undefined} fallback={Fallback ? : undefined} {...rest} diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx new file mode 100644 index 0000000000..d8c09f11ad --- /dev/null +++ b/src/app/templates/activity/Activity.tsx @@ -0,0 +1,159 @@ +import React, { memo } from 'react'; + +import clsx from 'clsx'; + +import { IconBase } from 'app/atoms'; +import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { Activity } from 'lib/activity'; +import { t } from 'lib/i18n'; +import { Activity as LegacyActivity } from 'lib/temple/activity-new'; +import { useBooleanState } from 'lib/ui/hooks'; +import { useExplorerHref } from 'temple/front/block-explorers'; +import { EvmChain, TezosChain } from 'temple/front/chains'; + +import { EvmActivityOperationComponent } from './EvmActivityOperation'; +import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; +import { TezosActivityOperationComponent } from './TezosActivityOperation'; + +interface TezosActivityComponentProps { + activity: LegacyActivity; + chain: TezosChain; + accountAddress: string; +} + +export const TezosActivityComponent = memo(({ activity, chain, accountAddress }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + + const { hash } = activity; + + const blockExplorerUrl = useExplorerHref(chain.chainId, hash); + + const operations = activity.operations; + + return ( +
+ {operations.slice(0, 3).map((operation, i) => ( + + {i > 0 && } + + + + ))} + + {operations.length > 3 ? ( + <> + + + {expanded + ? operations.slice(3).map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} + + ) : null} +
+ ); +}); + +interface EvmActivityComponentProps { + activity: Activity; + chain: EvmChain; +} + +export const EvmActivityComponent = memo(({ activity, chain }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + + const { hash, blockExplorerUrl } = activity; + + const operations = activity.operations; + + return ( +
+ {operations.slice(0, 3).map((operation, i) => ( + + {i > 0 && } + + + + ))} + + {operations.length > 3 ? ( + <> + + + {expanded + ? operations.slice(3).map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} + + ) : null} +
+ ); +}); + +interface ActivityBaseComponentProps { + // +} + +const ActivityBaseComponent = memo(() => { + // +}); + +const InteractionsConnector = memo(() => ( +
+ +
+)); diff --git a/src/app/templates/activity/ActivityItemBase.tsx b/src/app/templates/activity/ActivityOperationBase.tsx similarity index 96% rename from src/app/templates/activity/ActivityItemBase.tsx rename to src/app/templates/activity/ActivityOperationBase.tsx index 0f30d6c462..16779208da 100644 --- a/src/app/templates/activity/ActivityItemBase.tsx +++ b/src/app/templates/activity/ActivityOperationBase.tsx @@ -8,13 +8,12 @@ import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; +import { ActivityKindEnum, InfinitySymbol } from 'lib/activity'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; import { EvmAssetIcon, TezosAssetIcon } from '../AssetIcon'; -import { ActivityKindEnum, InfinitySymbol } from './utils'; - interface Props { chainId: string | number; kind: ActivityKindEnum; @@ -33,7 +32,14 @@ interface AssetProp { iconURL?: string; } -export const ActivityItemBaseComponent: FC = ({ kind, hash, chainId, networkName, asset, blockExplorerUrl }) => { +export const ActivityOperationBaseComponent: FC = ({ + kind, + hash, + chainId, + networkName, + asset, + blockExplorerUrl +}) => { const assetSlug = asset ? typeof chainId === 'number' ? toEvmAssetSlug(asset.contract, asset.tokenId) diff --git a/src/app/templates/activity/ActivityTabContainer.tsx b/src/app/templates/activity/ActivityTabContainer.tsx new file mode 100644 index 0000000000..4d5a284488 --- /dev/null +++ b/src/app/templates/activity/ActivityTabContainer.tsx @@ -0,0 +1,43 @@ +import React, { FC, useMemo } from 'react'; + +import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; +import { useShouldShowPartnersPromoSelector } from 'app/store/partners-promotion/selectors'; +import { PartnersPromotion, PartnersPromotionVariant } from 'app/templates/partners-promotion'; +import { TEMPLE_TOKEN_SLUG } from 'lib/assets'; +import { t } from 'lib/i18n/react'; + +import { ReactivateAdsBanner } from './ReactivateAdsBanner'; + +interface Props extends PropsWithChildren { + chainId: string | number; + assetSlug?: string; +} + +export const ActivityTabContainer: FC = ({ children, chainId, assetSlug }) => { + const shouldShowPartnersPromo = useShouldShowPartnersPromoSelector(); + + const promotion = useMemo(() => { + if (shouldShowPartnersPromo) + return ( + + ); + + return assetSlug === TEMPLE_TOKEN_SLUG ? : null; + }, [shouldShowPartnersPromo, chainId, assetSlug]); + + return ( + +
+ {promotion} + + {children} +
+
+ ); +}; diff --git a/src/app/templates/activity/EvmActivityOperation.tsx b/src/app/templates/activity/EvmActivityOperation.tsx new file mode 100644 index 0000000000..980d335c6a --- /dev/null +++ b/src/app/templates/activity/EvmActivityOperation.tsx @@ -0,0 +1,29 @@ +import React, { memo } from 'react'; + +import type { EvmOperation } from 'lib/activity'; +import { EvmChain } from 'temple/front'; + +import { ActivityOperationBaseComponent } from './ActivityOperationBase'; + +interface Props { + hash: string; + operation: EvmOperation; + chain: EvmChain; + networkName: string; + blockExplorerUrl?: string; +} + +export const EvmActivityOperationComponent = memo( + ({ hash, operation, chain, networkName, blockExplorerUrl }) => { + return ( + + ); + } +); diff --git a/src/app/templates/activity/TezosActivityOperation.tsx b/src/app/templates/activity/TezosActivityOperation.tsx new file mode 100644 index 0000000000..8b2b4c8170 --- /dev/null +++ b/src/app/templates/activity/TezosActivityOperation.tsx @@ -0,0 +1,64 @@ +import React, { memo, useMemo } from 'react'; + +import { ActivityKindEnum, TezosActivityAsset, TezosOperation, formatLegacyTezosOperation } from 'lib/activity'; +import { TEZ_TOKEN_SLUG } from 'lib/assets'; +import { toTezosAssetSlug } from 'lib/assets/utils'; +import { getAssetSymbol, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; +import { ActivityOperation as LegacyActivityOperation } from 'lib/temple/activity-new/types'; +import { TezosChain } from 'temple/front'; + +import { ActivityOperationBaseComponent } from './ActivityOperationBase'; + +interface Props { + hash: string; + operation: LegacyActivityOperation; + chain: TezosChain; + networkName: string; + blockExplorerUrl: string | nullish; + accountAddress: string; +} + +export const TezosActivityOperationComponent = memo( + ({ hash, operation: legacyOperation, chain, networkName, blockExplorerUrl, accountAddress }) => { + const assetSlug = + legacyOperation.type === 'transaction' + ? toTezosAssetSlug(legacyOperation.contractAddress ?? TEZ_TOKEN_SLUG, legacyOperation.tokenId) + : ''; + + const assetMetadata = useTezosAssetMetadata(assetSlug, chain.chainId); + + const operation = useMemo(() => { + const operation = formatLegacyTezosOperation(legacyOperation, accountAddress); + // console.log(hash, operation, legacyOperation); + + if (!assetMetadata) return operation; + + if (operation.kind === ActivityKindEnum.send || operation.kind === ActivityKindEnum.receive) { + const asset: TezosActivityAsset = { + contract: legacyOperation.contractAddress ?? TEZ_TOKEN_SLUG, + // @ts-expect-error + tokenId: legacyOperation.tokenId, + amount: legacyOperation.amountSigned, + decimals: assetMetadata.decimals, + nft: isTezosCollectibleMetadata(assetMetadata), + symbol: getAssetSymbol(assetMetadata, true) + }; + + operation.asset = asset; + } + + return operation; + }, [assetMetadata, legacyOperation, accountAddress]); + + return ( + + ); + } +); diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index 19ab340cff..6d4549e47e 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -1,29 +1,25 @@ import React, { FC, memo } from 'react'; -import clsx from 'clsx'; +import InfiniteScroll from 'react-infinite-scroll-component'; -import { IconBase, SyncSpinner } from 'app/atoms'; +import { SyncSpinner } from 'app/atoms'; import { EmptyState } from 'app/atoms/EmptyState'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; -import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { ContentContainer } from 'app/layouts/containers'; +import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; +import { APP_CONTENT_PAPER_DOM_ID, ContentContainer, SCROLL_DOCUMENT } from 'app/layouts/containers'; import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; +import { EvmActivity, parseGoldRushTransaction } from 'lib/activity'; import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; -import { t } from 'lib/i18n'; +import { useGetEvmAssetMetadata } from 'lib/metadata'; import { useTypedSWR } from 'lib/swr'; -import { useBooleanState } from 'lib/ui/hooks'; -import { useAccountAddressForEvm } from 'temple/front'; -import { OneOfChains, useEvmChainByChainId } from 'temple/front/chains'; -import { TempleChainKind } from 'temple/types'; +import useTezosActivities from 'lib/temple/activity-new/hook'; +import { useAccountAddressForEvm, useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; +import { useEvmChainByChainId } from 'temple/front/chains'; -import { ActivityItemBaseComponent } from './ActivityItemBase'; -import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; -import { TezosActivityTab } from './tezos'; -import { Activity, EvmOperation, parseGoldRushTransaction } from './utils'; +import { TezosActivityComponent, EvmActivityComponent } from './Activity'; +import { ActivityTabContainer } from './ActivityTabContainer'; -export { TezosActivityTab }; - -export const ActivityWithChainSelect = memo(() => { +export const MultichainActivityTab = memo(() => { const chainSelectController = useChainSelectController(); const network = chainSelectController.value; @@ -34,121 +30,130 @@ export const ActivityWithChainSelect = memo(() => { - {network.kind === 'tezos' ? ( - - ) : ( - - )} + + {network.kind === 'tezos' ? ( + + ) : ( + + )} + ); }); -interface EvmActivityTabProps { - chainId: number; +const INITIAL_NUMBER = 30; +const LOAD_STEP = 30; + +interface TezosActivityTabProps { + tezosChainId: string; assetSlug?: string; } -export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { - const network = useEvmChainByChainId(chainId); - const accountAddress = useAccountAddressForEvm(); +export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { + const network = useTezosChainByChainId(tezosChainId); + const accountAddress = useAccountAddressForTezos(); if (!network || !accountAddress) throw new DeadEndBoundaryError(); - const { data, isLoading: isSyncing } = useTypedSWR(['evm-activity-history', chainId, accountAddress], async () => { - return await getEvmTransactions(accountAddress, chainId, 0); - }); + const { + loading, + reachedTheEnd, + list: activities, + loadMore + } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); + + useLoadPartnersPromo(); - console.log('Data:', data); + if (activities.length === 0 && !loading && reachedTheEnd) { + return ; + } - console.log(1, data?.items.length); - console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); + const retryInitialLoad = () => loadMore(INITIAL_NUMBER); + const loadMoreActivities = () => loadMore(LOAD_STEP); - const activities = data?.items.map(item => parseGoldRushTransaction(item, chainId, accountAddress)) ?? []; + const loadNext = activities.length === 0 ? retryInitialLoad : loadMoreActivities; + + const onScroll = loading || reachedTheEnd ? undefined : buildOnScroll(loadNext); return ( -
- {activities.length ? ( - <> - {activities.map(activity => ( - - ))} - - {isSyncing && } - - ) : isSyncing ? ( - - ) : ( - - )} -
+ } + onScroll={onScroll} + scrollableTarget={SCROLL_DOCUMENT ? undefined : APP_CONTENT_PAPER_DOM_ID} + > + {activities.map(activity => ( + + ))} + ); -}; +}); -const ActivityComponent: FC<{ activity: Activity; chain: OneOfChains }> = ({ activity, chain }) => { - if (activity.chain !== TempleChainKind.EVM) throw new Error('Tezos activities in dev'); +interface EvmActivityTabProps { + chainId: number; + assetSlug?: string; +} - const [expanded, , , toggleExpanded] = useBooleanState(false); +export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { + const network = useEvmChainByChainId(chainId); + const accountAddress = useAccountAddressForEvm(); - const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + if (!network || !accountAddress) throw new DeadEndBoundaryError(); - const { hash, blockExplorerUrl } = activity; + useLoadPartnersPromo(); - const operations = activity.operations as EvmOperation[]; + const getMetadata = useGetEvmAssetMetadata(chainId); - return ( -
- {operations.slice(0, 3).map((operation, i) => ( - - {i > 0 && } - - - + const { data: activities = [], isLoading: isSyncing } = useTypedSWR( + ['evm-activity-history', chainId, accountAddress], + async () => { + const data = await getEvmTransactions(accountAddress, chainId, 0); + + console.log('Data:', data); + + console.log(1, data?.items.length); + console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); + + const activities = + data?.items.map(item => parseGoldRushTransaction(item, chainId, accountAddress, getMetadata)) ?? + []; + + return activities; + } + ); + + return activities.length ? ( + <> + {activities.map(activity => ( + ))} - {operations.length > 3 ? ( - <> - - - {expanded - ? operations.slice(3).map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} - - ) : null} -
+ {isSyncing && } + + ) : isSyncing ? ( + + ) : ( + ); }; -const InteractionsConnector = memo(() => ( -
- -
-)); +/** + * Build onscroll listener to trigger next loading, when fetching data resulted in error. + * `InfiniteScroll.props.next` won't be triggered in this case. + */ +const buildOnScroll = + (next: EmptyFn) => + ({ target }: { target: EventTarget | null }) => { + const elem: HTMLElement = + target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); + const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; + if (atBottom) next(); + }; diff --git a/src/app/templates/activity/tezos.tsx b/src/app/templates/activity/tezos.tsx deleted file mode 100644 index 8fe84c0b48..0000000000 --- a/src/app/templates/activity/tezos.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { memo, useMemo } from 'react'; - -import clsx from 'clsx'; -import InfiniteScroll from 'react-infinite-scroll-component'; - -import { SyncSpinner } from 'app/atoms'; -import { useAppEnv } from 'app/env'; -import { DeadEndBoundaryError } from 'app/ErrorBoundary'; -import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { ReactComponent as LayersIcon } from 'app/icons/layers.svg'; -import { useShouldShowPartnersPromoSelector } from 'app/store/partners-promotion/selectors'; -import { PartnersPromotion, PartnersPromotionVariant } from 'app/templates/partners-promotion'; -import { TEMPLE_TOKEN_SLUG } from 'lib/assets'; -import { T } from 'lib/i18n/react'; -import useTezosActivities from 'lib/temple/activity-new/hook'; -import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; - -import { ActivityItem } from './ActivityItem'; -import { ReactivateAdsBanner } from './ReactivateAdsBanner'; - -const INITIAL_NUMBER = 30; -const LOAD_STEP = 30; -interface Props { - tezosChainId: string; - assetSlug?: string; -} - -export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { - const network = useTezosChainByChainId(tezosChainId); - const accountAddress = useAccountAddressForTezos(); - if (!network || !accountAddress) throw new DeadEndBoundaryError(); - - const { - loading, - reachedTheEnd, - list: activities, - loadMore - } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); - - const { popup } = useAppEnv(); - - const shouldShowPartnersPromo = useShouldShowPartnersPromoSelector(); - useLoadPartnersPromo(); - - const promotion = useMemo(() => { - if (shouldShowPartnersPromo) - return ( - - ); - - return assetSlug === TEMPLE_TOKEN_SLUG ? : null; - }, [shouldShowPartnersPromo, assetSlug]); - - if (activities.length === 0 && !loading && reachedTheEnd) { - return ( -
- {promotion} - - - -

- -

-
- ); - } - - const retryInitialLoad = () => loadMore(INITIAL_NUMBER); - const loadMoreActivities = () => loadMore(LOAD_STEP); - - const loadNext = activities.length === 0 ? retryInitialLoad : loadMoreActivities; - - const onScroll = loading || reachedTheEnd ? undefined : buildOnScroll(loadNext); - - return ( -
- {promotion} - - } - onScroll={onScroll} - > - {activities.map(activity => ( - - ))} - -
- ); -}); - -/** - * Build onscroll listener to trigger next loading, when fetching data resulted in error. - * `InfiniteScroll.props.next` won't be triggered in this case. - */ -const buildOnScroll = - (next: EmptyFn) => - ({ target }: { target: EventTarget | null }) => { - const elem: HTMLElement = - target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); - const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; - if (atBottom) next(); - }; diff --git a/src/app/templates/partners-promotion/index.tsx b/src/app/templates/partners-promotion/index.tsx index cc478a3f20..b59d13fcec 100644 --- a/src/app/templates/partners-promotion/index.tsx +++ b/src/app/templates/partners-promotion/index.tsx @@ -29,6 +29,7 @@ interface PartnersPromotionProps { id: string; pageName: string; withPersonaProvider?: boolean; + className?: string; } type AdsProviderLocalName = Exclude; @@ -37,128 +38,131 @@ const shouldBeHiddenTemporarily = (hiddenAt: number) => { return Date.now() - hiddenAt < AD_HIDING_TIMEOUT; }; -export const PartnersPromotion = memo(({ variant, id, pageName, withPersonaProvider }) => { - const isImageAd = variant === PartnersPromotionVariant.Image; - const adsViewerAddress = useAdsViewerPkh(); - const { popup } = useAppEnv(); - const dispatch = useDispatch(); - const hiddenAt = usePromotionHidingTimestampSelector(id); - const shouldShowPartnersPromo = useShouldShowPartnersPromoSelector(); - - const isAnalyticsSentRef = useRef(false); - - const [isHiddenTemporarily, setIsHiddenTemporarily] = useState(shouldBeHiddenTemporarily(hiddenAt)); - const [providerName, setProviderName] = useState('Optimal'); - const [adError, setAdError] = useState(false); - const [adIsReady, setAdIsReady] = useState(false); - - useEffect(() => { - const newIsHiddenTemporarily = shouldBeHiddenTemporarily(hiddenAt); - setIsHiddenTemporarily(newIsHiddenTemporarily); - - if (newIsHiddenTemporarily) { - const timeout = setTimeout( - () => setIsHiddenTemporarily(false), - Math.max(Date.now() - hiddenAt + AD_HIDING_TIMEOUT, 0) - ); - - return () => clearTimeout(timeout); +export const PartnersPromotion = memo( + ({ variant, id, pageName, withPersonaProvider, className }) => { + const isImageAd = variant === PartnersPromotionVariant.Image; + const adsViewerAddress = useAdsViewerPkh(); + const { popup } = useAppEnv(); + const dispatch = useDispatch(); + const hiddenAt = usePromotionHidingTimestampSelector(id); + const shouldShowPartnersPromo = useShouldShowPartnersPromoSelector(); + + const isAnalyticsSentRef = useRef(false); + + const [isHiddenTemporarily, setIsHiddenTemporarily] = useState(shouldBeHiddenTemporarily(hiddenAt)); + const [providerName, setProviderName] = useState('Optimal'); + const [adError, setAdError] = useState(false); + const [adIsReady, setAdIsReady] = useState(false); + + useEffect(() => { + const newIsHiddenTemporarily = shouldBeHiddenTemporarily(hiddenAt); + setIsHiddenTemporarily(newIsHiddenTemporarily); + + if (newIsHiddenTemporarily) { + const timeout = setTimeout( + () => setIsHiddenTemporarily(false), + Math.max(Date.now() - hiddenAt + AD_HIDING_TIMEOUT, 0) + ); + + return () => clearTimeout(timeout); + } + + return; + }, [hiddenAt]); + + const handleAdRectSeen = useCallback(() => { + if (isAnalyticsSentRef.current) return; + + postAdImpression(adsViewerAddress, AdsProviderTitle[providerName], { pageName }); + + isAnalyticsSentRef.current = true; + }, [providerName, pageName, adsViewerAddress]); + + const handleClosePartnersPromoClick = useCallback>( + e => { + e.preventDefault(); + e.stopPropagation(); + dispatch(hidePromotionAction({ timestamp: Date.now(), id })); + }, + [id, dispatch] + ); + + const handleOptimalError = useCallback(() => setProviderName('HypeLab'), []); + const handleHypelabError = useCallback( + () => (withPersonaProvider ? setProviderName('Persona') : setAdError(true)), + [withPersonaProvider] + ); + const handlePersonaError = useCallback(() => setAdError(true), []); + + const handleAdReady = useCallback(() => setAdIsReady(true), []); + + if (!shouldShowPartnersPromo || adError || isHiddenTemporarily) { + return null; } - return; - }, [hiddenAt]); - - const handleAdRectSeen = useCallback(() => { - if (isAnalyticsSentRef.current) return; - - postAdImpression(adsViewerAddress, AdsProviderTitle[providerName], { pageName }); - - isAnalyticsSentRef.current = true; - }, [providerName, pageName, adsViewerAddress]); - - const handleClosePartnersPromoClick = useCallback>( - e => { - e.preventDefault(); - e.stopPropagation(); - dispatch(hidePromotionAction({ timestamp: Date.now(), id })); - }, - [id, dispatch] - ); - - const handleOptimalError = useCallback(() => setProviderName('HypeLab'), []); - const handleHypelabError = useCallback( - () => (withPersonaProvider ? setProviderName('Persona') : setAdError(true)), - [withPersonaProvider] - ); - const handlePersonaError = useCallback(() => setAdError(true), []); - - const handleAdReady = useCallback(() => setAdIsReady(true), []); - - if (!shouldShowPartnersPromo || adError || isHiddenTemporarily) { - return null; + return ( +
+ {(() => { + switch (providerName) { + case 'Optimal': + return ( + + ); + case 'HypeLab': + return ( + + ); + case 'Persona': + return ( + + ); + } + })()} + + {!adIsReady && ( +
+ +
+ )} +
+ ); } - - return ( -
- {(() => { - switch (providerName) { - case 'Optimal': - return ( - - ); - case 'HypeLab': - return ( - - ); - case 'Persona': - return ( - - ); - } - })()} - - {!adIsReady && ( -
- -
- )} -
- ); -}); +); diff --git a/src/app/templates/activity/utils.ts b/src/lib/activity/evm.ts similarity index 69% rename from src/app/templates/activity/utils.ts rename to src/lib/activity/evm.ts index b855b55f39..39a7794ba4 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/lib/activity/evm.ts @@ -1,68 +1,27 @@ import { GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { toEvmAssetSlug } from 'lib/assets/utils'; +import { EvmAssetMetadataGetter, getAssetSymbol } from 'lib/metadata'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -export enum ActivityKindEnum { - interaction, - send, - receive, - swap, - approve -} - -export type Activity = TezosActivity | EvmActivity; - -interface TezosActivity { - chain: TempleChainKind.Tezos; - chainId: string; - hash: string; - operations: TezosOperation[]; -} - -interface TezosOperation { - kind: ActivityKindEnum; -} - -interface EvmActivity { - chain: TempleChainKind.EVM; - chainId: number; - hash: string; - blockExplorerUrl?: string; - operations: EvmOperation[]; -} - -export interface EvmOperation { - kind: ActivityKindEnum; - asset?: EvmActivityAsset; -} - -interface EvmActivityAsset { - contract: string; - tokenId?: string; - amount?: string | typeof InfinitySymbol; - decimals: number; - nft?: boolean; - symbol?: string; - iconURL?: string; -} - -export const InfinitySymbol = Symbol('Infinity'); +import { ActivityKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from './types'; export function parseGoldRushTransaction( item: GoldRushTransaction, chainId: number, - accountAddress: string + accountAddress: string, + getMetadata: EvmAssetMetadataGetter ): EvmActivity { const logEvents = item.log_events ?? []; const operations = logEvents.map(logEvent => { if (!logEvent.decoded?.params) return { kind: ActivityKindEnum.interaction }; + const contractAddress = getEvmAddressSafe(logEvent.sender_address); const fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); const toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const decimals = logEvent.sender_contract_decimals; + let decimals = logEvent.sender_contract_decimals; const iconURL = logEvent.sender_logo_url ?? undefined; if (logEvent.decoded.name === 'Transfer') { @@ -73,21 +32,28 @@ export function parseGoldRushTransaction( return ActivityKindEnum.interaction; })(); - if (kind === ActivityKindEnum.interaction) return { kind }; - - if (!contractAddress || decimals == null) return { kind }; + if (kind === ActivityKindEnum.interaction || !contractAddress) return { kind }; const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; const nft = logEvent.decoded.params[2]?.indexed ?? false; + const tokenId = nft ? amountOrTokenId : undefined; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; const amount = nft ? '1' : amountOrTokenId; + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; const asset: EvmActivityAsset = { contract: contractAddress, - tokenId: nft ? amountOrTokenId : undefined, + tokenId, amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, - decimals: nft ? 0 : decimals ?? 0, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + decimals, + symbol, nft, iconURL }; @@ -98,17 +64,28 @@ export function parseGoldRushTransaction( if (logEvent.decoded.name === 'Approval' && fromAddress === accountAddress) { const kind = ActivityKindEnum.approve; + if (!contractAddress) return { kind }; + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; const nft = logEvent.decoded.params[2]?.indexed ?? false; - if (!contractAddress || decimals == null) return { kind }; + const tokenId = nft ? amountOrTokenId : undefined; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; + + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; const asset: EvmActivityAsset = { contract: contractAddress, - tokenId: nft ? amountOrTokenId : undefined, + tokenId, amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs decimals, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + symbol, nft, iconURL }; @@ -129,7 +106,7 @@ export function parseGoldRushTransaction( const asset: EvmActivityAsset = { contract: contractAddress, amount: InfinitySymbol, - decimals, + decimals: NaN, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, nft: true, iconURL @@ -151,16 +128,19 @@ export function parseGoldRushTransaction( if (!kind) return null; - const decimals = item.gas_metadata?.contract_decimals; + const metadata = getMetadata(EVM_TOKEN_SLUG); + const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; + if (decimals == null) return null; const value: string = item.value?.toString() ?? '0'; + const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; const asset: EvmActivityAsset = { contract: EVM_TOKEN_SLUG, amount: kind === ActivityKindEnum.send ? `-${value}` : value, - decimals: decimals ?? 0, - symbol: item.gas_metadata?.contract_ticker_symbol + decimals, + symbol }; return { kind, asset }; diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts new file mode 100644 index 0000000000..0c6b324006 --- /dev/null +++ b/src/lib/activity/index.ts @@ -0,0 +1,7 @@ +export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivityAsset, EvmOperation } from './types'; + +export { ActivityKindEnum, InfinitySymbol } from './types'; + +export { parseGoldRushTransaction } from './evm'; + +export { formatLegacyTezosOperation } from './tezos'; diff --git a/src/lib/activity/tezos.ts b/src/lib/activity/tezos.ts new file mode 100644 index 0000000000..acfd96a541 --- /dev/null +++ b/src/lib/activity/tezos.ts @@ -0,0 +1,59 @@ +import { Activity as LegacyActivity, ActivityOperation as LegacyActivityOperation } from 'lib/temple/activity-new'; +import { OperStackItemTypeEnum } from 'lib/temple/activity-new/types'; +import { TempleChainKind } from 'temple/types'; + +import { TezosActivity, ActivityKindEnum, TezosOperation } from './types'; + +export function formatLegacyTezosActivity(_activity: LegacyActivity, chainId: string, address: string): TezosActivity { + const hash = _activity.hash; + + return { + hash, + chain: TempleChainKind.Tezos, + chainId, + operations: _activity.operations.map(oper => formatLegacyTezosOperation(oper, address)) + }; +} + +export function formatLegacyTezosOperation(oper: LegacyActivityOperation, address: string): TezosOperation { + if (oper.type === 'transaction') { + if (isZero(oper.amountSigned)) { + return { + kind: ActivityKindEnum.interaction, + subkind: OperStackItemTypeEnum.Interaction + // with: oper.destination.address, + // entrypoint: oper.entrypoint + }; + } else if (oper.source.address === address) { + return { + kind: ActivityKindEnum.send, + subkind: OperStackItemTypeEnum.TransferTo + // to: oper.destination.address + }; + } else if (oper.destination.address === address) { + return { + kind: ActivityKindEnum.receive, + subkind: OperStackItemTypeEnum.TransferFrom + // from: oper.source.address + }; + } + + return { + kind: ActivityKindEnum.interaction, + subkind: OperStackItemTypeEnum.Interaction + }; + } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { + return { + kind: ActivityKindEnum.interaction, + subkind: OperStackItemTypeEnum.Delegation + // to: oper.destination.address + }; + } + + return { + kind: ActivityKindEnum.interaction, + subkind: OperStackItemTypeEnum.Other + }; +} + +const isZero = (val: string) => Number(val) === 0; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts new file mode 100644 index 0000000000..14ec3928ff --- /dev/null +++ b/src/lib/activity/types.ts @@ -0,0 +1,62 @@ +import { OperStackItemTypeEnum } from 'lib/temple/activity-new/types'; +import { TempleChainKind } from 'temple/types'; + +export enum ActivityKindEnum { + interaction, + send, + receive, + swap, + approve +} + +export type Activity = TezosActivity | EvmActivity; + +export interface TezosActivity { + chain: TempleChainKind.Tezos; + chainId: string; + hash: string; + blockExplorerUrl?: string; + operations: TezosOperation[]; +} + +export interface TezosOperation { + kind: ActivityKindEnum; + /** @deprecated */ + subkind?: OperStackItemTypeEnum; + asset?: TezosActivityAsset; +} + +export interface TezosActivityAsset { + contract: string; + tokenId?: string; + amount?: string | typeof InfinitySymbol; + decimals: number; + nft?: boolean; + symbol?: string; + iconURL?: string; +} + +export interface EvmActivity { + chain: TempleChainKind.EVM; + chainId: number; + hash: string; + blockExplorerUrl?: string; + operations: EvmOperation[]; +} + +export interface EvmOperation { + kind: ActivityKindEnum; + asset?: EvmActivityAsset; +} + +export interface EvmActivityAsset { + contract: string; + tokenId?: string; + amount?: string | typeof InfinitySymbol; + decimals: number; + nft?: boolean; + symbol?: string; + iconURL?: string; +} + +export const InfinitySymbol = Symbol('Infinity'); diff --git a/src/lib/metadata/index.ts b/src/lib/metadata/index.ts index 2563c8e19e..3e36f0cfbc 100644 --- a/src/lib/metadata/index.ts +++ b/src/lib/metadata/index.ts @@ -1,8 +1,14 @@ import { useCallback, useEffect, useRef } from 'react'; import { dispatch } from 'app/store'; -import { useEvmCollectibleMetadataSelector } from 'app/store/evm/collectibles-metadata/selectors'; -import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; +import { + useEvmCollectibleMetadataSelector, + useEvmChainCollectiblesMetadataRecordSelector +} from 'app/store/evm/collectibles-metadata/selectors'; +import { + useEvmTokenMetadataSelector, + useEvmChainTokensMetadataRecordSelector +} from 'app/store/evm/tokens-metadata/selectors'; import { loadCollectiblesMetadataAction } from 'app/store/tezos/collectibles-metadata/actions'; import { useAllCollectiblesMetadataSelector, @@ -25,7 +31,14 @@ import { useEvmChainByChainId } from 'temple/front/chains'; import { isTezosDcpChainId } from 'temple/networks'; import { TEZOS_METADATA, FILM_METADATA } from './defaults'; -import { AssetMetadataBase, EvmAssetMetadataBase, TokenMetadata } from './types'; +import { + AssetMetadataBase, + EvmAssetMetadataBase, + EvmCollectibleMetadata, + EvmNativeTokenMetadata, + EvmTokenMetadata, + TokenMetadata +} from './types'; export type { AssetMetadataBase, TokenMetadata } from './types'; export { isCollectible, isTezosCollectibleMetadata, getAssetSymbol, getTokenName } from './utils'; @@ -49,6 +62,25 @@ export const useEvmAssetMetadata = (slug: string, evmChainId: number): EvmAssetM return isEvmNativeTokenSlug(slug) ? network?.currency : tokenMetadata || collectibleMetadata; }; +export const useGetEvmAssetMetadata = (chainId: number) => { + const network = useEvmChainByChainId(chainId); + const tokensMetadatas = useEvmChainTokensMetadataRecordSelector(chainId); + const collectiblesMetadatas = useEvmChainCollectiblesMetadataRecordSelector(chainId); + + return useCallback( + (slug: string) => { + if (isEvmNativeTokenSlug(slug)) return network?.currency; + + return tokensMetadatas[slug] || collectiblesMetadatas[slug]; + }, + [tokensMetadatas, collectiblesMetadatas, network] + ); +}; + +export type EvmAssetMetadataGetter = ( + slug: string +) => EvmNativeTokenMetadata | EvmTokenMetadata | EvmCollectibleMetadata | undefined; + type TokenMetadataGetter = (slug: string) => TokenMetadata | undefined; export const useGetTokenMetadata = () => { diff --git a/src/lib/temple/activity-new/helpers.ts b/src/lib/temple/activity-new/helpers.ts index 7b7384a522..8fbd843904 100644 --- a/src/lib/temple/activity-new/helpers.ts +++ b/src/lib/temple/activity-new/helpers.ts @@ -1,5 +1,6 @@ import BigNumber from 'bignumber.js'; +import { toTokenSlug } from 'lib/assets'; import type { Activity } from 'lib/temple/activity-new'; import { OperStackItemInterface, OperStackItemTypeEnum } from './types'; @@ -61,5 +62,3 @@ export function buildMoneyDiffs(activity: Activity) { } const isZero = (val: BigNumber.Value) => new BigNumber(val).isZero(); - -const toTokenSlug = (contractAddress: string, tokenId: string | number = 0) => `${contractAddress}_${tokenId}`; diff --git a/src/lib/temple/activity-new/index.ts b/src/lib/temple/activity-new/index.ts index 1b18e5035f..c447b74b90 100644 --- a/src/lib/temple/activity-new/index.ts +++ b/src/lib/temple/activity-new/index.ts @@ -1,3 +1,3 @@ -export type { Activity } from './types'; +export type { Activity, ActivityOperation } from './types'; export { buildOperStack, buildMoneyDiffs } from './helpers'; From 844ef6d3c0c0b66d242c749515a0e3522868d497 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Sep 2024 02:28:03 +0300 Subject: [PATCH 09/74] TW-1479: [EVM] Transactions history. [EVM] + InfiniteScroll --- src/app/atoms/InfiniteScroll.tsx | 56 ++++++++++ src/app/templates/activity/index.tsx | 122 +++++++++++++-------- src/lib/activity/evm.ts | 5 +- src/lib/apis/temple/endpoints/evm/index.ts | 6 +- 4 files changed, 139 insertions(+), 50 deletions(-) create mode 100644 src/app/atoms/InfiniteScroll.tsx diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx new file mode 100644 index 0000000000..4420b90793 --- /dev/null +++ b/src/app/atoms/InfiniteScroll.tsx @@ -0,0 +1,56 @@ +import React, { FC, ReactNode } from 'react'; + +import ReactInfiniteScrollComponent from 'react-infinite-scroll-component'; + +import { APP_CONTENT_PAPER_DOM_ID, SCROLL_DOCUMENT } from 'app/layouts/containers'; + +import { SyncSpinner } from './SyncSpinner'; + +interface Props extends PropsWithChildren { + itemsLength: number; + isSyncing: boolean; + reachedTheEnd: boolean; + retryInitialLoad: EmptyFn; + loadMore: EmptyFn; + loader?: ReactNode; +} + +export const InfiniteScroll: FC = ({ + itemsLength, + isSyncing, + reachedTheEnd, + retryInitialLoad, + loadMore, + loader = , + children +}) => { + const loadNext = itemsLength ? loadMore : retryInitialLoad; + + const onScroll = isSyncing || reachedTheEnd ? undefined : buildOnScroll(loadNext); + + return ( + + {children} + + ); +}; + +/** + * Build onscroll listener to trigger next loading, when fetching data resulted in error. + * `InfiniteScroll.props.next` won't be triggered in this case. + */ +const buildOnScroll = + (next: EmptyFn) => + ({ target }: { target: EventTarget | null }) => { + const elem: HTMLElement = + target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); + const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; + if (atBottom) next(); + }; diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index 6d4549e47e..179a2c9009 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -1,18 +1,18 @@ import React, { FC, memo } from 'react'; -import InfiniteScroll from 'react-infinite-scroll-component'; +import { AxiosError } from 'axios'; -import { SyncSpinner } from 'app/atoms'; import { EmptyState } from 'app/atoms/EmptyState'; +import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { APP_CONTENT_PAPER_DOM_ID, ContentContainer, SCROLL_DOCUMENT } from 'app/layouts/containers'; +import { ContentContainer } from 'app/layouts/containers'; import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; import { EvmActivity, parseGoldRushTransaction } from 'lib/activity'; import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; import { useGetEvmAssetMetadata } from 'lib/metadata'; -import { useTypedSWR } from 'lib/swr'; import useTezosActivities from 'lib/temple/activity-new/hook'; +import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; import { useAccountAddressForEvm, useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; @@ -69,21 +69,13 @@ export const TezosActivityTab = memo(({ tezosChainId, ass return ; } - const retryInitialLoad = () => loadMore(INITIAL_NUMBER); - const loadMoreActivities = () => loadMore(LOAD_STEP); - - const loadNext = activities.length === 0 ? retryInitialLoad : loadMoreActivities; - - const onScroll = loading || reachedTheEnd ? undefined : buildOnScroll(loadNext); - return ( } - onScroll={onScroll} - scrollableTarget={SCROLL_DOCUMENT ? undefined : APP_CONTENT_PAPER_DOM_ID} + itemsLength={activities.length} + isSyncing={Boolean(loading)} + reachedTheEnd={reachedTheEnd} + retryInitialLoad={() => loadMore(INITIAL_NUMBER)} + loadMore={() => loadMore(LOAD_STEP)} > {activities.map(activity => ( = ({ chainId, assetSlug }) useLoadPartnersPromo(); - const getMetadata = useGetEvmAssetMetadata(chainId); + const [isLoading, setIsLoading] = useSafeState(true); + const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); + const [activities, setActivities] = useSafeState([]); + const [currentPage, setCurrentPage] = useSafeState(undefined); - const { data: activities = [], isLoading: isSyncing } = useTypedSWR( - ['evm-activity-history', chainId, accountAddress], - async () => { - const data = await getEvmTransactions(accountAddress, chainId, 0); + const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + + const loadActivities = async (activities: EvmActivity[], shouldStop: () => boolean, page?: number) => { + // if (isLoading) return; + + setIsLoading(true); + + let newActivities: EvmActivity[], newPage: number; + try { + const data = await getEvmTransactions(accountAddress, chainId, page); console.log('Data:', data); console.log(1, data?.items.length); console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); - const activities = + newPage = data.current_page; + + newActivities = data?.items.map(item => parseGoldRushTransaction(item, chainId, accountAddress, getMetadata)) ?? []; - return activities; + if (shouldStop()) return; + } catch (error) { + if (shouldStop()) return; + setIsLoading(false); + if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); + console.error(error); + + return; } - ); - return activities.length ? ( - <> + setActivities(activities.concat(newActivities)); + setIsLoading(false); + setCurrentPage(newPage); + if (newPage <= 1 || newActivities.length === 0) setReachedTheEnd(true); + }; + + /** Loads more of older items */ + function loadMore() { + if (isLoading || reachedTheEnd) return; + loadActivities(activities, stopAndBuildChecker(), currentPage ? currentPage - 1 : undefined); + } + + useDidMount(() => { + loadActivities([], stopAndBuildChecker()); + + return stopLoading; + }); + + useDidUpdate(() => { + setActivities([]); + setIsLoading(false); + setReachedTheEnd(false); + + loadActivities([], stopAndBuildChecker()); + }, [chainId, accountAddress, assetSlug]); + + const getMetadata = useGetEvmAssetMetadata(chainId); + + if (activities.length === 0 && !isLoading && reachedTheEnd) { + return ; + } + + return ( + {activities.map(activity => ( ))} - - {isSyncing && } - - ) : isSyncing ? ( - - ) : ( - + ); }; - -/** - * Build onscroll listener to trigger next loading, when fetching data resulted in error. - * `InfiniteScroll.props.next` won't be triggered in this case. - */ -const buildOnScroll = - (next: EmptyFn) => - ({ target }: { target: EventTarget | null }) => { - const elem: HTMLElement = - target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); - const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; - if (atBottom) next(); - }; diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index 39a7794ba4..9139170fbb 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -119,6 +119,10 @@ export function parseGoldRushTransaction( }); const gasOperation: EvmOperation | null = (() => { + const value: string = item.value?.toString() ?? '0'; + + if (value === '0') return null; + const kind = (() => { if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; @@ -133,7 +137,6 @@ export function parseGoldRushTransaction( if (decimals == null) return null; - const value: string = item.value?.toString() ?? '0'; const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; const asset: EvmActivityAsset = { diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index 0ffa7005ab..6efe339bc7 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -15,8 +15,10 @@ export const getEvmTokensMetadata = (walletAddress: string, chainId: ChainID) => export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/collectibles-metadata', walletAddress, chainId); -export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page: number) => - buildEvmRequest<{ items: GoldRushTransaction[] }>('/transactions', walletAddress, chainId, { page }); +export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number) => + buildEvmRequest<{ items: GoldRushTransaction[]; current_page: number }>('/transactions', walletAddress, chainId, { + page + }); const buildEvmRequest = (url: string, walletAddress: string, chainId: ChainID, params?: object) => templeWalletApi From bd3cf329a2ef81c41c139ecce6ea661a123bc9cd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Sep 2024 23:56:35 +0300 Subject: [PATCH 10/74] TW-1479: [EVM] Transaction history. Token page --- src/app/atoms/InfiniteScroll.tsx | 17 +- src/app/templates/activity/EvmActivityTab.tsx | 160 +++++++++++++++++ .../templates/activity/TezosActivityTab.tsx | 57 ++++++ src/app/templates/activity/index.tsx | 164 +----------------- 4 files changed, 236 insertions(+), 162 deletions(-) create mode 100644 src/app/templates/activity/EvmActivityTab.tsx create mode 100644 src/app/templates/activity/TezosActivityTab.tsx diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index 4420b90793..8bbc4d849f 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode } from 'react'; +import React, { FC, ReactNode, useEffect } from 'react'; import ReactInfiniteScrollComponent from 'react-infinite-scroll-component'; @@ -21,19 +21,28 @@ export const InfiniteScroll: FC = ({ reachedTheEnd, retryInitialLoad, loadMore, - loader = , + loader, children }) => { const loadNext = itemsLength ? loadMore : retryInitialLoad; const onScroll = isSyncing || reachedTheEnd ? undefined : buildOnScroll(loadNext); + useEffect(() => { + if (SCROLL_DOCUMENT || isSyncing || reachedTheEnd) return; + + const scrollableElem = document.getElementById(APP_CONTENT_PAPER_DOM_ID); + if (!scrollableElem || scrollableElem.scrollTop) return; + + if (scrollableElem.offsetHeight === scrollableElem.clientHeight) loadNext(); + }, [isSyncing, reachedTheEnd]); + return ( )} onScroll={onScroll} scrollableTarget={SCROLL_DOCUMENT ? undefined : APP_CONTENT_PAPER_DOM_ID} > @@ -51,6 +60,8 @@ const buildOnScroll = ({ target }: { target: EventTarget | null }) => { const elem: HTMLElement = target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); + const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; + if (atBottom) next(); }; diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityTab.tsx new file mode 100644 index 0000000000..8a50c0cdba --- /dev/null +++ b/src/app/templates/activity/EvmActivityTab.tsx @@ -0,0 +1,160 @@ +import React, { FC } from 'react'; + +import { AxiosError } from 'axios'; + +import { EmptyState } from 'app/atoms/EmptyState'; +import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; +import { DeadEndBoundaryError } from 'app/ErrorBoundary'; +import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; +import { EvmActivity, parseGoldRushTransaction } from 'lib/activity'; +import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; +import { fromAssetSlug } from 'lib/assets'; +import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; +import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; +import { useAccountAddressForEvm } from 'temple/front'; +import { useEvmChainByChainId } from 'temple/front/chains'; + +import { EvmActivityComponent } from './Activity'; +interface EvmActivityTabProps { + chainId: number; + assetSlug?: string; +} + +export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { + const network = useEvmChainByChainId(chainId); + const accountAddress = useAccountAddressForEvm(); + + if (!network || !accountAddress) throw new DeadEndBoundaryError(); + + useLoadPartnersPromo(); + + const [isLoading, setIsLoading] = useSafeState(true); + const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); + const [activities, setActivities] = useSafeState([]); + const [currentPage, setCurrentPage] = useSafeState(undefined); + + const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + + const getMetadata = useGetEvmAssetMetadata(chainId); + + const loadActivities = async (activities: EvmActivity[], shouldStop: () => boolean, page?: number) => { + // if (isLoading) return; + + setIsLoading(true); + + let newActivities: EvmActivity[], newPage: number; + try { + // const data = await getEvmTransactions(accountAddress, chainId, page); + + // console.log('Data:', data); + + // console.log(1, data?.items.length); + // console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); + + // newPage = data.current_page; + + // newActivities = data.items.map(item => + // parseGoldRushTransaction(item, chainId, accountAddress, getMetadata) + // ); + + const data = await getEvmAssetTransactions(accountAddress, chainId, getMetadata, assetSlug, page); + + newPage = data.page; + newActivities = data.activities; + + if (shouldStop()) return; + } catch (error) { + if (shouldStop()) return; + setIsLoading(false); + if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); + console.error(error); + + return; + } + + setActivities(activities.concat(newActivities)); + setIsLoading(false); + setCurrentPage(newPage); + if (newPage <= 1 || newActivities.length === 0) setReachedTheEnd(true); + }; + + /** Loads more of older items */ + function loadMore() { + if (isLoading || reachedTheEnd) return; + loadActivities(activities, stopAndBuildChecker(), currentPage ? currentPage - 1 : undefined); + } + + useDidMount(() => { + loadActivities([], stopAndBuildChecker()); + + return stopLoading; + }); + + useDidUpdate(() => { + setActivities([]); + setIsLoading(false); + setReachedTheEnd(false); + + loadActivities([], stopAndBuildChecker()); + }, [chainId, accountAddress, assetSlug]); + + if (activities.length === 0 && !isLoading && reachedTheEnd) { + return ; + } + + return ( + + {activities.map(activity => ( + + ))} + + ); +}; + +async function getEvmAssetTransactions( + walletAddress: string, + chainId: number, + getMetadata: EvmAssetMetadataGetter, + assetSlug?: string, + page?: number +) { + if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { + const { items, current_page } = await getEvmTransactions(walletAddress, chainId, page); + + return { + activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), + page: current_page + }; + } + + const [contract, tokenId] = fromAssetSlug(assetSlug); + + let nextPage = page; + + while (nextPage == null || nextPage > 0) { + const data = await getEvmTransactions(walletAddress, chainId, nextPage); + + const newPage = data.current_page; + + const activities = data.items + .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) + .filter(a => + a.operations.some( + ({ asset }) => asset && asset.contract === contract && (asset.tokenId == null || asset.tokenId === tokenId) + ) + ); + + if (activities.length || newPage <= 1) return { activities, page: newPage }; + + nextPage = newPage - 1; + } + + return { page: 1, activities: [] }; +} diff --git a/src/app/templates/activity/TezosActivityTab.tsx b/src/app/templates/activity/TezosActivityTab.tsx new file mode 100644 index 0000000000..0b7b927fa5 --- /dev/null +++ b/src/app/templates/activity/TezosActivityTab.tsx @@ -0,0 +1,57 @@ +import React, { memo } from 'react'; + +import { EmptyState } from 'app/atoms/EmptyState'; +import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; +import { DeadEndBoundaryError } from 'app/ErrorBoundary'; +import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; +import useTezosActivities from 'lib/temple/activity-new/hook'; +import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; + +import { TezosActivityComponent } from './Activity'; + +const INITIAL_NUMBER = 30; +const LOAD_STEP = 30; + +interface TezosActivityTabProps { + tezosChainId: string; + assetSlug?: string; +} + +export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { + const network = useTezosChainByChainId(tezosChainId); + const accountAddress = useAccountAddressForTezos(); + + if (!network || !accountAddress) throw new DeadEndBoundaryError(); + + useLoadPartnersPromo(); + + const { + loading, + reachedTheEnd, + list: activities, + loadMore + } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); + + if (activities.length === 0 && !loading && reachedTheEnd) { + return ; + } + + return ( + loadMore(INITIAL_NUMBER)} + loadMore={() => loadMore(LOAD_STEP)} + > + {activities.map(activity => ( + + ))} + + ); +}); diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx index 179a2c9009..cc6059442c 100644 --- a/src/app/templates/activity/index.tsx +++ b/src/app/templates/activity/index.tsx @@ -1,23 +1,13 @@ -import React, { FC, memo } from 'react'; +import React, { memo } from 'react'; -import { AxiosError } from 'axios'; - -import { EmptyState } from 'app/atoms/EmptyState'; -import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; -import { DeadEndBoundaryError } from 'app/ErrorBoundary'; -import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { ContentContainer } from 'app/layouts/containers'; import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; -import { EvmActivity, parseGoldRushTransaction } from 'lib/activity'; -import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; -import { useGetEvmAssetMetadata } from 'lib/metadata'; -import useTezosActivities from 'lib/temple/activity-new/hook'; -import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; -import { useAccountAddressForEvm, useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; -import { useEvmChainByChainId } from 'temple/front/chains'; -import { TezosActivityComponent, EvmActivityComponent } from './Activity'; import { ActivityTabContainer } from './ActivityTabContainer'; +import { EvmActivityTab } from './EvmActivityTab'; +import { TezosActivityTab } from './TezosActivityTab'; + +export { TezosActivityTab, EvmActivityTab }; export const MultichainActivityTab = memo(() => { const chainSelectController = useChainSelectController(); @@ -41,147 +31,3 @@ export const MultichainActivityTab = memo(() => { ); }); - -const INITIAL_NUMBER = 30; -const LOAD_STEP = 30; - -interface TezosActivityTabProps { - tezosChainId: string; - assetSlug?: string; -} - -export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { - const network = useTezosChainByChainId(tezosChainId); - const accountAddress = useAccountAddressForTezos(); - - if (!network || !accountAddress) throw new DeadEndBoundaryError(); - - const { - loading, - reachedTheEnd, - list: activities, - loadMore - } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); - - useLoadPartnersPromo(); - - if (activities.length === 0 && !loading && reachedTheEnd) { - return ; - } - - return ( - loadMore(INITIAL_NUMBER)} - loadMore={() => loadMore(LOAD_STEP)} - > - {activities.map(activity => ( - - ))} - - ); -}); - -interface EvmActivityTabProps { - chainId: number; - assetSlug?: string; -} - -export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { - const network = useEvmChainByChainId(chainId); - const accountAddress = useAccountAddressForEvm(); - - if (!network || !accountAddress) throw new DeadEndBoundaryError(); - - useLoadPartnersPromo(); - - const [isLoading, setIsLoading] = useSafeState(true); - const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - const [activities, setActivities] = useSafeState([]); - const [currentPage, setCurrentPage] = useSafeState(undefined); - - const { stop: stopLoading, stopAndBuildChecker } = useStopper(); - - const loadActivities = async (activities: EvmActivity[], shouldStop: () => boolean, page?: number) => { - // if (isLoading) return; - - setIsLoading(true); - - let newActivities: EvmActivity[], newPage: number; - try { - const data = await getEvmTransactions(accountAddress, chainId, page); - - console.log('Data:', data); - - console.log(1, data?.items.length); - console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); - - newPage = data.current_page; - - newActivities = - data?.items.map(item => parseGoldRushTransaction(item, chainId, accountAddress, getMetadata)) ?? - []; - - if (shouldStop()) return; - } catch (error) { - if (shouldStop()) return; - setIsLoading(false); - if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); - console.error(error); - - return; - } - - setActivities(activities.concat(newActivities)); - setIsLoading(false); - setCurrentPage(newPage); - if (newPage <= 1 || newActivities.length === 0) setReachedTheEnd(true); - }; - - /** Loads more of older items */ - function loadMore() { - if (isLoading || reachedTheEnd) return; - loadActivities(activities, stopAndBuildChecker(), currentPage ? currentPage - 1 : undefined); - } - - useDidMount(() => { - loadActivities([], stopAndBuildChecker()); - - return stopLoading; - }); - - useDidUpdate(() => { - setActivities([]); - setIsLoading(false); - setReachedTheEnd(false); - - loadActivities([], stopAndBuildChecker()); - }, [chainId, accountAddress, assetSlug]); - - const getMetadata = useGetEvmAssetMetadata(chainId); - - if (activities.length === 0 && !isLoading && reachedTheEnd) { - return ; - } - - return ( - - {activities.map(activity => ( - - ))} - - ); -}; From c934a03d14a7aeb5d75e7e4e5fed962d406b6421 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Sep 2024 02:04:10 +0300 Subject: [PATCH 11/74] TW-1479: [EVM] Transaction history. API calls. Refactor. With nextPage --- src/app/templates/activity/EvmActivityTab.tsx | 44 ++---- src/lib/apis/temple/endpoints/evm/index.ts | 19 +++ .../endpoints/evm/types/erc20-transfers.ts | 137 ++++++++++++++++++ .../endpoints/evm/types/transactions.ts | 10 +- .../apis/temple/endpoints/evm/types/utils.ts | 3 + 5 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts create mode 100644 src/lib/apis/temple/endpoints/evm/types/utils.ts diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityTab.tsx index 8a50c0cdba..b4961c0233 100644 --- a/src/app/templates/activity/EvmActivityTab.tsx +++ b/src/app/templates/activity/EvmActivityTab.tsx @@ -16,6 +16,7 @@ import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './Activity'; + interface EvmActivityTabProps { chainId: number; assetSlug?: string; @@ -32,7 +33,7 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) const [isLoading, setIsLoading] = useSafeState(true); const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); const [activities, setActivities] = useSafeState([]); - const [currentPage, setCurrentPage] = useSafeState(undefined); + const [nextPage, setNextPage] = useSafeState(undefined); const { stop: stopLoading, stopAndBuildChecker } = useStopper(); @@ -43,24 +44,11 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) setIsLoading(true); - let newActivities: EvmActivity[], newPage: number; + let newActivities: EvmActivity[], newNextPage: number | null; try { - // const data = await getEvmTransactions(accountAddress, chainId, page); - - // console.log('Data:', data); - - // console.log(1, data?.items.length); - // console.log(2, new Set(data?.items.map(item => item.tx_hash)).size); - - // newPage = data.current_page; - - // newActivities = data.items.map(item => - // parseGoldRushTransaction(item, chainId, accountAddress, getMetadata) - // ); - const data = await getEvmAssetTransactions(accountAddress, chainId, getMetadata, assetSlug, page); - newPage = data.page; + newNextPage = data.nextPage; newActivities = data.activities; if (shouldStop()) return; @@ -75,14 +63,14 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) setActivities(activities.concat(newActivities)); setIsLoading(false); - setCurrentPage(newPage); - if (newPage <= 1 || newActivities.length === 0) setReachedTheEnd(true); + setNextPage(newNextPage); + if (newNextPage === null || newActivities.length === 0) setReachedTheEnd(true); }; /** Loads more of older items */ function loadMore() { - if (isLoading || reachedTheEnd) return; - loadActivities(activities, stopAndBuildChecker(), currentPage ? currentPage - 1 : undefined); + if (isLoading || reachedTheEnd || nextPage === null) return; + loadActivities(activities, stopAndBuildChecker(), nextPage); } useDidMount(() => { @@ -126,23 +114,21 @@ async function getEvmAssetTransactions( page?: number ) { if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { - const { items, current_page } = await getEvmTransactions(walletAddress, chainId, page); + const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page); return { activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), - page: current_page + nextPage }; } const [contract, tokenId] = fromAssetSlug(assetSlug); - let nextPage = page; + let nextPage: number | nullish = page; - while (nextPage == null || nextPage > 0) { + while (nextPage !== null) { const data = await getEvmTransactions(walletAddress, chainId, nextPage); - const newPage = data.current_page; - const activities = data.items .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) .filter(a => @@ -151,10 +137,10 @@ async function getEvmAssetTransactions( ) ); - if (activities.length || newPage <= 1) return { activities, page: newPage }; + if (activities.length) return { activities, nextPage: data.nextPage }; - nextPage = newPage - 1; + nextPage = data.nextPage; } - return { page: 1, activities: [] }; + return { nextPage: null, activities: [] }; } diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index 6efe339bc7..9843963cb7 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -1,6 +1,7 @@ import { templeWalletApi } from '../templewallet.api'; import { BalancesResponse, ChainID, NftAddressBalanceNftResponse } from './api.interfaces'; +import { Erc20TransfersResponse } from './types/erc20-transfers'; import { GoldRushTransaction } from './types/transactions'; export type { GoldRushTransaction }; @@ -18,6 +19,24 @@ export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: Chain export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number) => buildEvmRequest<{ items: GoldRushTransaction[]; current_page: number }>('/transactions', walletAddress, chainId, { page + }).then(({ items, current_page }) => ({ + items, + /** null | \> 0 */ + nextPage: current_page > 1 ? current_page - 1 : null + })); + +export const getEvmERC20Transfers = (walletAddress: string, chainId: ChainID, contractAddress: string, page?: number) => + buildEvmRequest('/erc20-transfers', walletAddress, chainId, { + contractAddress, + page + }).then(({ items, pagination }) => { + const withoutNextPage = items && pagination ? items.length < pagination.page_size : true; + + return { + items: items ?? [], + /** null | \> 0 */ + nextPage: withoutNextPage ? null : pagination?.page_number ?? 0 + 1 + }; }); const buildEvmRequest = (url: string, walletAddress: string, chainId: ChainID, params?: object) => diff --git a/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts b/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts new file mode 100644 index 0000000000..f16705b17a --- /dev/null +++ b/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts @@ -0,0 +1,137 @@ +import { Nullable } from './utils'; + +export type Erc20TransfersResponse = Nullable<{ + items: Nullable[]; + pagination: Pagination; +}>; + +interface Pagination { + /** * True is there is another page. */ + has_more: boolean; + /** * The requested page number. */ + page_number: number; + /** * The requested number of items on the current page. */ + page_size: number; + /** @deprecated // Always null + * The total number of items across all pages for this request. + */ + total_count: number; +} + +interface BlockTransactionWithContractTransfers { + /** * The block signed timestamp in UTC. */ + block_signed_at: Date; + /** * The height of the block. */ + block_height: number; + /** * The hash of the block. Use it to remove transactions from re-org-ed blocks. */ + block_hash: string; + /** * The requested transaction hash. */ + tx_hash: string; + /** * The offset is the position of the tx in the block. */ + tx_offset: number; + /** * Whether or not transaction is successful. */ + successful: boolean; + /** * The address of the miner. */ + miner_address: string; + /** * The sender's wallet address. */ + from_address: string; + /** * The label of `from` address. */ + from_address_label: string; + /** * The receiver's wallet address. */ + to_address: string; + /** * The label of `to` address. */ + to_address_label: string; + /** * The value attached to this tx. */ + value: bigint; + /** * The value attached in `quote-currency` to this tx. */ + value_quote: number; + /** * A prettier version of the quote for rendering purposes. */ + pretty_value_quote: string; + /** * The requested chain native gas token metadata. */ + gas_metadata: ContractMetadata; + gas_offered: number; + /** * The gas spent for this tx. */ + gas_spent: number; + /** * The gas price at the time of this tx. */ + gas_price: number; + /** * The transaction's gas_price * gas_spent, denoted in wei. */ + fees_paid: bigint; + /** * The gas spent in `quote-currency` denomination. */ + gas_quote: number; + /** * A prettier version of the quote for rendering purposes. */ + pretty_gas_quote: string; + /** * The native gas exchange rate for the requested `quote-currency`. */ + gas_quote_rate: number; + transfers: Nullable[]; +} + +interface ContractMetadata { + /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ + contract_decimals: number; + /** * The string returned by the `name()` method. */ + contract_name: string; + /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ + contract_ticker_symbol: string; + /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ + contract_address: string; + /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ + supports_erc: string[]; + /** * The contract logo URL. */ + logo_url: string; +} + +interface TokenTransferItem { + /** * The block signed timestamp in UTC. */ + block_signed_at: Date; + /** * The requested transaction hash. */ + tx_hash: string; + /** * The sender's wallet address. */ + from_address: string; + /** * The label of `from` address. */ + from_address_label: string; + /** * The receiver's wallet address. */ + to_address: string; + /** * The label of `to` address. */ + to_address_label: string; + /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ + contract_decimals: number; + /** * The string returned by the `name()` method. */ + contract_name: string; + /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ + contract_ticker_symbol: string; + /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ + contract_address: string; + /** * The contract logo URL. */ + logo_url: string; + /** * Categorizes token transactions as either `transfer-in` or `transfer-out`, indicating whether tokens are being received or sent from an account. */ + transfer_type: string; + /** * The delta attached to this transfer. */ + delta: bigint; + /** * The asset balance. Use `contract_decimals` to scale this balance for display purposes. */ + balance: bigint; + /** * The exchange rate for the requested quote currency. */ + quote_rate: number; + /** * The current delta converted to fiat in `quote-currency`. */ + delta_quote: number; + /** * A prettier version of the quote for rendering purposes. */ + pretty_delta_quote: string; + /** * The current balance converted to fiat in `quote-currency`. */ + balance_quote: number; + /** * Additional details on which transfer events were invoked. Defaults to `true`. */ + method_calls: Nullable[]; + /** * The explorer links for this transaction. */ + explorers: Explorer[]; +} + +interface MethodCallsForTransfers { + /** * The address of the sender. */ + sender_address: string; + method: string; +} + +interface Explorer { + /** * The name of the explorer. */ + label: string; + /** * The URL of the explorer. */ + url: string; +} diff --git a/src/lib/apis/temple/endpoints/evm/types/transactions.ts b/src/lib/apis/temple/endpoints/evm/types/transactions.ts index 0debaf8b5f..7c85ca195f 100644 --- a/src/lib/apis/temple/endpoints/evm/types/transactions.ts +++ b/src/lib/apis/temple/endpoints/evm/types/transactions.ts @@ -1,8 +1,8 @@ -type Nullable = { - [P in keyof T]: T[P] | null; -}; +import { Nullable } from './utils'; -export type GoldRushTransaction = Nullable<{ +export type GoldRushTransaction = Nullable; + +interface Transaction { /** * The block signed timestamp in UTC. */ block_signed_at: Date; /** * The height of the block. */ @@ -58,7 +58,7 @@ export type GoldRushTransaction = Nullable<{ log_events: Nullable[]; /** * The details related to the safe transaction. */ safe_details: Nullable[]; -}>; +} interface NftSalesReport { /** * The offset is the position of the log entry within an event log. */ diff --git a/src/lib/apis/temple/endpoints/evm/types/utils.ts b/src/lib/apis/temple/endpoints/evm/types/utils.ts new file mode 100644 index 0000000000..72c965e63b --- /dev/null +++ b/src/lib/apis/temple/endpoints/evm/types/utils.ts @@ -0,0 +1,3 @@ +export type Nullable = { + [P in keyof T]: T[P] | null; +}; From 97b08a2004f8b8758c0bf1161e5af9e7c882dd70 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Sep 2024 19:37:43 +0300 Subject: [PATCH 12/74] TW-1479: [EVM] Transaction history. Token page. Using endpoint V2 --- src/app/templates/activity/Activity.tsx | 8 -- src/app/templates/activity/EvmActivityTab.tsx | 41 ++++--- .../activity/TezosActivityOperation.tsx | 1 - src/lib/activity/evm.ts | 102 ++++++++++++++---- src/lib/activity/index.ts | 2 +- src/lib/activity/tezos.ts | 5 +- src/lib/apis/temple/endpoints/evm/index.ts | 4 +- .../endpoints/evm/types/erc20-transfers.ts | 4 +- 8 files changed, 112 insertions(+), 55 deletions(-) diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index d8c09f11ad..4db9937e75 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -144,14 +144,6 @@ export const EvmActivityComponent = memo(({ activity, ); }); -interface ActivityBaseComponentProps { - // -} - -const ActivityBaseComponent = memo(() => { - // -}); - const InteractionsConnector = memo(() => (
diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityTab.tsx index b4961c0233..49134ce254 100644 --- a/src/app/templates/activity/EvmActivityTab.tsx +++ b/src/app/templates/activity/EvmActivityTab.tsx @@ -6,8 +6,8 @@ import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { EvmActivity, parseGoldRushTransaction } from 'lib/activity'; -import { getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; +import { EvmActivity, parseGoldRushTransaction, parseGoldRushERC20Transfer } from 'lib/activity'; +import { getEvmERC20Transfers, getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; import { fromAssetSlug } from 'lib/assets'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; @@ -122,25 +122,32 @@ async function getEvmAssetTransactions( }; } - const [contract, tokenId] = fromAssetSlug(assetSlug); + const [contract] = fromAssetSlug(assetSlug); - let nextPage: number | nullish = page; + // let nextPage: number | nullish = page; - while (nextPage !== null) { - const data = await getEvmTransactions(walletAddress, chainId, nextPage); + // while (nextPage !== null) { + // const data = await getEvmTransactions(walletAddress, chainId, nextPage); - const activities = data.items - .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) - .filter(a => - a.operations.some( - ({ asset }) => asset && asset.contract === contract && (asset.tokenId == null || asset.tokenId === tokenId) - ) - ); + // const activities = data.items + // .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) + // .filter(a => + // a.operations.some( + // ({ asset }) => asset && asset.contract === contract && (asset.tokenId == null || asset.tokenId === tokenId) + // ) + // ); - if (activities.length) return { activities, nextPage: data.nextPage }; + // if (activities.length) return { activities, nextPage: data.nextPage }; - nextPage = data.nextPage; - } + // nextPage = data.nextPage; + // } + + // return { nextPage: null, activities: [] }; - return { nextPage: null, activities: [] }; + const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page); + + return { + activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), + nextPage + }; } diff --git a/src/app/templates/activity/TezosActivityOperation.tsx b/src/app/templates/activity/TezosActivityOperation.tsx index 8b2b4c8170..f62eca8c9a 100644 --- a/src/app/templates/activity/TezosActivityOperation.tsx +++ b/src/app/templates/activity/TezosActivityOperation.tsx @@ -29,7 +29,6 @@ export const TezosActivityOperationComponent = memo( const operation = useMemo(() => { const operation = formatLegacyTezosOperation(legacyOperation, accountAddress); - // console.log(hash, operation, legacyOperation); if (!assetMetadata) return operation; diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index 9139170fbb..0fc536dbac 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -1,4 +1,4 @@ -import { GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; +import { GoldRushERC20Transfer, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { EvmAssetMetadataGetter, getAssetSymbol } from 'lib/metadata'; @@ -118,36 +118,57 @@ export function parseGoldRushTransaction( return { kind: ActivityKindEnum.interaction }; }); - const gasOperation: EvmOperation | null = (() => { - const value: string = item.value?.toString() ?? '0'; + const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); - if (value === '0') return null; + if (gasOperation) operations.unshift(gasOperation); - const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; + return { + chain: TempleChainKind.EVM, + chainId, + hash: item.tx_hash!, + blockExplorerUrl: item.explorers?.[0]?.url, + operations + }; +} - return null; - })(); +export function parseGoldRushERC20Transfer( + item: GoldRushERC20Transfer, + chainId: number, + accountAddress: string, + getMetadata: EvmAssetMetadataGetter +): EvmActivity { + const operations = + item.transfers?.map(transfer => { + const kind = transfer.transfer_type === 'IN' ? ActivityKindEnum.receive : ActivityKindEnum.send; - if (!kind) return null; + const contractAddress = getEvmAddressSafe(transfer.contract_address); - const metadata = getMetadata(EVM_TOKEN_SLUG); - const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; + if (contractAddress == null) return { kind }; - if (decimals == null) return null; + const slug = toEvmAssetSlug(contractAddress); + const metadata = getMetadata(slug); - const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; + const decimals = metadata?.decimals ?? transfer.contract_decimals; - const asset: EvmActivityAsset = { - contract: EVM_TOKEN_SLUG, - amount: kind === ActivityKindEnum.send ? `-${value}` : value, - decimals, - symbol - }; + if (decimals == null) return { kind: ActivityKindEnum.interaction }; - return { kind, asset }; - })(); + const nft = false; + const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; + const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, + decimals, + symbol, + nft, + iconURL: transfer.logo_url ?? undefined + }; + + return { kind, asset }; + }) ?? []; + + const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); if (gasOperation) operations.unshift(gasOperation); @@ -155,7 +176,42 @@ export function parseGoldRushTransaction( chain: TempleChainKind.EVM, chainId, hash: item.tx_hash!, - blockExplorerUrl: item.explorers?.[0]?.url, + blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, operations }; } + +function parseGasTransfer( + item: GoldRushTransaction | GoldRushERC20Transfer, + accountAddress: string, + getMetadata: EvmAssetMetadataGetter +): EvmOperation | null { + const value: string = item.value?.toString() ?? '0'; + + if (value === '0') return null; + + const kind = (() => { + if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; + if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; + + return null; + })(); + + if (!kind) return null; + + const metadata = getMetadata(EVM_TOKEN_SLUG); + const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; + + if (decimals == null) return null; + + const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; + + const asset: EvmActivityAsset = { + contract: EVM_TOKEN_SLUG, + amount: kind === ActivityKindEnum.send ? `-${value}` : value, + decimals, + symbol + }; + + return { kind, asset }; +} diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 0c6b324006..9e66c6349c 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -2,6 +2,6 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivit export { ActivityKindEnum, InfinitySymbol } from './types'; -export { parseGoldRushTransaction } from './evm'; +export { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './evm'; export { formatLegacyTezosOperation } from './tezos'; diff --git a/src/lib/activity/tezos.ts b/src/lib/activity/tezos.ts index acfd96a541..0c93e545da 100644 --- a/src/lib/activity/tezos.ts +++ b/src/lib/activity/tezos.ts @@ -17,14 +17,15 @@ export function formatLegacyTezosActivity(_activity: LegacyActivity, chainId: st export function formatLegacyTezosOperation(oper: LegacyActivityOperation, address: string): TezosOperation { if (oper.type === 'transaction') { - if (isZero(oper.amountSigned)) { + if (isZero(oper.amountSigned)) return { kind: ActivityKindEnum.interaction, subkind: OperStackItemTypeEnum.Interaction // with: oper.destination.address, // entrypoint: oper.entrypoint }; - } else if (oper.source.address === address) { + + if (oper.source.address === address) { return { kind: ActivityKindEnum.send, subkind: OperStackItemTypeEnum.TransferTo diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index 9843963cb7..ab8db87b21 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -1,10 +1,10 @@ import { templeWalletApi } from '../templewallet.api'; import { BalancesResponse, ChainID, NftAddressBalanceNftResponse } from './api.interfaces'; -import { Erc20TransfersResponse } from './types/erc20-transfers'; +import { Erc20TransfersResponse, GoldRushERC20Transfer } from './types/erc20-transfers'; import { GoldRushTransaction } from './types/transactions'; -export type { GoldRushTransaction }; +export type { GoldRushTransaction, GoldRushERC20Transfer }; export const getEvmBalances = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/balances', walletAddress, chainId); diff --git a/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts b/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts index f16705b17a..89484bc469 100644 --- a/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts +++ b/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts @@ -1,10 +1,12 @@ import { Nullable } from './utils'; export type Erc20TransfersResponse = Nullable<{ - items: Nullable[]; + items: GoldRushERC20Transfer[]; pagination: Pagination; }>; +export type GoldRushERC20Transfer = Nullable; + interface Pagination { /** * True is there is another page. */ has_more: boolean; From 2a06a966e449d910bd51005db477790453618c17 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Sep 2024 22:47:25 +0300 Subject: [PATCH 13/74] TW-1479: [EVM] Transactions history. [Tezos] Fix 'receive' operations --- src/lib/activity/tezos.ts | 10 ++-- src/lib/apis/tzkt/utils.ts | 24 +++++---- src/lib/temple/activity-new/helpers.ts | 14 ++--- src/lib/temple/activity-new/types.ts | 7 ++- src/lib/temple/activity-new/utils.ts | 75 +++++++++++++++----------- 5 files changed, 74 insertions(+), 56 deletions(-) diff --git a/src/lib/activity/tezos.ts b/src/lib/activity/tezos.ts index 0c93e545da..83356fc478 100644 --- a/src/lib/activity/tezos.ts +++ b/src/lib/activity/tezos.ts @@ -25,17 +25,17 @@ export function formatLegacyTezosOperation(oper: LegacyActivityOperation, addres // entrypoint: oper.entrypoint }; - if (oper.source.address === address) { + if (oper.from.address === address) { return { kind: ActivityKindEnum.send, subkind: OperStackItemTypeEnum.TransferTo - // to: oper.destination.address + // to: oper.to.address }; - } else if (oper.destination.address === address) { + } else if (oper.to.address === address) { return { kind: ActivityKindEnum.receive, subkind: OperStackItemTypeEnum.TransferFrom - // from: oper.source.address + // from: oper.from.address }; } @@ -43,7 +43,7 @@ export function formatLegacyTezosOperation(oper: LegacyActivityOperation, addres kind: ActivityKindEnum.interaction, subkind: OperStackItemTypeEnum.Interaction }; - } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { + } else if (oper.type === 'delegation' && oper.source.address === address && oper.target) { return { kind: ActivityKindEnum.interaction, subkind: OperStackItemTypeEnum.Delegation diff --git a/src/lib/apis/tzkt/utils.ts b/src/lib/apis/tzkt/utils.ts index 256244232a..56558942ab 100644 --- a/src/lib/apis/tzkt/utils.ts +++ b/src/lib/apis/tzkt/utils.ts @@ -3,14 +3,16 @@ import { TzktAccount } from './types'; export const calcTzktAccountSpendableTezBalance = ({ balance, stakedBalance, unstakedBalance }: TzktAccount) => ((balance ?? 0) - (stakedBalance ?? 0) - (unstakedBalance ?? 0)).toFixed(); -type ParameterFa12 = { +interface ParameterFa12 { entrypoint: string; - value: { - to: string; - from: string; - value: string; - }; -}; + value: ParameterFa12Value; +} + +interface ParameterFa12Value { + to: string; + from: string; + value: string; +} interface Fa2Transaction { to_: string; @@ -23,17 +25,17 @@ interface Fa2OpParams { from_: string; } -export type ParameterFa2 = { +export interface ParameterFa2 { entrypoint: string; value: Fa2OpParams[]; -}; -type ParameterLiquidityBaking = { +} +interface ParameterLiquidityBaking { entrypoint: string; value: { target: string; quantity: string; // can be 'number' or '-number }; -}; +} export function isTzktOperParam(param: any): param is { entrypoint: string; diff --git a/src/lib/temple/activity-new/helpers.ts b/src/lib/temple/activity-new/helpers.ts index 8fbd843904..2a249b6cd9 100644 --- a/src/lib/temple/activity-new/helpers.ts +++ b/src/lib/temple/activity-new/helpers.ts @@ -13,24 +13,24 @@ export function buildOperStack(activity: Activity, address: string) { if (isZero(oper.amountSigned)) { opStack.push({ type: OperStackItemTypeEnum.Interaction, - with: oper.destination.address, + with: oper.target.address, entrypoint: oper.entrypoint }); - } else if (oper.source.address === address) { + } else if (oper.from.address === address) { opStack.push({ type: OperStackItemTypeEnum.TransferTo, - to: oper.destination.address + to: oper.to.address }); - } else if (oper.destination.address === address) { + } else if (oper.to.address === address) { opStack.push({ type: OperStackItemTypeEnum.TransferFrom, - from: oper.source.address + from: oper.from.address }); } - } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { + } else if (oper.type === 'delegation' && oper.source.address === address && oper.target) { opStack.push({ type: OperStackItemTypeEnum.Delegation, - to: oper.destination.address + to: oper.target.address }); } else { opStack.push({ diff --git a/src/lib/temple/activity-new/types.ts b/src/lib/temple/activity-new/types.ts index 596a0400e5..ffd93fdeaa 100644 --- a/src/lib/temple/activity-new/types.ts +++ b/src/lib/temple/activity-new/types.ts @@ -8,6 +8,7 @@ export interface OperationsGroup { export type ActivityStatus = TzktOperation['status'] | 'pending'; export type ActivityMember = TzktAlias; + export interface Activity { hash: string; /** ISO string */ @@ -30,14 +31,16 @@ export interface ActivityOperationBase extends PickedPropsFromTzktOperation { export interface ActivityTransactionOperation extends ActivityOperationBase { type: 'transaction'; - destination: ActivityMember; + from: ActivityMember; + to: ActivityMember; + target: ActivityMember; entrypoint?: string; tokenId?: string; } export interface ActivityOtherOperation extends ActivityOperationBase { type: Exclude; - destination?: ActivityMember; + target?: ActivityMember; } export type ActivityOperation = ActivityTransactionOperation | ActivityOtherOperation; diff --git a/src/lib/temple/activity-new/utils.ts b/src/lib/temple/activity-new/utils.ts index b8ea628f7d..6f14aa7b39 100644 --- a/src/lib/temple/activity-new/utils.ts +++ b/src/lib/temple/activity-new/utils.ts @@ -1,5 +1,3 @@ -import { BigNumber } from 'bignumber.js'; - import { TzktOperation, TzktTransactionOperation } from 'lib/apis/tzkt'; import { isTzktOperParam, @@ -9,6 +7,7 @@ import { ParameterFa2 } from 'lib/apis/tzkt/utils'; import { isTruthy } from 'lib/utils'; +import { ZERO } from 'lib/utils/numbers'; import type { OperationsGroup, @@ -58,7 +57,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti ...activityOperBase, type: 'delegation' }; - if (operation.newDelegate) activityOper.destination = operation.newDelegate; + if (operation.newDelegate) activityOper.target = operation.newDelegate; return activityOper; } case 'origination': { @@ -69,7 +68,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti ...activityOperBase, type: 'origination' }; - if (operation.originatedContract) activityOper.destination = operation.originatedContract; + if (operation.originatedContract) activityOper.target = operation.originatedContract; return activityOper; } default: @@ -81,17 +80,28 @@ function reduceOneTzktTransactionOperation( address: string, operation: TzktTransactionOperation ): ActivityTransactionOperation | null { - function _buildReturn(args: { amount: string; source: ActivityMember; contractAddress?: string; tokenId?: string }) { - const { amount, source, contractAddress, tokenId } = args; + function _buildReturn(args: { + amount: string; + from: ActivityMember; + to?: ActivityMember; + source?: ActivityMember; + contractAddress?: string; + tokenId?: string; + }) { + const { amount, from, to = operation.target, source = operation.sender, contractAddress, tokenId } = args; const activityOperBase = buildActivityOperBase(operation, address, amount, source); const activityOper: ActivityTransactionOperation = { ...activityOperBase, type: 'transaction', - destination: operation.target + target: operation.target, + from, + to }; + if (contractAddress != null) activityOper.contractAddress = contractAddress; if (tokenId != null) activityOper.tokenId = tokenId; if (isTzktOperParam(operation.parameter)) activityOper.entrypoint = operation.parameter.entrypoint; + return activityOper; } @@ -100,10 +110,10 @@ function reduceOneTzktTransactionOperation( if (parameter == null) { if (operation.target.address !== address && operation.sender.address !== address) return null; - const source = operation.sender; + const from = operation.sender; const amount = String(operation.amount); - return _buildReturn({ amount, source }); + return _buildReturn({ amount, from }); } else if (isTzktOperParam_Fa2(parameter)) { const values = reduceParameterFa2Values(parameter.value, address); const firstVal = values[0]; @@ -113,32 +123,33 @@ function reduceOneTzktTransactionOperation( const contractAddress = operation.target.address; const amount = firstVal.amount; const tokenId = firstVal.tokenId; - const source = firstVal.from === address ? { ...operation.sender, address } : operation.sender; + const from = firstVal.from === address ? { ...operation.sender, address } : operation.sender; + const to = firstVal.toRelAddress ? { address } : operation.target; - return _buildReturn({ amount, source, contractAddress, tokenId }); + return _buildReturn({ amount, from, to, source: from, contractAddress, tokenId }); } else if (isTzktOperParam_Fa12(parameter)) { if (parameter.entrypoint === 'approve') return null; - const source = { ...operation.sender }; - if (parameter.value.from === address) source.address = address; - else if (parameter.value.to === address) source.address = parameter.value.from; + const from = { ...operation.sender }; + if (parameter.value.from === address) from.address = address; + else if (parameter.value.to === address) from.address = parameter.value.from; else return null; const contractAddress = operation.target.address; const amount = parameter.value.value; - return _buildReturn({ amount, source, contractAddress }); + return _buildReturn({ amount, from, source: from, contractAddress }); } else if (isTzktOperParam_LiquidityBaking(parameter)) { - const source = operation.sender; + const from = operation.sender; const contractAddress = operation.target.address; const amount = parameter.value.quantity; - return _buildReturn({ amount, source, contractAddress }); + return _buildReturn({ amount, from, contractAddress }); } else { - const source = operation.sender; + const from = operation.sender; const amount = String(operation.amount); - return _buildReturn({ amount, source }); + return _buildReturn({ amount, from }); } } @@ -161,6 +172,7 @@ function buildActivityOperBase(operation: TzktOperation, address: string, amount function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: string) { const result: { from: string; + toRelAddress?: boolean; amount: string; tokenId: string; }[] = []; @@ -171,31 +183,32 @@ function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: str Visit https://tezos.b9lab.com/fa2 - There is a link to code in Smartpy IDE. Fa2 token-standard/smartcontract literally has it in its code. */ + const tokenId = val.txs[0]!.token_id; const from = val.from_; + if (val.from_ === relAddress) { - const amount = val.txs.reduce((acc, tx) => acc.plus(tx.amount), new BigNumber(0)); + const amount = val.txs.reduce((acc, tx) => acc.plus(tx.amount), ZERO); + if (amount.isZero()) continue; + result.push({ from, amount: amount.toFixed(), - tokenId: val.txs[0]!.token_id + tokenId }); + continue; } - let isValRel = false; - let amount = new BigNumber(0); - for (const tx of val.txs) { - if (tx.to_ === relAddress) { - amount = amount.plus(tx.amount); - if (isValRel === false) isValRel = true; - } - } - if (isValRel && amount.isZero() === false) + + const amount = val.txs.reduce((acc, tx) => (tx.to_ === relAddress ? acc.plus(tx.amount) : acc), ZERO); + + if (amount.isZero() === false) result.push({ from, + toRelAddress: true, amount: amount.toFixed(), - tokenId: val.txs[0]!.token_id + tokenId }); } From 117262c9e27fdcdec4bb01648b15da4299235fbd Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Sep 2024 23:44:45 +0300 Subject: [PATCH 14/74] TW-1479: [EVM] Transactions history. [Tezos] Refactor --- src/app/templates/activity/Activity.tsx | 2 + src/lib/activity/tezos.ts | 4 +- src/lib/temple/activity-new/helpers.ts | 10 ++-- src/lib/temple/activity-new/types.ts | 8 +-- src/lib/temple/activity-new/utils.ts | 68 ++++++++++++++----------- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index 4db9937e75..d5d75b8584 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -32,6 +32,8 @@ export const TezosActivityComponent = memo(({ activ const operations = activity.operations; + if (hash === 'oorAovoprUsZqr1HHYZ3RuLDitR8PqzM2t8GEzGmCGboKSFDMNX') console.log('O:', operations); + return (
{operations.slice(0, 3).map((operation, i) => ( diff --git a/src/lib/activity/tezos.ts b/src/lib/activity/tezos.ts index 83356fc478..eab97c132d 100644 --- a/src/lib/activity/tezos.ts +++ b/src/lib/activity/tezos.ts @@ -31,7 +31,7 @@ export function formatLegacyTezosOperation(oper: LegacyActivityOperation, addres subkind: OperStackItemTypeEnum.TransferTo // to: oper.to.address }; - } else if (oper.to.address === address) { + } else if (oper.to?.address === address) { return { kind: ActivityKindEnum.receive, subkind: OperStackItemTypeEnum.TransferFrom @@ -43,7 +43,7 @@ export function formatLegacyTezosOperation(oper: LegacyActivityOperation, addres kind: ActivityKindEnum.interaction, subkind: OperStackItemTypeEnum.Interaction }; - } else if (oper.type === 'delegation' && oper.source.address === address && oper.target) { + } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { return { kind: ActivityKindEnum.interaction, subkind: OperStackItemTypeEnum.Delegation diff --git a/src/lib/temple/activity-new/helpers.ts b/src/lib/temple/activity-new/helpers.ts index 2a249b6cd9..650c43781b 100644 --- a/src/lib/temple/activity-new/helpers.ts +++ b/src/lib/temple/activity-new/helpers.ts @@ -13,24 +13,24 @@ export function buildOperStack(activity: Activity, address: string) { if (isZero(oper.amountSigned)) { opStack.push({ type: OperStackItemTypeEnum.Interaction, - with: oper.target.address, + with: oper.destination.address, entrypoint: oper.entrypoint }); } else if (oper.from.address === address) { opStack.push({ type: OperStackItemTypeEnum.TransferTo, - to: oper.to.address + to: oper.to?.address ?? '' }); - } else if (oper.to.address === address) { + } else if (oper.to?.address === address) { opStack.push({ type: OperStackItemTypeEnum.TransferFrom, from: oper.from.address }); } - } else if (oper.type === 'delegation' && oper.source.address === address && oper.target) { + } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { opStack.push({ type: OperStackItemTypeEnum.Delegation, - to: oper.target.address + to: oper.destination.address }); } else { opStack.push({ diff --git a/src/lib/temple/activity-new/types.ts b/src/lib/temple/activity-new/types.ts index ffd93fdeaa..fba1ab51c0 100644 --- a/src/lib/temple/activity-new/types.ts +++ b/src/lib/temple/activity-new/types.ts @@ -23,7 +23,6 @@ type PickedPropsFromTzktOperation = Pick; export interface ActivityOperationBase extends PickedPropsFromTzktOperation { contractAddress?: string; - source: ActivityMember; status: ActivityStatus; amountSigned: string; addedAt: string; @@ -32,15 +31,16 @@ export interface ActivityOperationBase extends PickedPropsFromTzktOperation { export interface ActivityTransactionOperation extends ActivityOperationBase { type: 'transaction'; from: ActivityMember; - to: ActivityMember; - target: ActivityMember; + to?: ActivityMember; + destination: ActivityMember; entrypoint?: string; tokenId?: string; } export interface ActivityOtherOperation extends ActivityOperationBase { type: Exclude; - target?: ActivityMember; + source: ActivityMember; + destination?: ActivityMember; } export type ActivityOperation = ActivityTransactionOperation | ActivityOtherOperation; diff --git a/src/lib/temple/activity-new/utils.ts b/src/lib/temple/activity-new/utils.ts index 6f14aa7b39..80592c61bf 100644 --- a/src/lib/temple/activity-new/utils.ts +++ b/src/lib/temple/activity-new/utils.ts @@ -52,23 +52,24 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti case 'delegation': { if (operation.sender.address !== address) return null; - const activityOperBase = buildActivityOperBase(operation, address, '0', operation.sender); + const activityOperBase = buildActivityOperBase(operation, '0', operation.sender.address === address); const activityOper: ActivityOtherOperation = { ...activityOperBase, + source: operation.sender, type: 'delegation' }; - if (operation.newDelegate) activityOper.target = operation.newDelegate; + if (operation.newDelegate) activityOper.destination = operation.newDelegate; return activityOper; } case 'origination': { - const source = operation.sender; const amount = operation.contractBalance ? operation.contractBalance.toString() : '0'; - const activityOperBase = buildActivityOperBase(operation, address, amount, source); + const activityOperBase = buildActivityOperBase(operation, amount, operation.sender.address === address); const activityOper: ActivityOtherOperation = { ...activityOperBase, + source: operation.sender, type: 'origination' }; - if (operation.originatedContract) activityOper.target = operation.originatedContract; + if (operation.originatedContract) activityOper.destination = operation.originatedContract; return activityOper; } default: @@ -84,16 +85,15 @@ function reduceOneTzktTransactionOperation( amount: string; from: ActivityMember; to?: ActivityMember; - source?: ActivityMember; contractAddress?: string; tokenId?: string; }) { - const { amount, from, to = operation.target, source = operation.sender, contractAddress, tokenId } = args; - const activityOperBase = buildActivityOperBase(operation, address, amount, source); + const { amount, from, to, contractAddress, tokenId } = args; + const activityOperBase = buildActivityOperBase(operation, amount, from.address === address); const activityOper: ActivityTransactionOperation = { ...activityOperBase, type: 'transaction', - target: operation.target, + destination: operation.target, from, to }; @@ -111,9 +111,10 @@ function reduceOneTzktTransactionOperation( if (operation.target.address !== address && operation.sender.address !== address) return null; const from = operation.sender; + const to = operation.target; const amount = String(operation.amount); - return _buildReturn({ amount, from }); + return _buildReturn({ amount, from, to }); } else if (isTzktOperParam_Fa2(parameter)) { const values = reduceParameterFa2Values(parameter.value, address); const firstVal = values[0]; @@ -123,12 +124,12 @@ function reduceOneTzktTransactionOperation( const contractAddress = operation.target.address; const amount = firstVal.amount; const tokenId = firstVal.tokenId; - const from = firstVal.from === address ? { ...operation.sender, address } : operation.sender; - const to = firstVal.toRelAddress ? { address } : operation.target; + const from = { ...operation.sender, address: firstVal.fromAddress }; + const to = firstVal.isToRelAddress ? { address } : undefined; - return _buildReturn({ amount, from, to, source: from, contractAddress, tokenId }); + return _buildReturn({ amount, from, to, contractAddress, tokenId }); } else if (isTzktOperParam_Fa12(parameter)) { - if (parameter.entrypoint === 'approve') return null; + if (parameter.entrypoint === 'approve') return null; // TODO: Implement const from = { ...operation.sender }; if (parameter.value.from === address) from.address = address; @@ -137,45 +138,50 @@ function reduceOneTzktTransactionOperation( const contractAddress = operation.target.address; const amount = parameter.value.value; + const to = operation.target; - return _buildReturn({ amount, from, source: from, contractAddress }); + return _buildReturn({ amount, from, to, contractAddress }); } else if (isTzktOperParam_LiquidityBaking(parameter)) { const from = operation.sender; + const to = operation.target; const contractAddress = operation.target.address; const amount = parameter.value.quantity; - return _buildReturn({ amount, from, contractAddress }); + return _buildReturn({ amount, from, to, contractAddress }); } else { const from = operation.sender; + const to = operation.target; const amount = String(operation.amount); - return _buildReturn({ amount, from }); + return _buildReturn({ amount, from, to }); } } -function buildActivityOperBase(operation: TzktOperation, address: string, amount: string, source: ActivityMember) { +function buildActivityOperBase(operation: TzktOperation, amount: string, from: boolean) { const { id, level, timestamp: addedAt } = operation; const reducedOperation: ActivityOperationBase = { id, level, - source, - amountSigned: source.address === address ? `-${amount}` : amount, + amountSigned: from ? `-${amount}` : amount, status: stringToActivityStatus(operation.status), addedAt }; + return reducedOperation; } +interface ReducedParameterFa2Values { + fromAddress: string; + isToRelAddress?: boolean; + amount: string; + tokenId: string; +} + /** * Items with zero cumulative amount value are filtered out */ function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: string) { - const result: { - from: string; - toRelAddress?: boolean; - amount: string; - tokenId: string; - }[] = []; + const result: ReducedParameterFa2Values[] = []; for (const val of values) { /* @@ -185,15 +191,15 @@ function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: str */ const tokenId = val.txs[0]!.token_id; - const from = val.from_; + const fromAddress = val.from_; - if (val.from_ === relAddress) { + if (fromAddress === relAddress) { const amount = val.txs.reduce((acc, tx) => acc.plus(tx.amount), ZERO); if (amount.isZero()) continue; result.push({ - from, + fromAddress, amount: amount.toFixed(), tokenId }); @@ -205,8 +211,8 @@ function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: str if (amount.isZero() === false) result.push({ - from, - toRelAddress: true, + fromAddress, + isToRelAddress: true, amount: amount.toFixed(), tokenId }); From 5701f9575934ab7942ad81877cf77a34184aa7a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Sep 2024 00:46:55 +0300 Subject: [PATCH 15/74] TW-1479: [EVM] Transactions history. [Tezos] Clean-up --- src/app/templates/activity/Activity.tsx | 4 +- src/app/templates/activity/ActivityItem.tsx | 109 ------------------ src/app/templates/activity/MoneyDiffView.tsx | 50 -------- src/app/templates/activity/OperStack.tsx | 53 --------- src/app/templates/activity/OperStackItem.tsx | 105 ----------------- .../activity/TezosActivityOperation.tsx | 33 ++---- src/lib/activity/index.ts | 2 +- src/lib/activity/tezos.ts | 102 ++++++++++------ src/lib/activity/types.ts | 3 - src/lib/temple/activity-new/fetch.ts | 40 ++++--- src/lib/temple/activity-new/helpers.ts | 64 ---------- src/lib/temple/activity-new/hook.ts | 23 +++- src/lib/temple/activity-new/index.ts | 4 +- src/lib/temple/activity-new/types.ts | 82 +++---------- src/lib/temple/activity-new/utils.ts | 40 +++---- 15 files changed, 155 insertions(+), 559 deletions(-) delete mode 100644 src/app/templates/activity/ActivityItem.tsx delete mode 100644 src/app/templates/activity/MoneyDiffView.tsx delete mode 100644 src/app/templates/activity/OperStack.tsx delete mode 100644 src/app/templates/activity/OperStackItem.tsx delete mode 100644 src/lib/temple/activity-new/helpers.ts diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index d5d75b8584..ffe55247dd 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -6,7 +6,7 @@ import { IconBase } from 'app/atoms'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { Activity } from 'lib/activity'; import { t } from 'lib/i18n'; -import { Activity as LegacyActivity } from 'lib/temple/activity-new'; +import { TezosPreActivity } from 'lib/temple/activity-new'; import { useBooleanState } from 'lib/ui/hooks'; import { useExplorerHref } from 'temple/front/block-explorers'; import { EvmChain, TezosChain } from 'temple/front/chains'; @@ -16,7 +16,7 @@ import { ReactComponent as InteractionsConnectorSvg } from './interactions-conne import { TezosActivityOperationComponent } from './TezosActivityOperation'; interface TezosActivityComponentProps { - activity: LegacyActivity; + activity: TezosPreActivity; chain: TezosChain; accountAddress: string; } diff --git a/src/app/templates/activity/ActivityItem.tsx b/src/app/templates/activity/ActivityItem.tsx deleted file mode 100644 index fbc6792325..0000000000 --- a/src/app/templates/activity/ActivityItem.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useEffect, useState, useMemo, memo } from 'react'; - -import classNames from 'clsx'; -import formatDistanceToNow from 'date-fns/formatDistanceToNow'; - -import { HashChip } from 'app/atoms'; -import { MoneyDiffView } from 'app/templates/activity/MoneyDiffView'; -import { OperStack } from 'app/templates/activity/OperStack'; -import { OpenInExplorerChip } from 'app/templates/OpenInExplorerChip'; -import { getDateFnsLocale } from 'lib/i18n'; -import { t } from 'lib/i18n/react'; -import { Activity, buildOperStack, buildMoneyDiffs } from 'lib/temple/activity-new'; - -interface Props { - activity: Activity; - tezosChainId: string; - address: string; -} - -export const ActivityItem = memo(({ tezosChainId, activity, address }) => { - const { hash, addedAt, status } = activity; - - const operStack = useMemo(() => buildOperStack(activity, address), [activity, address]); - const moneyDiffs = useMemo(() => buildMoneyDiffs(activity), [activity]); - - return ( -
-
- - - - -
-
- -
-
- - - - - -
- -
- -
- {moneyDiffs.map(({ assetSlug, diff }, i) => ( - - ))} -
-
-
- ); -}); - -interface ActivityItemStatusCompProps { - activity: Activity; -} - -const ActivityItemStatusComp: React.FC = ({ activity }) => { - const explorerStatus = activity.status; - const content = explorerStatus ?? 'pending'; - const conditionalTextColor = explorerStatus ? 'text-red-600' : 'text-yellow-600'; - - return ( -
- - {t(content) || content} - -
- ); -}; - -type TimeProps = { - children: () => React.ReactElement; -}; - -const Time: React.FC = ({ children }) => { - const [value, setValue] = useState(children); - - useEffect(() => { - const interval = setInterval(() => { - setValue(children()); - }, 5_000); - - return () => { - clearInterval(interval); - }; - }, [setValue, children]); - - return value; -}; diff --git a/src/app/templates/activity/MoneyDiffView.tsx b/src/app/templates/activity/MoneyDiffView.tsx deleted file mode 100644 index 621c14704e..0000000000 --- a/src/app/templates/activity/MoneyDiffView.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { memo, useMemo } from 'react'; - -import BigNumber from 'bignumber.js'; -import classNames from 'clsx'; - -import Money from 'app/atoms/Money'; -import { useAppEnv } from 'app/env'; -import InFiat from 'app/templates/InFiat'; -import { useTezosAssetMetadata, getAssetSymbol } from 'lib/metadata'; - -interface Props { - tezosChainId: string; - assetId: string; - diff: string; - pending?: boolean; - className?: string; -} - -export const MoneyDiffView = memo(({ tezosChainId, assetId: assetSlug, diff, pending = false, className }) => { - const { popup } = useAppEnv(); - const metadata = useTezosAssetMetadata(assetSlug, tezosChainId); - - const diffBN = useMemo(() => new BigNumber(diff).div(metadata ? 10 ** metadata.decimals : 1), [diff, metadata]); - - const conditionalPopupClassName = popup ? 'text-xs' : 'text-sm'; - const conditionalDiffClassName = diffBN.gt(0) ? 'text-green-500' : 'text-red-700'; - const conditionalPendingClassName = pending ? 'text-yellow-600' : conditionalDiffClassName; - const showPlus = diffBN.gt(0) ? '+' : ''; - - return metadata ? ( -
-
- {showPlus} - {diffBN} - {getAssetSymbol(metadata, true)} -
- - {assetSlug && ( - - {({ balance, symbol }) => ( -
- {balance} - {symbol} -
- )} -
- )} -
- ) : null; -}); diff --git a/src/app/templates/activity/OperStack.tsx b/src/app/templates/activity/OperStack.tsx deleted file mode 100644 index 70c0a089ee..0000000000 --- a/src/app/templates/activity/OperStack.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { memo, useMemo, useState } from 'react'; - -import classNames from 'clsx'; - -import { OP_STACK_PREVIEW_SIZE } from 'app/defaults'; -import { ReactComponent as ChevronRightIcon } from 'app/icons/chevron-right.svg'; -import { ReactComponent as ChevronUpIcon } from 'app/icons/chevron-up.svg'; -import { T } from 'lib/i18n/react'; -import { OperStackItemInterface } from 'lib/temple/activity-new/types'; - -import { OperStackItem } from './OperStackItem'; - -interface Props { - operStack: OperStackItemInterface[]; - className?: string; -} - -export const OperStack = memo(({ operStack, className }) => { - const [expanded, setExpanded] = useState(false); - - const base = useMemo(() => operStack.filter((_, i) => i < OP_STACK_PREVIEW_SIZE), [operStack]); - const rest = useMemo(() => operStack.filter((_, i) => i >= OP_STACK_PREVIEW_SIZE), [operStack]); - - const ExpandIcon = expanded ? ChevronUpIcon : ChevronRightIcon; - - return ( -
- {base.map((item, i) => ( - - ))} - - {expanded && ( - <> - {rest.map((item, i) => ( - - ))} - - )} - - {rest.length > 0 && ( -
- -
- )} -
- ); -}); diff --git a/src/app/templates/activity/OperStackItem.tsx b/src/app/templates/activity/OperStackItem.tsx deleted file mode 100644 index 087e731108..0000000000 --- a/src/app/templates/activity/OperStackItem.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { memo } from 'react'; - -import { HashChip } from 'app/atoms'; -import { ReactComponent as ClipboardIcon } from 'app/icons/clipboard.svg'; -import { TID, T } from 'lib/i18n'; -import { OperStackItemInterface, OperStackItemTypeEnum } from 'lib/temple/activity-new/types'; - -interface Props { - item: OperStackItemInterface; -} - -export const OperStackItem = memo(({ item }) => { - switch (item.type) { - case OperStackItemTypeEnum.Delegation: - return ( - } - argsNode={} - /> - ); - - case OperStackItemTypeEnum.Origination: - return } />; - - case OperStackItemTypeEnum.Interaction: - return ( - - - - - } - argsNode={} - /> - ); - - case OperStackItemTypeEnum.TransferFrom: - return ( - - ↓ - - } - argsNode={} - /> - ); - - case OperStackItemTypeEnum.TransferTo: - return ( - - ↑ - - } - argsNode={} - /> - ); - - case OperStackItemTypeEnum.Other: - return ( - `${w.charAt(0).toUpperCase()}${w.substring(1)}`) - .join(' ')} - /> - ); - } -}); - -interface StackItemBaseProps { - titleNode: React.ReactNode; - argsNode?: React.ReactNode; -} -const StackItemBase: React.FC = ({ titleNode, argsNode }) => { - return ( -
-
{titleNode}
- - {argsNode} -
- ); -}; - -interface StackItemArgsProps { - i18nKey: TID; - args: string[]; -} - -const StackItemArgs = memo(({ i18nKey, args }) => ( - - ( - - - {index === args.length - 1 ? null : ', '} - - ))} - /> - -)); diff --git a/src/app/templates/activity/TezosActivityOperation.tsx b/src/app/templates/activity/TezosActivityOperation.tsx index f62eca8c9a..c891b87d69 100644 --- a/src/app/templates/activity/TezosActivityOperation.tsx +++ b/src/app/templates/activity/TezosActivityOperation.tsx @@ -1,17 +1,17 @@ import React, { memo, useMemo } from 'react'; -import { ActivityKindEnum, TezosActivityAsset, TezosOperation, formatLegacyTezosOperation } from 'lib/activity'; +import { TezosOperation, parseTezosPreActivityOperation } from 'lib/activity'; import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { toTezosAssetSlug } from 'lib/assets/utils'; -import { getAssetSymbol, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; -import { ActivityOperation as LegacyActivityOperation } from 'lib/temple/activity-new/types'; +import { useTezosAssetMetadata } from 'lib/metadata'; +import { TezosPreActivityOperation } from 'lib/temple/activity-new/types'; import { TezosChain } from 'temple/front'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; interface Props { hash: string; - operation: LegacyActivityOperation; + operation: TezosPreActivityOperation; chain: TezosChain; networkName: string; blockExplorerUrl: string | nullish; @@ -27,27 +27,10 @@ export const TezosActivityOperationComponent = memo( const assetMetadata = useTezosAssetMetadata(assetSlug, chain.chainId); - const operation = useMemo(() => { - const operation = formatLegacyTezosOperation(legacyOperation, accountAddress); - - if (!assetMetadata) return operation; - - if (operation.kind === ActivityKindEnum.send || operation.kind === ActivityKindEnum.receive) { - const asset: TezosActivityAsset = { - contract: legacyOperation.contractAddress ?? TEZ_TOKEN_SLUG, - // @ts-expect-error - tokenId: legacyOperation.tokenId, - amount: legacyOperation.amountSigned, - decimals: assetMetadata.decimals, - nft: isTezosCollectibleMetadata(assetMetadata), - symbol: getAssetSymbol(assetMetadata, true) - }; - - operation.asset = asset; - } - - return operation; - }, [assetMetadata, legacyOperation, accountAddress]); + const operation = useMemo( + () => parseTezosPreActivityOperation(legacyOperation, accountAddress, assetMetadata), + [assetMetadata, legacyOperation, accountAddress] + ); return ( (oper => formatLegacyTezosOperation(oper, address)) + operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)) }; } -export function formatLegacyTezosOperation(oper: LegacyActivityOperation, address: string): TezosOperation { - if (oper.type === 'transaction') { - if (isZero(oper.amountSigned)) - return { - kind: ActivityKindEnum.interaction, - subkind: OperStackItemTypeEnum.Interaction - // with: oper.destination.address, - // entrypoint: oper.entrypoint - }; +export function parseTezosPreActivityOperation( + preOperation: TezosPreActivityOperation, + address: string, + assetMetadata?: AssetMetadataBase +): TezosOperation { + let tokenId: string | undefined; + + const operationBase: TezosOperation = (() => { + if (preOperation.type === 'transaction') { + tokenId = preOperation.tokenId; + + if (isZero(preOperation.amountSigned)) + return { + kind: ActivityKindEnum.interaction + // subkind: OperStackItemTypeEnum.Interaction + // with: oper.destination.address, + // entrypoint: oper.entrypoint + }; + + if (preOperation.from.address === address) { + return { + kind: ActivityKindEnum.send + // subkind: OperStackItemTypeEnum.TransferTo + // to: oper.to.address + }; + } else if (preOperation.to?.address === address) { + return { + kind: ActivityKindEnum.receive + // subkind: OperStackItemTypeEnum.TransferFrom + // from: oper.from.address + }; + } - if (oper.from.address === address) { return { - kind: ActivityKindEnum.send, - subkind: OperStackItemTypeEnum.TransferTo - // to: oper.to.address + kind: ActivityKindEnum.interaction + // subkind: OperStackItemTypeEnum.Interaction }; - } else if (oper.to?.address === address) { + } else if ( + preOperation.type === 'delegation' && + preOperation.source.address === address && + preOperation.destination + ) { return { - kind: ActivityKindEnum.receive, - subkind: OperStackItemTypeEnum.TransferFrom - // from: oper.from.address + kind: ActivityKindEnum.interaction + // subkind: OperStackItemTypeEnum.Delegation + // to: oper.destination.address }; } return { - kind: ActivityKindEnum.interaction, - subkind: OperStackItemTypeEnum.Interaction + kind: ActivityKindEnum.interaction + // subkind: OperStackItemTypeEnum.Other }; - } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { - return { - kind: ActivityKindEnum.interaction, - subkind: OperStackItemTypeEnum.Delegation - // to: oper.destination.address + })(); + + if (!assetMetadata) return operationBase; + + if (operationBase.kind === ActivityKindEnum.send || operationBase.kind === ActivityKindEnum.receive) { + const asset: TezosActivityAsset = { + contract: preOperation.contractAddress ?? TEZ_TOKEN_SLUG, + tokenId, + amount: preOperation.amountSigned, + decimals: assetMetadata.decimals, + nft: isTezosCollectibleMetadata(assetMetadata), + symbol: getAssetSymbol(assetMetadata, true) }; + + operationBase.asset = asset; } - return { - kind: ActivityKindEnum.interaction, - subkind: OperStackItemTypeEnum.Other - }; + return operationBase; } const isZero = (val: string) => Number(val) === 0; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 14ec3928ff..b334c2560b 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -1,4 +1,3 @@ -import { OperStackItemTypeEnum } from 'lib/temple/activity-new/types'; import { TempleChainKind } from 'temple/types'; export enum ActivityKindEnum { @@ -21,8 +20,6 @@ export interface TezosActivity { export interface TezosOperation { kind: ActivityKindEnum; - /** @deprecated */ - subkind?: OperStackItemTypeEnum; asset?: TezosActivityAsset; } diff --git a/src/lib/temple/activity-new/fetch.ts b/src/lib/temple/activity-new/fetch.ts index e31526a9e9..b671c59e4c 100644 --- a/src/lib/temple/activity-new/fetch.ts +++ b/src/lib/temple/activity-new/fetch.ts @@ -5,24 +5,26 @@ import { detectTokenStandard } from 'lib/assets/standards'; import { filterUnique } from 'lib/utils'; import { getReadOnlyTezos } from 'temple/tezos'; -import type { Activity, OperationsGroup } from './types'; -import { operationsGroupToActivity } from './utils'; +import type { OperationsGroup } from './types'; const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; -export default async function fetchActivities( +export interface TezosActivityOlderThan { + hash: string; + oldestTzktOperation: TzktOperation; +} + +export default async function fetchTezosOperationsGroups( chainId: TzktApiChainId, rpcUrl: string, accountAddress: string, assetSlug: string | undefined, pseudoLimit: number, - olderThan?: Activity -): Promise { + olderThan?: TezosActivityOlderThan +) { const operations = await fetchOperations(chainId, rpcUrl, accountAddress, assetSlug, pseudoLimit, olderThan); - const groups = await fetchOperGroupsForOperations(chainId, operations, olderThan); - - return groups.map(group => operationsGroupToActivity(group, accountAddress)); + return await fetchOperGroupsForOperations(chainId, operations, olderThan); } /** @@ -37,7 +39,7 @@ async function fetchOperations( accAddress: string, assetSlug: string | undefined, pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ): Promise { if (assetSlug) { const [contractAddress, tokenId] = (assetSlug ?? '').split('_'); @@ -65,7 +67,7 @@ const fetchOperations_TEZ = ( chainId: TzktApiChainId, accountAddress: string, pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) => TZKT.fetchGetOperationsTransactions(chainId, { 'anyof.sender.target.initiator': accountAddress, @@ -79,7 +81,7 @@ const fetchOperations_Contract = ( chainId: TzktApiChainId, accountAddress: string, pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) => TZKT.fetchGetAccountOperations(chainId, accountAddress, { type: 'transaction', @@ -95,7 +97,7 @@ const fetchOperations_Token_Fa_1_2 = ( accountAddress: string, contractAddress: string, pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) => TZKT.fetchGetOperationsTransactions(chainId, { limit: pseudoLimit, @@ -112,7 +114,7 @@ const fetchOperations_Token_Fa_2 = ( contractAddress: string, tokenId = '0', pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) => TZKT.fetchGetOperationsTransactions(chainId, { limit: pseudoLimit, @@ -130,7 +132,7 @@ async function fetchOperations_Any( chainId: TzktApiChainId, accountAddress: string, pseudoLimit: number, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) { const limit = pseudoLimit; @@ -169,7 +171,7 @@ function fetchIncomingOperTransactions_Fa_1_2( chainId: TzktApiChainId, accountAddress: string, endLimitation: { limit: number } | { newerThen: string }, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) { const bottomParams = 'limit' in endLimitation ? endLimitation : { 'timestamp.ge': endLimitation.newerThen }; @@ -189,7 +191,7 @@ function fetchIncomingOperTransactions_Fa_2( chainId: TzktApiChainId, accountAddress: string, endLimitation: { limit: number } | { newerThen: string }, - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) { const bottomParams = 'limit' in endLimitation ? endLimitation : { 'timestamp.ge': endLimitation.newerThen }; @@ -213,7 +215,7 @@ function fetchIncomingOperTransactions_Fa_2( async function fetchOperGroupsForOperations( chainId: TzktApiChainId, operations: TzktOperation[], - olderThan?: Activity + olderThan?: TezosActivityOlderThan ) { const uniqueHashes = filterUnique(operations.map(d => d.hash)); @@ -237,4 +239,6 @@ async function fetchOperGroupsForOperations( * > `{"code":400,"errors":{"lastId":"The value '331626822238208' is not valid."}}` * > when it's not true! */ -const buildOlderThanParam = (olderThan?: Activity) => ({ 'timestamp.lt': olderThan?.oldestTzktOperation?.timestamp }); +const buildOlderThanParam = (olderThan?: TezosActivityOlderThan) => ({ + 'timestamp.lt': olderThan?.oldestTzktOperation?.timestamp +}); diff --git a/src/lib/temple/activity-new/helpers.ts b/src/lib/temple/activity-new/helpers.ts deleted file mode 100644 index 650c43781b..0000000000 --- a/src/lib/temple/activity-new/helpers.ts +++ /dev/null @@ -1,64 +0,0 @@ -import BigNumber from 'bignumber.js'; - -import { toTokenSlug } from 'lib/assets'; -import type { Activity } from 'lib/temple/activity-new'; - -import { OperStackItemInterface, OperStackItemTypeEnum } from './types'; - -export function buildOperStack(activity: Activity, address: string) { - const opStack: OperStackItemInterface[] = []; - - for (const oper of activity.operations) { - if (oper.type === 'transaction') { - if (isZero(oper.amountSigned)) { - opStack.push({ - type: OperStackItemTypeEnum.Interaction, - with: oper.destination.address, - entrypoint: oper.entrypoint - }); - } else if (oper.from.address === address) { - opStack.push({ - type: OperStackItemTypeEnum.TransferTo, - to: oper.to?.address ?? '' - }); - } else if (oper.to?.address === address) { - opStack.push({ - type: OperStackItemTypeEnum.TransferFrom, - from: oper.from.address - }); - } - } else if (oper.type === 'delegation' && oper.source.address === address && oper.destination) { - opStack.push({ - type: OperStackItemTypeEnum.Delegation, - to: oper.destination.address - }); - } else { - opStack.push({ - type: OperStackItemTypeEnum.Other, - name: oper.type - }); - } - } - - return opStack.sort((a, b) => a.type - b.type); -} - -interface MoneyDiff { - assetSlug: string; - diff: string; -} - -export function buildMoneyDiffs(activity: Activity) { - const diffs: MoneyDiff[] = []; - - for (const oper of activity.operations) { - if (oper.type !== 'transaction' || isZero(oper.amountSigned)) continue; - const assetSlug = oper.contractAddress == null ? 'tez' : toTokenSlug(oper.contractAddress, oper.tokenId); - const diff = new BigNumber(oper.amountSigned).toFixed(); - diffs.push({ assetSlug, diff }); - } - - return diffs; -} - -const isZero = (val: BigNumber.Value) => new BigNumber(val).isZero(); diff --git a/src/lib/temple/activity-new/hook.ts b/src/lib/temple/activity-new/hook.ts index 18c3ed927c..006130815f 100644 --- a/src/lib/temple/activity-new/hook.ts +++ b/src/lib/temple/activity-new/hook.ts @@ -2,8 +2,9 @@ import { isKnownChainId } from 'lib/apis/tzkt/api'; import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; import { TezosNetworkEssentials } from 'temple/networks'; -import fetchActivities from './fetch'; -import type { Activity } from './types'; +import fetchTezosOperationsGroups from './fetch'; +import type { TezosPreActivity } from './types'; +import { preparseTezosOperationsGroup } from './utils'; type TLoading = 'init' | 'more' | false; @@ -16,12 +17,12 @@ export default function useTezosActivities( const { chainId, rpcBaseURL } = network; const [loading, setLoading] = useSafeState(isKnownChainId(chainId) && 'init'); - const [activities, setActivities] = useSafeState([]); + const [activities, setActivities] = useSafeState([]); const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); const { stop: stopLoading, stopAndBuildChecker } = useStopper(); - async function loadActivities(pseudoLimit: number, activities: Activity[], shouldStop: () => boolean) { + async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], shouldStop: () => boolean) { if (!isKnownChainId(chainId)) { setLoading(false); setReachedTheEnd(true); @@ -31,9 +32,19 @@ export default function useTezosActivities( setLoading(activities.length ? 'more' : 'init'); const lastActivity = activities[activities.length - 1]; - let newActivities: Activity[]; + let newActivities: TezosPreActivity[]; try { - newActivities = await fetchActivities(chainId, rpcBaseURL, accountAddress, assetSlug, pseudoLimit, lastActivity); + const groups = await fetchTezosOperationsGroups( + chainId, + rpcBaseURL, + accountAddress, + assetSlug, + pseudoLimit, + lastActivity + ); + + newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress)); + if (shouldStop()) return; } catch (error) { if (shouldStop()) return; diff --git a/src/lib/temple/activity-new/index.ts b/src/lib/temple/activity-new/index.ts index c447b74b90..64b0f17818 100644 --- a/src/lib/temple/activity-new/index.ts +++ b/src/lib/temple/activity-new/index.ts @@ -1,3 +1 @@ -export type { Activity, ActivityOperation } from './types'; - -export { buildOperStack, buildMoneyDiffs } from './helpers'; +export type { TezosPreActivity, TezosPreActivityOperation } from './types'; diff --git a/src/lib/temple/activity-new/types.ts b/src/lib/temple/activity-new/types.ts index fba1ab51c0..cbe39a815a 100644 --- a/src/lib/temple/activity-new/types.ts +++ b/src/lib/temple/activity-new/types.ts @@ -5,94 +5,42 @@ export interface OperationsGroup { operations: TzktOperation[]; } -export type ActivityStatus = TzktOperation['status'] | 'pending'; +export type TezosPreActivityStatus = TzktOperation['status'] | 'pending'; -export type ActivityMember = TzktAlias; +export type OperationMember = TzktAlias; -export interface Activity { +export interface TezosPreActivity { hash: string; /** ISO string */ addedAt: string; - status: ActivityStatus; + status: TezosPreActivityStatus; oldestTzktOperation: TzktOperation; /** Sorted new-to-old */ - operations: ActivityOperation[]; + operations: TezosPreActivityOperation[]; } type PickedPropsFromTzktOperation = Pick; -export interface ActivityOperationBase extends PickedPropsFromTzktOperation { +export interface TezosPreActivityOperationBase extends PickedPropsFromTzktOperation { contractAddress?: string; - status: ActivityStatus; + status: TezosPreActivityStatus; amountSigned: string; addedAt: string; } -export interface ActivityTransactionOperation extends ActivityOperationBase { +export interface TezosPreActivityTransactionOperation extends TezosPreActivityOperationBase { type: 'transaction'; - from: ActivityMember; - to?: ActivityMember; - destination: ActivityMember; + from: OperationMember; + to?: OperationMember; + destination: OperationMember; entrypoint?: string; tokenId?: string; } -export interface ActivityOtherOperation extends ActivityOperationBase { +export interface TezosPreActivityOtherOperation extends TezosPreActivityOperationBase { type: Exclude; - source: ActivityMember; - destination?: ActivityMember; + source: OperationMember; + destination?: OperationMember; } -export type ActivityOperation = ActivityTransactionOperation | ActivityOtherOperation; - -export enum OperStackItemTypeEnum { - TransferTo, - TransferFrom, - Delegation, - Interaction, - Origination, - Other -} - -export type OperStackItemInterface = - | TransferFromItem - | TransferToItem - | DelegationItem - | InteractionItem - | OriginationItem - | OtherItem; - -interface OperStackItemBase { - type: OperStackItemTypeEnum; -} - -interface TransferFromItem extends OperStackItemBase { - type: OperStackItemTypeEnum.TransferFrom; - from: string; -} - -interface TransferToItem extends OperStackItemBase { - type: OperStackItemTypeEnum.TransferTo; - to: string; -} - -interface DelegationItem extends OperStackItemBase { - type: OperStackItemTypeEnum.Delegation; - to: string; -} - -interface InteractionItem extends OperStackItemBase { - type: OperStackItemTypeEnum.Interaction; - with: string; - entrypoint?: string; -} - -interface OriginationItem extends OperStackItemBase { - type: OperStackItemTypeEnum.Origination; - contract?: string; -} - -interface OtherItem extends OperStackItemBase { - type: OperStackItemTypeEnum.Other; - name: TzktOperationType; -} +export type TezosPreActivityOperation = TezosPreActivityTransactionOperation | TezosPreActivityOtherOperation; diff --git a/src/lib/temple/activity-new/utils.ts b/src/lib/temple/activity-new/utils.ts index 80592c61bf..93498a52b1 100644 --- a/src/lib/temple/activity-new/utils.ts +++ b/src/lib/temple/activity-new/utils.ts @@ -11,16 +11,16 @@ import { ZERO } from 'lib/utils/numbers'; import type { OperationsGroup, - ActivityStatus, - Activity, - ActivityOperationBase, - ActivityTransactionOperation, - ActivityOtherOperation, - ActivityOperation, - ActivityMember + TezosPreActivityStatus, + TezosPreActivity, + TezosPreActivityOperationBase, + TezosPreActivityTransactionOperation, + TezosPreActivityOtherOperation, + TezosPreActivityOperation, + OperationMember } from './types'; -export function operationsGroupToActivity({ hash, operations }: OperationsGroup, address: string): Activity { +export function preparseTezosOperationsGroup({ hash, operations }: OperationsGroup, address: string): TezosPreActivity { const firstOperation = operations[0]!; const oldestTzktOperation = operations[operations.length - 1]!; const addedAt = firstOperation.timestamp; @@ -36,7 +36,7 @@ export function operationsGroupToActivity({ hash, operations }: OperationsGroup, }; } -function reduceTzktOperations(operations: TzktOperation[], address: string): ActivityOperation[] { +function reduceTzktOperations(operations: TzktOperation[], address: string): TezosPreActivityOperation[] { const reducedOperations = operations.map(op => reduceOneTzktOperation(op, address)).filter(isTruthy); return reducedOperations; @@ -45,7 +45,7 @@ function reduceTzktOperations(operations: TzktOperation[], address: string): Act /** * (i) Does not mutate operation object */ -function reduceOneTzktOperation(operation: TzktOperation, address: string): ActivityOperation | null { +function reduceOneTzktOperation(operation: TzktOperation, address: string): TezosPreActivityOperation | null { switch (operation.type) { case 'transaction': return reduceOneTzktTransactionOperation(address, operation); @@ -53,7 +53,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti if (operation.sender.address !== address) return null; const activityOperBase = buildActivityOperBase(operation, '0', operation.sender.address === address); - const activityOper: ActivityOtherOperation = { + const activityOper: TezosPreActivityOtherOperation = { ...activityOperBase, source: operation.sender, type: 'delegation' @@ -64,7 +64,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti case 'origination': { const amount = operation.contractBalance ? operation.contractBalance.toString() : '0'; const activityOperBase = buildActivityOperBase(operation, amount, operation.sender.address === address); - const activityOper: ActivityOtherOperation = { + const activityOper: TezosPreActivityOtherOperation = { ...activityOperBase, source: operation.sender, type: 'origination' @@ -80,17 +80,17 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Acti function reduceOneTzktTransactionOperation( address: string, operation: TzktTransactionOperation -): ActivityTransactionOperation | null { +): TezosPreActivityTransactionOperation | null { function _buildReturn(args: { amount: string; - from: ActivityMember; - to?: ActivityMember; + from: OperationMember; + to?: OperationMember; contractAddress?: string; tokenId?: string; }) { const { amount, from, to, contractAddress, tokenId } = args; const activityOperBase = buildActivityOperBase(operation, amount, from.address === address); - const activityOper: ActivityTransactionOperation = { + const activityOper: TezosPreActivityTransactionOperation = { ...activityOperBase, type: 'transaction', destination: operation.target, @@ -159,7 +159,7 @@ function reduceOneTzktTransactionOperation( function buildActivityOperBase(operation: TzktOperation, amount: string, from: boolean) { const { id, level, timestamp: addedAt } = operation; - const reducedOperation: ActivityOperationBase = { + const reducedOperation: TezosPreActivityOperationBase = { id, level, amountSigned: from ? `-${amount}` : amount, @@ -221,13 +221,13 @@ function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: str return result; } -function stringToActivityStatus(status: string): ActivityStatus { - if (['applied', 'backtracked', 'skipped', 'failed'].includes(status)) return status as ActivityStatus; +function stringToActivityStatus(status: string): TezosPreActivityStatus { + if (['applied', 'backtracked', 'skipped', 'failed'].includes(status)) return status as TezosPreActivityStatus; return 'pending'; } -function deriveActivityStatus(items: { status: ActivityStatus }[]): ActivityStatus { +function deriveActivityStatus(items: { status: TezosPreActivityStatus }[]): TezosPreActivityStatus { if (items.find(o => o.status === 'pending')) return 'pending'; if (items.find(o => o.status === 'applied')) return 'applied'; if (items.find(o => o.status === 'backtracked')) return 'backtracked'; From 4b033069aa3e0ac8b38fb64018089cf333f04bfe Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Sep 2024 01:10:20 +0300 Subject: [PATCH 16/74] TW-1479: [EVM] Transactions history. [Tezos] -- lib/temple/activity --- TODO.md | 2 + src/app/templates/activity/Activity.tsx | 4 +- .../activity/TezosActivityOperation.tsx | 2 +- .../templates/activity/TezosActivityTab.tsx | 94 ++++++++++++++++++- .../activity-new => activity/tezos}/fetch.ts | 4 +- src/lib/activity/{tezos.ts => tezos/index.ts} | 6 +- .../utils.ts => activity/tezos/pre-parse.ts} | 12 ++- .../activity-new => activity/tezos}/types.ts | 8 +- src/lib/activity/types.ts | 5 + src/lib/temple/activity-new/hook.ts | 88 ----------------- src/lib/temple/activity-new/index.ts | 1 - 11 files changed, 117 insertions(+), 109 deletions(-) rename src/lib/{temple/activity-new => activity/tezos}/fetch.ts (98%) rename src/lib/activity/{tezos.ts => tezos/index.ts} (96%) rename src/lib/{temple/activity-new/utils.ts => activity/tezos/pre-parse.ts} (96%) rename src/lib/{temple/activity-new => activity/tezos}/types.ts (87%) delete mode 100644 src/lib/temple/activity-new/hook.ts delete mode 100644 src/lib/temple/activity-new/index.ts diff --git a/TODO.md b/TODO.md index bfe9d0a187..be050e7ad2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,5 @@ +- Remove `lib/temple/activity` + - Prepare activities from TZKT in one parse - Approve amount - NFTs diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index ffe55247dd..cd5fd33370 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -5,8 +5,8 @@ import clsx from 'clsx'; import { IconBase } from 'app/atoms'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { Activity } from 'lib/activity'; +import { TezosPreActivity } from 'lib/activity/tezos/types'; import { t } from 'lib/i18n'; -import { TezosPreActivity } from 'lib/temple/activity-new'; import { useBooleanState } from 'lib/ui/hooks'; import { useExplorerHref } from 'temple/front/block-explorers'; import { EvmChain, TezosChain } from 'temple/front/chains'; @@ -32,8 +32,6 @@ export const TezosActivityComponent = memo(({ activ const operations = activity.operations; - if (hash === 'oorAovoprUsZqr1HHYZ3RuLDitR8PqzM2t8GEzGmCGboKSFDMNX') console.log('O:', operations); - return (
{operations.slice(0, 3).map((operation, i) => ( diff --git a/src/app/templates/activity/TezosActivityOperation.tsx b/src/app/templates/activity/TezosActivityOperation.tsx index c891b87d69..bb488fbbd0 100644 --- a/src/app/templates/activity/TezosActivityOperation.tsx +++ b/src/app/templates/activity/TezosActivityOperation.tsx @@ -1,10 +1,10 @@ import React, { memo, useMemo } from 'react'; import { TezosOperation, parseTezosPreActivityOperation } from 'lib/activity'; +import { TezosPreActivityOperation } from 'lib/activity/tezos/types'; import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { toTezosAssetSlug } from 'lib/assets/utils'; import { useTezosAssetMetadata } from 'lib/metadata'; -import { TezosPreActivityOperation } from 'lib/temple/activity-new/types'; import { TezosChain } from 'temple/front'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; diff --git a/src/app/templates/activity/TezosActivityTab.tsx b/src/app/templates/activity/TezosActivityTab.tsx index 0b7b927fa5..f57a7736ca 100644 --- a/src/app/templates/activity/TezosActivityTab.tsx +++ b/src/app/templates/activity/TezosActivityTab.tsx @@ -4,8 +4,13 @@ import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import useTezosActivities from 'lib/temple/activity-new/hook'; +import { preparseTezosOperationsGroup } from 'lib/activity/tezos'; +import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; +import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { isKnownChainId } from 'lib/apis/tzkt/api'; +import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; +import { TezosNetworkEssentials } from 'temple/networks'; import { TezosActivityComponent } from './Activity'; @@ -26,20 +31,20 @@ export const TezosActivityTab = memo(({ tezosChainId, ass useLoadPartnersPromo(); const { - loading, + isLoading, reachedTheEnd, list: activities, loadMore } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); - if (activities.length === 0 && !loading && reachedTheEnd) { + if (activities.length === 0 && !isLoading && reachedTheEnd) { return ; } return ( loadMore(INITIAL_NUMBER)} loadMore={() => loadMore(LOAD_STEP)} @@ -55,3 +60,84 @@ export const TezosActivityTab = memo(({ tezosChainId, ass ); }); + +type TLoading = 'init' | 'more' | false; + +function useTezosActivities( + network: TezosNetworkEssentials, + accountAddress: string, + initialPseudoLimit: number, + assetSlug?: string +) { + const { chainId, rpcBaseURL } = network; + + const [isLoading, setIsLoading] = useSafeState(isKnownChainId(chainId) && 'init'); + const [activities, setActivities] = useSafeState([]); + const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); + + const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + + async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], shouldStop: () => boolean) { + if (!isKnownChainId(chainId)) { + setIsLoading(false); + setReachedTheEnd(true); + return; + } + + setIsLoading(activities.length ? 'more' : 'init'); + const lastActivity = activities[activities.length - 1]; + + let newActivities: TezosPreActivity[]; + try { + const groups = await fetchTezosOperationsGroups( + chainId, + rpcBaseURL, + accountAddress, + assetSlug, + pseudoLimit, + lastActivity + ); + + newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress)); + + if (shouldStop()) return; + } catch (error) { + if (shouldStop()) return; + setIsLoading(false); + console.error(error); + + return; + } + + setActivities(activities.concat(newActivities)); + setIsLoading(false); + if (newActivities.length === 0) setReachedTheEnd(true); + } + + /** Loads more of older items */ + function loadMore(pseudoLimit: number) { + if (isLoading || reachedTheEnd) return; + loadActivities(pseudoLimit, activities, stopAndBuildChecker()); + } + + useDidMount(() => { + loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); + + return stopLoading; + }); + + useDidUpdate(() => { + setActivities([]); + setIsLoading('init'); + setReachedTheEnd(false); + + loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); + }, [chainId, accountAddress, assetSlug]); + + return { + isLoading, + reachedTheEnd, + list: activities, + loadMore + }; +} diff --git a/src/lib/temple/activity-new/fetch.ts b/src/lib/activity/tezos/fetch.ts similarity index 98% rename from src/lib/temple/activity-new/fetch.ts rename to src/lib/activity/tezos/fetch.ts index b671c59e4c..017b27f18d 100644 --- a/src/lib/temple/activity-new/fetch.ts +++ b/src/lib/activity/tezos/fetch.ts @@ -5,7 +5,7 @@ import { detectTokenStandard } from 'lib/assets/standards'; import { filterUnique } from 'lib/utils'; import { getReadOnlyTezos } from 'temple/tezos'; -import type { OperationsGroup } from './types'; +import type { TempleTzktOperationsGroup } from './types'; const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; @@ -221,7 +221,7 @@ async function fetchOperGroupsForOperations( if (olderThan && uniqueHashes[0] === olderThan.hash) uniqueHashes.splice(1); - const groups: OperationsGroup[] = []; + const groups: TempleTzktOperationsGroup[] = []; for (const hash of uniqueHashes) { const operations = await TZKT.refetchOnce429(() => TZKT.fetchGetOperationsByHash(chainId, hash), 1000); operations.sort((b, a) => a.id - b.id); diff --git a/src/lib/activity/tezos.ts b/src/lib/activity/tezos/index.ts similarity index 96% rename from src/lib/activity/tezos.ts rename to src/lib/activity/tezos/index.ts index cdde0dc45b..259d92f771 100644 --- a/src/lib/activity/tezos.ts +++ b/src/lib/activity/tezos/index.ts @@ -1,9 +1,11 @@ +import { TezosPreActivity, TezosPreActivityOperation } from 'lib/activity/tezos/types'; import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { AssetMetadataBase, getAssetSymbol, isTezosCollectibleMetadata } from 'lib/metadata'; -import { TezosPreActivity, TezosPreActivityOperation } from 'lib/temple/activity-new'; import { TempleChainKind } from 'temple/types'; -import { TezosActivity, ActivityKindEnum, TezosOperation, TezosActivityAsset } from './types'; +import { TezosActivity, ActivityKindEnum, TezosOperation, TezosActivityAsset } from '../types'; + +export { preparseTezosOperationsGroup } from './pre-parse'; export function formatLegacyTezosActivity( _activity: TezosPreActivity, diff --git a/src/lib/temple/activity-new/utils.ts b/src/lib/activity/tezos/pre-parse.ts similarity index 96% rename from src/lib/temple/activity-new/utils.ts rename to src/lib/activity/tezos/pre-parse.ts index 93498a52b1..40c0536cc4 100644 --- a/src/lib/temple/activity-new/utils.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -9,18 +9,22 @@ import { import { isTruthy } from 'lib/utils'; import { ZERO } from 'lib/utils/numbers'; +import { OperationMember } from '../types'; + +import { TempleTzktOperationsGroup } from './types'; import type { - OperationsGroup, TezosPreActivityStatus, TezosPreActivity, TezosPreActivityOperationBase, TezosPreActivityTransactionOperation, TezosPreActivityOtherOperation, - TezosPreActivityOperation, - OperationMember + TezosPreActivityOperation } from './types'; -export function preparseTezosOperationsGroup({ hash, operations }: OperationsGroup, address: string): TezosPreActivity { +export function preparseTezosOperationsGroup( + { hash, operations }: TempleTzktOperationsGroup, + address: string +): TezosPreActivity { const firstOperation = operations[0]!; const oldestTzktOperation = operations[operations.length - 1]!; const addedAt = firstOperation.timestamp; diff --git a/src/lib/temple/activity-new/types.ts b/src/lib/activity/tezos/types.ts similarity index 87% rename from src/lib/temple/activity-new/types.ts rename to src/lib/activity/tezos/types.ts index cbe39a815a..54b1de8765 100644 --- a/src/lib/temple/activity-new/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -1,14 +1,14 @@ -import { TzktOperation, TzktAlias, TzktOperationType } from 'lib/apis/tzkt'; +import type { TzktOperation, TzktOperationType } from 'lib/apis/tzkt'; -export interface OperationsGroup { +import type { OperationMember } from '../types'; + +export interface TempleTzktOperationsGroup { hash: string; operations: TzktOperation[]; } export type TezosPreActivityStatus = TzktOperation['status'] | 'pending'; -export type OperationMember = TzktAlias; - export interface TezosPreActivity { hash: string; /** ISO string */ diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index b334c2560b..51ab9bdece 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -56,4 +56,9 @@ export interface EvmActivityAsset { iconURL?: string; } +export interface OperationMember { + address: string; + alias?: string; +} + export const InfinitySymbol = Symbol('Infinity'); diff --git a/src/lib/temple/activity-new/hook.ts b/src/lib/temple/activity-new/hook.ts deleted file mode 100644 index 006130815f..0000000000 --- a/src/lib/temple/activity-new/hook.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { isKnownChainId } from 'lib/apis/tzkt/api'; -import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; -import { TezosNetworkEssentials } from 'temple/networks'; - -import fetchTezosOperationsGroups from './fetch'; -import type { TezosPreActivity } from './types'; -import { preparseTezosOperationsGroup } from './utils'; - -type TLoading = 'init' | 'more' | false; - -export default function useTezosActivities( - network: TezosNetworkEssentials, - accountAddress: string, - initialPseudoLimit: number, - assetSlug?: string -) { - const { chainId, rpcBaseURL } = network; - - const [loading, setLoading] = useSafeState(isKnownChainId(chainId) && 'init'); - const [activities, setActivities] = useSafeState([]); - const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - - const { stop: stopLoading, stopAndBuildChecker } = useStopper(); - - async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], shouldStop: () => boolean) { - if (!isKnownChainId(chainId)) { - setLoading(false); - setReachedTheEnd(true); - return; - } - - setLoading(activities.length ? 'more' : 'init'); - const lastActivity = activities[activities.length - 1]; - - let newActivities: TezosPreActivity[]; - try { - const groups = await fetchTezosOperationsGroups( - chainId, - rpcBaseURL, - accountAddress, - assetSlug, - pseudoLimit, - lastActivity - ); - - newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress)); - - if (shouldStop()) return; - } catch (error) { - if (shouldStop()) return; - setLoading(false); - console.error(error); - - return; - } - - setActivities(activities.concat(newActivities)); - setLoading(false); - if (newActivities.length === 0) setReachedTheEnd(true); - } - - /** Loads more of older items */ - function loadMore(pseudoLimit: number) { - if (loading || reachedTheEnd) return; - loadActivities(pseudoLimit, activities, stopAndBuildChecker()); - } - - useDidMount(() => { - loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); - - return stopLoading; - }); - - useDidUpdate(() => { - setActivities([]); - setLoading('init'); - setReachedTheEnd(false); - - loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); - }, [chainId, accountAddress, assetSlug]); - - return { - loading, - reachedTheEnd, - list: activities, - loadMore - }; -} diff --git a/src/lib/temple/activity-new/index.ts b/src/lib/temple/activity-new/index.ts deleted file mode 100644 index 64b0f17818..0000000000 --- a/src/lib/temple/activity-new/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { TezosPreActivity, TezosPreActivityOperation } from './types'; From 6be2e1d01e5425c035d92958e35b8e577c5dba8d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 22 Sep 2024 17:03:12 +0300 Subject: [PATCH 17/74] TW-1479: [EVM] Transactions history. [Tezos] + Approve --- .../activity/ActivityOperationBase.tsx | 2 +- src/lib/activity/tezos/index.ts | 8 +- src/lib/activity/tezos/pre-parse.ts | 64 +++++++++---- src/lib/activity/tezos/types.ts | 1 + src/lib/apis/tzkt/utils.ts | 93 ++++++++++++++----- 5 files changed, 129 insertions(+), 39 deletions(-) diff --git a/src/app/templates/activity/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityOperationBase.tsx index 16779208da..79cb2ed140 100644 --- a/src/app/templates/activity/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityOperationBase.tsx @@ -61,7 +61,7 @@ export const ActivityOperationBaseComponent: FC = ({ '∞ ' ) : ( <> - {asset.amount.startsWith('-') ? null : '+'} + {kind === ActivityKindEnum.approve || asset.amount.startsWith('-') ? null : '+'} {atomsToTokens(asset.amount, asset.decimals)}{' '} ) diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 259d92f771..e64d32f79d 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -33,6 +33,8 @@ export function parseTezosPreActivityOperation( if (preOperation.type === 'transaction') { tokenId = preOperation.tokenId; + if (preOperation.subtype === 'approve') return { kind: ActivityKindEnum.approve }; + if (isZero(preOperation.amountSigned)) return { kind: ActivityKindEnum.interaction @@ -79,7 +81,11 @@ export function parseTezosPreActivityOperation( if (!assetMetadata) return operationBase; - if (operationBase.kind === ActivityKindEnum.send || operationBase.kind === ActivityKindEnum.receive) { + if ( + operationBase.kind === ActivityKindEnum.send || + operationBase.kind === ActivityKindEnum.receive || + operationBase.kind === ActivityKindEnum.approve + ) { const asset: TezosActivityAsset = { contract: preOperation.contractAddress ?? TEZ_TOKEN_SLUG, tokenId, diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index 40c0536cc4..53bcc406b4 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -2,9 +2,10 @@ import { TzktOperation, TzktTransactionOperation } from 'lib/apis/tzkt'; import { isTzktOperParam, isTzktOperParam_Fa12, - isTzktOperParam_Fa2, + isTzktOperParam_Fa2_approve, + isTzktOperParam_Fa2_transfer, isTzktOperParam_LiquidityBaking, - ParameterFa2 + ParameterFa2Transfer } from 'lib/apis/tzkt/utils'; import { isTruthy } from 'lib/utils'; import { ZERO } from 'lib/utils/numbers'; @@ -91,12 +92,20 @@ function reduceOneTzktTransactionOperation( to?: OperationMember; contractAddress?: string; tokenId?: string; + subtype?: TezosPreActivityTransactionOperation['subtype']; }) { - const { amount, from, to, contractAddress, tokenId } = args; - const activityOperBase = buildActivityOperBase(operation, amount, from.address === address); + const { amount, from, to, contractAddress, tokenId, subtype } = args; + + const activityOperBase = buildActivityOperBase( + operation, + amount, + subtype === 'approve' ? false : from.address === address + ); + const activityOper: TezosPreActivityTransactionOperation = { ...activityOperBase, type: 'transaction', + subtype, destination: operation.target, from, to @@ -119,8 +128,8 @@ function reduceOneTzktTransactionOperation( const amount = String(operation.amount); return _buildReturn({ amount, from, to }); - } else if (isTzktOperParam_Fa2(parameter)) { - const values = reduceParameterFa2Values(parameter.value, address); + } else if (isTzktOperParam_Fa2_transfer(parameter)) { + const values = reduceParameterFa2TransferValues(parameter.value, address); const firstVal = values[0]; // (!) Here we abandon other but 1st non-zero-amount values if (firstVal == null) return null; @@ -131,18 +140,41 @@ function reduceOneTzktTransactionOperation( const from = { ...operation.sender, address: firstVal.fromAddress }; const to = firstVal.isToRelAddress ? { address } : undefined; - return _buildReturn({ amount, from, to, contractAddress, tokenId }); - } else if (isTzktOperParam_Fa12(parameter)) { - if (parameter.entrypoint === 'approve') return null; // TODO: Implement - - const from = { ...operation.sender }; - if (parameter.value.from === address) from.address = address; - else if (parameter.value.to === address) from.address = parameter.value.from; - else return null; + return _buildReturn({ amount, from, to, contractAddress, tokenId, subtype: 'transfer' }); + } else if (isTzktOperParam_Fa2_approve(parameter)) { + const add_operator = parameter.value[0].add_operator; + const from = operation.sender; + const to = { address: add_operator.operator }; + const amount = String(operation.amount); const contractAddress = operation.target.address; + const tokenId = add_operator.token_id; + + return _buildReturn({ + amount, + from, + to, + subtype: 'approve', + contractAddress, + tokenId + }); + } else if (isTzktOperParam_Fa12(parameter)) { const amount = parameter.value.value; - const to = operation.target; + const contractAddress = operation.target.address; + + if (parameter.entrypoint === 'approve') { + if (amount === '0') return null; + + const from = operation.sender; + const to = { address: parameter.value.spender }; + + return _buildReturn({ amount, from, to, contractAddress, subtype: 'approve' }); + } + + const from = { ...operation.sender, address: parameter.value.from }; + const to = { address: parameter.value.to }; + + if (from.address !== address && to.address !== address) return null; return _buildReturn({ amount, from, to, contractAddress }); } else if (isTzktOperParam_LiquidityBaking(parameter)) { @@ -184,7 +216,7 @@ interface ReducedParameterFa2Values { /** * Items with zero cumulative amount value are filtered out */ -function reduceParameterFa2Values(values: ParameterFa2['value'], relAddress: string) { +function reduceParameterFa2TransferValues(values: ParameterFa2Transfer['value'], relAddress: string) { const result: ReducedParameterFa2Values[] = []; for (const val of values) { diff --git a/src/lib/activity/tezos/types.ts b/src/lib/activity/tezos/types.ts index 54b1de8765..b5eb394120 100644 --- a/src/lib/activity/tezos/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -30,6 +30,7 @@ export interface TezosPreActivityOperationBase extends PickedPropsFromTzktOperat export interface TezosPreActivityTransactionOperation extends TezosPreActivityOperationBase { type: 'transaction'; + subtype?: 'transfer' | 'approve'; from: OperationMember; to?: OperationMember; destination: OperationMember; diff --git a/src/lib/apis/tzkt/utils.ts b/src/lib/apis/tzkt/utils.ts index 56558942ab..d4ad5f6d29 100644 --- a/src/lib/apis/tzkt/utils.ts +++ b/src/lib/apis/tzkt/utils.ts @@ -3,16 +3,22 @@ import { TzktAccount } from './types'; export const calcTzktAccountSpendableTezBalance = ({ balance, stakedBalance, unstakedBalance }: TzktAccount) => ((balance ?? 0) - (stakedBalance ?? 0) - (unstakedBalance ?? 0)).toFixed(); -interface ParameterFa12 { - entrypoint: string; - value: ParameterFa12Value; -} - -interface ParameterFa12Value { - to: string; - from: string; - value: string; -} +type ParameterFa12 = + | { + entrypoint: 'transfer'; + value: { + to: string; + from: string; + value: string; + }; + } + | { + entrypoint: 'approve'; + value: { + spender: string; + value: string; + }; + }; interface Fa2Transaction { to_: string; @@ -20,15 +26,30 @@ interface Fa2Transaction { token_id: string; } -interface Fa2OpParams { - txs: Fa2Transaction[]; - from_: string; +interface ParameterFa2 { + entrypoint: string; + value: any[]; } -export interface ParameterFa2 { +export interface ParameterFa2Transfer extends ParameterFa2 { entrypoint: string; - value: Fa2OpParams[]; + value: { + txs: Fa2Transaction[]; + from_: string; + }[]; } + +export interface ParameterFa2Approve extends ParameterFa2 { + entrypoint: 'update_operators'; + value: { + add_operator: { + operator: string; + owner: string; + token_id: string; + }; + }[]; +} + interface ParameterLiquidityBaking { entrypoint: string; value: { @@ -49,9 +70,24 @@ export function isTzktOperParam(param: any): param is { export function isTzktOperParam_Fa12(param: any): param is ParameterFa12 { if (!isTzktOperParam(param)) return false; if (param.value == null) return false; - if (typeof param.value.to !== 'string') return false; - if (typeof param.value.from !== 'string') return false; - if (typeof param.value.value !== 'string') return false; + + if (param.entrypoint === 'approve') { + if (typeof param.value.spender !== 'string') return false; + if (typeof param.value.value !== 'string') return false; + } else { + // 'transfer' + if (typeof param.value.to !== 'string') return false; + if (typeof param.value.from !== 'string') return false; + if (typeof param.value.value !== 'string') return false; + } + + return true; +} + +export function isTzktOperParam_Fa2(param: any): param is ParameterFa2 { + if (!isTzktOperParam(param)) return false; + if (!Array.isArray(param.value)) return false; + if (param.value[0] == null) return true; return true; } @@ -60,9 +96,24 @@ export function isTzktOperParam_Fa12(param: any): param is ParameterFa12 { * (!) Might only refer to `param.entrypoint === 'transfer'` case * (?) So, would this check be enough? */ -export function isTzktOperParam_Fa2(param: any): param is ParameterFa2 { - if (!isTzktOperParam(param)) return false; - if (!Array.isArray(param.value)) return false; +export function isTzktOperParam_Fa2_approve(param: any): param is ParameterFa2Approve { + if (!isTzktOperParam_Fa2(param)) return false; + const add_operator = param.value[0]?.add_operator; + if (add_operator == null) return false; + + if (typeof add_operator.operator !== 'string') return false; + if (typeof add_operator.owner !== 'string') return false; + if (typeof add_operator.token_id !== 'string') return false; + + return true; +} + +/** + * (!) Might only refer to `param.entrypoint === 'transfer'` case + * (?) So, would this check be enough? + */ +export function isTzktOperParam_Fa2_transfer(param: any): param is ParameterFa2Transfer { + if (!isTzktOperParam_Fa2(param)) return false; let item = param.value[0]; if (item == null) return true; if (typeof item.from_ !== 'string') return false; From 92346073cf8b8ea017c5d619d4521b32f0fd108f Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 22 Sep 2024 21:35:08 +0300 Subject: [PATCH 18/74] TW-1479: [EVM] Transactions history. + Kind 'Transfer' --- .../activity/ActivityOperationBase.tsx | 40 +++++++------ src/lib/activity/evm.ts | 56 +++++++++++++------ src/lib/activity/index.ts | 2 +- src/lib/activity/tezos/index.ts | 52 ++++++++--------- src/lib/activity/tezos/pre-parse.ts | 25 ++++++--- src/lib/activity/tezos/types.ts | 5 +- src/lib/activity/types.ts | 12 ++-- 7 files changed, 116 insertions(+), 76 deletions(-) diff --git a/src/app/templates/activity/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityOperationBase.tsx index 79cb2ed140..b4501b3876 100644 --- a/src/app/templates/activity/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityOperationBase.tsx @@ -8,7 +8,7 @@ import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; -import { ActivityKindEnum, InfinitySymbol } from 'lib/activity'; +import { ActivityOperKindEnum, InfinitySymbol } from 'lib/activity'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -16,7 +16,7 @@ import { EvmAssetIcon, TezosAssetIcon } from '../AssetIcon'; interface Props { chainId: string | number; - kind: ActivityKindEnum; + kind: ActivityOperKindEnum; hash: string; networkName: string; asset?: AssetProp; @@ -50,7 +50,11 @@ export const ActivityOperationBaseComponent: FC = ({ if (!asset) return {}; const amountForFiat = - typeof asset.amount === 'string' && (kind === ActivityKindEnum.receive || kind === ActivityKindEnum.send) + typeof asset.amount === 'string' && + (kind === ActivityOperKindEnum.transferTo_FromAccount || + kind === ActivityOperKindEnum.transferFrom_ToAccount || + kind === ActivityOperKindEnum.transferFrom || + kind === ActivityOperKindEnum.transferTo) ? atomsToTokens(asset.amount, asset.decimals) : null; @@ -61,7 +65,7 @@ export const ActivityOperationBaseComponent: FC = ({ '∞ ' ) : ( <> - {kind === ActivityKindEnum.approve || asset.amount.startsWith('-') ? null : '+'} + {kind === ActivityOperKindEnum.approve || asset.amount.startsWith('-') ? null : '+'} {atomsToTokens(asset.amount, asset.decimals)}{' '} ) @@ -153,18 +157,22 @@ export const ActivityOperationBaseComponent: FC = ({ ); }; -const ActivityKindTitle: Record = { - [ActivityKindEnum.interaction]: 'Interaction', - [ActivityKindEnum.send]: 'Send', - [ActivityKindEnum.receive]: 'Receive', - [ActivityKindEnum.swap]: 'Swap', - [ActivityKindEnum.approve]: 'Approve' +const ActivityKindTitle: Record = { + [ActivityOperKindEnum.interaction]: 'Interaction', + [ActivityOperKindEnum.transferFrom_ToAccount]: 'Send', + [ActivityOperKindEnum.transferTo_FromAccount]: 'Receive', + [ActivityOperKindEnum.transferFrom]: 'Transfer', + [ActivityOperKindEnum.transferTo]: 'Transfer', + [ActivityOperKindEnum.swap]: 'Swap', + [ActivityOperKindEnum.approve]: 'Approve' }; -const ActivityKindIconSvg: Record = { - [ActivityKindEnum.interaction]: DocumentsSvg, - [ActivityKindEnum.send]: SendSvg, - [ActivityKindEnum.receive]: IncomeSvg, - [ActivityKindEnum.swap]: SwapSvg, - [ActivityKindEnum.approve]: DocumentsSvg +const ActivityKindIconSvg: Record = { + [ActivityOperKindEnum.interaction]: DocumentsSvg, + [ActivityOperKindEnum.transferFrom_ToAccount]: SendSvg, + [ActivityOperKindEnum.transferTo_FromAccount]: IncomeSvg, + [ActivityOperKindEnum.transferFrom]: DocumentsSvg, + [ActivityOperKindEnum.transferTo]: DocumentsSvg, + [ActivityOperKindEnum.swap]: SwapSvg, + [ActivityOperKindEnum.approve]: DocumentsSvg }; diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index 0fc536dbac..5844abce35 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -5,7 +5,7 @@ import { EvmAssetMetadataGetter, getAssetSymbol } from 'lib/metadata'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from './types'; +import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from './types'; export function parseGoldRushTransaction( item: GoldRushTransaction, @@ -16,7 +16,7 @@ export function parseGoldRushTransaction( const logEvents = item.log_events ?? []; const operations = logEvents.map(logEvent => { - if (!logEvent.decoded?.params) return { kind: ActivityKindEnum.interaction }; + if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; const contractAddress = getEvmAddressSafe(logEvent.sender_address); const fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); @@ -26,13 +26,22 @@ export function parseGoldRushTransaction( if (logEvent.decoded.name === 'Transfer') { const kind = (() => { - if (fromAddress === accountAddress) return ActivityKindEnum.send; - if (toAddress === accountAddress) return ActivityKindEnum.receive; - - return ActivityKindEnum.interaction; + if (toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + if (fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + } + + return ActivityOperKindEnum.interaction; })(); - if (kind === ActivityKindEnum.interaction || !contractAddress) return { kind }; + if (kind === ActivityOperKindEnum.interaction || !contractAddress) return { kind }; const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; const nft = logEvent.decoded.params[2]?.indexed ?? false; @@ -51,7 +60,7 @@ export function parseGoldRushTransaction( const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, + amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${amount}` : amount, decimals, symbol, nft, @@ -62,7 +71,7 @@ export function parseGoldRushTransaction( } if (logEvent.decoded.name === 'Approval' && fromAddress === accountAddress) { - const kind = ActivityKindEnum.approve; + const kind = ActivityOperKindEnum.approve; if (!contractAddress) return { kind }; @@ -99,7 +108,7 @@ export function parseGoldRushTransaction( logEvent.decoded.params[2]?.value === true && fromAddress === accountAddress ) { - const kind = ActivityKindEnum.approve; + const kind = ActivityOperKindEnum.approve; if (!contractAddress || decimals == null) return { kind }; @@ -115,7 +124,7 @@ export function parseGoldRushTransaction( return { kind, asset }; } - return { kind: ActivityKindEnum.interaction }; + return { kind: ActivityOperKindEnum.interaction }; }); const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); @@ -139,7 +148,17 @@ export function parseGoldRushERC20Transfer( ): EvmActivity { const operations = item.transfers?.map(transfer => { - const kind = transfer.transfer_type === 'IN' ? ActivityKindEnum.receive : ActivityKindEnum.send; + const kind = (() => { + if (transfer.transfer_type === 'IN') { + return item.to_address === transfer.contract_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + return item.to_address === transfer.contract_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + })(); const contractAddress = getEvmAddressSafe(transfer.contract_address); @@ -150,7 +169,7 @@ export function parseGoldRushERC20Transfer( const decimals = metadata?.decimals ?? transfer.contract_decimals; - if (decimals == null) return { kind: ActivityKindEnum.interaction }; + if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; const nft = false; const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; @@ -158,7 +177,10 @@ export function parseGoldRushERC20Transfer( const asset: EvmActivityAsset = { contract: contractAddress, - amount: kind === ActivityKindEnum.send ? `-${amount}` : amount, + amount: + kind === ActivityOperKindEnum.transferFrom_ToAccount || kind === ActivityOperKindEnum.transferFrom + ? `-${amount}` + : amount, decimals, symbol, nft, @@ -191,8 +213,8 @@ function parseGasTransfer( if (value === '0') return null; const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityKindEnum.send; - if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityKindEnum.receive; + if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityOperKindEnum.transferFrom_ToAccount; + if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityOperKindEnum.transferTo_FromAccount; return null; })(); @@ -208,7 +230,7 @@ function parseGasTransfer( const asset: EvmActivityAsset = { contract: EVM_TOKEN_SLUG, - amount: kind === ActivityKindEnum.send ? `-${value}` : value, + amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value, decimals, symbol }; diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 7170a98a9c..4bd8838d69 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,6 +1,6 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivityAsset, EvmOperation } from './types'; -export { ActivityKindEnum, InfinitySymbol } from './types'; +export { ActivityOperKindEnum, InfinitySymbol } from './types'; export { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './evm'; diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index e64d32f79d..b210667d23 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -1,9 +1,10 @@ import { TezosPreActivity, TezosPreActivityOperation } from 'lib/activity/tezos/types'; import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { AssetMetadataBase, getAssetSymbol, isTezosCollectibleMetadata } from 'lib/metadata'; +import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; -import { TezosActivity, ActivityKindEnum, TezosOperation, TezosActivityAsset } from '../types'; +import { TezosActivity, ActivityOperKindEnum, TezosOperation, TezosActivityAsset } from '../types'; export { preparseTezosOperationsGroup } from './pre-parse'; @@ -33,48 +34,45 @@ export function parseTezosPreActivityOperation( if (preOperation.type === 'transaction') { tokenId = preOperation.tokenId; - if (preOperation.subtype === 'approve') return { kind: ActivityKindEnum.approve }; + if (preOperation.subtype === 'approve') return { kind: ActivityOperKindEnum.approve }; if (isZero(preOperation.amountSigned)) return { - kind: ActivityKindEnum.interaction - // subkind: OperStackItemTypeEnum.Interaction + kind: ActivityOperKindEnum.interaction // with: oper.destination.address, // entrypoint: oper.entrypoint }; - if (preOperation.from.address === address) { + if (preOperation.from.address === address) return { - kind: ActivityKindEnum.send - // subkind: OperStackItemTypeEnum.TransferTo - // to: oper.to.address + kind: + preOperation.to.length === 1 && !isTezosContractAddress(preOperation.to[0].address) + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom }; - } else if (preOperation.to?.address === address) { + + if (preOperation.to.some(member => member.address === address)) return { - kind: ActivityKindEnum.receive - // subkind: OperStackItemTypeEnum.TransferFrom - // from: oper.from.address + kind: isTezosContractAddress(preOperation.from.address) + ? ActivityOperKindEnum.transferTo + : ActivityOperKindEnum.transferTo_FromAccount }; - } return { - kind: ActivityKindEnum.interaction - // subkind: OperStackItemTypeEnum.Interaction + kind: ActivityOperKindEnum.interaction }; - } else if ( - preOperation.type === 'delegation' && - preOperation.source.address === address && - preOperation.destination - ) { + } + + if (preOperation.type === 'delegation' && preOperation.sender.address === address && preOperation.destination) { return { - kind: ActivityKindEnum.interaction - // subkind: OperStackItemTypeEnum.Delegation + kind: ActivityOperKindEnum.interaction + // subkind: ActivitySubKindEnum.Delegation // to: oper.destination.address }; } return { - kind: ActivityKindEnum.interaction + kind: ActivityOperKindEnum.interaction // subkind: OperStackItemTypeEnum.Other }; })(); @@ -82,9 +80,11 @@ export function parseTezosPreActivityOperation( if (!assetMetadata) return operationBase; if ( - operationBase.kind === ActivityKindEnum.send || - operationBase.kind === ActivityKindEnum.receive || - operationBase.kind === ActivityKindEnum.approve + operationBase.kind === ActivityOperKindEnum.transferFrom_ToAccount || + operationBase.kind === ActivityOperKindEnum.transferTo_FromAccount || + operationBase.kind === ActivityOperKindEnum.transferFrom || + operationBase.kind === ActivityOperKindEnum.transferTo || + operationBase.kind === ActivityOperKindEnum.approve ) { const asset: TezosActivityAsset = { contract: preOperation.contractAddress ?? TEZ_TOKEN_SLUG, diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index 53bcc406b4..c675066414 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -60,7 +60,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Tezo const activityOperBase = buildActivityOperBase(operation, '0', operation.sender.address === address); const activityOper: TezosPreActivityOtherOperation = { ...activityOperBase, - source: operation.sender, + sender: operation.sender, type: 'delegation' }; if (operation.newDelegate) activityOper.destination = operation.newDelegate; @@ -71,7 +71,7 @@ function reduceOneTzktOperation(operation: TzktOperation, address: string): Tezo const activityOperBase = buildActivityOperBase(operation, amount, operation.sender.address === address); const activityOper: TezosPreActivityOtherOperation = { ...activityOperBase, - source: operation.sender, + sender: operation.sender, type: 'origination' }; if (operation.originatedContract) activityOper.destination = operation.originatedContract; @@ -89,7 +89,7 @@ function reduceOneTzktTransactionOperation( function _buildReturn(args: { amount: string; from: OperationMember; - to?: OperationMember; + to: OperationMember | string[]; contractAddress?: string; tokenId?: string; subtype?: TezosPreActivityTransactionOperation['subtype']; @@ -108,7 +108,7 @@ function reduceOneTzktTransactionOperation( subtype, destination: operation.target, from, - to + to: Array.isArray(to) ? to.map(address => ({ address })) : [to] }; if (contractAddress != null) activityOper.contractAddress = contractAddress; @@ -138,7 +138,7 @@ function reduceOneTzktTransactionOperation( const amount = firstVal.amount; const tokenId = firstVal.tokenId; const from = { ...operation.sender, address: firstVal.fromAddress }; - const to = firstVal.isToRelAddress ? { address } : undefined; + const to = firstVal.toAddresses; return _buildReturn({ amount, from, to, contractAddress, tokenId, subtype: 'transfer' }); } else if (isTzktOperParam_Fa2_approve(parameter)) { @@ -194,10 +194,11 @@ function reduceOneTzktTransactionOperation( } function buildActivityOperBase(operation: TzktOperation, amount: string, from: boolean) { - const { id, level, timestamp: addedAt } = operation; + const { id, level, sender, timestamp: addedAt } = operation; const reducedOperation: TezosPreActivityOperationBase = { id, level, + sender, amountSigned: from ? `-${amount}` : amount, status: stringToActivityStatus(operation.status), addedAt @@ -208,7 +209,7 @@ function buildActivityOperBase(operation: TzktOperation, amount: string, from: b interface ReducedParameterFa2Values { fromAddress: string; - isToRelAddress?: boolean; + toAddresses: string[]; amount: string; tokenId: string; } @@ -230,12 +231,18 @@ function reduceParameterFa2TransferValues(values: ParameterFa2Transfer['value'], const fromAddress = val.from_; if (fromAddress === relAddress) { - const amount = val.txs.reduce((acc, tx) => acc.plus(tx.amount), ZERO); + let amount = ZERO; + const toAddresses = val.txs.map(tx => { + amount = amount.plus(tx.amount); + + return tx.to_; + }); if (amount.isZero()) continue; result.push({ fromAddress, + toAddresses, amount: amount.toFixed(), tokenId }); @@ -248,7 +255,7 @@ function reduceParameterFa2TransferValues(values: ParameterFa2Transfer['value'], if (amount.isZero() === false) result.push({ fromAddress, - isToRelAddress: true, + toAddresses: [relAddress], // Not interested in all the other `tx.to_`s at the moment amount: amount.toFixed(), tokenId }); diff --git a/src/lib/activity/tezos/types.ts b/src/lib/activity/tezos/types.ts index b5eb394120..11fb6a7ceb 100644 --- a/src/lib/activity/tezos/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -22,6 +22,7 @@ export interface TezosPreActivity { type PickedPropsFromTzktOperation = Pick; export interface TezosPreActivityOperationBase extends PickedPropsFromTzktOperation { + sender: OperationMember; contractAddress?: string; status: TezosPreActivityStatus; amountSigned: string; @@ -32,7 +33,8 @@ export interface TezosPreActivityTransactionOperation extends TezosPreActivityOp type: 'transaction'; subtype?: 'transfer' | 'approve'; from: OperationMember; - to?: OperationMember; + /** Optional - parser is not keeping all of `txs`'s `to_`s, reducing to total amount */ + to: OperationMember[]; destination: OperationMember; entrypoint?: string; tokenId?: string; @@ -40,7 +42,6 @@ export interface TezosPreActivityTransactionOperation extends TezosPreActivityOp export interface TezosPreActivityOtherOperation extends TezosPreActivityOperationBase { type: Exclude; - source: OperationMember; destination?: OperationMember; } diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 51ab9bdece..fe412b0541 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -1,9 +1,11 @@ import { TempleChainKind } from 'temple/types'; -export enum ActivityKindEnum { +export enum ActivityOperKindEnum { interaction, - send, - receive, + transferFrom, + transferTo, + transferFrom_ToAccount, + transferTo_FromAccount, swap, approve } @@ -19,7 +21,7 @@ export interface TezosActivity { } export interface TezosOperation { - kind: ActivityKindEnum; + kind: ActivityOperKindEnum; asset?: TezosActivityAsset; } @@ -42,7 +44,7 @@ export interface EvmActivity { } export interface EvmOperation { - kind: ActivityKindEnum; + kind: ActivityOperKindEnum; asset?: EvmActivityAsset; } From 00500f92b6a848961cad5904f0bfb4cf33418bea Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 22 Sep 2024 22:03:52 +0300 Subject: [PATCH 19/74] TW-1479: [EVM] Transactions history. [Tezos] Restored opers order. Reversed back --- src/lib/activity/tezos/fetch.ts | 9 ++------- src/lib/activity/tezos/pre-parse.ts | 21 +++++++-------------- src/lib/activity/tezos/types.ts | 9 ++++++--- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/lib/activity/tezos/fetch.ts b/src/lib/activity/tezos/fetch.ts index 017b27f18d..a6d5e8397d 100644 --- a/src/lib/activity/tezos/fetch.ts +++ b/src/lib/activity/tezos/fetch.ts @@ -5,15 +5,10 @@ import { detectTokenStandard } from 'lib/assets/standards'; import { filterUnique } from 'lib/utils'; import { getReadOnlyTezos } from 'temple/tezos'; -import type { TempleTzktOperationsGroup } from './types'; +import type { TempleTzktOperationsGroup, TezosActivityOlderThan } from './types'; const LIQUIDITY_BAKING_DEX_ADDRESS = 'KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5'; -export interface TezosActivityOlderThan { - hash: string; - oldestTzktOperation: TzktOperation; -} - export default async function fetchTezosOperationsGroups( chainId: TzktApiChainId, rpcUrl: string, @@ -224,7 +219,7 @@ async function fetchOperGroupsForOperations( const groups: TempleTzktOperationsGroup[] = []; for (const hash of uniqueHashes) { const operations = await TZKT.refetchOnce429(() => TZKT.fetchGetOperationsByHash(chainId, hash), 1000); - operations.sort((b, a) => a.id - b.id); + groups.push({ hash, operations diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index c675066414..1fa0f0cd87 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -23,30 +23,23 @@ import type { } from './types'; export function preparseTezosOperationsGroup( - { hash, operations }: TempleTzktOperationsGroup, + { hash, operations: groupOperations }: TempleTzktOperationsGroup, address: string ): TezosPreActivity { - const firstOperation = operations[0]!; - const oldestTzktOperation = operations[operations.length - 1]!; - const addedAt = firstOperation.timestamp; - const activityOperations = reduceTzktOperations(operations, address); - const status = deriveActivityStatus(activityOperations); + const lastOperation = groupOperations[groupOperations.length - 1]!; + const addedAt = lastOperation.timestamp; + const operations = groupOperations.map(op => reduceOneTzktOperation(op, address)).filter(isTruthy); + const status = deriveActivityStatus(operations); return { hash, addedAt, status, - operations: activityOperations, - oldestTzktOperation + operations, + oldestTzktOperation: lastOperation }; } -function reduceTzktOperations(operations: TzktOperation[], address: string): TezosPreActivityOperation[] { - const reducedOperations = operations.map(op => reduceOneTzktOperation(op, address)).filter(isTruthy); - - return reducedOperations; -} - /** * (i) Does not mutate operation object */ diff --git a/src/lib/activity/tezos/types.ts b/src/lib/activity/tezos/types.ts index 11fb6a7ceb..992078b24a 100644 --- a/src/lib/activity/tezos/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -9,13 +9,16 @@ export interface TempleTzktOperationsGroup { export type TezosPreActivityStatus = TzktOperation['status'] | 'pending'; -export interface TezosPreActivity { +export interface TezosActivityOlderThan { hash: string; + oldestTzktOperation: TzktOperation; +} + +export interface TezosPreActivity extends TezosActivityOlderThan { /** ISO string */ addedAt: string; status: TezosPreActivityStatus; - oldestTzktOperation: TzktOperation; - /** Sorted new-to-old */ + /** Sorted old-to-new */ operations: TezosPreActivityOperation[]; } From 794e47bcf1f92442c0c69bfdb6e1c00f288a516a Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Sep 2024 09:18:05 +0300 Subject: [PATCH 20/74] TW-1479: [EVM] Transactions history. Fix InfiniteScroll --- src/app/atoms/InfiniteScroll.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index 8bbc4d849f..d5c9ba8453 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -28,13 +28,16 @@ export const InfiniteScroll: FC = ({ const onScroll = isSyncing || reachedTheEnd ? undefined : buildOnScroll(loadNext); + // For when there are too few items to make initial scroll for loadMore: useEffect(() => { if (SCROLL_DOCUMENT || isSyncing || reachedTheEnd) return; const scrollableElem = document.getElementById(APP_CONTENT_PAPER_DOM_ID); - if (!scrollableElem || scrollableElem.scrollTop) return; - if (scrollableElem.offsetHeight === scrollableElem.clientHeight) loadNext(); + if (!scrollableElem || scrollableElem.scrollTop || scrollableElem.scrollHeight > scrollableElem.clientHeight) + return; + + if (isScrollAtTheEnd(scrollableElem)) loadNext(); }, [isSyncing, reachedTheEnd]); return ( @@ -61,7 +64,10 @@ const buildOnScroll = const elem: HTMLElement = target instanceof Document ? (target.scrollingElement! as HTMLElement) : (target as HTMLElement); - const atBottom = 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; - - if (atBottom) next(); + if (isScrollAtTheEnd(elem)) next(); }; + +function isScrollAtTheEnd(elem: Element) { + // return 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; + return elem.scrollHeight === elem.clientHeight + elem.scrollTop; +} From 36aa7b4d33fe2bbf8514708ac3b758f9972321f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Sep 2024 10:03:10 +0300 Subject: [PATCH 21/74] TW-1479: [EVM] Transactions history. [EVM] + TransferSingle support --- src/lib/activity/evm.ts | 230 +++++++++++++++++++++++++--------------- 1 file changed, 145 insertions(+), 85 deletions(-) diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index 5844abce35..0324dfec46 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -2,6 +2,7 @@ import { GoldRushERC20Transfer, GoldRushTransaction } from 'lib/apis/temple/endp import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { EvmAssetMetadataGetter, getAssetSymbol } from 'lib/metadata'; +import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; @@ -15,117 +16,176 @@ export function parseGoldRushTransaction( ): EvmActivity { const logEvents = item.log_events ?? []; - const operations = logEvents.map(logEvent => { - if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; + const operations = logEvents + .map(logEvent => { + if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; + + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const _fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); + const _toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); + let decimals = logEvent.sender_contract_decimals; + const iconURL = logEvent.sender_logo_url ?? undefined; + + if (logEvent.decoded.name === 'Transfer') { + const kind = (() => { + if (_toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); - const toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); - let decimals = logEvent.sender_contract_decimals; - const iconURL = logEvent.sender_logo_url ?? undefined; + if (_fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + } - if (logEvent.decoded.name === 'Transfer') { - const kind = (() => { - if (toAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; - } + return null; + })(); - if (fromAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; - } + if (kind == null || !contractAddress) return { kind: ActivityOperKindEnum.interaction }; - return ActivityOperKindEnum.interaction; - })(); + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; + const nft = logEvent.decoded.params[2]?.indexed ?? false; + const tokenId = nft ? amountOrTokenId : undefined; - if (kind === ActivityOperKindEnum.interaction || !contractAddress) return { kind }; + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); - const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; - const nft = logEvent.decoded.params[2]?.indexed ?? false; - const tokenId = nft ? amountOrTokenId : undefined; + decimals = metadata?.decimals ?? decimals; - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); + if (decimals == null) return { kind }; - decimals = metadata?.decimals ?? decimals; + const amount = nft ? '1' : amountOrTokenId; + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; - if (decimals == null) return { kind }; + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft, + iconURL + }; - const amount = nft ? '1' : amountOrTokenId; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + return { kind, asset }; + } - const asset: EvmActivityAsset = { - contract: contractAddress, - tokenId, - amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${amount}` : amount, - decimals, - symbol, - nft, - iconURL - }; + if (logEvent.decoded.name === 'TransferSingle') { + const fromAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); + const toAddress = getEvmAddressSafe(logEvent.decoded.params[2]?.value); + + const kind = (() => { + if (toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } - return { kind, asset }; - } + if (fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + } - if (logEvent.decoded.name === 'Approval' && fromAddress === accountAddress) { - const kind = ActivityOperKindEnum.approve; + return null; + })(); - if (!contractAddress) return { kind }; + if (kind == null || !contractAddress) return null; - const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; - const nft = logEvent.decoded.params[2]?.indexed ?? false; + const tokenId = logEvent.decoded.params[3]?.value ?? '0'; - const tokenId = nft ? amountOrTokenId : undefined; + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); + decimals = metadata?.decimals ?? decimals; - decimals = metadata?.decimals ?? decimals; + if (decimals == null) return { kind }; - if (decimals == null) return { kind }; + const amount = '1'; + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft: true, + iconURL + }; - const asset: EvmActivityAsset = { - contract: contractAddress, - tokenId, - amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs - decimals, - symbol, - nft, - iconURL - }; + return { kind, asset }; + } - return { kind, asset }; - } + if (logEvent.decoded.name === 'Approval') { + if (_fromAddress !== accountAddress) return null; - if ( - logEvent.decoded.name === 'ApprovalForAll' && - // @ts-expect-error // `value` is not always `:string` - logEvent.decoded.params[2]?.value === true && - fromAddress === accountAddress - ) { - const kind = ActivityOperKindEnum.approve; + const kind = ActivityOperKindEnum.approve; - if (!contractAddress || decimals == null) return { kind }; + if (!contractAddress) return { kind }; - const asset: EvmActivityAsset = { - contract: contractAddress, - amount: InfinitySymbol, - decimals: NaN, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft: true, - iconURL - }; + const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; + const nft = logEvent.decoded.params[2]?.indexed ?? false; - return { kind, asset }; - } + const tokenId = nft ? amountOrTokenId : undefined; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; + + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs + decimals, + symbol, + nft, + iconURL + }; + + return { kind, asset }; + } + + if (logEvent.decoded.name === 'ApprovalForAll') { + if ( + // @ts-expect-error // `value` is not always `:string` + logEvent.decoded.params[2]?.value !== true || + _fromAddress !== accountAddress + ) + return null; + + const kind = ActivityOperKindEnum.approve; + + if (!contractAddress) return { kind }; + + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: InfinitySymbol, + decimals: NaN, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft: true, + iconURL + }; + + return { kind, asset }; + } - return { kind: ActivityOperKindEnum.interaction }; - }); + return { kind: ActivityOperKindEnum.interaction }; + }) + .filter(isTruthy); const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); @@ -178,7 +238,7 @@ export function parseGoldRushERC20Transfer( const asset: EvmActivityAsset = { contract: contractAddress, amount: - kind === ActivityOperKindEnum.transferFrom_ToAccount || kind === ActivityOperKindEnum.transferFrom + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${amount}` : amount, decimals, From c80d6a27c392010bb6882eac4e754d893c2f82fc Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Sep 2024 11:27:55 +0300 Subject: [PATCH 22/74] TW-1479: [EVM] Transactions history. Refactor. OperationBaseComponent --- src/app/templates/activity/Activity.tsx | 151 ------------------ .../ActivityOperationBase.tsx | 2 +- .../activity/ActivityItem/EvmActivity.tsx | 75 +++++++++ .../EvmActivityOperation.tsx | 0 .../ActivityItem/InteractionsConnector.tsx | 9 ++ .../activity/ActivityItem/TezosActivity.tsx | 81 ++++++++++ .../TezosActivityOperation.tsx | 10 +- .../templates/activity/ActivityItem/index.ts | 2 + .../interactions-connector.svg | 0 src/app/templates/activity/EvmActivityTab.tsx | 2 +- .../templates/activity/TezosActivityTab.tsx | 2 +- 11 files changed, 175 insertions(+), 159 deletions(-) delete mode 100644 src/app/templates/activity/Activity.tsx rename src/app/templates/activity/{ => ActivityItem}/ActivityOperationBase.tsx (98%) create mode 100644 src/app/templates/activity/ActivityItem/EvmActivity.tsx rename src/app/templates/activity/{ => ActivityItem}/EvmActivityOperation.tsx (100%) create mode 100644 src/app/templates/activity/ActivityItem/InteractionsConnector.tsx create mode 100644 src/app/templates/activity/ActivityItem/TezosActivity.tsx rename src/app/templates/activity/{ => ActivityItem}/TezosActivityOperation.tsx (74%) create mode 100644 src/app/templates/activity/ActivityItem/index.ts rename src/app/templates/activity/{ => ActivityItem}/interactions-connector.svg (100%) diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx deleted file mode 100644 index cd5fd33370..0000000000 --- a/src/app/templates/activity/Activity.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { memo } from 'react'; - -import clsx from 'clsx'; - -import { IconBase } from 'app/atoms'; -import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { Activity } from 'lib/activity'; -import { TezosPreActivity } from 'lib/activity/tezos/types'; -import { t } from 'lib/i18n'; -import { useBooleanState } from 'lib/ui/hooks'; -import { useExplorerHref } from 'temple/front/block-explorers'; -import { EvmChain, TezosChain } from 'temple/front/chains'; - -import { EvmActivityOperationComponent } from './EvmActivityOperation'; -import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; -import { TezosActivityOperationComponent } from './TezosActivityOperation'; - -interface TezosActivityComponentProps { - activity: TezosPreActivity; - chain: TezosChain; - accountAddress: string; -} - -export const TezosActivityComponent = memo(({ activity, chain, accountAddress }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); - - const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - - const { hash } = activity; - - const blockExplorerUrl = useExplorerHref(chain.chainId, hash); - - const operations = activity.operations; - - return ( -
- {operations.slice(0, 3).map((operation, i) => ( - - {i > 0 && } - - - - ))} - - {operations.length > 3 ? ( - <> - - - {expanded - ? operations.slice(3).map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} - - ) : null} -
- ); -}); - -interface EvmActivityComponentProps { - activity: Activity; - chain: EvmChain; -} - -export const EvmActivityComponent = memo(({ activity, chain }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); - - const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - - const { hash, blockExplorerUrl } = activity; - - const operations = activity.operations; - - return ( -
- {operations.slice(0, 3).map((operation, i) => ( - - {i > 0 && } - - - - ))} - - {operations.length > 3 ? ( - <> - - - {expanded - ? operations.slice(3).map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} - - ) : null} -
- ); -}); - -const InteractionsConnector = memo(() => ( -
- -
-)); diff --git a/src/app/templates/activity/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx similarity index 98% rename from src/app/templates/activity/ActivityOperationBase.tsx rename to src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index b4501b3876..4e874a5010 100644 --- a/src/app/templates/activity/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -12,7 +12,7 @@ import { ActivityOperKindEnum, InfinitySymbol } from 'lib/activity'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; -import { EvmAssetIcon, TezosAssetIcon } from '../AssetIcon'; +import { EvmAssetIcon, TezosAssetIcon } from '../../AssetIcon'; interface Props { chainId: string | number; diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx new file mode 100644 index 0000000000..7b4c47a5e3 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -0,0 +1,75 @@ +import React, { memo } from 'react'; + +import clsx from 'clsx'; + +import { IconBase } from 'app/atoms'; +import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { Activity } from 'lib/activity'; +import { t } from 'lib/i18n'; +import { useBooleanState } from 'lib/ui/hooks'; +import { EvmChain } from 'temple/front/chains'; + +import { EvmActivityOperationComponent } from './EvmActivityOperation'; +import { InteractionsConnector } from './InteractionsConnector'; + +interface Props { + activity: Activity; + chain: EvmChain; +} + +export const EvmActivityComponent = memo(({ activity, chain }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + + const { hash, blockExplorerUrl } = activity; + + const operations = activity.operations; + + return ( +
+ {operations.slice(0, 3).map((operation, i) => ( + + {i > 0 && } + + + + ))} + + {operations.length > 3 ? ( + <> + + + {expanded + ? operations.slice(3).map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} + + ) : null} +
+ ); +}); diff --git a/src/app/templates/activity/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx similarity index 100% rename from src/app/templates/activity/EvmActivityOperation.tsx rename to src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx diff --git a/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx b/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx new file mode 100644 index 0000000000..86af11a920 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx @@ -0,0 +1,9 @@ +import React, { memo } from 'react'; + +import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; + +export const InteractionsConnector = memo(() => ( +
+ +
+)); diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx new file mode 100644 index 0000000000..a76cb915b9 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -0,0 +1,81 @@ +import React, { memo } from 'react'; + +import clsx from 'clsx'; + +import { IconBase } from 'app/atoms'; +import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { t } from 'lib/i18n'; +import { useBooleanState } from 'lib/ui/hooks'; +import { useExplorerHref } from 'temple/front/block-explorers'; +import { TezosChain } from 'temple/front/chains'; + +import { InteractionsConnector } from './InteractionsConnector'; +import { TezosActivityOperationComponent } from './TezosActivityOperation'; + +interface Props { + activity: TezosPreActivity; + chain: TezosChain; + accountAddress: string; +} + +export const TezosActivityComponent = memo(({ activity, chain, accountAddress }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + + const { hash } = activity; + + const blockExplorerUrl = useExplorerHref(chain.chainId, hash); + + const operations = activity.operations; + + return ( +
+ {operations.slice(0, 3).map((operation, i) => ( + + {i > 0 && } + + + + ))} + + {operations.length > 3 ? ( + <> + + + {expanded + ? operations.slice(3).map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} + + ) : null} +
+ ); +}); diff --git a/src/app/templates/activity/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx similarity index 74% rename from src/app/templates/activity/TezosActivityOperation.tsx rename to src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index bb488fbbd0..ed5047ce24 100644 --- a/src/app/templates/activity/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -19,17 +19,17 @@ interface Props { } export const TezosActivityOperationComponent = memo( - ({ hash, operation: legacyOperation, chain, networkName, blockExplorerUrl, accountAddress }) => { + ({ hash, operation: preOperation, chain, networkName, blockExplorerUrl, accountAddress }) => { const assetSlug = - legacyOperation.type === 'transaction' - ? toTezosAssetSlug(legacyOperation.contractAddress ?? TEZ_TOKEN_SLUG, legacyOperation.tokenId) + preOperation.type === 'transaction' + ? toTezosAssetSlug(preOperation.contractAddress ?? TEZ_TOKEN_SLUG, preOperation.tokenId) : ''; const assetMetadata = useTezosAssetMetadata(assetSlug, chain.chainId); const operation = useMemo( - () => parseTezosPreActivityOperation(legacyOperation, accountAddress, assetMetadata), - [assetMetadata, legacyOperation, accountAddress] + () => parseTezosPreActivityOperation(preOperation, accountAddress, assetMetadata), + [assetMetadata, preOperation, accountAddress] ); return ( diff --git a/src/app/templates/activity/ActivityItem/index.ts b/src/app/templates/activity/ActivityItem/index.ts new file mode 100644 index 0000000000..3b3b009344 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/index.ts @@ -0,0 +1,2 @@ +export { TezosActivityComponent } from './TezosActivity'; +export { EvmActivityComponent } from './EvmActivity'; diff --git a/src/app/templates/activity/interactions-connector.svg b/src/app/templates/activity/ActivityItem/interactions-connector.svg similarity index 100% rename from src/app/templates/activity/interactions-connector.svg rename to src/app/templates/activity/ActivityItem/interactions-connector.svg diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityTab.tsx index 49134ce254..33e151034f 100644 --- a/src/app/templates/activity/EvmActivityTab.tsx +++ b/src/app/templates/activity/EvmActivityTab.tsx @@ -15,7 +15,7 @@ import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hook import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; -import { EvmActivityComponent } from './Activity'; +import { EvmActivityComponent } from './ActivityItem'; interface EvmActivityTabProps { chainId: number; diff --git a/src/app/templates/activity/TezosActivityTab.tsx b/src/app/templates/activity/TezosActivityTab.tsx index f57a7736ca..4f6fe9a541 100644 --- a/src/app/templates/activity/TezosActivityTab.tsx +++ b/src/app/templates/activity/TezosActivityTab.tsx @@ -12,7 +12,7 @@ import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hook import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { TezosNetworkEssentials } from 'temple/networks'; -import { TezosActivityComponent } from './Activity'; +import { TezosActivityComponent } from './ActivityItem'; const INITIAL_NUMBER = 30; const LOAD_STEP = 30; From 3b52785a60b463db89aca4e2b628a4b849908227 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Sep 2024 04:13:44 +0300 Subject: [PATCH 23/74] TW-1479: [EVM] Transactions history. [Tezos] + Bundles --- .../ActivityItem/ActivityOperationBase.tsx | 19 +- .../activity/ActivityItem/TezosActivity.tsx | 184 +++++++++++++----- .../ActivityItem/TezosActivityOperation.tsx | 15 +- .../templates/activity/TezosActivityTab.tsx | 15 +- src/lib/activity/evm.ts | 74 +++---- src/lib/activity/tezos/index.ts | 19 +- src/lib/activity/tezos/pre-parse.ts | 34 ++-- src/lib/activity/tezos/types.ts | 4 +- src/lib/activity/types.ts | 33 ++-- src/lib/activity/utils.ts | 10 + 10 files changed, 248 insertions(+), 159 deletions(-) create mode 100644 src/lib/activity/utils.ts diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 4e874a5010..028c8a3a5b 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -9,6 +9,7 @@ import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; import { ActivityOperKindEnum, InfinitySymbol } from 'lib/activity'; +import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -16,14 +17,14 @@ import { EvmAssetIcon, TezosAssetIcon } from '../../AssetIcon'; interface Props { chainId: string | number; - kind: ActivityOperKindEnum; + kind: ActivityOperKindEnum | 'bundle'; hash: string; networkName: string; - asset?: AssetProp; + asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; } -interface AssetProp { +export interface ActivityItemBaseAssetProp { contract: string; tokenId?: string; amount?: string | typeof InfinitySymbol; @@ -50,11 +51,7 @@ export const ActivityOperationBaseComponent: FC = ({ if (!asset) return {}; const amountForFiat = - typeof asset.amount === 'string' && - (kind === ActivityOperKindEnum.transferTo_FromAccount || - kind === ActivityOperKindEnum.transferFrom_ToAccount || - kind === ActivityOperKindEnum.transferFrom || - kind === ActivityOperKindEnum.transferTo) + typeof asset.amount === 'string' && (kind === 'bundle' || isTransferActivityOperKind(kind)) ? atomsToTokens(asset.amount, asset.decimals) : null; @@ -157,7 +154,8 @@ export const ActivityOperationBaseComponent: FC = ({ ); }; -const ActivityKindTitle: Record = { +const ActivityKindTitle: Record = { + bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', [ActivityOperKindEnum.transferFrom_ToAccount]: 'Send', [ActivityOperKindEnum.transferTo_FromAccount]: 'Receive', @@ -167,7 +165,8 @@ const ActivityKindTitle: Record = { [ActivityOperKindEnum.approve]: 'Approve' }; -const ActivityKindIconSvg: Record = { +const ActivityKindIconSvg: Record = { + bundle: DocumentsSvg, [ActivityOperKindEnum.interaction]: DocumentsSvg, [ActivityOperKindEnum.transferFrom_ToAccount]: SendSvg, [ActivityOperKindEnum.transferTo_FromAccount]: IncomeSvg, diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index a76cb915b9..67eea61836 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -1,15 +1,21 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import clsx from 'clsx'; import { IconBase } from 'app/atoms'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { TezosActivityAsset, parseTezosPreActivityOperation } from 'lib/activity'; import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { isTransferActivityOperKind } from 'lib/activity/utils'; +import { toTezosAssetSlug } from 'lib/assets/utils'; import { t } from 'lib/i18n'; -import { useBooleanState } from 'lib/ui/hooks'; +import { useGetChainTokenOrGasMetadata } from 'lib/metadata'; +import { useBooleanState, useMemoWithCompare } from 'lib/ui/hooks'; +import { ZERO } from 'lib/utils/numbers'; import { useExplorerHref } from 'temple/front/block-explorers'; import { TezosChain } from 'temple/front/chains'; +import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; import { InteractionsConnector } from './InteractionsConnector'; import { TezosActivityOperationComponent } from './TezosActivityOperation'; @@ -17,65 +23,141 @@ interface Props { activity: TezosPreActivity; chain: TezosChain; accountAddress: string; + assetSlug?: string; } -export const TezosActivityComponent = memo(({ activity, chain, accountAddress }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); - +export const TezosActivityComponent = memo(({ activity, chain, accountAddress, assetSlug }) => { const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; + const { hash, operations } = activity; + + const blockExplorerUrl = useExplorerHref(chain.chainId, hash) ?? undefined; + + if (operations.length === 1) + return ( +
+ +
+ ); + + return ( + + ); +}); + +const TezosActivityBatchComponent = memo<{ + activity: TezosPreActivity; + chainId: string; + assetSlug?: string; + blockExplorerUrl?: string; + accountAddress: string; + networkName: string; +}>(({ activity, chainId, assetSlug, blockExplorerUrl, accountAddress, networkName }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + const { hash } = activity; - const blockExplorerUrl = useExplorerHref(chain.chainId, hash); + const preOperations = activity.operations; + + const getMetadata = useGetChainTokenOrGasMetadata(chainId); - const operations = activity.operations; + const operations = useMemoWithCompare( + () => + preOperations.map(o => { + const slug = o.contract ? toTezosAssetSlug(o.contract, o.tokenId) : undefined; + return parseTezosPreActivityOperation(o, accountAddress, slug ? getMetadata(slug) : undefined); + }), + [preOperations, getMetadata, accountAddress] + ); + + const faceSlug = useMemo(() => { + if (assetSlug) return assetSlug; + + for (const { kind, asset } of operations) { + if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) + return toTezosAssetSlug(asset.contract, asset.tokenId); + } + + return; + }, [operations, assetSlug]); + + const batchAsset = useMemo(() => { + if (!faceSlug) return; + + let faceAsset: TezosActivityAsset | undefined; + let faceAmount = ZERO; + + for (const { kind, asset } of operations) { + if ( + typeof asset?.amount === 'string' && + toTezosAssetSlug(asset.contract, asset.tokenId) === faceSlug && + isTransferActivityOperKind(kind) + ) { + faceAmount = faceAmount.plus(asset.amount); + if (!faceAsset) faceAsset = asset; + } + } + + if (!faceAsset) return; + + const batchAsset: ActivityItemBaseAssetProp = { + ...faceAsset, + amount: faceAmount.toFixed() + }; + + return batchAsset; + }, [operations, faceSlug]); return (
- {operations.slice(0, 3).map((operation, i) => ( - - {i > 0 && } - - - - ))} - - {operations.length > 3 ? ( - <> - - - {expanded - ? operations.slice(3).map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} - - ) : null} + + + + + {expanded + ? operations.map((operation, j) => ( + + {j > 0 && } + + + + )) + : null}
); }); diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index ed5047ce24..a47b379d3a 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -2,30 +2,25 @@ import React, { memo, useMemo } from 'react'; import { TezosOperation, parseTezosPreActivityOperation } from 'lib/activity'; import { TezosPreActivityOperation } from 'lib/activity/tezos/types'; -import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { toTezosAssetSlug } from 'lib/assets/utils'; import { useTezosAssetMetadata } from 'lib/metadata'; -import { TezosChain } from 'temple/front'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; interface Props { hash: string; operation: TezosPreActivityOperation; - chain: TezosChain; + chainId: string; networkName: string; blockExplorerUrl: string | nullish; accountAddress: string; } export const TezosActivityOperationComponent = memo( - ({ hash, operation: preOperation, chain, networkName, blockExplorerUrl, accountAddress }) => { - const assetSlug = - preOperation.type === 'transaction' - ? toTezosAssetSlug(preOperation.contractAddress ?? TEZ_TOKEN_SLUG, preOperation.tokenId) - : ''; + ({ hash, operation: preOperation, chainId, networkName, blockExplorerUrl, accountAddress }) => { + const assetSlug = preOperation.contract ? toTezosAssetSlug(preOperation.contract, preOperation.tokenId) : ''; - const assetMetadata = useTezosAssetMetadata(assetSlug, chain.chainId); + const assetMetadata = useTezosAssetMetadata(assetSlug, chainId); const operation = useMemo( () => parseTezosPreActivityOperation(preOperation, accountAddress, assetMetadata), @@ -36,7 +31,7 @@ export const TezosActivityOperationComponent = memo( (({ tezosChainId, ass useLoadPartnersPromo(); - const { - isLoading, - reachedTheEnd, - list: activities, - loadMore - } = useTezosActivities(network, accountAddress, INITIAL_NUMBER, assetSlug); + const { activities, isLoading, reachedTheEnd, loadMore } = useTezosActivities( + network, + accountAddress, + INITIAL_NUMBER, + assetSlug + ); if (activities.length === 0 && !isLoading && reachedTheEnd) { return ; @@ -55,6 +55,7 @@ export const TezosActivityTab = memo(({ tezosChainId, ass activity={activity} chain={network} accountAddress={accountAddress} + assetSlug={assetSlug} /> ))} @@ -137,7 +138,7 @@ function useTezosActivities( return { isLoading, reachedTheEnd, - list: activities, + activities, loadMore }; } diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index 0324dfec46..f203e120a5 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -196,7 +196,8 @@ export function parseGoldRushTransaction( chainId, hash: item.tx_hash!, blockExplorerUrl: item.explorers?.[0]?.url, - operations + operations, + operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length }; } @@ -206,49 +207,50 @@ export function parseGoldRushERC20Transfer( accountAddress: string, getMetadata: EvmAssetMetadataGetter ): EvmActivity { - const operations = - item.transfers?.map(transfer => { - const kind = (() => { - if (transfer.transfer_type === 'IN') { - return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; - } + const transfers = item.transfers ?? []; + const operations = transfers.map(transfer => { + const kind = (() => { + if (transfer.transfer_type === 'IN') { return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; - })(); + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + return item.to_address === transfer.contract_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + })(); - const contractAddress = getEvmAddressSafe(transfer.contract_address); + const contractAddress = getEvmAddressSafe(transfer.contract_address); - if (contractAddress == null) return { kind }; + if (contractAddress == null) return { kind }; - const slug = toEvmAssetSlug(contractAddress); - const metadata = getMetadata(slug); + const slug = toEvmAssetSlug(contractAddress); + const metadata = getMetadata(slug); - const decimals = metadata?.decimals ?? transfer.contract_decimals; + const decimals = metadata?.decimals ?? transfer.contract_decimals; - if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; + if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; - const nft = false; - const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; - const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; + const nft = false; + const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; + const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; - const asset: EvmActivityAsset = { - contract: contractAddress, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, - decimals, - symbol, - nft, - iconURL: transfer.logo_url ?? undefined - }; + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft, + iconURL: transfer.logo_url ?? undefined + }; - return { kind, asset }; - }) ?? []; + return { kind, asset }; + }); const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); @@ -259,7 +261,8 @@ export function parseGoldRushERC20Transfer( chainId, hash: item.tx_hash!, blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, - operations + operations, + operationsCount: gasOperation ? transfers.length + 1 : transfers.length }; } @@ -275,6 +278,7 @@ function parseGasTransfer( const kind = (() => { if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityOperKindEnum.transferFrom_ToAccount; if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityOperKindEnum.transferTo_FromAccount; + // TODO: Check for transfers to contracts return null; })(); diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index b210667d23..9a4c31858d 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -1,10 +1,10 @@ import { TezosPreActivity, TezosPreActivityOperation } from 'lib/activity/tezos/types'; -import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { AssetMetadataBase, getAssetSymbol, isTezosCollectibleMetadata } from 'lib/metadata'; import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; import { TezosActivity, ActivityOperKindEnum, TezosOperation, TezosActivityAsset } from '../types'; +import { isTransferActivityOperKind } from '../utils'; export { preparseTezosOperationsGroup } from './pre-parse'; @@ -19,7 +19,8 @@ export function formatLegacyTezosActivity( hash, chain: TempleChainKind.Tezos, chainId, - operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)) + operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)), + operationsCount: _activity.operations.length }; } @@ -36,7 +37,7 @@ export function parseTezosPreActivityOperation( if (preOperation.subtype === 'approve') return { kind: ActivityOperKindEnum.approve }; - if (isZero(preOperation.amountSigned)) + if (preOperation.subtype !== 'transfer' || isZero(preOperation.amountSigned)) return { kind: ActivityOperKindEnum.interaction // with: oper.destination.address, @@ -77,17 +78,11 @@ export function parseTezosPreActivityOperation( }; })(); - if (!assetMetadata) return operationBase; + if (!assetMetadata || !preOperation.contract) return operationBase; - if ( - operationBase.kind === ActivityOperKindEnum.transferFrom_ToAccount || - operationBase.kind === ActivityOperKindEnum.transferTo_FromAccount || - operationBase.kind === ActivityOperKindEnum.transferFrom || - operationBase.kind === ActivityOperKindEnum.transferTo || - operationBase.kind === ActivityOperKindEnum.approve - ) { + if (isTransferActivityOperKind(operationBase.kind) || operationBase.kind === ActivityOperKindEnum.approve) { const asset: TezosActivityAsset = { - contract: preOperation.contractAddress ?? TEZ_TOKEN_SLUG, + contract: preOperation.contract, tokenId, amount: preOperation.amountSigned, decimals: assetMetadata.decimals, diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index 1fa0f0cd87..4bde1e6c37 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -7,6 +7,7 @@ import { isTzktOperParam_LiquidityBaking, ParameterFa2Transfer } from 'lib/apis/tzkt/utils'; +import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { isTruthy } from 'lib/utils'; import { ZERO } from 'lib/utils/numbers'; @@ -83,11 +84,11 @@ function reduceOneTzktTransactionOperation( amount: string; from: OperationMember; to: OperationMember | string[]; - contractAddress?: string; + contract?: string; tokenId?: string; subtype?: TezosPreActivityTransactionOperation['subtype']; }) { - const { amount, from, to, contractAddress, tokenId, subtype } = args; + const { amount, from, to, contract, tokenId, subtype } = args; const activityOperBase = buildActivityOperBase( operation, @@ -101,11 +102,11 @@ function reduceOneTzktTransactionOperation( subtype, destination: operation.target, from, - to: Array.isArray(to) ? to.map(address => ({ address })) : [to] + to: Array.isArray(to) ? to.map(address => ({ address })) : [to], + contract, + tokenId }; - if (contractAddress != null) activityOper.contractAddress = contractAddress; - if (tokenId != null) activityOper.tokenId = tokenId; if (isTzktOperParam(operation.parameter)) activityOper.entrypoint = operation.parameter.entrypoint; return activityOper; @@ -119,28 +120,29 @@ function reduceOneTzktTransactionOperation( const from = operation.sender; const to = operation.target; const amount = String(operation.amount); + const contract = TEZ_TOKEN_SLUG; - return _buildReturn({ amount, from, to }); + return _buildReturn({ amount, from, to, contract, subtype: 'transfer' }); } else if (isTzktOperParam_Fa2_transfer(parameter)) { const values = reduceParameterFa2TransferValues(parameter.value, address); const firstVal = values[0]; // (!) Here we abandon other but 1st non-zero-amount values if (firstVal == null) return null; - const contractAddress = operation.target.address; + const contract = operation.target.address; const amount = firstVal.amount; const tokenId = firstVal.tokenId; const from = { ...operation.sender, address: firstVal.fromAddress }; const to = firstVal.toAddresses; - return _buildReturn({ amount, from, to, contractAddress, tokenId, subtype: 'transfer' }); + return _buildReturn({ amount, from, to, contract, tokenId, subtype: 'transfer' }); } else if (isTzktOperParam_Fa2_approve(parameter)) { const add_operator = parameter.value[0].add_operator; const from = operation.sender; const to = { address: add_operator.operator }; const amount = String(operation.amount); - const contractAddress = operation.target.address; + const contract = operation.target.address; const tokenId = add_operator.token_id; return _buildReturn({ @@ -148,12 +150,12 @@ function reduceOneTzktTransactionOperation( from, to, subtype: 'approve', - contractAddress, + contract, tokenId }); } else if (isTzktOperParam_Fa12(parameter)) { const amount = parameter.value.value; - const contractAddress = operation.target.address; + const contract = operation.target.address; if (parameter.entrypoint === 'approve') { if (amount === '0') return null; @@ -161,22 +163,22 @@ function reduceOneTzktTransactionOperation( const from = operation.sender; const to = { address: parameter.value.spender }; - return _buildReturn({ amount, from, to, contractAddress, subtype: 'approve' }); + return _buildReturn({ amount, from, to, contract, subtype: 'approve' }); } const from = { ...operation.sender, address: parameter.value.from }; const to = { address: parameter.value.to }; - if (from.address !== address && to.address !== address) return null; + // if (from.address !== address && to.address !== address) return null; - return _buildReturn({ amount, from, to, contractAddress }); + return _buildReturn({ amount, from, to, contract, subtype: 'transfer' }); } else if (isTzktOperParam_LiquidityBaking(parameter)) { const from = operation.sender; const to = operation.target; - const contractAddress = operation.target.address; + const contract = operation.target.address; const amount = parameter.value.quantity; - return _buildReturn({ amount, from, to, contractAddress }); + return _buildReturn({ amount, from, to, contract, subtype: 'transfer' }); } else { const from = operation.sender; const to = operation.target; diff --git a/src/lib/activity/tezos/types.ts b/src/lib/activity/tezos/types.ts index 992078b24a..e08cc16c21 100644 --- a/src/lib/activity/tezos/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -26,7 +26,8 @@ type PickedPropsFromTzktOperation = Pick; export interface TezosPreActivityOperationBase extends PickedPropsFromTzktOperation { sender: OperationMember; - contractAddress?: string; + contract?: string; + tokenId?: string; status: TezosPreActivityStatus; amountSigned: string; addedAt: string; @@ -40,7 +41,6 @@ export interface TezosPreActivityTransactionOperation extends TezosPreActivityOp to: OperationMember[]; destination: OperationMember; entrypoint?: string; - tokenId?: string; } export interface TezosPreActivityOtherOperation extends TezosPreActivityOperationBase { diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index fe412b0541..cccbb636d9 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -12,10 +12,16 @@ export enum ActivityOperKindEnum { export type Activity = TezosActivity | EvmActivity; -export interface TezosActivity { +interface ChainActivityBase { + chain: TempleChainKind; + hash: string; + /** Original, not filtered number of operations */ + operationsCount: number; +} + +export interface TezosActivity extends ChainActivityBase { chain: TempleChainKind.Tezos; chainId: string; - hash: string; blockExplorerUrl?: string; operations: TezosOperation[]; } @@ -25,20 +31,11 @@ export interface TezosOperation { asset?: TezosActivityAsset; } -export interface TezosActivityAsset { - contract: string; - tokenId?: string; - amount?: string | typeof InfinitySymbol; - decimals: number; - nft?: boolean; - symbol?: string; - iconURL?: string; -} +export interface TezosActivityAsset extends ActivityAssetBase {} -export interface EvmActivity { +export interface EvmActivity extends ChainActivityBase { chain: TempleChainKind.EVM; chainId: number; - hash: string; blockExplorerUrl?: string; operations: EvmOperation[]; } @@ -48,14 +45,18 @@ export interface EvmOperation { asset?: EvmActivityAsset; } -export interface EvmActivityAsset { +export interface EvmActivityAsset extends ActivityAssetBase { + iconURL?: string; +} + +interface ActivityAssetBase { contract: string; tokenId?: string; - amount?: string | typeof InfinitySymbol; + /** Signed (with `-` if applicable) */ + amount?: string | typeof InfinitySymbol; // TODO: Try without symbol decimals: number; nft?: boolean; symbol?: string; - iconURL?: string; } export interface OperationMember { diff --git a/src/lib/activity/utils.ts b/src/lib/activity/utils.ts new file mode 100644 index 0000000000..1892279a59 --- /dev/null +++ b/src/lib/activity/utils.ts @@ -0,0 +1,10 @@ +import { ActivityOperKindEnum } from './types'; + +export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { + return ( + kind === ActivityOperKindEnum.transferTo_FromAccount || + kind === ActivityOperKindEnum.transferFrom_ToAccount || + kind === ActivityOperKindEnum.transferFrom || + kind === ActivityOperKindEnum.transferTo + ); +} From 5f328219cd8b8f470b2e780a0511df289394904f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Sep 2024 22:05:05 +0300 Subject: [PATCH 24/74] TW-1479: [EVM] Transactions history. [Tezos] Token page. -- Asset icons --- .../ActivityItem/ActivityOperationBase.tsx | 48 ++++++++++--------- .../activity/ActivityItem/TezosActivity.tsx | 20 ++++---- .../ActivityItem/TezosActivityOperation.tsx | 4 +- .../templates/activity/TezosActivityTab.tsx | 4 +- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 028c8a3a5b..2d071508ec 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -15,13 +15,16 @@ import { atomsToTokens } from 'lib/temple/helpers'; import { EvmAssetIcon, TezosAssetIcon } from '../../AssetIcon'; +type Kind = ActivityOperKindEnum | 'bundle'; + interface Props { chainId: string | number; - kind: ActivityOperKindEnum | 'bundle'; + kind: Kind; hash: string; networkName: string; asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; + withoutAssetIcon?: boolean; } export interface ActivityItemBaseAssetProp { @@ -39,7 +42,8 @@ export const ActivityOperationBaseComponent: FC = ({ chainId, networkName, asset, - blockExplorerUrl + blockExplorerUrl, + withoutAssetIcon }) => { const assetSlug = asset ? typeof chainId === 'number' @@ -87,26 +91,24 @@ export const ActivityOperationBaseComponent: FC = ({ return (
- {assetSlug ? ( - typeof chainId === 'number' ? ( - - ) : ( - - ) - ) : ( + {withoutAssetIcon || !assetSlug ? ( + ) : typeof chainId === 'number' ? ( + + ) : ( + )} @@ -154,7 +156,7 @@ export const ActivityOperationBaseComponent: FC = ({ ); }; -const ActivityKindTitle: Record = { +const ActivityKindTitle: Record = { bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', [ActivityOperKindEnum.transferFrom_ToAccount]: 'Send', @@ -165,7 +167,7 @@ const ActivityKindTitle: Record = { [ActivityOperKindEnum.approve]: 'Approve' }; -const ActivityKindIconSvg: Record = { +const ActivityKindIconSvg: Record = { bundle: DocumentsSvg, [ActivityOperKindEnum.interaction]: DocumentsSvg, [ActivityOperKindEnum.transferFrom_ToAccount]: SendSvg, diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 67eea61836..de35bd8a4c 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -35,16 +35,15 @@ export const TezosActivityComponent = memo(({ activity, chain, accountAdd if (operations.length === 1) return ( -
- -
+ ); return ( @@ -131,6 +130,7 @@ const TezosActivityBatchComponent = memo<{ networkName={networkName} asset={batchAsset} blockExplorerUrl={blockExplorerUrl} + withoutAssetIcon={Boolean(assetSlug)} /> - - {expanded - ? operations.slice(3).map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} - - ) : null} -
+ ); }); + +interface BatchProps { + activity: EvmActivity; + chainId: number; + assetSlug?: string; + blockExplorerUrl?: string; + networkName: string; +} + +const EvmActivityBatchComponent = memo( + ({ activity, chainId, assetSlug, blockExplorerUrl, networkName }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); + + const { hash, operations } = activity; + + const faceSlug = useMemo(() => { + if (assetSlug) return assetSlug; + + for (const { kind, asset } of operations) { + if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) + return toEvmAssetSlug(asset.contract, asset.tokenId); + } + + return; + }, [operations, assetSlug]); + + const batchAsset = useMemo(() => { + if (!faceSlug) return; + + let faceAsset: EvmActivityAsset | undefined; + let faceAmount = ZERO; + + for (const { kind, asset } of operations) { + if ( + typeof asset?.amount === 'string' && + toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug && + isTransferActivityOperKind(kind) + ) { + faceAmount = faceAmount.plus(asset.amount); + if (!faceAsset) faceAsset = asset; + } + } + + if (!faceAsset) return; + + const batchAsset: ActivityItemBaseAssetProp = { + ...faceAsset, + amount: faceAmount.toFixed() + }; + + return batchAsset; + }, [operations, faceSlug]); + + return ( +
+ + + + + {expanded + ? operations.map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} +
+ ); + } +); diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 980d335c6a..4fb38f0fcd 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -11,10 +11,11 @@ interface Props { chain: EvmChain; networkName: string; blockExplorerUrl?: string; + withoutAssetIcon?: boolean; } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chain, networkName, blockExplorerUrl }) => { + ({ hash, operation, chain, networkName, blockExplorerUrl, withoutAssetIcon }) => { return ( ( networkName={networkName} asset={operation.asset} blockExplorerUrl={blockExplorerUrl} + withoutAssetIcon={withoutAssetIcon} /> ); } diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index de35bd8a4c..4c2b6e1ebc 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -58,106 +58,110 @@ export const TezosActivityComponent = memo(({ activity, chain, accountAdd ); }); -const TezosActivityBatchComponent = memo<{ +interface BatchProps { activity: TezosPreActivity; chainId: string; assetSlug?: string; blockExplorerUrl?: string; accountAddress: string; networkName: string; -}>(({ activity, chainId, assetSlug, blockExplorerUrl, accountAddress, networkName }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); +} - const { hash } = activity; +const TezosActivityBatchComponent = memo( + ({ activity, chainId, assetSlug, blockExplorerUrl, accountAddress, networkName }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); - const preOperations = activity.operations; + const { hash } = activity; - const getMetadata = useGetChainTokenOrGasMetadata(chainId); + const preOperations = activity.operations; - const operations = useMemoWithCompare( - () => - preOperations.map(o => { - const slug = o.contract ? toTezosAssetSlug(o.contract, o.tokenId) : undefined; - return parseTezosPreActivityOperation(o, accountAddress, slug ? getMetadata(slug) : undefined); - }), - [preOperations, getMetadata, accountAddress] - ); + const getMetadata = useGetChainTokenOrGasMetadata(chainId); + + const operations = useMemoWithCompare( + () => + preOperations.map(o => { + const slug = o.contract ? toTezosAssetSlug(o.contract, o.tokenId) : undefined; + return parseTezosPreActivityOperation(o, accountAddress, slug ? getMetadata(slug) : undefined); + }), + [preOperations, getMetadata, accountAddress] + ); - const faceSlug = useMemo(() => { - if (assetSlug) return assetSlug; + const faceSlug = useMemo(() => { + if (assetSlug) return assetSlug; - for (const { kind, asset } of operations) { - if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) - return toTezosAssetSlug(asset.contract, asset.tokenId); - } + for (const { kind, asset } of operations) { + if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) + return toTezosAssetSlug(asset.contract, asset.tokenId); + } - return; - }, [operations, assetSlug]); + return; + }, [operations, assetSlug]); - const batchAsset = useMemo(() => { - if (!faceSlug) return; + const batchAsset = useMemo(() => { + if (!faceSlug) return; - let faceAsset: TezosActivityAsset | undefined; - let faceAmount = ZERO; + let faceAsset: TezosActivityAsset | undefined; + let faceAmount = ZERO; - for (const { kind, asset } of operations) { - if ( - typeof asset?.amount === 'string' && - toTezosAssetSlug(asset.contract, asset.tokenId) === faceSlug && - isTransferActivityOperKind(kind) - ) { - faceAmount = faceAmount.plus(asset.amount); - if (!faceAsset) faceAsset = asset; + for (const { kind, asset } of operations) { + if ( + typeof asset?.amount === 'string' && + toTezosAssetSlug(asset.contract, asset.tokenId) === faceSlug && + isTransferActivityOperKind(kind) + ) { + faceAmount = faceAmount.plus(asset.amount); + if (!faceAsset) faceAsset = asset; + } } - } - - if (!faceAsset) return; - const batchAsset: ActivityItemBaseAssetProp = { - ...faceAsset, - amount: faceAmount.toFixed() - }; + if (!faceAsset) return; - return batchAsset; - }, [operations, faceSlug]); + const batchAsset: ActivityItemBaseAssetProp = { + ...faceAsset, + amount: faceAmount.toFixed() + }; - return ( -
- + return batchAsset; + }, [operations, faceSlug]); - - - {expanded - ? operations.map((operation, j) => ( - - {j > 0 && } - - - - )) - : null} -
- ); -}); + return ( +
+ + + + + {expanded + ? operations.map((operation, j) => ( + + {j > 0 && } + + + + )) + : null} +
+ ); + } +); diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityTab.tsx index 33e151034f..7714ea8896 100644 --- a/src/app/templates/activity/EvmActivityTab.tsx +++ b/src/app/templates/activity/EvmActivityTab.tsx @@ -100,7 +100,7 @@ export const EvmActivityTab: FC = ({ chainId, assetSlug }) loadMore={loadMore} > {activities.map(activity => ( - + ))} ); diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm.ts index f203e120a5..747bf8cee8 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm.ts @@ -187,7 +187,7 @@ export function parseGoldRushTransaction( }) .filter(isTruthy); - const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length), getMetadata); if (gasOperation) operations.unshift(gasOperation); @@ -252,7 +252,7 @@ export function parseGoldRushERC20Transfer( return { kind, asset }; }); - const gasOperation = parseGasTransfer(item, accountAddress, getMetadata); + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(transfers.length), getMetadata); if (gasOperation) operations.unshift(gasOperation); @@ -269,6 +269,8 @@ export function parseGoldRushERC20Transfer( function parseGasTransfer( item: GoldRushTransaction | GoldRushERC20Transfer, accountAddress: string, + /** Only way to suspect transfering to a contract, not an account */ + partOfBatch: boolean, getMetadata: EvmAssetMetadataGetter ): EvmOperation | null { const value: string = item.value?.toString() ?? '0'; @@ -276,9 +278,10 @@ function parseGasTransfer( if (value === '0') return null; const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) return ActivityOperKindEnum.transferFrom_ToAccount; - if (getEvmAddressSafe(item.to_address) === accountAddress) return ActivityOperKindEnum.transferTo_FromAccount; - // TODO: Check for transfers to contracts + if (getEvmAddressSafe(item.from_address) === accountAddress) + return partOfBatch ? ActivityOperKindEnum.transferFrom : ActivityOperKindEnum.transferFrom_ToAccount; + if (getEvmAddressSafe(item.to_address) === accountAddress) + return partOfBatch ? ActivityOperKindEnum.transferTo : ActivityOperKindEnum.transferTo_FromAccount; return null; })(); From 8bf64641d9182cc9a0ded1da03226822c09d5431 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 25 Sep 2024 01:53:54 +0300 Subject: [PATCH 26/74] TW-1479: [EVM] Transactions history. Explore Tab -> Page --- src/app/PageRouter.tsx | 5 +++ src/app/layouts/PageLayout/DefaultHeader.tsx | 4 ++- src/app/layouts/PageLayout/index.tsx | 6 ++-- src/app/layouts/containers.tsx | 2 +- src/app/pages/AccountSettings/index.tsx | 1 - src/app/pages/Activity/index.tsx | 29 ++++++++++++++++ src/app/pages/Home/ActionButtonsBar.tsx | 7 +--- src/app/pages/Home/Home.tsx | 5 +-- .../pages/Home/OtherComponents/AssetTab.tsx | 28 ++++++++-------- src/app/pages/Receive/Receive.tsx | 2 +- src/app/pages/Settings/Settings.tsx | 2 +- src/app/templates/SettingsGeneral/index.tsx | 10 +++--- ...ontainer.tsx => ActivityListContainer.tsx} | 2 +- ...EvmActivityTab.tsx => EvmActivityList.tsx} | 4 +-- .../templates/activity/TezosActivityTab.tsx | 4 +-- src/app/templates/activity/index.ts | 5 +++ src/app/templates/activity/index.tsx | 33 ------------------- 17 files changed, 74 insertions(+), 75 deletions(-) create mode 100644 src/app/pages/Activity/index.tsx rename src/app/templates/activity/{ActivityTabContainer.tsx => ActivityListContainer.tsx} (93%) rename src/app/templates/activity/{EvmActivityTab.tsx => EvmActivityList.tsx} (97%) create mode 100644 src/app/templates/activity/index.ts delete mode 100644 src/app/templates/activity/index.tsx diff --git a/src/app/PageRouter.tsx b/src/app/PageRouter.tsx index e049e1295f..7c20c9c76c 100644 --- a/src/app/PageRouter.tsx +++ b/src/app/PageRouter.tsx @@ -23,6 +23,7 @@ import { Notifications, NotificationsItem } from 'lib/notifications/components'; import { useTempleClient } from 'lib/temple/front'; import * as Woozie from 'lib/woozie'; +import { ActivityPage } from './pages/Activity'; import { ImportWallet } from './pages/ImportWallet'; import { Market } from './pages/Market'; import { StakingPage } from './pages/Staking'; @@ -74,6 +75,10 @@ const ROUTE_MAP = Woozie.createMap([ )) ], + [ + '/activity/:chainKind?/:chainId?', + onlyReady(({ chainKind, chainId }) => ) + ], ['/connect-ledger', onlyReady(onlyInFullPage(() => ))], ['/receive', onlyReady(() => )], [ diff --git a/src/app/layouts/PageLayout/DefaultHeader.tsx b/src/app/layouts/PageLayout/DefaultHeader.tsx index 5ec03979cf..949e12f3a3 100644 --- a/src/app/layouts/PageLayout/DefaultHeader.tsx +++ b/src/app/layouts/PageLayout/DefaultHeader.tsx @@ -68,7 +68,9 @@ export const DefaultHeader = memo>(
- {pageTitle &&
{pageTitle}
} + {pageTitle && ( +
{pageTitle}
+ )}
{headerRightElem}
diff --git a/src/app/layouts/PageLayout/index.tsx b/src/app/layouts/PageLayout/index.tsx index dadc15ccad..09441bdbea 100644 --- a/src/app/layouts/PageLayout/index.tsx +++ b/src/app/layouts/PageLayout/index.tsx @@ -42,6 +42,7 @@ export interface PageLayoutProps extends DefaultHeaderProps, ScrollEdgesVisibili /** With this given, header props are ignored */ Header?: ComponentType; contentPadding?: boolean; + dimBg?: boolean; paperClassName?: string; headerChildren?: ReactNode; } @@ -50,7 +51,7 @@ const PageLayout: FC> = ({ Header, children, contentPadding = true, - paperClassName, + dimBg = true, headerChildren, onBottomEdgeVisibilityChange, bottomEdgeThreshold, @@ -74,7 +75,6 @@ const PageLayout: FC> = ({
> = ({ > {Header ?
: {headerChildren}} -
+
{children}
diff --git a/src/app/layouts/containers.tsx b/src/app/layouts/containers.tsx index e5a6a69c38..8064bac578 100644 --- a/src/app/layouts/containers.tsx +++ b/src/app/layouts/containers.tsx @@ -29,7 +29,7 @@ export const ContentContainer = forwardRef(({ id }) => { diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx new file mode 100644 index 0000000000..d603dc7ac6 --- /dev/null +++ b/src/app/pages/Activity/index.tsx @@ -0,0 +1,29 @@ +import React, { memo } from 'react'; + +import PageLayout from 'app/layouts/PageLayout'; +import { ActivityListContainer, EvmActivityList, TezosActivityList } from 'app/templates/activity'; +import { ChainSelectSection, useChainSelectController } from 'app/templates/ChainSelect'; + +interface Props { + chainKind: string; + chainId: string; +} + +export const ActivityPage = memo(({ chainKind, chainId }) => { + const chainSelectController = useChainSelectController(); + const network = chainSelectController.value; + + return ( + + + + + {network.kind === 'tezos' ? ( + + ) : ( + + )} + + + ); +}); diff --git a/src/app/pages/Home/ActionButtonsBar.tsx b/src/app/pages/Home/ActionButtonsBar.tsx index 11a3cb269e..39e6b615bc 100644 --- a/src/app/pages/Home/ActionButtonsBar.tsx +++ b/src/app/pages/Home/ActionButtonsBar.tsx @@ -63,12 +63,7 @@ export const ActionButtonsBar = memo(({ chainKind, chainId, assetSlug testID={HomeSelectors.swapButton} /> - + (props => { {showScamTokenAlert && } -
+
{chainKind && chainId && assetSlug ? ( ) : ( @@ -109,8 +108,6 @@ const Home = memo(props => { switch (tabSlug) { case 'collectibles': return ; - case 'activity': - return ; default: return ; } diff --git a/src/app/pages/Home/OtherComponents/AssetTab.tsx b/src/app/pages/Home/OtherComponents/AssetTab.tsx index db032e0e07..e026cbcc92 100644 --- a/src/app/pages/Home/OtherComponents/AssetTab.tsx +++ b/src/app/pages/Home/OtherComponents/AssetTab.tsx @@ -3,8 +3,8 @@ import React, { FC, memo } from 'react'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import { useLocationSearchParamValue } from 'app/hooks/use-location'; import { ContentContainer } from 'app/layouts/containers'; -import { TezosActivityTab, EvmActivityTab } from 'app/templates/activity'; -import { ActivityTabContainer } from 'app/templates/activity/ActivityTabContainer'; +import { TezosActivityList, EvmActivityList } from 'app/templates/activity'; +import { ActivityListContainer } from 'app/templates/activity/ActivityListContainer'; import AssetInfo from 'app/templates/AssetInfo'; import { TabsBar, TabsBarTabInterface } from 'app/templates/TabBar'; import { TEZ_TOKEN_SLUG, isTezAsset } from 'lib/assets'; @@ -53,9 +53,9 @@ const TezosGasTab = memo<{ tezosChainId: string }>(({ tezosChainId }) => { {tabName === 'activity' ? ( - - - + + + ) : ( @@ -79,9 +79,9 @@ const TezosTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - - - + + + ) : ( )} @@ -96,9 +96,9 @@ interface EvmAssetTabProps { const EvmAssetTab: FC = ({ chainId, assetSlug }) => isEvmNativeTokenSlug(assetSlug) ? ( - - - + + + ) : ( ); @@ -112,9 +112,9 @@ const EvmTokenTab = memo(({ chainId, assetSlug }) => { {tabName === 'activity' ? ( - - - + + + ) : ( )} diff --git a/src/app/pages/Receive/Receive.tsx b/src/app/pages/Receive/Receive.tsx index 54b406058b..0ad30a4a05 100644 --- a/src/app/pages/Receive/Receive.tsx +++ b/src/app/pages/Receive/Receive.tsx @@ -27,7 +27,7 @@ export const Receive = memo(() => { const resetReceivePayload = useCallback(() => setReceivePayload(null), []); return ( - } paperClassName="!bg-background"> + }> {receivePayload && } diff --git a/src/app/pages/Settings/Settings.tsx b/src/app/pages/Settings/Settings.tsx index 097fbd7fdd..11a7d22359 100644 --- a/src/app/pages/Settings/Settings.tsx +++ b/src/app/pages/Settings/Settings.tsx @@ -152,7 +152,7 @@ const Settings = memo(({ tabSlug }) => { return ( {!activeTab && } diff --git a/src/app/templates/SettingsGeneral/index.tsx b/src/app/templates/SettingsGeneral/index.tsx index 79bc7cbe87..c0501d3181 100644 --- a/src/app/templates/SettingsGeneral/index.tsx +++ b/src/app/templates/SettingsGeneral/index.tsx @@ -2,11 +2,11 @@ import React, { memo } from 'react'; import { NotificationsSettings } from 'lib/notifications/components'; -import AnalyticsSettings from './Components/AnalyticsSettings'; -import FiatCurrencySelect from './Components/FiatCurrencySelect'; -import LocaleSelect from './Components/LocaleSelect'; -import LockUpSettings from './Components/LockUpSettings'; -import PopupSettings from './Components/PopupSettings'; +import AnalyticsSettings from './components/AnalyticsSettings'; +import FiatCurrencySelect from './components/FiatCurrencySelect'; +import LocaleSelect from './components/LocaleSelect'; +import LockUpSettings from './components/LockUpSettings'; +import PopupSettings from './components/PopupSettings'; const GeneralSettings = memo(() => (
diff --git a/src/app/templates/activity/ActivityTabContainer.tsx b/src/app/templates/activity/ActivityListContainer.tsx similarity index 93% rename from src/app/templates/activity/ActivityTabContainer.tsx rename to src/app/templates/activity/ActivityListContainer.tsx index 4d5a284488..ab634c659d 100644 --- a/src/app/templates/activity/ActivityTabContainer.tsx +++ b/src/app/templates/activity/ActivityListContainer.tsx @@ -13,7 +13,7 @@ interface Props extends PropsWithChildren { assetSlug?: string; } -export const ActivityTabContainer: FC = ({ children, chainId, assetSlug }) => { +export const ActivityListContainer: FC = ({ children, chainId, assetSlug }) => { const shouldShowPartnersPromo = useShouldShowPartnersPromoSelector(); const promotion = useMemo(() => { diff --git a/src/app/templates/activity/EvmActivityTab.tsx b/src/app/templates/activity/EvmActivityList.tsx similarity index 97% rename from src/app/templates/activity/EvmActivityTab.tsx rename to src/app/templates/activity/EvmActivityList.tsx index 7714ea8896..cdf8f96398 100644 --- a/src/app/templates/activity/EvmActivityTab.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -17,12 +17,12 @@ import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; -interface EvmActivityTabProps { +interface Props { chainId: number; assetSlug?: string; } -export const EvmActivityTab: FC = ({ chainId, assetSlug }) => { +export const EvmActivityList: FC = ({ chainId, assetSlug }) => { const network = useEvmChainByChainId(chainId); const accountAddress = useAccountAddressForEvm(); diff --git a/src/app/templates/activity/TezosActivityTab.tsx b/src/app/templates/activity/TezosActivityTab.tsx index 37dc675776..2af647154c 100644 --- a/src/app/templates/activity/TezosActivityTab.tsx +++ b/src/app/templates/activity/TezosActivityTab.tsx @@ -17,12 +17,12 @@ import { TezosActivityComponent } from './ActivityItem'; const INITIAL_NUMBER = 30; const LOAD_STEP = 30; -interface TezosActivityTabProps { +interface Props { tezosChainId: string; assetSlug?: string; } -export const TezosActivityTab = memo(({ tezosChainId, assetSlug }) => { +export const TezosActivityList = memo(({ tezosChainId, assetSlug }) => { const network = useTezosChainByChainId(tezosChainId); const accountAddress = useAccountAddressForTezos(); diff --git a/src/app/templates/activity/index.ts b/src/app/templates/activity/index.ts new file mode 100644 index 0000000000..9d54b74553 --- /dev/null +++ b/src/app/templates/activity/index.ts @@ -0,0 +1,5 @@ +import { ActivityListContainer } from './ActivityListContainer'; +import { EvmActivityList } from './EvmActivityList'; +import { TezosActivityList } from './TezosActivityTab'; + +export { ActivityListContainer, TezosActivityList, EvmActivityList }; diff --git a/src/app/templates/activity/index.tsx b/src/app/templates/activity/index.tsx deleted file mode 100644 index cc6059442c..0000000000 --- a/src/app/templates/activity/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { memo } from 'react'; - -import { ContentContainer } from 'app/layouts/containers'; -import { useChainSelectController, ChainSelectSection } from 'app/templates/ChainSelect'; - -import { ActivityTabContainer } from './ActivityTabContainer'; -import { EvmActivityTab } from './EvmActivityTab'; -import { TezosActivityTab } from './TezosActivityTab'; - -export { TezosActivityTab, EvmActivityTab }; - -export const MultichainActivityTab = memo(() => { - const chainSelectController = useChainSelectController(); - const network = chainSelectController.value; - - return ( - <> -
- - - - - - {network.kind === 'tezos' ? ( - - ) : ( - - )} - - - - ); -}); From c3f15098883f432897a78a8f2bbac6255c7f4951 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 26 Sep 2024 23:03:20 +0300 Subject: [PATCH 27/74] TW-1479: [EVM] Transactions history. Filters. By network. WIP --- src/app/PageRouter.tsx | 5 +- src/app/a11y/ContentFader/index.tsx | 9 -- src/app/a11y/ContentFader/styles.module.css | 17 -- src/app/a11y/content-fader/index.tsx | 5 + src/app/a11y/content-fader/styles.module.css | 3 + src/app/atoms/PageModal/index.tsx | 20 ++- src/app/atoms/TextButton.tsx | 24 ++- src/app/layouts/PageLayout/index.tsx | 5 +- src/app/pages/Activity/index.tsx | 149 +++++++++++++++--- .../AddTokenModal/SelectNetworkPage.tsx | 9 +- .../Tokens/components/TokensTabBase.tsx | 2 +- .../ImportAccountModal/forms/private-key.tsx | 1 - .../ImportAccountModal/forms/watch-only.tsx | 1 - src/app/templates/NetworkSelectModal.tsx | 55 +++++-- src/app/templates/SeedPhraseInput/index.tsx | 1 - .../activity/ActivityItem/EvmActivity.tsx | 11 +- .../activity/ActivityItem/TezosActivity.tsx | 11 +- tailwind.config.js | 1 - 18 files changed, 229 insertions(+), 100 deletions(-) delete mode 100644 src/app/a11y/ContentFader/index.tsx delete mode 100644 src/app/a11y/ContentFader/styles.module.css create mode 100644 src/app/a11y/content-fader/index.tsx create mode 100644 src/app/a11y/content-fader/styles.module.css diff --git a/src/app/PageRouter.tsx b/src/app/PageRouter.tsx index 7c20c9c76c..7f5ffa2e8f 100644 --- a/src/app/PageRouter.tsx +++ b/src/app/PageRouter.tsx @@ -75,10 +75,7 @@ const ROUTE_MAP = Woozie.createMap([ )) ], - [ - '/activity/:chainKind?/:chainId?', - onlyReady(({ chainKind, chainId }) => ) - ], + ['/activity/:chainKind?/:chainId?', onlyReady(() => )], ['/connect-ledger', onlyReady(onlyInFullPage(() => ))], ['/receive', onlyReady(() => )], [ diff --git a/src/app/a11y/ContentFader/index.tsx b/src/app/a11y/ContentFader/index.tsx deleted file mode 100644 index 0d43af54ae..0000000000 --- a/src/app/a11y/ContentFader/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -import clsx from 'clsx'; - -import ModStyles from './styles.module.css'; - -export const ACTIVATE_CONTENT_FADER_CLASSNAME = ModStyles.fadeContent; - -export const ContentFader = () =>
; diff --git a/src/app/a11y/ContentFader/styles.module.css b/src/app/a11y/ContentFader/styles.module.css deleted file mode 100644 index 1a6818db28..0000000000 --- a/src/app/a11y/ContentFader/styles.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.contentFader { - position: absolute; - inset: 0; - - border-radius: inherit; - - /* Since it always hangs over content, we make it pervious */ - pointer-events: none; - - background-color: black; - opacity: 0; - transition: opacity 200ms ease-in-out; -} - -.fadeContent .contentFader { - opacity: 0.15; -} diff --git a/src/app/a11y/content-fader/index.tsx b/src/app/a11y/content-fader/index.tsx new file mode 100644 index 0000000000..3db76fd327 --- /dev/null +++ b/src/app/a11y/content-fader/index.tsx @@ -0,0 +1,5 @@ +import ModStyles from './styles.module.css'; + +export const ACTIVATE_CONTENT_FADER_CLASSNAME = ModStyles.activateContentFader; + +export const FADABLE_CONTENT_CLASSNAME = ModStyles.fadeableContent; diff --git a/src/app/a11y/content-fader/styles.module.css b/src/app/a11y/content-fader/styles.module.css new file mode 100644 index 0000000000..5f0e4541a7 --- /dev/null +++ b/src/app/a11y/content-fader/styles.module.css @@ -0,0 +1,3 @@ +.activateContentFader .fadeableContent { + filter: brightness(0.85); +} diff --git a/src/app/atoms/PageModal/index.tsx b/src/app/atoms/PageModal/index.tsx index 179525d139..d11dde790e 100644 --- a/src/app/atoms/PageModal/index.tsx +++ b/src/app/atoms/PageModal/index.tsx @@ -3,7 +3,7 @@ import React, { PropsWithChildren, memo } from 'react'; import clsx from 'clsx'; import Modal from 'react-modal'; -import { ACTIVATE_CONTENT_FADER_CLASSNAME } from 'app/a11y/ContentFader'; +import { ACTIVATE_CONTENT_FADER_CLASSNAME } from 'app/a11y/content-fader'; import { useAppEnv } from 'app/env'; import { ReactComponent as ChevronLeftIcon } from 'app/icons/base/chevron_left.svg'; import { ReactComponent as ExIcon } from 'app/icons/base/x.svg'; @@ -11,6 +11,7 @@ import { LAYOUT_CONTAINER_CLASSNAME } from 'app/layouts/containers'; import { TestIDProps } from 'lib/analytics'; import { IconBase } from '../IconBase'; +import { SuspenseContainer } from '../SuspenseContainer'; import ModStyles from './styles.module.css'; @@ -21,10 +22,21 @@ interface Props extends TestIDProps { onRequestClose: EmptyFn; onGoBack?: EmptyFn; animated?: boolean; + contentPadding?: boolean; } export const PageModal = memo>( - ({ title, opened, shouldShowBackButton, onRequestClose, onGoBack, children, testID, animated = true }) => { + ({ + title, + opened, + shouldShowBackButton, + onRequestClose, + onGoBack, + children, + testID, + animated = true, + contentPadding = false + }) => { const { fullPage } = useAppEnv(); return ( @@ -67,7 +79,9 @@ export const PageModal = memo>(
-
{children}
+
+ {children} +
); } diff --git a/src/app/atoms/TextButton.tsx b/src/app/atoms/TextButton.tsx index 4eeb56d33e..f654f9499a 100644 --- a/src/app/atoms/TextButton.tsx +++ b/src/app/atoms/TextButton.tsx @@ -19,22 +19,21 @@ interface Props extends TestIDProps { export const TextButton = memo( forwardRef>( ({ Icon, color, onClick, testID, testIDProperties, children, className }, ref) => { - const { textClassName, iconClassName } = useMemo(() => { + const { btnClassName, iconClassName } = useMemo(() => { switch (color) { case 'black': return { - textClassName: 'focus:text-black', + btnClassName: 'hover:bg-secondary-low focus:text-black', iconClassName: 'text-secondary focus:text-secondary-hover' }; - case 'blue': + case 'grey': return { - textClassName: 'text-secondary focus:text-secondary-hover', - iconClassName: 'text-secondary focus:text-secondary-hover' + btnClassName: 'text-grey-1 hover:bg-grey-4 focus:text-grey-2', + iconClassName: 'text-grey-2 focus:text-grey-3' }; - default: + default: // blue return { - textClassName: 'text-grey-1 focus:text-grey-2', - iconClassName: 'text-grey-2 focus:text-grey-3' + btnClassName: 'text-secondary hover:bg-secondary-low focus:text-secondary-hover' }; } }, [color]); @@ -42,16 +41,13 @@ export const TextButton = memo( return ( ); diff --git a/src/app/layouts/PageLayout/index.tsx b/src/app/layouts/PageLayout/index.tsx index 09441bdbea..113a2d75ad 100644 --- a/src/app/layouts/PageLayout/index.tsx +++ b/src/app/layouts/PageLayout/index.tsx @@ -2,7 +2,7 @@ import React, { ComponentType, createContext, FC, ReactNode, RefObject, useConte import clsx from 'clsx'; -import { ContentFader } from 'app/a11y/ContentFader'; +import { FADABLE_CONTENT_CLASSNAME } from 'app/a11y/content-fader'; import DocBg from 'app/a11y/DocBg'; import Spinner from 'app/atoms/Spinner/Spinner'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; @@ -143,6 +143,7 @@ const ContentPaper: FC = ({ id={APP_CONTENT_PAPER_DOM_ID} className={clsx( LAYOUT_CONTAINER_CLASSNAME, + FADABLE_CONTENT_CLASSNAME, 'relative flex flex-col bg-white', !SCROLL_DOCUMENT && 'overflow-y-auto', appEnv.fullPage && 'min-h-80 rounded-md shadow-bottom', @@ -150,8 +151,6 @@ const ContentPaper: FC = ({ )} > {children} - - ); diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index d603dc7ac6..ee7df6fddc 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -1,29 +1,140 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo, useState } from 'react'; +import { IconBase } from 'app/atoms'; +import { PageModal } from 'app/atoms/PageModal'; +import { TextButton } from 'app/atoms/TextButton'; +import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; +import { ReactComponent as FilterOffIcon } from 'app/icons/base/filteroff.svg'; import PageLayout from 'app/layouts/PageLayout'; +import { useAssetsFilterOptionsSelector } from 'app/store/assets-filter-options/selectors'; +import { FilterChain } from 'app/store/assets-filter-options/state'; import { ActivityListContainer, EvmActivityList, TezosActivityList } from 'app/templates/activity'; -import { ChainSelectSection, useChainSelectController } from 'app/templates/ChainSelect'; +import { NetworkSelectModalContent } from 'app/templates/NetworkSelectModal'; +import { T } from 'lib/i18n'; +import { useBooleanState } from 'lib/ui/hooks'; +import { UNDER_DEVELOPMENT_MSG } from 'temple/evm/under_dev_msg'; +import { useAllEvmChains, useAllTezosChains } from 'temple/front'; +import { TempleChainKind } from 'temple/types'; -interface Props { - chainKind: string; - chainId: string; -} +export const ActivityPage = memo(() => { + const { filterChain: initFilterChain } = useAssetsFilterOptionsSelector(); + + const [filterChain, setFilterChain] = useState(initFilterChain); -export const ActivityPage = memo(({ chainKind, chainId }) => { - const chainSelectController = useChainSelectController(); - const network = chainSelectController.value; + const [filtersModalOpen, setFiltersModalOpen, setFiltersModalClosed] = useBooleanState(false); + + const handleChainChange = useMemo( + () => + initFilterChain + ? undefined + : (chain: FilterChain) => { + setFilterChain(chain); + setFiltersModalClosed(); + }, + [] + ); return ( - - - - - {network.kind === 'tezos' ? ( - - ) : ( - - )} - + + } + > + + + {filterChain ? ( + + {filterChain.kind === 'tezos' ? ( + + ) : ( + + )} + + ) : ( +
{UNDER_DEVELOPMENT_MSG}
+ )}
); }); + +interface FiltersModalProps { + open: boolean; + filterChain: FilterChain; + handleChainChange?: (chain: FilterChain) => void; + onRequestClose: EmptyFn; +} + +const FiltersModal = memo(({ open, filterChain, handleChainChange, onRequestClose }) => { + const [networksMode, setModeToNetworks, setModeToFilters] = useBooleanState(false); + + return ( + + {networksMode && handleChainChange ? ( + { + handleChainChange(chain); + setModeToFilters(); + }} + /> + ) : ( + + )} + + ); +}); + +interface FiltersMainContentProps { + filterChain: FilterChain; + onOpenNetworksClick?: EmptyFn; +} + +const FiltersMainContent = memo(({ filterChain, onOpenNetworksClick }) => { + const tezosChains = useAllTezosChains(); + const evmChains = useAllEvmChains(); + + const chain = useMemo(() => { + if (!filterChain) return null; + + if (filterChain.kind === TempleChainKind.Tezos) return tezosChains[filterChain.chainId]; + + return evmChains[filterChain.chainId]; + }, [filterChain, evmChains, tezosChains]); + + const disabledNetworkChange = !onOpenNetworksClick; + + return ( + <> +
+ Filter by network + + + {chain ? chain.nameI18nKey ? : chain.name : 'All Networks'} + +
+ +
Filters by activity kind
+ + ); +}); diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx index de661c9ed6..fe10b64825 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx @@ -10,6 +10,7 @@ import { SearchBarField } from 'app/templates/SearchField'; import { navigate } from 'lib/woozie'; import { EvmChain, + OneOfChains, TezosChain, useAccountAddressForEvm, useAccountAddressForTezos, @@ -20,8 +21,8 @@ import { type Network = EvmChain | TezosChain; interface SelectNetworkPageProps { - selectedNetwork: Network; - onNetworkSelect: (network: Network) => void; + selectedNetwork: OneOfChains; + onNetworkSelect: (network: OneOfChains) => void; } export const SelectNetworkPage: FC = ({ selectedNetwork, onNetworkSelect }) => { @@ -42,13 +43,13 @@ export const SelectNetworkPage: FC = ({ selectedNetwork, const filteredNetworks = useMemo( () => searchValueDebounced.length - ? searchAndFilterNetworksByName(sortedNetworks, searchValueDebounced) + ? searchAndFilterNetworksByName(sortedNetworks, searchValueDebounced) : sortedNetworks, [searchValueDebounced, sortedNetworks] ); const handleNetworkSelect = useCallback( - (network: Network | string) => { + (network: OneOfChains | string) => { if (typeof network === 'string') return; onNetworkSelect(network); diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/TokensTabBase.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/TokensTabBase.tsx index 08947c43d1..f0cf18b661 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/TokensTabBase.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/TokensTabBase.tsx @@ -59,7 +59,7 @@ export const TokensTabBase: FC> = ({ {filtersOpened ? ( ) : ( - 0}> + {manageActive ? null : } {tokensCount === 0 ? ( diff --git a/src/app/templates/ImportAccountModal/forms/private-key.tsx b/src/app/templates/ImportAccountModal/forms/private-key.tsx index c73c13d17e..cdcd5e3b31 100644 --- a/src/app/templates/ImportAccountModal/forms/private-key.tsx +++ b/src/app/templates/ImportAccountModal/forms/private-key.tsx @@ -106,7 +106,6 @@ export const PrivateKeyForm = memo(({ onSuccess }) => { additonalActionButtons={ keyValue ? null : ( (({ onSuccess }) => { additonalActionButtons={ addressValue ? null : ( (({ opened, selectedNetwork, onRequestClose }) => { + const handleNetworkSelect = useCallback( + (network: OneOfChains | null) => { + dispatch(setAssetsFilterChain(network)); + onRequestClose(); + }, + [onRequestClose] + ); + + return ( + + + + ); +}); + +interface ContentProps { + opened: boolean; + selectedNetwork: FilterChain; + handleNetworkSelect: (chain: OneOfChains | null) => void; +} + +export const NetworkSelectModalContent = memo(({ opened, selectedNetwork, handleNetworkSelect }) => { const accountTezAddress = useAccountAddressForTezos(); const accountEvmAddress = useAccountAddressForEvm(); @@ -71,23 +96,22 @@ export const NetworkSelectModal = memo(({ opened, selectedNetwork, onRequ else if (!opened) setAttractSelectedNetwork(true); }, [opened, searchValueDebounced]); - const handleNetworkSelect = useCallback( - (network: Network) => { - dispatch(setAssetsFilterChain(typeof network === 'string' ? null : network)); - onRequestClose(); + const onNetworkSelect = useCallback( + (network: OneOfChains | string) => { + handleNetworkSelect(typeof network === 'string' ? null : network); }, - [onRequestClose] + [handleNetworkSelect] ); return ( - -
+ <> +
navigate('settings/networks')} />
-
+
{filteredNetworks.length === 0 && } {filteredNetworks.map(network => ( @@ -97,21 +121,21 @@ export const NetworkSelectModal = memo(({ opened, selectedNetwork, onRequ activeNetwork={selectedNetwork} attractSelf={attractSelectedNetwork} showBalance - onClick={handleNetworkSelect} + onClick={onNetworkSelect} /> ))}
- + ); }); interface NetworkProps { - network: Network; + network: OneOfChains | string; activeNetwork: FilterChain; attractSelf?: boolean; showBalance?: boolean; iconSize?: Size; - onClick?: (network: Network) => void; + onClick?: (network: OneOfChains | string) => void; } export const Network: FC = ({ @@ -154,8 +178,10 @@ export const Network: FC = ({ >
{Icon} +
{isAllNetworks ? ALL_NETWORKS : network.name} + {showBalance && ( :{' '} @@ -164,6 +190,7 @@ export const Network: FC = ({ )}
+
); diff --git a/src/app/templates/SeedPhraseInput/index.tsx b/src/app/templates/SeedPhraseInput/index.tsx index 73574baca9..bdcbce7948 100644 --- a/src/app/templates/SeedPhraseInput/index.tsx +++ b/src/app/templates/SeedPhraseInput/index.tsx @@ -235,7 +235,6 @@ export const SeedPhraseInput: FC = ({ ) : ( ( - {expanded - ? operations.map((operation, j) => ( + +
+ {operations.map((operation, j) => ( {j > 0 && } @@ -143,8 +145,9 @@ const EvmActivityBatchComponent = memo( blockExplorerUrl={blockExplorerUrl} /> - )) - : null} + ))} +
+
); } diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 4c2b6e1ebc..c2eab6a7fb 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -3,6 +3,7 @@ import React, { memo, useMemo } from 'react'; import clsx from 'clsx'; import { IconBase } from 'app/atoms'; +import { PageModal } from 'app/atoms/PageModal'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { TezosActivityAsset, parseTezosPreActivityOperation } from 'lib/activity'; import { TezosPreActivity } from 'lib/activity/tezos/types'; @@ -145,8 +146,9 @@ const TezosActivityBatchComponent = memo( - {expanded - ? operations.map((operation, j) => ( + +
+ {operations.map((operation, j) => ( {j > 0 && } @@ -159,8 +161,9 @@ const TezosActivityBatchComponent = memo( blockExplorerUrl={blockExplorerUrl} /> - )) - : null} + ))} +
+
); } diff --git a/tailwind.config.js b/tailwind.config.js index d5ab1cff4e..f2d5cd37ca 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -269,7 +269,6 @@ module.exports = { 45: '45', header: 50, sticky: 100, - 'content-fade': 200, 'overlay-promo': 300, overlay: 400, 'overlay-confirm': 500, From b9dd96a9e472c59e14dba656a9978012ec450807 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 1 Oct 2024 02:42:45 +0300 Subject: [PATCH 28/74] TW-1479: [EVM] Transactions history. Multichain. WIP: For EVM --- src/app/pages/Activity/index.tsx | 10 +- .../templates/activity/EvmActivityList.tsx | 58 +----- src/app/templates/activity/MultichainList.tsx | 189 ++++++++++++++++++ ...sActivityTab.tsx => TezosActivityList.tsx} | 5 +- src/app/templates/activity/index.ts | 5 +- src/lib/activity/evm/fetch.ts | 56 ++++++ src/lib/activity/evm/index.ts | 3 + src/lib/activity/{evm.ts => evm/parse.ts} | 10 +- src/lib/activity/tezos/index.ts | 3 +- src/lib/activity/tezos/pre-parse.ts | 6 +- src/lib/activity/tezos/types.ts | 1 + src/lib/activity/types.ts | 1 + src/lib/metadata/index.ts | 25 ++- 13 files changed, 301 insertions(+), 71 deletions(-) create mode 100644 src/app/templates/activity/MultichainList.tsx rename src/app/templates/activity/{TezosActivityTab.tsx => TezosActivityList.tsx} (99%) create mode 100644 src/lib/activity/evm/fetch.ts create mode 100644 src/lib/activity/evm/index.ts rename src/lib/activity/{evm.ts => evm/parse.ts} (97%) diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index ee7df6fddc..df59c11bc4 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -8,11 +8,15 @@ import { ReactComponent as FilterOffIcon } from 'app/icons/base/filteroff.svg'; import PageLayout from 'app/layouts/PageLayout'; import { useAssetsFilterOptionsSelector } from 'app/store/assets-filter-options/selectors'; import { FilterChain } from 'app/store/assets-filter-options/state'; -import { ActivityListContainer, EvmActivityList, TezosActivityList } from 'app/templates/activity'; +import { + ActivityListContainer, + EvmActivityList, + TezosActivityList, + MultichainActivityList +} from 'app/templates/activity'; import { NetworkSelectModalContent } from 'app/templates/NetworkSelectModal'; import { T } from 'lib/i18n'; import { useBooleanState } from 'lib/ui/hooks'; -import { UNDER_DEVELOPMENT_MSG } from 'temple/evm/under_dev_msg'; import { useAllEvmChains, useAllTezosChains } from 'temple/front'; import { TempleChainKind } from 'temple/types'; @@ -57,7 +61,7 @@ export const ActivityPage = memo(() => { )} ) : ( -
{UNDER_DEVELOPMENT_MSG}
+ )}
); diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index cdf8f96398..cdc01ac161 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -6,11 +6,9 @@ import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { EvmActivity, parseGoldRushTransaction, parseGoldRushERC20Transfer } from 'lib/activity'; -import { getEvmERC20Transfers, getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; -import { fromAssetSlug } from 'lib/assets'; -import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; -import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; +import { EvmActivity } from 'lib/activity'; +import { getEvmAssetTransactions } from 'lib/activity/evm'; +import { useGetEvmChainAssetMetadata } from 'lib/metadata'; import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; @@ -37,7 +35,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { const { stop: stopLoading, stopAndBuildChecker } = useStopper(); - const getMetadata = useGetEvmAssetMetadata(chainId); + const getMetadata = useGetEvmChainAssetMetadata(chainId); const loadActivities = async (activities: EvmActivity[], shouldStop: () => boolean, page?: number) => { // if (isLoading) return; @@ -51,7 +49,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { newNextPage = data.nextPage; newActivities = data.activities; - if (shouldStop()) return; + if (shouldStop()) return; // TODO: If so - save time on parsing then) } catch (error) { if (shouldStop()) return; setIsLoading(false); @@ -105,49 +103,3 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { ); }; - -async function getEvmAssetTransactions( - walletAddress: string, - chainId: number, - getMetadata: EvmAssetMetadataGetter, - assetSlug?: string, - page?: number -) { - if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { - const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page); - - return { - activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), - nextPage - }; - } - - const [contract] = fromAssetSlug(assetSlug); - - // let nextPage: number | nullish = page; - - // while (nextPage !== null) { - // const data = await getEvmTransactions(walletAddress, chainId, nextPage); - - // const activities = data.items - // .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) - // .filter(a => - // a.operations.some( - // ({ asset }) => asset && asset.contract === contract && (asset.tokenId == null || asset.tokenId === tokenId) - // ) - // ); - - // if (activities.length) return { activities, nextPage: data.nextPage }; - - // nextPage = data.nextPage; - // } - - // return { nextPage: null, activities: [] }; - - const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page); - - return { - activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), - nextPage - }; -} diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx new file mode 100644 index 0000000000..7054ce6539 --- /dev/null +++ b/src/app/templates/activity/MultichainList.tsx @@ -0,0 +1,189 @@ +import React, { memo, useEffect, useMemo, useState } from 'react'; + +import { EmptyState } from 'app/atoms/EmptyState'; +import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; +import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; +import { Activity, EvmActivity } from 'lib/activity'; +import { getEvmAssetTransactions } from 'lib/activity/evm'; +import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; +import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; +import { + useAccountAddressForEvm, + useAccountAddressForTezos, + useAllEvmChains, + useAllTezosChains, + useEnabledEvmChains, + useEnabledTezosChains +} from 'temple/front'; + +import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; + +export const MultichainActivityList = memo(() => { + useLoadPartnersPromo(); + + const tezosChains = useEnabledTezosChains(); + const evmChains = useEnabledEvmChains(); + + const allTezosChains = useAllTezosChains(); + const allEvmChains = useAllEvmChains(); + + const tezAccAddress = useAccountAddressForTezos(); + const evmAccAddress = useAccountAddressForEvm(); + + const evmLoaders = useMemo( + () => (evmAccAddress ? evmChains.map(chain => new EvmActivityLoader(chain.chainId, evmAccAddress)) : []), + [evmChains, evmAccAddress] + ); + + const [isLoading, setIsLoading] = useSafeState(true); + const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); + const [activities, setActivities] = useState<(EvmActivity | TezosPreActivity)[]>([]); + + const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + + const getEvmMetadata = useGetEvmAssetMetadata(); + + async function loadActivities(shouldStop: () => boolean) { + if (shouldStop()) return; + + setIsLoading(true); + + await Promise.allSettled(evmLoaders.map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId)))); + + if (shouldStop()) return; + + let edgeDate: string | undefined; + + for (const l of evmLoaders) { + if (l.reachedTheEnd || l.lastError) continue; + + const lastAct = l.activities.at(-1); + if (!lastAct) continue; + + if (!edgeDate) { + edgeDate = lastAct.createdAt; + continue; + } + + if (lastAct.createdAt > edgeDate) edgeDate = lastAct.createdAt; + } + + const newActivities = evmLoaders + .map(l => (edgeDate ? l.activities.filter(a => a.createdAt >= edgeDate) : l.activities)) // TODO: Optimize + .flat() + .toSorted((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); + + setActivities(newActivities); + setIsLoading(false); + + if (activities.length === newActivities.length) setReachedTheEnd(true); + } + + /** Loads more of older items */ + function loadMore() { + if (isLoading || reachedTheEnd) return; + loadActivities(stopAndBuildChecker()); + } + + useDidMount(() => { + loadActivities(stopAndBuildChecker()); + + return stopLoading; + }); + + useDidUpdate(() => { + setActivities([]); + setIsLoading(true); + setReachedTheEnd(false); + + loadActivities(stopAndBuildChecker()); + }, [tezAccAddress, evmAccAddress]); + + // const displayActivities: (EvmActivity | TezosPreActivity)[] = []; + + if (activities.length === 0 && !isLoading && reachedTheEnd) { + return ; + } + + return ( + + {activities.map(activity => + 'oldestTzktOperation' in activity ? ( + + ) : ( + + ) + )} + + ); +}); + +function isTezosActivity(activity: EvmActivity | TezosPreActivity): activity is TezosPreActivity { + return 'oldestTzktOperation' in activity; +} + +// interface EvmChainActivity { +// activity: +// } + +class EvmActivityLoader { + activities: EvmActivity[] = []; + private nextPage: number | nullish; + // private isLoading = false; + // private reachedTheEnd = false; + lastError: unknown; + + constructor(readonly chainId: number, readonly accountAddress: string) { + // + } + + get reachedTheEnd() { + return this.nextPage === null; + } + + async loadNext(getMetadata: EvmAssetMetadataGetter, assetSlug?: string) { + try { + // if (this.isLoading) return; + // this.isLoading = true; + + const { accountAddress, chainId, nextPage } = this; + + if (nextPage === null) return; + + const { nextPage: newNextPage, activities: newActivities } = await getEvmAssetTransactions( + accountAddress, + chainId, + getMetadata, + assetSlug, + nextPage + ); + // TODO: Apply if shouldn't have stopped only + + this.nextPage = newNextPage; + + if (newActivities.length) this.activities = this.activities.concat(newActivities); + // else this.reachedTheEnd = true; + + // if (newNextPage == null) this.reachedTheEnd = true; + + // this.isLoading = false; + + delete this.lastError; + } catch (error) { + console.error(error); + this.lastError = error; + } + } +} diff --git a/src/app/templates/activity/TezosActivityTab.tsx b/src/app/templates/activity/TezosActivityList.tsx similarity index 99% rename from src/app/templates/activity/TezosActivityTab.tsx rename to src/app/templates/activity/TezosActivityList.tsx index 2af647154c..63eb20ed47 100644 --- a/src/app/templates/activity/TezosActivityTab.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -98,10 +98,9 @@ function useTezosActivitiesLoadingLogic( pseudoLimit, lastActivity ); - - newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress)); - if (shouldStop()) return; + + newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); } catch (error) { if (shouldStop()) return; setIsLoading(false); diff --git a/src/app/templates/activity/index.ts b/src/app/templates/activity/index.ts index 9d54b74553..ab15af72a8 100644 --- a/src/app/templates/activity/index.ts +++ b/src/app/templates/activity/index.ts @@ -1,5 +1,6 @@ import { ActivityListContainer } from './ActivityListContainer'; import { EvmActivityList } from './EvmActivityList'; -import { TezosActivityList } from './TezosActivityTab'; +import { MultichainActivityList } from './MultichainList'; +import { TezosActivityList } from './TezosActivityList'; -export { ActivityListContainer, TezosActivityList, EvmActivityList }; +export { ActivityListContainer, MultichainActivityList, TezosActivityList, EvmActivityList }; diff --git a/src/lib/activity/evm/fetch.ts b/src/lib/activity/evm/fetch.ts new file mode 100644 index 0000000000..7aa88b6893 --- /dev/null +++ b/src/lib/activity/evm/fetch.ts @@ -0,0 +1,56 @@ +import { getEvmERC20Transfers, getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; +import { fromAssetSlug } from 'lib/assets'; +import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { EvmAssetMetadataGetter } from 'lib/metadata'; + +import { EvmActivity } from '../types'; + +import { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './parse'; + +export async function getEvmAssetTransactions( + walletAddress: string, + chainId: number, + getMetadata: EvmAssetMetadataGetter, + assetSlug?: string, + page?: number +) { + if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { + const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page); + + return { + activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), + nextPage + }; + } + + const [contract] = fromAssetSlug(assetSlug); + + /* Way to do the rest here through GoldRush API v3 + let nextPage: number | nullish = page; + + while (nextPage !== null) { + const data = await getEvmTransactions(walletAddress, chainId, nextPage); + + const activities = data.items + .map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)) + .filter(a => + a.operations.some( + ({ asset }) => asset && asset.contract === contract && (asset.tokenId == null || asset.tokenId === tokenId) + ) + ); + + if (activities.length) return { activities, nextPage: data.nextPage }; + + nextPage = data.nextPage; + } + + return { nextPage: null, activities: [] }; + */ + + const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page); + + return { + activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), + nextPage + }; +} diff --git a/src/lib/activity/evm/index.ts b/src/lib/activity/evm/index.ts new file mode 100644 index 0000000000..3d0c3ac4a4 --- /dev/null +++ b/src/lib/activity/evm/index.ts @@ -0,0 +1,3 @@ +export { getEvmAssetTransactions } from './fetch'; + +export { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './parse'; diff --git a/src/lib/activity/evm.ts b/src/lib/activity/evm/parse.ts similarity index 97% rename from src/lib/activity/evm.ts rename to src/lib/activity/evm/parse.ts index 747bf8cee8..ac66990b8e 100644 --- a/src/lib/activity/evm.ts +++ b/src/lib/activity/evm/parse.ts @@ -6,7 +6,7 @@ import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from './types'; +import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from '../types'; export function parseGoldRushTransaction( item: GoldRushTransaction, @@ -15,6 +15,7 @@ export function parseGoldRushTransaction( getMetadata: EvmAssetMetadataGetter ): EvmActivity { const logEvents = item.log_events ?? []; + const createdAt = item.block_signed_at as unknown as string; const operations = logEvents .map(logEvent => { @@ -197,7 +198,8 @@ export function parseGoldRushTransaction( hash: item.tx_hash!, blockExplorerUrl: item.explorers?.[0]?.url, operations, - operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length + operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length, + createdAt }; } @@ -208,6 +210,7 @@ export function parseGoldRushERC20Transfer( getMetadata: EvmAssetMetadataGetter ): EvmActivity { const transfers = item.transfers ?? []; + const createdAt = item.block_signed_at as unknown as string; const operations = transfers.map(transfer => { const kind = (() => { @@ -262,7 +265,8 @@ export function parseGoldRushERC20Transfer( hash: item.tx_hash!, blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, operations, - operationsCount: gasOperation ? transfers.length + 1 : transfers.length + operationsCount: gasOperation ? transfers.length + 1 : transfers.length, + createdAt }; } diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 9a4c31858d..36a58dc4ed 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -20,7 +20,8 @@ export function formatLegacyTezosActivity( chain: TempleChainKind.Tezos, chainId, operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)), - operationsCount: _activity.operations.length + operationsCount: _activity.operations.length, + createdAt: _activity.addedAt }; } diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index 4bde1e6c37..bf87b26c76 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -25,7 +25,8 @@ import type { export function preparseTezosOperationsGroup( { hash, operations: groupOperations }: TempleTzktOperationsGroup, - address: string + address: string, + chainId: string ): TezosPreActivity { const lastOperation = groupOperations[groupOperations.length - 1]!; const addedAt = lastOperation.timestamp; @@ -37,7 +38,8 @@ export function preparseTezosOperationsGroup( addedAt, status, operations, - oldestTzktOperation: lastOperation + oldestTzktOperation: lastOperation, + chainId }; } diff --git a/src/lib/activity/tezos/types.ts b/src/lib/activity/tezos/types.ts index e08cc16c21..590fa4ae5b 100644 --- a/src/lib/activity/tezos/types.ts +++ b/src/lib/activity/tezos/types.ts @@ -20,6 +20,7 @@ export interface TezosPreActivity extends TezosActivityOlderThan { status: TezosPreActivityStatus; /** Sorted old-to-new */ operations: TezosPreActivityOperation[]; + chainId: string; } type PickedPropsFromTzktOperation = Pick; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index cccbb636d9..ebd0965790 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -17,6 +17,7 @@ interface ChainActivityBase { hash: string; /** Original, not filtered number of operations */ operationsCount: number; + createdAt: string; } export interface TezosActivity extends ChainActivityBase { diff --git a/src/lib/metadata/index.ts b/src/lib/metadata/index.ts index 3e36f0cfbc..bc24e2779a 100644 --- a/src/lib/metadata/index.ts +++ b/src/lib/metadata/index.ts @@ -3,11 +3,13 @@ import { useCallback, useEffect, useRef } from 'react'; import { dispatch } from 'app/store'; import { useEvmCollectibleMetadataSelector, - useEvmChainCollectiblesMetadataRecordSelector + useEvmChainCollectiblesMetadataRecordSelector, + useEvmCollectiblesMetadataRecordSelector } from 'app/store/evm/collectibles-metadata/selectors'; import { useEvmTokenMetadataSelector, - useEvmChainTokensMetadataRecordSelector + useEvmChainTokensMetadataRecordSelector, + useEvmTokensMetadataRecordSelector } from 'app/store/evm/tokens-metadata/selectors'; import { loadCollectiblesMetadataAction } from 'app/store/tezos/collectibles-metadata/actions'; import { @@ -26,7 +28,7 @@ import { isTezAsset } from 'lib/assets'; import { fromChainAssetSlug } from 'lib/assets/utils'; import { isTruthy } from 'lib/utils'; import { isEvmNativeTokenSlug } from 'lib/utils/evm.utils'; -import { useAllTezosChains } from 'temple/front'; +import { useAllEvmChains, useAllTezosChains } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; import { isTezosDcpChainId } from 'temple/networks'; @@ -62,7 +64,7 @@ export const useEvmAssetMetadata = (slug: string, evmChainId: number): EvmAssetM return isEvmNativeTokenSlug(slug) ? network?.currency : tokenMetadata || collectibleMetadata; }; -export const useGetEvmAssetMetadata = (chainId: number) => { +export const useGetEvmChainAssetMetadata = (chainId: number) => { const network = useEvmChainByChainId(chainId); const tokensMetadatas = useEvmChainTokensMetadataRecordSelector(chainId); const collectiblesMetadatas = useEvmChainCollectiblesMetadataRecordSelector(chainId); @@ -77,6 +79,21 @@ export const useGetEvmAssetMetadata = (chainId: number) => { ); }; +export const useGetEvmAssetMetadata = () => { + const allEvmChains = useAllEvmChains(); + const tokensMetadatas = useEvmTokensMetadataRecordSelector(); + const collectiblesMetadatas = useEvmCollectiblesMetadataRecordSelector(); + + return useCallback( + (slug: string, chainId: number) => { + if (isEvmNativeTokenSlug(slug)) return allEvmChains[chainId]?.currency; + + return tokensMetadatas[chainId]?.[slug] || collectiblesMetadatas[chainId]?.[slug]; + }, + [tokensMetadatas, collectiblesMetadatas, allEvmChains] + ); +}; + export type EvmAssetMetadataGetter = ( slug: string ) => EvmNativeTokenMetadata | EvmTokenMetadata | EvmCollectibleMetadata | undefined; From 99ab0cf9f7268d3fbe1e9bccd5135e19dcca39a2 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 1 Oct 2024 03:07:45 +0300 Subject: [PATCH 29/74] TW-1479: [EVM] Transactions history. Multichain. EVM. Optimization: loops reduction --- src/app/templates/activity/MultichainList.tsx | 32 ++++++++++++------- src/lib/activity/evm/parse.ts | 8 ++--- src/lib/activity/tezos/index.ts | 2 +- src/lib/activity/types.ts | 3 +- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 7054ce6539..6d35649f40 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -62,22 +62,29 @@ export const MultichainActivityList = memo(() => { if (!lastAct) continue; if (!edgeDate) { - edgeDate = lastAct.createdAt; + edgeDate = lastAct.addedAt; continue; } - if (lastAct.createdAt > edgeDate) edgeDate = lastAct.createdAt; + if (lastAct.addedAt > edgeDate) edgeDate = lastAct.addedAt; } const newActivities = evmLoaders - .map(l => (edgeDate ? l.activities.filter(a => a.createdAt >= edgeDate) : l.activities)) // TODO: Optimize - .flat() - .toSorted((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); + .map(l => { + if (!edgeDate) return l.activities; - setActivities(newActivities); - setIsLoading(false); + // return l.activities.filter(a => a.addedAt >= edgeDate); + + const lastIndex = l.activities.findLastIndex(a => a.addedAt >= edgeDate); + + return lastIndex === -1 ? [] : l.activities.slice(0, lastIndex + 1); + }) + .flat(); if (activities.length === newActivities.length) setReachedTheEnd(true); + else setActivities(newActivities); + + setIsLoading(false); } /** Loads more of older items */ @@ -100,21 +107,24 @@ export const MultichainActivityList = memo(() => { loadActivities(stopAndBuildChecker()); }, [tezAccAddress, evmAccAddress]); - // const displayActivities: (EvmActivity | TezosPreActivity)[] = []; + const displayActivities = useMemo( + () => activities.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)), + [activities] + ); - if (activities.length === 0 && !isLoading && reachedTheEnd) { + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } return ( - {activities.map(activity => + {displayActivities.map(activity => 'oldestTzktOperation' in activity ? ( (logEvent => { @@ -199,7 +199,7 @@ export function parseGoldRushTransaction( blockExplorerUrl: item.explorers?.[0]?.url, operations, operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length, - createdAt + addedAt }; } @@ -210,7 +210,7 @@ export function parseGoldRushERC20Transfer( getMetadata: EvmAssetMetadataGetter ): EvmActivity { const transfers = item.transfers ?? []; - const createdAt = item.block_signed_at as unknown as string; + const addedAt = item.block_signed_at as unknown as string; const operations = transfers.map(transfer => { const kind = (() => { @@ -266,7 +266,7 @@ export function parseGoldRushERC20Transfer( blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, operations, operationsCount: gasOperation ? transfers.length + 1 : transfers.length, - createdAt + addedAt }; } diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 36a58dc4ed..75c88b3593 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -21,7 +21,7 @@ export function formatLegacyTezosActivity( chainId, operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)), operationsCount: _activity.operations.length, - createdAt: _activity.addedAt + addedAt: _activity.addedAt }; } diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index ebd0965790..33e4c416f6 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -17,7 +17,8 @@ interface ChainActivityBase { hash: string; /** Original, not filtered number of operations */ operationsCount: number; - createdAt: string; + /** ISO string */ + addedAt: string; } export interface TezosActivity extends ChainActivityBase { From 5625eeb02729df06ba0060ac2ff6ed4dcc713e9e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Oct 2024 02:52:43 +0300 Subject: [PATCH 30/74] TW-1479: [EVM] Transactions history. Multichain. + AbortSignal --- .../templates/activity/EvmActivityList.tsx | 20 +-- src/app/templates/activity/MultichainList.tsx | 117 +++++++++++++++--- .../templates/activity/TezosActivityList.tsx | 20 +-- src/app/templates/activity/index.ts | 10 +- src/lib/activity/evm/fetch.ts | 7 +- src/lib/apis/temple/endpoints/evm/index.ts | 47 +++++-- src/lib/ui/hooks/index.ts | 2 +- src/lib/ui/hooks/useAbortSignal.ts | 24 ++++ src/lib/ui/hooks/useStopper.ts | 17 --- 9 files changed, 191 insertions(+), 73 deletions(-) create mode 100644 src/lib/ui/hooks/useAbortSignal.ts delete mode 100644 src/lib/ui/hooks/useStopper.ts diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index cdc01ac161..1f63697bb7 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -9,7 +9,7 @@ import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { EvmActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; -import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; +import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; @@ -33,25 +33,25 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { const [activities, setActivities] = useSafeState([]); const [nextPage, setNextPage] = useSafeState(undefined); - const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); const getMetadata = useGetEvmChainAssetMetadata(chainId); - const loadActivities = async (activities: EvmActivity[], shouldStop: () => boolean, page?: number) => { + const loadActivities = async (activities: EvmActivity[], signal: AbortSignal, page?: number) => { // if (isLoading) return; setIsLoading(true); let newActivities: EvmActivity[], newNextPage: number | null; try { - const data = await getEvmAssetTransactions(accountAddress, chainId, getMetadata, assetSlug, page); + const data = await getEvmAssetTransactions(accountAddress, chainId, getMetadata, assetSlug, page, signal); newNextPage = data.nextPage; newActivities = data.activities; - if (shouldStop()) return; // TODO: If so - save time on parsing then) + if (signal.aborted) return; // TODO: If so - save time on parsing then) } catch (error) { - if (shouldStop()) return; + if (signal.aborted) return; setIsLoading(false); if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); console.error(error); @@ -68,13 +68,13 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { /** Loads more of older items */ function loadMore() { if (isLoading || reachedTheEnd || nextPage === null) return; - loadActivities(activities, stopAndBuildChecker(), nextPage); + loadActivities(activities, abortAndRenewSignal(), nextPage); } useDidMount(() => { - loadActivities([], stopAndBuildChecker()); + loadActivities([], abortAndRenewSignal()); - return stopLoading; + return abortLoading; }); useDidUpdate(() => { @@ -82,7 +82,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { setIsLoading(false); setReachedTheEnd(false); - loadActivities([], stopAndBuildChecker()); + loadActivities([], abortAndRenewSignal()); }, [chainId, accountAddress, assetSlug]); if (activities.length === 0 && !isLoading && reachedTheEnd) { diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 6d35649f40..92279d278c 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -5,9 +5,14 @@ import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { Activity, EvmActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; +import { preparseTezosOperationsGroup } from 'lib/activity/tezos'; +import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { TzktApiChainId } from 'lib/apis/tzkt'; +import { isKnownChainId as isKnownTzktChainId } from 'lib/apis/tzkt/api'; import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; -import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; +import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; +import { isTruthy } from 'lib/utils'; import { useAccountAddressForEvm, useAccountAddressForTezos, @@ -31,6 +36,20 @@ export const MultichainActivityList = memo(() => { const tezAccAddress = useAccountAddressForTezos(); const evmAccAddress = useAccountAddressForEvm(); + const tezosLoaders = useMemo( + () => + tezAccAddress + ? tezosChains + .map(chain => + isKnownTzktChainId(chain.chainId) + ? new TezosActivityLoader(chain.chainId, tezAccAddress, chain.rpcBaseURL) + : null + ) + .filter(isTruthy) + : [], + [tezosChains, tezAccAddress] + ); + const evmLoaders = useMemo( () => (evmAccAddress ? evmChains.map(chain => new EvmActivityLoader(chain.chainId, evmAccAddress)) : []), [evmChains, evmAccAddress] @@ -40,22 +59,29 @@ export const MultichainActivityList = memo(() => { const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); const [activities, setActivities] = useState<(EvmActivity | TezosPreActivity)[]>([]); - const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); const getEvmMetadata = useGetEvmAssetMetadata(); - async function loadActivities(shouldStop: () => boolean) { - if (shouldStop()) return; + async function loadActivities(signal: AbortSignal) { + if (signal.aborted) return; setIsLoading(true); - await Promise.allSettled(evmLoaders.map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId)))); + const allLoaders = [...tezosLoaders, ...evmLoaders]; + const lastEdgeDate = activities.at(-1)?.addedAt; + + await Promise.allSettled( + evmLoaders + .map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId), lastEdgeDate, signal)) + .concat(tezosLoaders.map(l => l.loadNext(lastEdgeDate, signal))) + ); - if (shouldStop()) return; + if (signal.aborted) return; let edgeDate: string | undefined; - for (const l of evmLoaders) { + for (const l of allLoaders) { if (l.reachedTheEnd || l.lastError) continue; const lastAct = l.activities.at(-1); @@ -69,7 +95,7 @@ export const MultichainActivityList = memo(() => { if (lastAct.addedAt > edgeDate) edgeDate = lastAct.addedAt; } - const newActivities = evmLoaders + const newActivities = allLoaders .map(l => { if (!edgeDate) return l.activities; @@ -90,13 +116,13 @@ export const MultichainActivityList = memo(() => { /** Loads more of older items */ function loadMore() { if (isLoading || reachedTheEnd) return; - loadActivities(stopAndBuildChecker()); + loadActivities(abortAndRenewSignal()); } useDidMount(() => { - loadActivities(stopAndBuildChecker()); + loadActivities(abortAndRenewSignal()); - return stopLoading; + return abortLoading; }); useDidUpdate(() => { @@ -104,7 +130,7 @@ export const MultichainActivityList = memo(() => { setIsLoading(true); setReachedTheEnd(false); - loadActivities(stopAndBuildChecker()); + loadActivities(abortAndRenewSignal()); }, [tezAccAddress, evmAccAddress]); const displayActivities = useMemo( @@ -163,7 +189,17 @@ class EvmActivityLoader { return this.nextPage === null; } - async loadNext(getMetadata: EvmAssetMetadataGetter, assetSlug?: string) { + async loadNext( + getMetadata: EvmAssetMetadataGetter, + edgeDate: string | undefined, + signal: AbortSignal, + assetSlug?: string + ) { + if (edgeDate) { + const lastAct = this.activities.at(-1); + if (lastAct && lastAct.addedAt > edgeDate) return; + } + try { // if (this.isLoading) return; // this.isLoading = true; @@ -177,9 +213,11 @@ class EvmActivityLoader { chainId, getMetadata, assetSlug, - nextPage + nextPage, + signal ); - // TODO: Apply if shouldn't have stopped only + + if (signal.aborted) return; this.nextPage = newNextPage; @@ -197,3 +235,52 @@ class EvmActivityLoader { } } } + +class TezosActivityLoader { + activities: TezosPreActivity[] = []; + reachedTheEnd = false; + lastError: unknown; + + constructor( + readonly chainId: TzktApiChainId, + readonly accountAddress: string, + private rpcBaseURL: string, + private pseudoLimit = 30 + ) { + // + } + + async loadNext(edgeDate: string | undefined, signal: AbortSignal, assetSlug?: string) { + if (edgeDate) { + const lastAct = this.activities.at(-1); + if (lastAct && lastAct.addedAt > edgeDate) return; + } + + try { + const { accountAddress, chainId, rpcBaseURL } = this; + + const lastActivity = this.activities.at(-1); + + const groups = await fetchTezosOperationsGroups( + chainId, + rpcBaseURL, + accountAddress, + assetSlug, + this.pseudoLimit, + lastActivity + ); + + if (signal.aborted) return; + + const newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); + + if (newActivities.length) this.activities = this.activities.concat(newActivities); + else this.reachedTheEnd = true; + + delete this.lastError; + } catch (error) { + console.error(error); + this.lastError = error; + } + } +} diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index 63eb20ed47..4914b2dc8c 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -8,7 +8,7 @@ import { preparseTezosOperationsGroup } from 'lib/activity/tezos'; import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; import { TezosPreActivity } from 'lib/activity/tezos/types'; import { isKnownChainId } from 'lib/apis/tzkt/api'; -import { useDidMount, useDidUpdate, useSafeState, useStopper } from 'lib/ui/hooks'; +import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { TezosNetworkEssentials } from 'temple/networks'; @@ -76,9 +76,9 @@ function useTezosActivitiesLoadingLogic( const [activities, setActivities] = useSafeState([]); const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - const { stop: stopLoading, stopAndBuildChecker } = useStopper(); + const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); - async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], shouldStop: () => boolean) { + async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], signal: AbortSignal) { if (!isKnownChainId(chainId)) { setIsLoading(false); setReachedTheEnd(true); @@ -86,7 +86,7 @@ function useTezosActivitiesLoadingLogic( } setIsLoading(activities.length ? 'more' : 'init'); - const lastActivity = activities[activities.length - 1]; + const lastActivity = activities.at(-1); let newActivities: TezosPreActivity[]; try { @@ -98,11 +98,11 @@ function useTezosActivitiesLoadingLogic( pseudoLimit, lastActivity ); - if (shouldStop()) return; + if (signal.aborted) return; newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); } catch (error) { - if (shouldStop()) return; + if (signal.aborted) return; setIsLoading(false); console.error(error); @@ -117,13 +117,13 @@ function useTezosActivitiesLoadingLogic( /** Loads more of older items */ function loadMore(pseudoLimit: number) { if (isLoading || reachedTheEnd) return; - loadActivities(pseudoLimit, activities, stopAndBuildChecker()); + loadActivities(pseudoLimit, activities, abortAndRenewSignal()); } useDidMount(() => { - loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); + loadActivities(initialPseudoLimit, [], abortAndRenewSignal()); - return stopLoading; + return abortLoading; }); useDidUpdate(() => { @@ -131,7 +131,7 @@ function useTezosActivitiesLoadingLogic( setIsLoading('init'); setReachedTheEnd(false); - loadActivities(initialPseudoLimit, [], stopAndBuildChecker()); + loadActivities(initialPseudoLimit, [], abortAndRenewSignal()); }, [chainId, accountAddress, assetSlug]); return { diff --git a/src/app/templates/activity/index.ts b/src/app/templates/activity/index.ts index ab15af72a8..530b2f4cd6 100644 --- a/src/app/templates/activity/index.ts +++ b/src/app/templates/activity/index.ts @@ -1,6 +1,4 @@ -import { ActivityListContainer } from './ActivityListContainer'; -import { EvmActivityList } from './EvmActivityList'; -import { MultichainActivityList } from './MultichainList'; -import { TezosActivityList } from './TezosActivityList'; - -export { ActivityListContainer, MultichainActivityList, TezosActivityList, EvmActivityList }; +export { ActivityListContainer } from './ActivityListContainer'; +export { EvmActivityList } from './EvmActivityList'; +export { MultichainActivityList } from './MultichainList'; +export { TezosActivityList } from './TezosActivityList'; diff --git a/src/lib/activity/evm/fetch.ts b/src/lib/activity/evm/fetch.ts index 7aa88b6893..99964ed306 100644 --- a/src/lib/activity/evm/fetch.ts +++ b/src/lib/activity/evm/fetch.ts @@ -12,10 +12,11 @@ export async function getEvmAssetTransactions( chainId: number, getMetadata: EvmAssetMetadataGetter, assetSlug?: string, - page?: number + page?: number, + signal?: AbortSignal ) { if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { - const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page); + const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page, signal); return { activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), @@ -47,7 +48,7 @@ export async function getEvmAssetTransactions( return { nextPage: null, activities: [] }; */ - const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page); + const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page, signal); return { activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index ab8db87b21..87c547c0a0 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -16,20 +16,38 @@ export const getEvmTokensMetadata = (walletAddress: string, chainId: ChainID) => export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/collectibles-metadata', walletAddress, chainId); -export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number) => - buildEvmRequest<{ items: GoldRushTransaction[]; current_page: number }>('/transactions', walletAddress, chainId, { - page - }).then(({ items, current_page }) => ({ +export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number, signal?: AbortSignal) => + buildEvmRequest<{ items: GoldRushTransaction[]; current_page: number }>( + '/transactions', + walletAddress, + chainId, + { + page + }, + signal + ).then(({ items, current_page }) => ({ items, /** null | \> 0 */ nextPage: current_page > 1 ? current_page - 1 : null })); -export const getEvmERC20Transfers = (walletAddress: string, chainId: ChainID, contractAddress: string, page?: number) => - buildEvmRequest('/erc20-transfers', walletAddress, chainId, { - contractAddress, - page - }).then(({ items, pagination }) => { +export const getEvmERC20Transfers = ( + walletAddress: string, + chainId: ChainID, + contractAddress: string, + page?: number, + signal?: AbortSignal +) => + buildEvmRequest( + '/erc20-transfers', + walletAddress, + chainId, + { + contractAddress, + page + }, + signal + ).then(({ items, pagination }) => { const withoutNextPage = items && pagination ? items.length < pagination.page_size : true; return { @@ -39,10 +57,17 @@ export const getEvmERC20Transfers = (walletAddress: string, chainId: ChainID, co }; }); -const buildEvmRequest = (url: string, walletAddress: string, chainId: ChainID, params?: object) => +const buildEvmRequest = ( + url: string, + walletAddress: string, + chainId: ChainID, + params?: object, + signal?: AbortSignal +) => templeWalletApi .get(`evm${url}`, { - params: { ...params, walletAddress, chainId } + params: { ...params, walletAddress, chainId }, + signal }) .then( res => res.data, diff --git a/src/lib/ui/hooks/index.ts b/src/lib/ui/hooks/index.ts index eb83b09065..e6b3ece91e 100644 --- a/src/lib/ui/hooks/index.ts +++ b/src/lib/ui/hooks/index.ts @@ -10,7 +10,7 @@ export { useTimeout } from './useTimeout'; export { useInterval } from './useInterval'; -export { useStopper } from './useStopper'; +export { useAbortSignal } from './useAbortSignal'; export { useMemoWithCompare } from './useMemoWithCompare'; diff --git a/src/lib/ui/hooks/useAbortSignal.ts b/src/lib/ui/hooks/useAbortSignal.ts new file mode 100644 index 0000000000..33c7ebbeac --- /dev/null +++ b/src/lib/ui/hooks/useAbortSignal.ts @@ -0,0 +1,24 @@ +import { useMemo, useRef } from 'react'; + +export function useAbortSignal() { + const ref = useRef(); + + return useMemo( + () => ({ + abort: () => { + ref.current?.abort(); + + ref.current = null; + }, + abortAndRenewSignal: () => { + ref.current?.abort(); + + const ac = new AbortController(); + ref.current = ac; + + return ac.signal; + } + }), + [] + ); +} diff --git a/src/lib/ui/hooks/useStopper.ts b/src/lib/ui/hooks/useStopper.ts deleted file mode 100644 index 696c3664a5..0000000000 --- a/src/lib/ui/hooks/useStopper.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo, useRef } from 'react'; - -export function useStopper() { - const symbolRef = useRef(null); - - return useMemo(() => { - const updateSymbolRef = () => (symbolRef.current = Symbol()); - - return { - stop: () => void updateSymbolRef(), - stopAndBuildChecker: () => { - const symb = updateSymbolRef(); - return () => symb !== symbolRef.current; - } - }; - }, [symbolRef]); -} From 66ac409876afd613357a5d3ca45d58c4cb8ba5b7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Oct 2024 16:22:59 +0300 Subject: [PATCH 31/74] TW-1479: [EVM] Transactions history. Multichain. + useActivitiesLoadingLogic() --- .../templates/activity/EvmActivityList.tsx | 84 +++++------ src/app/templates/activity/MultichainList.tsx | 130 +++++++---------- .../templates/activity/TezosActivityList.tsx | 134 ++++++------------ src/app/templates/activity/loading-logic.ts | 38 +++++ src/lib/activity/evm/fetch.ts | 4 + src/lib/activity/tezos/fetch.ts | 4 +- 6 files changed, 171 insertions(+), 223 deletions(-) create mode 100644 src/app/templates/activity/loading-logic.ts diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index 1f63697bb7..73c81f0927 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -9,11 +9,12 @@ import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { EvmActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; -import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; +import { useSafeState } from 'lib/ui/hooks'; import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; +import { useActivitiesLoadingLogic } from './loading-logic'; interface Props { chainId: number; @@ -28,62 +29,47 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { useLoadPartnersPromo(); - const [isLoading, setIsLoading] = useSafeState(true); - const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - const [activities, setActivities] = useSafeState([]); const [nextPage, setNextPage] = useSafeState(undefined); - const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); - const getMetadata = useGetEvmChainAssetMetadata(chainId); - const loadActivities = async (activities: EvmActivity[], signal: AbortSignal, page?: number) => { - // if (isLoading) return; - - setIsLoading(true); - - let newActivities: EvmActivity[], newNextPage: number | null; - try { - const data = await getEvmAssetTransactions(accountAddress, chainId, getMetadata, assetSlug, page, signal); + const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = + useActivitiesLoadingLogic( + async (initial, signal) => { + const page = initial ? undefined : nextPage; + if (page === null) return; - newNextPage = data.nextPage; - newActivities = data.activities; + const currActivities = initial ? [] : activities; - if (signal.aborted) return; // TODO: If so - save time on parsing then) - } catch (error) { - if (signal.aborted) return; - setIsLoading(false); - if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); - console.error(error); + setIsLoading(currActivities.length ? 'more' : 'init'); - return; - } - - setActivities(activities.concat(newActivities)); - setIsLoading(false); - setNextPage(newNextPage); - if (newNextPage === null || newActivities.length === 0) setReachedTheEnd(true); - }; - - /** Loads more of older items */ - function loadMore() { - if (isLoading || reachedTheEnd || nextPage === null) return; - loadActivities(activities, abortAndRenewSignal(), nextPage); - } + try { + const { activities: newActivities, nextPage: newNextPage } = await getEvmAssetTransactions( + accountAddress, + chainId, + getMetadata, + assetSlug, + page, + signal + ); - useDidMount(() => { - loadActivities([], abortAndRenewSignal()); + if (signal.aborted) return; - return abortLoading; - }); + setActivities(currActivities.concat(newActivities)); + setNextPage(newNextPage); + if (newNextPage === null || newActivities.length === 0) setReachedTheEnd(true); + } catch (error) { + if (signal.aborted) return; - useDidUpdate(() => { - setActivities([]); - setIsLoading(false); - setReachedTheEnd(false); + console.error(error); + if (error instanceof AxiosError && error.status === 501) setReachedTheEnd(true); + } - loadActivities([], abortAndRenewSignal()); - }, [chainId, accountAddress, assetSlug]); + setIsLoading(false); + }, + [chainId, accountAddress, assetSlug], + () => setNextPage(undefined) + ); if (activities.length === 0 && !isLoading && reachedTheEnd) { return ; @@ -92,10 +78,10 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { return ( {activities.map(activity => ( diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 92279d278c..6432305a13 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -1,5 +1,7 @@ import React, { memo, useEffect, useMemo, useState } from 'react'; +import { AxiosError } from 'axios'; + import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; @@ -11,7 +13,6 @@ import { TezosPreActivity } from 'lib/activity/tezos/types'; import { TzktApiChainId } from 'lib/apis/tzkt'; import { isKnownChainId as isKnownTzktChainId } from 'lib/apis/tzkt/api'; import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; -import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; import { isTruthy } from 'lib/utils'; import { useAccountAddressForEvm, @@ -23,6 +24,7 @@ import { } from 'temple/front'; import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; +import { useActivitiesLoadingLogic } from './loading-logic'; export const MultichainActivityList = memo(() => { useLoadPartnersPromo(); @@ -55,83 +57,63 @@ export const MultichainActivityList = memo(() => { [evmChains, evmAccAddress] ); - const [isLoading, setIsLoading] = useSafeState(true); - const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - const [activities, setActivities] = useState<(EvmActivity | TezosPreActivity)[]>([]); - - const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); - const getEvmMetadata = useGetEvmAssetMetadata(); - async function loadActivities(signal: AbortSignal) { - if (signal.aborted) return; - - setIsLoading(true); - - const allLoaders = [...tezosLoaders, ...evmLoaders]; - const lastEdgeDate = activities.at(-1)?.addedAt; + const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = + useActivitiesLoadingLogic( + async (initial, signal) => { + if (signal.aborted) return; - await Promise.allSettled( - evmLoaders - .map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId), lastEdgeDate, signal)) - .concat(tezosLoaders.map(l => l.loadNext(lastEdgeDate, signal))) - ); + const currActivities = initial ? [] : activities; - if (signal.aborted) return; + setIsLoading(currActivities.length ? 'more' : 'init'); - let edgeDate: string | undefined; + const allLoaders = [...tezosLoaders, ...evmLoaders]; + const lastEdgeDate = currActivities.at(-1)?.addedAt; - for (const l of allLoaders) { - if (l.reachedTheEnd || l.lastError) continue; + await Promise.allSettled( + evmLoaders + .map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId), lastEdgeDate, signal)) + .concat(tezosLoaders.map(l => l.loadNext(lastEdgeDate, signal))) + ); - const lastAct = l.activities.at(-1); - if (!lastAct) continue; + if (signal.aborted) return; - if (!edgeDate) { - edgeDate = lastAct.addedAt; - continue; - } + let edgeDate: string | undefined; - if (lastAct.addedAt > edgeDate) edgeDate = lastAct.addedAt; - } + for (const l of allLoaders) { + if (l.reachedTheEnd || l.lastError) continue; - const newActivities = allLoaders - .map(l => { - if (!edgeDate) return l.activities; + const lastAct = l.activities.at(-1); + if (!lastAct) continue; - // return l.activities.filter(a => a.addedAt >= edgeDate); + if (!edgeDate) { + edgeDate = lastAct.addedAt; + continue; + } - const lastIndex = l.activities.findLastIndex(a => a.addedAt >= edgeDate); + if (lastAct.addedAt > edgeDate) edgeDate = lastAct.addedAt; + } - return lastIndex === -1 ? [] : l.activities.slice(0, lastIndex + 1); - }) - .flat(); + const newActivities = allLoaders + .map(l => { + if (!edgeDate) return l.activities; - if (activities.length === newActivities.length) setReachedTheEnd(true); - else setActivities(newActivities); + // return l.activities.filter(a => a.addedAt >= edgeDate); - setIsLoading(false); - } - - /** Loads more of older items */ - function loadMore() { - if (isLoading || reachedTheEnd) return; - loadActivities(abortAndRenewSignal()); - } + const lastIndex = l.activities.findLastIndex(a => a.addedAt >= edgeDate); - useDidMount(() => { - loadActivities(abortAndRenewSignal()); + return lastIndex === -1 ? [] : l.activities.slice(0, lastIndex + 1); + }) + .flat(); - return abortLoading; - }); + if (currActivities.length === newActivities.length) setReachedTheEnd(true); + else setActivities(newActivities); - useDidUpdate(() => { - setActivities([]); - setIsLoading(true); - setReachedTheEnd(false); - - loadActivities(abortAndRenewSignal()); - }, [tezAccAddress, evmAccAddress]); + setIsLoading(false); + }, + [tezosLoaders, evmLoaders] + ); const displayActivities = useMemo( () => activities.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)), @@ -145,13 +127,13 @@ export const MultichainActivityList = memo(() => { return ( {displayActivities.map(activity => - 'oldestTzktOperation' in activity ? ( + isTezosActivity(activity) ? ( (({ tezosChainId, assetSlug }) => { useLoadPartnersPromo(); - const { activities, isLoading, reachedTheEnd, loadMore } = useTezosActivitiesLoadingLogic( - network, - accountAddress, - INITIAL_NUMBER, - assetSlug - ); + const { chainId, rpcBaseURL } = network; + + const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = + useActivitiesLoadingLogic( + async (initial, signal) => { + if (!isKnownChainId(chainId)) { + setIsLoading(false); + setReachedTheEnd(true); + return; + } + + const currActivities = initial ? [] : activities; + + setIsLoading(currActivities.length ? 'more' : 'init'); + + const olderThan = currActivities.at(-1); + + try { + const groups = await fetchTezosOperationsGroups(chainId, rpcBaseURL, accountAddress, assetSlug, olderThan); + + if (signal.aborted) return; + + const newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); + + setActivities(currActivities.concat(newActivities)); + if (newActivities.length === 0) setReachedTheEnd(true); + } catch (error) { + if (signal.aborted) return; + + console.error(error); + } + + setIsLoading(false); + }, + [chainId, accountAddress, assetSlug], + undefined, + isKnownChainId(chainId) + ); if (activities.length === 0 && !isLoading && reachedTheEnd) { return ; @@ -46,8 +74,8 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug }) => { itemsLength={activities.length} isSyncing={Boolean(isLoading)} reachedTheEnd={reachedTheEnd} - retryInitialLoad={() => loadMore(INITIAL_NUMBER)} - loadMore={() => loadMore(LOAD_STEP)} + retryInitialLoad={loadNext} + loadMore={loadNext} > {activities.map(activity => ( (({ tezosChainId, assetSlug }) => { ); }); - -type TLoading = 'init' | 'more' | false; - -function useTezosActivitiesLoadingLogic( - network: TezosNetworkEssentials, - accountAddress: string, - initialPseudoLimit: number, - assetSlug?: string -) { - const { chainId, rpcBaseURL } = network; - - const [isLoading, setIsLoading] = useSafeState(isKnownChainId(chainId) && 'init'); - const [activities, setActivities] = useSafeState([]); - const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); - - const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); - - async function loadActivities(pseudoLimit: number, activities: TezosPreActivity[], signal: AbortSignal) { - if (!isKnownChainId(chainId)) { - setIsLoading(false); - setReachedTheEnd(true); - return; - } - - setIsLoading(activities.length ? 'more' : 'init'); - const lastActivity = activities.at(-1); - - let newActivities: TezosPreActivity[]; - try { - const groups = await fetchTezosOperationsGroups( - chainId, - rpcBaseURL, - accountAddress, - assetSlug, - pseudoLimit, - lastActivity - ); - if (signal.aborted) return; - - newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); - } catch (error) { - if (signal.aborted) return; - setIsLoading(false); - console.error(error); - - return; - } - - setActivities(activities.concat(newActivities)); - setIsLoading(false); - if (newActivities.length === 0) setReachedTheEnd(true); - } - - /** Loads more of older items */ - function loadMore(pseudoLimit: number) { - if (isLoading || reachedTheEnd) return; - loadActivities(pseudoLimit, activities, abortAndRenewSignal()); - } - - useDidMount(() => { - loadActivities(initialPseudoLimit, [], abortAndRenewSignal()); - - return abortLoading; - }); - - useDidUpdate(() => { - setActivities([]); - setIsLoading('init'); - setReachedTheEnd(false); - - loadActivities(initialPseudoLimit, [], abortAndRenewSignal()); - }, [chainId, accountAddress, assetSlug]); - - return { - isLoading, - reachedTheEnd, - activities, - loadMore - }; -} diff --git a/src/app/templates/activity/loading-logic.ts b/src/app/templates/activity/loading-logic.ts new file mode 100644 index 0000000000..58fed63ef5 --- /dev/null +++ b/src/app/templates/activity/loading-logic.ts @@ -0,0 +1,38 @@ +import { useDidMount, useDidUpdate, useSafeState, useAbortSignal } from 'lib/ui/hooks'; +import { useWillUnmount } from 'lib/ui/hooks/useWillUnmount'; + +type TLoading = 'init' | 'more' | false; + +export function useActivitiesLoadingLogic( + loadActivities: (initial: boolean, signal: AbortSignal) => Promise, + resetDeps: unknown[], + onReset?: EmptyFn, + initialIsLoading = true +) { + const [isLoading, setIsLoading] = useSafeState(initialIsLoading && 'init'); + const [activities, setActivities] = useSafeState([]); + const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); + + const { abort: abortLoading, abortAndRenewSignal } = useAbortSignal(); + + function loadNext() { + if (isLoading || reachedTheEnd) return; + + loadActivities(false, abortAndRenewSignal()); + } + + useDidMount(() => void loadActivities(true, abortAndRenewSignal())); + + useWillUnmount(abortLoading); + + useDidUpdate(() => { + setActivities([]); + setIsLoading('init'); + setReachedTheEnd(false); + onReset?.(); + + loadActivities(true, abortAndRenewSignal()); + }, resetDeps); + + return { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext }; +} diff --git a/src/lib/activity/evm/fetch.ts b/src/lib/activity/evm/fetch.ts index 99964ed306..345eb5f36d 100644 --- a/src/lib/activity/evm/fetch.ts +++ b/src/lib/activity/evm/fetch.ts @@ -18,6 +18,8 @@ export async function getEvmAssetTransactions( if (!assetSlug || assetSlug === EVM_TOKEN_SLUG) { const { items, nextPage } = await getEvmTransactions(walletAddress, chainId, page, signal); + signal?.throwIfAborted(); + return { activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), nextPage @@ -50,6 +52,8 @@ export async function getEvmAssetTransactions( const { items, nextPage } = await getEvmERC20Transfers(walletAddress, chainId, contract, page, signal); + signal?.throwIfAborted(); + return { activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), nextPage diff --git a/src/lib/activity/tezos/fetch.ts b/src/lib/activity/tezos/fetch.ts index a6d5e8397d..abd27c34fa 100644 --- a/src/lib/activity/tezos/fetch.ts +++ b/src/lib/activity/tezos/fetch.ts @@ -14,8 +14,8 @@ export default async function fetchTezosOperationsGroups( rpcUrl: string, accountAddress: string, assetSlug: string | undefined, - pseudoLimit: number, - olderThan?: TezosActivityOlderThan + olderThan?: TezosActivityOlderThan, + pseudoLimit = 30 ) { const operations = await fetchOperations(chainId, rpcUrl, accountAddress, assetSlug, pseudoLimit, olderThan); From ac81a2eac3c1fabdc2a84f96a9706662457c3eb9 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Oct 2024 18:08:49 +0300 Subject: [PATCH 32/74] TW-1479: [EVM] Transactions history. InfiniteScroll. Fix loader show-up --- src/app/atoms/InfiniteScroll.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index d5c9ba8453..65bd97b8be 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -43,13 +43,15 @@ export const InfiniteScroll: FC = ({ return ( )} onScroll={onScroll} + loader={null} // Doesn't always show this way scrollableTarget={SCROLL_DOCUMENT ? undefined : APP_CONTENT_PAPER_DOM_ID} > {children} + + {isSyncing ? loader ?? : null} ); }; From a9c381391ebc617e97287774e72049ac58b7f712 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Oct 2024 18:14:16 +0300 Subject: [PATCH 33/74] TW-1479: [EVM] Transactions history. Multichain. Refactor --- src/app/atoms/InfiniteScroll.tsx | 1 - src/app/defaults.ts | 2 - .../AssetImage/AssetImageStacked.tsx | 4 +- src/app/templates/NetworkSelectModal.tsx | 1 - .../templates/activity/EvmActivityList.tsx | 6 +-- src/app/templates/activity/MultichainList.tsx | 39 +++++++------------ .../templates/activity/TezosActivityList.tsx | 6 +-- src/app/templates/activity/loading-logic.ts | 6 +-- src/lib/activity/evm/index.ts | 2 - src/lib/activity/index.ts | 2 - src/lib/activity/types.ts | 1 + src/lib/apis/tzkt/index.ts | 1 - src/lib/apis/tzkt/types.ts | 2 +- src/lib/apis/tzkt/utils.ts | 4 +- 14 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index 65bd97b8be..727e5ab6dc 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -70,6 +70,5 @@ const buildOnScroll = }; function isScrollAtTheEnd(elem: Element) { - // return 0 === elem.offsetHeight - elem.clientHeight - elem.scrollTop; return elem.scrollHeight === elem.clientHeight + elem.scrollTop; } diff --git a/src/app/defaults.ts b/src/app/defaults.ts index d1f109f5a9..ad961b80a6 100644 --- a/src/app/defaults.ts +++ b/src/app/defaults.ts @@ -1,8 +1,6 @@ import { t } from 'lib/i18n'; import { TempleAccountType } from 'lib/temple/types'; -export const OP_STACK_PREVIEW_SIZE = 2; - export class ArtificialError extends Error {} export class NotEnoughFundsError extends ArtificialError {} export class ZeroBalanceError extends NotEnoughFundsError {} diff --git a/src/app/templates/AssetImage/AssetImageStacked.tsx b/src/app/templates/AssetImage/AssetImageStacked.tsx index 15c107ede2..266a02a727 100644 --- a/src/app/templates/AssetImage/AssetImageStacked.tsx +++ b/src/app/templates/AssetImage/AssetImageStacked.tsx @@ -59,7 +59,7 @@ export const EvmAssetImageStacked: FC = ({ evmChainId return ; }; -export interface AssetImageStackedProps +interface AssetImageStackedProps extends Pick< ImageStackedProps, 'loader' | 'fallback' | 'className' | 'style' | 'onStackLoaded' | 'onStackFailed' | 'alt' @@ -68,7 +68,7 @@ export interface AssetImageStackedProps size?: number; } -export const AssetImageStacked: FC = ({ +const AssetImageStacked: FC = ({ sources, className, size, diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 17120c08aa..8f46cb7bf8 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -1,6 +1,5 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { pick } from 'lodash'; import { useDebounce } from 'use-debounce'; import { IconBase } from 'app/atoms'; diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index 73c81f0927..0f63f41064 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -39,9 +39,9 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { const page = initial ? undefined : nextPage; if (page === null) return; - const currActivities = initial ? [] : activities; + setIsLoading(true); - setIsLoading(currActivities.length ? 'more' : 'init'); + const currActivities = initial ? [] : activities; try { const { activities: newActivities, nextPage: newNextPage } = await getEvmAssetTransactions( @@ -78,7 +78,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { return ( { async (initial, signal) => { if (signal.aborted) return; - const currActivities = initial ? [] : activities; + setIsLoading(true); - setIsLoading(currActivities.length ? 'more' : 'init'); + const currActivities = initial ? [] : activities; const allLoaders = [...tezosLoaders, ...evmLoaders]; const lastEdgeDate = currActivities.at(-1)?.addedAt; @@ -127,7 +127,7 @@ export const MultichainActivityList = memo(() => { return ( (({ tezosChainId, assetSlug }) => { return; } - const currActivities = initial ? [] : activities; + setIsLoading(true); - setIsLoading(currActivities.length ? 'more' : 'init'); + const currActivities = initial ? [] : activities; const olderThan = currActivities.at(-1); @@ -72,7 +72,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug }) => { return ( ( loadActivities: (initial: boolean, signal: AbortSignal) => Promise, resetDeps: unknown[], onReset?: EmptyFn, initialIsLoading = true ) { - const [isLoading, setIsLoading] = useSafeState(initialIsLoading && 'init'); + const [isLoading, setIsLoading] = useSafeState(initialIsLoading); const [activities, setActivities] = useSafeState([]); const [reachedTheEnd, setReachedTheEnd] = useSafeState(false); @@ -27,7 +25,7 @@ export function useActivitiesLoadingLogic( useDidUpdate(() => { setActivities([]); - setIsLoading('init'); + setIsLoading(true); setReachedTheEnd(false); onReset?.(); diff --git a/src/lib/activity/evm/index.ts b/src/lib/activity/evm/index.ts index 3d0c3ac4a4..53ff241bd4 100644 --- a/src/lib/activity/evm/index.ts +++ b/src/lib/activity/evm/index.ts @@ -1,3 +1 @@ export { getEvmAssetTransactions } from './fetch'; - -export { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './parse'; diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 4bd8838d69..fb7ffd8194 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -2,6 +2,4 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivit export { ActivityOperKindEnum, InfinitySymbol } from './types'; -export { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './evm'; - export { parseTezosPreActivityOperation } from './tezos'; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 33e4c416f6..b479c5bdab 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -10,6 +10,7 @@ export enum ActivityOperKindEnum { approve } +// @ts-prune-ignore-next export type Activity = TezosActivity | EvmActivity; interface ChainActivityBase { diff --git a/src/lib/apis/tzkt/index.ts b/src/lib/apis/tzkt/index.ts index e47d4e4e16..e18edea9fd 100644 --- a/src/lib/apis/tzkt/index.ts +++ b/src/lib/apis/tzkt/index.ts @@ -2,7 +2,6 @@ export type { TzktOperation, TzktTokenTransfer, TzktRewardsEntry, - TzktAlias, TzktOperationType, TzktTransactionOperation } from './types'; diff --git a/src/lib/apis/tzkt/types.ts b/src/lib/apis/tzkt/types.ts index 884237653c..51bed52c75 100644 --- a/src/lib/apis/tzkt/types.ts +++ b/src/lib/apis/tzkt/types.ts @@ -9,7 +9,7 @@ export type TzktQuoteCurrency = 'None' | 'Btc' | 'Eur' | 'Usd' | 'Cny' | 'Jpy' | type TzktOperationStatus = 'applied' | 'failed' | 'backtracked' | 'skipped'; -export interface TzktAlias { +interface TzktAlias { alias?: string; address: string; } diff --git a/src/lib/apis/tzkt/utils.ts b/src/lib/apis/tzkt/utils.ts index d4ad5f6d29..813638802d 100644 --- a/src/lib/apis/tzkt/utils.ts +++ b/src/lib/apis/tzkt/utils.ts @@ -39,7 +39,7 @@ export interface ParameterFa2Transfer extends ParameterFa2 { }[]; } -export interface ParameterFa2Approve extends ParameterFa2 { +interface ParameterFa2Approve extends ParameterFa2 { entrypoint: 'update_operators'; value: { add_operator: { @@ -84,7 +84,7 @@ export function isTzktOperParam_Fa12(param: any): param is ParameterFa12 { return true; } -export function isTzktOperParam_Fa2(param: any): param is ParameterFa2 { +function isTzktOperParam_Fa2(param: any): param is ParameterFa2 { if (!isTzktOperParam(param)) return false; if (!Array.isArray(param.value)) return false; if (param.value[0] == null) return true; From 62fa623613ba35070d1482b4274e1ab720c6830b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Oct 2024 23:35:47 +0300 Subject: [PATCH 34/74] TW-1479: [EVM] Transactions history. Approve kind. + Unlimited --- .../ActivityItem/ActivityOperationBase.tsx | 71 +++++++++---------- src/app/templates/activity/MultichainList.tsx | 1 + src/lib/activity/evm/parse.ts | 14 ++-- src/lib/activity/index.ts | 2 +- src/lib/activity/tezos/index.ts | 6 +- src/lib/activity/types.ts | 6 +- src/lib/activity/utils.ts | 4 ++ src/lib/metadata/utils.ts | 8 ++- 8 files changed, 60 insertions(+), 52 deletions(-) diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 2d071508ec..5946ed086a 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC, ReactNode, useCallback, useMemo } from 'react'; import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; @@ -8,7 +8,7 @@ import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; -import { ActivityOperKindEnum, InfinitySymbol } from 'lib/activity'; +import { ActivityOperKindEnum } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -30,7 +30,7 @@ interface Props { export interface ActivityItemBaseAssetProp { contract: string; tokenId?: string; - amount?: string | typeof InfinitySymbol; + amount?: string | null; decimals: number; symbol?: string; iconURL?: string; @@ -51,34 +51,44 @@ export const ActivityOperationBaseComponent: FC = ({ : toTezosAssetSlug(asset.contract, asset.tokenId) : null; - const { amountForFiat, amountJsx } = useMemo(() => { - if (!asset) return {}; + const amountJsx = useMemo(() => { + if (!asset) return null; - const amountForFiat = - typeof asset.amount === 'string' && (kind === 'bundle' || isTransferActivityOperKind(kind)) - ? atomsToTokens(asset.amount, asset.decimals) - : null; - - const amountJsx = ( + return (
- {asset.amount ? ( - asset.amount === InfinitySymbol ? ( - '∞ ' - ) : ( - <> - {kind === ActivityOperKindEnum.approve || asset.amount.startsWith('-') ? null : '+'} - {atomsToTokens(asset.amount, asset.decimals)}{' '} - - ) + {kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( + <> + {asset.amount.startsWith('-') ? null : '+'} + {atomsToTokens(asset.amount, asset.decimals)}{' '} + ) : null} - - {asset.symbol || '???'} + {asset.symbol || '---'}
); - - return { amountForFiat, amountJsx }; }, [asset, kind]); + const fiatJsx = useMemo(() => { + if (!asset) return null; + + if (!asset.amount) return asset.amount === null ? 'Unlimited' : null; + + if (kind === ActivityOperKindEnum.approve) + return {atomsToTokens(asset.amount, asset.decimals)}; + + if (!assetSlug) return null; + + const amountForFiat = + kind === 'bundle' || isTransferActivityOperKind(kind) ? atomsToTokens(asset.amount, asset.decimals) : null; + + return amountForFiat ? ( + <> + {amountForFiat.isPositive() && '+'} + + + + ) : null; + }, [asset, kind, assetSlug, chainId]); + const IconFallback = useCallback( () => (
@@ -138,18 +148,7 @@ export const ActivityOperationBaseComponent: FC = ({ - {amountForFiat && assetSlug ? ( -
- {amountForFiat.isPositive() && '+'} - - -
- ) : null} +
{fiatJsx}
diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 41f31575ec..80cb19a992 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -99,6 +99,7 @@ export const MultichainActivityList = memo(() => { .map(l => { if (!edgeDate) return l.activities; + // Not optimal, since activities are sorted already: // return l.activities.filter(a => a.addedAt >= edgeDate); const lastIndex = l.activities.findLastIndex(a => a.addedAt >= edgeDate); diff --git a/src/lib/activity/evm/parse.ts b/src/lib/activity/evm/parse.ts index b7b39761cc..9835f9910c 100644 --- a/src/lib/activity/evm/parse.ts +++ b/src/lib/activity/evm/parse.ts @@ -1,12 +1,13 @@ import { GoldRushERC20Transfer, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { toEvmAssetSlug } from 'lib/assets/utils'; -import { EvmAssetMetadataGetter, getAssetSymbol } from 'lib/metadata'; +import { EvmAssetMetadataGetter } from 'lib/metadata'; import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation, InfinitySymbol } from '../types'; +import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../types'; +import { getAssetSymbol } from '../utils'; export function parseGoldRushTransaction( item: GoldRushTransaction, @@ -133,8 +134,9 @@ export function parseGoldRushTransaction( if (!contractAddress) return { kind }; - const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; - const nft = logEvent.decoded.params[2]?.indexed ?? false; + const amountOrTokenIdParam = logEvent.decoded.params.at(2); + const amountOrTokenId: string = amountOrTokenIdParam?.value ?? '0'; + const nft = amountOrTokenIdParam?.indexed ?? false; const tokenId = nft ? amountOrTokenId : undefined; @@ -150,7 +152,7 @@ export function parseGoldRushTransaction( const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: nft ? '1' : undefined, // Often this amount is too large for non-NFTs + amount: nft ? '1' : amountOrTokenId, decimals, symbol, nft, @@ -174,7 +176,7 @@ export function parseGoldRushTransaction( const asset: EvmActivityAsset = { contract: contractAddress, - amount: InfinitySymbol, + amount: null, decimals: NaN, symbol: logEvent.sender_contract_ticker_symbol ?? undefined, nft: true, diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index fb7ffd8194..64f57bb648 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,5 +1,5 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivityAsset, EvmOperation } from './types'; -export { ActivityOperKindEnum, InfinitySymbol } from './types'; +export { ActivityOperKindEnum } from './types'; export { parseTezosPreActivityOperation } from './tezos'; diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 75c88b3593..e8e026ffa2 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -1,10 +1,10 @@ import { TezosPreActivity, TezosPreActivityOperation } from 'lib/activity/tezos/types'; -import { AssetMetadataBase, getAssetSymbol, isTezosCollectibleMetadata } from 'lib/metadata'; +import { AssetMetadataBase, isTezosCollectibleMetadata } from 'lib/metadata'; import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; import { TezosActivity, ActivityOperKindEnum, TezosOperation, TezosActivityAsset } from '../types'; -import { isTransferActivityOperKind } from '../utils'; +import { getAssetSymbol, isTransferActivityOperKind } from '../utils'; export { preparseTezosOperationsGroup } from './pre-parse'; @@ -88,7 +88,7 @@ export function parseTezosPreActivityOperation( amount: preOperation.amountSigned, decimals: assetMetadata.decimals, nft: isTezosCollectibleMetadata(assetMetadata), - symbol: getAssetSymbol(assetMetadata, true) + symbol: getAssetSymbol(assetMetadata) }; operationBase.asset = asset; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index b479c5bdab..ad76f44691 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -55,8 +55,8 @@ export interface EvmActivityAsset extends ActivityAssetBase { interface ActivityAssetBase { contract: string; tokenId?: string; - /** Signed (with `-` if applicable) */ - amount?: string | typeof InfinitySymbol; // TODO: Try without symbol + /** Signed (with `-` if applicable). `null` for 'unlimited' amount */ + amount?: string | null; decimals: number; nft?: boolean; symbol?: string; @@ -66,5 +66,3 @@ export interface OperationMember { address: string; alias?: string; } - -export const InfinitySymbol = Symbol('Infinity'); diff --git a/src/lib/activity/utils.ts b/src/lib/activity/utils.ts index 1892279a59..86540f37ea 100644 --- a/src/lib/activity/utils.ts +++ b/src/lib/activity/utils.ts @@ -1,3 +1,5 @@ +import { getAssetSymbol as getAssetSymbolFromMeta } from 'lib/metadata'; + import { ActivityOperKindEnum } from './types'; export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { @@ -8,3 +10,5 @@ export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { kind === ActivityOperKindEnum.transferTo ); } + +export const getAssetSymbol: typeof getAssetSymbolFromMeta = metadata => getAssetSymbolFromMeta(metadata, false, null); diff --git a/src/lib/metadata/utils.ts b/src/lib/metadata/utils.ts index d50d064048..88f467e467 100644 --- a/src/lib/metadata/utils.ts +++ b/src/lib/metadata/utils.ts @@ -12,8 +12,12 @@ import { EvmAssetMetadataBase } from './types'; -export function getAssetSymbol(metadata: EvmAssetMetadataBase | AssetMetadataBase | nullish, short = false) { - if (!metadata || !metadata.symbol) return '???'; +export function getAssetSymbol( + metadata: EvmAssetMetadataBase | AssetMetadataBase | nullish, + short = false, + fallback?: string | null +) { + if (!metadata || !metadata.symbol) return fallback === null ? undefined : fallback ?? '???'; if (!short) return metadata.symbol; return metadata.symbol === 'tez' ? TEZOS_SYMBOL : metadata.symbol.substring(0, 5); } From 0956fc9ce9d35792c493059009b700e441e88cc1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 3 Oct 2024 03:42:01 +0300 Subject: [PATCH 35/74] TW-1479: [EVM] Transactions history. Amounts crypto & fiat --- src/app/atoms/Money.tsx | 15 +++++- src/app/templates/InFiat.tsx | 52 ++++++++++--------- .../ActivityItem/ActivityOperationBase.tsx | 47 ++++++++++++----- src/lib/metadata/utils.ts | 4 +- 4 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/app/atoms/Money.tsx b/src/app/atoms/Money.tsx index 27ea0cbbbb..d93d1dc304 100644 --- a/src/app/atoms/Money.tsx +++ b/src/app/atoms/Money.tsx @@ -15,6 +15,8 @@ interface MoneyProps extends TestIDProps { roundingMode?: BigNumber.RoundingMode; shortened?: boolean; smallFractionFont?: boolean; + /** To show the '+' sign */ + withSign?: boolean; tooltip?: boolean; } @@ -29,6 +31,7 @@ const Money = memo( roundingMode = BigNumber.ROUND_DOWN, shortened, smallFractionFont = true, + withSign, tooltip = true, testID, testIDProperties @@ -61,6 +64,7 @@ const Money = memo( result={result} className={tippyClassName} bn={bn} + withSign={withSign} testID={testID} testIDProperties={testIDProperties} /> @@ -76,6 +80,7 @@ const Money = memo( cryptoDecimals={cryptoDecimals} roundingMode={roundingMode} smallFractionFont={smallFractionFont} + withSign={withSign} testID={testID} testIDProperties={testIDProperties} /> @@ -91,6 +96,7 @@ const Money = memo( isFiat={fiat} indexOfDecimal={indexOfDecimal} smallFractionFont={smallFractionFont} + withSign={withSign} testID={testID} testIDProperties={testIDProperties} /> @@ -105,9 +111,10 @@ interface JustMoneyProps extends TestIDProps { bn: BigNumber; className: string; result: string; + withSign?: boolean; } -const JustMoney: FC = ({ tooltip, bn, className, result, testID, testIDProperties }) => ( +const JustMoney: FC = ({ tooltip, bn, className, result, withSign, testID, testIDProperties }) => ( = ({ tooltip, bn, className, result, testID, testID={testID} testIDProperties={testIDProperties} > + {withSign && bn.isPositive() && '+'} {result} ); @@ -124,6 +132,7 @@ interface MoneyAnyFormatPropsBase extends TestIDProps { bn: BigNumber; className: string; smallFractionFont: boolean; + withSign?: boolean; } interface MoneyWithoutFormatProps extends MoneyAnyFormatPropsBase { @@ -152,6 +161,7 @@ const MoneyWithoutFormat: FC = ({ cryptoDecimals, roundingMode, smallFractionFont, + withSign, testID, testIDProperties }) => { @@ -174,6 +184,7 @@ const MoneyWithoutFormat: FC = ({ testID={testID} testIDProperties={testIDProperties} > + {withSign && bn.isPositive() && '+'} {result.slice(0, indexOfDecimal + 1)} {result.slice(indexOfDecimal + 1, result.length)} @@ -196,6 +207,7 @@ const MoneyWithFormat: FC = ({ indexOfDecimal, isFiat, smallFractionFont, + withSign, testID, testIDProperties }) => { @@ -215,6 +227,7 @@ const MoneyWithFormat: FC = ({ testID={testID} testIDProperties={testIDProperties} > + {withSign && bn.isPositive() && '+'} {result.slice(0, indexOfDecimal + 1)} {result.slice(indexOfDecimal + 1, result.length)} diff --git a/src/app/templates/InFiat.tsx b/src/app/templates/InFiat.tsx index 827b4be55c..0d58bf2466 100644 --- a/src/app/templates/InFiat.tsx +++ b/src/app/templates/InFiat.tsx @@ -1,16 +1,17 @@ import React, { FC, ReactElement, ReactNode, useMemo } from 'react'; -import { isDefined } from '@rnw-community/shared'; import BigNumber from 'bignumber.js'; import Money from 'app/atoms/Money'; import { TestIDProps } from 'lib/analytics'; import { useAssetFiatCurrencyPrice, useFiatCurrency } from 'lib/fiat-currency'; import { TEZOS_MAINNET_CHAIN_ID } from 'lib/temple/types'; +import { ZERO } from 'lib/utils/numbers'; interface OutputProps { balance: ReactNode; symbol: string; + noPrice: boolean; } interface Props extends TestIDProps { @@ -22,19 +23,20 @@ interface Props extends TestIDProps { shortened?: boolean; smallFractionFont?: boolean; showCents?: boolean; + withSign?: boolean; evm?: boolean; } -const InFiat: FC = props => { +const InFiatDefault: FC = props => { // TODO: show fiat value only for mainnet chains if (!props.evm && props.chainId !== TEZOS_MAINNET_CHAIN_ID) return null; - return ; + return ; }; -export default InFiat; +export default InFiatDefault; -const InFiatContent: FC = ({ +export const InFiat: FC = ({ evm, chainId, volume, @@ -44,6 +46,7 @@ const InFiatContent: FC = ({ shortened, smallFractionFont, showCents = true, + withSign, testID, testIDProperties }) => { @@ -51,33 +54,34 @@ const InFiatContent: FC = ({ const { selectedFiatCurrency } = useFiatCurrency(); const roundedInFiat = useMemo(() => { - if (!isDefined(price)) return new BigNumber(0); + if (price.isZero()) return ZERO; const inFiat = new BigNumber(volume).times(price); if (showCents) { return inFiat; } + return inFiat.integerValue(); }, [price, showCents, volume]); const cryptoDecimals = showCents ? undefined : 0; - return isDefined(price) - ? children({ - balance: ( - - {roundedInFiat} - - ), - symbol: selectedFiatCurrency.symbol - }) - : null; + return children({ + balance: ( + + {roundedInFiat} + + ), + symbol: selectedFiatCurrency.symbol, + noPrice: price.isZero() + }); }; diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 5946ed086a..67f4c691d7 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -7,7 +7,7 @@ import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; -import { FiatBalance } from 'app/pages/Home/OtherComponents/Tokens/components/Balance'; +import { InFiat } from 'app/templates/InFiat'; import { ActivityOperKindEnum } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; @@ -54,15 +54,18 @@ export const ActivityOperationBaseComponent: FC = ({ const amountJsx = useMemo(() => { if (!asset) return null; + const symbol = asset.symbol || (kind === ActivityOperKindEnum.approve ? '---' : ''); + const symbolStr = symbol.length > 6 ? `${symbol.slice(0, 6)}...` : symbol; + return ( -
+
{kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( - <> - {asset.amount.startsWith('-') ? null : '+'} - {atomsToTokens(asset.amount, asset.decimals)}{' '} - + + {atomsToTokens(asset.amount, asset.decimals)} + ) : null} - {asset.symbol || '---'} + + {symbolStr ? {symbolStr} : null}
); }, [asset, kind]); @@ -80,13 +83,29 @@ export const ActivityOperationBaseComponent: FC = ({ const amountForFiat = kind === 'bundle' || isTransferActivityOperKind(kind) ? atomsToTokens(asset.amount, asset.decimals) : null; - return amountForFiat ? ( - <> - {amountForFiat.isPositive() && '+'} + if (!amountForFiat) return null; - - - ) : null; + return ( + + {({ balance, symbol, noPrice }) => + noPrice ? ( + No value + ) : ( + <> + {balance} + {symbol} + + ) + } + + ); }, [asset, kind, assetSlug, chainId]); const IconFallback = useCallback( @@ -130,7 +149,7 @@ export const ActivityOperationBaseComponent: FC = ({
-
+
{ActivityKindTitle[kind]}
diff --git a/src/lib/metadata/utils.ts b/src/lib/metadata/utils.ts index 88f467e467..c9f90eae39 100644 --- a/src/lib/metadata/utils.ts +++ b/src/lib/metadata/utils.ts @@ -17,8 +17,10 @@ export function getAssetSymbol( short = false, fallback?: string | null ) { - if (!metadata || !metadata.symbol) return fallback === null ? undefined : fallback ?? '???'; + if (!metadata?.symbol) return fallback === null ? undefined : fallback ?? '???'; + if (!short) return metadata.symbol; + return metadata.symbol === 'tez' ? TEZOS_SYMBOL : metadata.symbol.substring(0, 5); } From be732c5edb2bdacb7b47b231bc7785bdc6b82862 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 4 Oct 2024 02:50:39 +0300 Subject: [PATCH 36/74] TW-1479: [EVM] Transactions history. Filters. WIP --- TODO.md | 14 +- package.json | 2 +- src/app/atoms/InfiniteScroll.tsx | 11 +- src/app/atoms/LoaderDebounce.tsx | 21 ++ .../SettingsCell.tsx} | 30 ++- src/app/hooks/ads/use-ad-timeout.ts | 2 +- src/app/pages/AccountSettings/index.tsx | 18 +- src/app/pages/Activity/index.tsx | 197 ++++++++++++------ .../templates/activity/EvmActivityList.tsx | 17 +- src/app/templates/activity/MultichainList.tsx | 22 +- .../templates/activity/TezosActivityList.tsx | 18 +- src/app/templates/activity/index.ts | 2 + src/app/templates/activity/loading-logic.ts | 10 +- src/app/templates/activity/utils.ts | 41 ++++ src/lib/activity/types.ts | 1 - src/lib/ui/hooks/useTimeout.ts | 11 +- yarn.lock | 8 +- 17 files changed, 309 insertions(+), 116 deletions(-) create mode 100644 src/app/atoms/LoaderDebounce.tsx rename src/app/{pages/AccountSettings/settings-cell.tsx => atoms/SettingsCell.tsx} (55%) create mode 100644 src/app/templates/activity/utils.ts diff --git a/TODO.md b/TODO.md index be050e7ad2..0a6d0c4caf 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,9 @@ - Remove `lib/temple/activity` - Prepare activities from TZKT in one parse -- Approve amount - NFTs -- Asset details (name, symbol, decimals) due to user's wallet set-up - `FAILED` // Look into `successful": false` - Block explorers href -- Date grouping -- Infinite scroll - New loader - @@ -28,3 +24,13 @@ - 'Show more/less' button logic - `TransferSingle` of ERC-1155 - + +### FOR PRE-REVIEW +- Filters by kind +- Filter by network dropdown +- Grouping by date +- Bundle modal +- - Optimise render +- - Design +- - Click on bundle item +- `const HIDE_INTERACTIONS = true;` diff --git a/package.json b/package.json index b9f86d0f48..45df51af89 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "ts-prune": "^0.10.3", "typescript": "^5.4.2", "typescript-eslint": "^7", - "use-debounce": "7.0.1", + "use-debounce": "^10", "use-force-update": "1.0.7", "use-onclickoutside": "0.4.1", "util": "0.11.1", diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index 727e5ab6dc..a5706c00af 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -1,9 +1,10 @@ -import React, { FC, ReactNode, useEffect } from 'react'; +import React, { FC, ReactElement, useEffect } from 'react'; import ReactInfiniteScrollComponent from 'react-infinite-scroll-component'; import { APP_CONTENT_PAPER_DOM_ID, SCROLL_DOCUMENT } from 'app/layouts/containers'; +import { LoaderDebounce } from './LoaderDebounce'; import { SyncSpinner } from './SyncSpinner'; interface Props extends PropsWithChildren { @@ -12,7 +13,7 @@ interface Props extends PropsWithChildren { reachedTheEnd: boolean; retryInitialLoad: EmptyFn; loadMore: EmptyFn; - loader?: ReactNode; + loader?: ReactElement; } export const InfiniteScroll: FC = ({ @@ -38,7 +39,7 @@ export const InfiniteScroll: FC = ({ return; if (isScrollAtTheEnd(scrollableElem)) loadNext(); - }, [isSyncing, reachedTheEnd]); + }, [isSyncing, itemsLength, reachedTheEnd]); return ( = ({ > {children} - {isSyncing ? loader ?? : null} + + {loader ?? } + ); }; diff --git a/src/app/atoms/LoaderDebounce.tsx b/src/app/atoms/LoaderDebounce.tsx new file mode 100644 index 0000000000..a9ed59dd51 --- /dev/null +++ b/src/app/atoms/LoaderDebounce.tsx @@ -0,0 +1,21 @@ +import { FC, ReactElement } from 'react'; + +import { useDebounce } from 'use-debounce'; + +import { useDidUpdate } from 'lib/ui/hooks'; + +interface Props { + isSyncing: boolean; + keepTime?: number; + children: ReactElement; +} + +export const LoaderDebounce: FC = ({ isSyncing, keepTime = 500, children }) => { + const [isSyncingDebounced, { flush }] = useDebounce(isSyncing, keepTime); + + useDidUpdate(() => { + if (isSyncing && !isSyncingDebounced) flush(); // Not keeping `false` debounced + }, [isSyncing]); + + return isSyncing || isSyncingDebounced ? children : null; +}; diff --git a/src/app/pages/AccountSettings/settings-cell.tsx b/src/app/atoms/SettingsCell.tsx similarity index 55% rename from src/app/pages/AccountSettings/settings-cell.tsx rename to src/app/atoms/SettingsCell.tsx index 45954933bd..a1c33e369b 100644 --- a/src/app/pages/AccountSettings/settings-cell.tsx +++ b/src/app/atoms/SettingsCell.tsx @@ -1,7 +1,9 @@ -import React, { FC, HTMLAttributes, ReactNode } from 'react'; +import React, { FC, HTMLAttributes, ReactElement, ReactNode } from 'react'; import clsx from 'clsx'; +import { Button } from 'app/atoms'; + interface ComponentBase { className?: string; children?: ReactNode; @@ -18,17 +20,17 @@ interface DivSettingsCellProps extends HTMLAttributes, SettingsC type FCSettingsCellProps

= P & SettingsCellPropsBase

& { Component: FC

}; -type SettingsCellProps

= P extends { Component: 'div' } +type SettingsCellSingleProps

= P extends { Component: 'div' } ? DivSettingsCellProps : FCSettingsCellProps

; -export const SettingsCell =

({ +export const SettingsCellSingle =

({ className, cellName: name, children, Component, ...restProps -}: SettingsCellProps

) => { +}: SettingsCellSingleProps

) => { return ( ({ ); }; + +interface Props { + title: ReactNode; + first?: boolean; + icon: ReactElement; + onClick: EmptyFn; +} + +export const SettingsCell: FC = ({ title, first, onClick, icon }) => { + return ( + + ); +}; diff --git a/src/app/hooks/ads/use-ad-timeout.ts b/src/app/hooks/ads/use-ad-timeout.ts index 37f499ed06..a31386ceda 100644 --- a/src/app/hooks/ads/use-ad-timeout.ts +++ b/src/app/hooks/ads/use-ad-timeout.ts @@ -1,5 +1,5 @@ import { useTimeout } from 'lib/ui/hooks'; export const useAdTimeout = (adIsReady: boolean, onTimeout: () => void, timeMs = 10000) => { - useTimeout(onTimeout, timeMs, !adIsReady); + useTimeout(onTimeout, timeMs, !adIsReady, [onTimeout]); }; diff --git a/src/app/pages/AccountSettings/index.tsx b/src/app/pages/AccountSettings/index.tsx index 0608f4dcf3..19aefc4d0b 100644 --- a/src/app/pages/AccountSettings/index.tsx +++ b/src/app/pages/AccountSettings/index.tsx @@ -7,6 +7,7 @@ import { AccountName } from 'app/atoms/AccountName'; import { CopyButton } from 'app/atoms/CopyButton'; import { EvmNetworksLogos, TezNetworkLogo } from 'app/atoms/NetworksLogos'; import { ActionsButtonsBox } from 'app/atoms/PageModal/actions-buttons-box'; +import { SettingsCellSingle } from 'app/atoms/SettingsCell'; import { StyledButton } from 'app/atoms/StyledButton'; import { TotalEquity } from 'app/atoms/TotalEquity'; import { useAllAccountsReactiveOnRemoval } from 'app/hooks/use-all-accounts-reactive'; @@ -25,7 +26,6 @@ import { EditAccountNameModal } from './edit-account-name-modal'; import { RemoveAccountModal } from './remove-account-modal'; import { RevealPrivateKeyModal } from './reveal-private-key-modal'; import { AccountSettingsSelectors } from './selectors'; -import { SettingsCell } from './settings-cell'; import { PrivateKeyPayload } from './types'; interface AccountSettingsProps { @@ -157,33 +157,33 @@ export const AccountSettings = memo(({ id }) => {

- } Component="div"> + } Component="div"> - + - } Component={Button} onClick={openEditNameModal} testID={AccountSettingsSelectors.editName} > - + {(account.type === TempleAccountType.HD || account.type === TempleAccountType.Imported) && ( - } Component={Button} onClick={openRevealPrivateKeyModal} testID={AccountSettingsSelectors.revealPrivateKey} > - + )}
@@ -193,7 +193,7 @@ export const AccountSettings = memo(({ id }) => {

{derivationPaths.map(({ chainName, path }) => ( - (({ id }) => { testIDProperties={{ chainName }} > {chainName === 'tezos' ? : } - + ))}
)} diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index df59c11bc4..adab8030a9 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -1,7 +1,8 @@ -import React, { memo, useMemo, useState } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; +import { RadioButton } from 'app/atoms/RadioButton'; import { TextButton } from 'app/atoms/TextButton'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { ReactComponent as FilterOffIcon } from 'app/icons/base/filteroff.svg'; @@ -12,7 +13,8 @@ import { ActivityListContainer, EvmActivityList, TezosActivityList, - MultichainActivityList + MultichainActivityList, + FilterKind } from 'app/templates/activity'; import { NetworkSelectModalContent } from 'app/templates/NetworkSelectModal'; import { T } from 'lib/i18n'; @@ -20,13 +22,25 @@ import { useBooleanState } from 'lib/ui/hooks'; import { useAllEvmChains, useAllTezosChains } from 'temple/front'; import { TempleChainKind } from 'temple/types'; +import { SettingsCell } from '../../atoms/SettingsCell'; + export const ActivityPage = memo(() => { const { filterChain: initFilterChain } = useAssetsFilterOptionsSelector(); const [filterChain, setFilterChain] = useState(initFilterChain); + const [filterKind, setFilterKind] = useState(null); + const [filtersModalOpen, setFiltersModalOpen, setFiltersModalClosed] = useBooleanState(false); + const handleFilterKindChange = useCallback( + (kind: FilterKind) => { + setFilterKind(kind); + if (kind !== filterKind) setFiltersModalClosed(); + }, + [filterKind] + ); + const handleChainChange = useMemo( () => initFilterChain @@ -47,7 +61,9 @@ export const ActivityPage = memo(() => { > @@ -55,13 +71,13 @@ export const ActivityPage = memo(() => { {filterChain ? ( {filterChain.kind === 'tezos' ? ( - + ) : ( - + )} ) : ( - + )} ); @@ -69,76 +85,123 @@ export const ActivityPage = memo(() => { interface FiltersModalProps { open: boolean; + filterKind: FilterKind; filterChain: FilterChain; + handleFilterKindChange: SyncFn; handleChainChange?: (chain: FilterChain) => void; onRequestClose: EmptyFn; } -const FiltersModal = memo(({ open, filterChain, handleChainChange, onRequestClose }) => { - const [networksMode, setModeToNetworks, setModeToFilters] = useBooleanState(false); - - return ( - - {networksMode && handleChainChange ? ( - { - handleChainChange(chain); - setModeToFilters(); - }} - /> - ) : ( - - )} - - ); -}); +const FiltersModal = memo( + ({ open, filterKind, filterChain, handleFilterKindChange, handleChainChange, onRequestClose }) => { + const [networksMode, setModeToNetworks, setModeToFilters] = useBooleanState(false); + + return ( + + {networksMode && handleChainChange ? ( + { + handleChainChange(chain); + setModeToFilters(); + }} + /> + ) : ( + + )} + + ); + } +); interface FiltersMainContentProps { + filterKind: FilterKind; filterChain: FilterChain; + handleFilterKindChange: SyncFn; onOpenNetworksClick?: EmptyFn; } -const FiltersMainContent = memo(({ filterChain, onOpenNetworksClick }) => { - const tezosChains = useAllTezosChains(); - const evmChains = useAllEvmChains(); - - const chain = useMemo(() => { - if (!filterChain) return null; - - if (filterChain.kind === TempleChainKind.Tezos) return tezosChains[filterChain.chainId]; - - return evmChains[filterChain.chainId]; - }, [filterChain, evmChains, tezosChains]); - - const disabledNetworkChange = !onOpenNetworksClick; - - return ( - <> -
- Filter by network - - - {chain ? chain.nameI18nKey ? : chain.name : 'All Networks'} - -
- -
Filters by activity kind
- - ); -}); +const FiltersMainContent = memo( + ({ filterKind, filterChain, handleFilterKindChange, onOpenNetworksClick }) => { + const tezosChains = useAllTezosChains(); + const evmChains = useAllEvmChains(); + + const chain = useMemo(() => { + if (!filterChain) return null; + + if (filterChain.kind === TempleChainKind.Tezos) return tezosChains[filterChain.chainId]; + + return evmChains[filterChain.chainId]; + }, [filterChain, evmChains, tezosChains]); + + const disabledNetworkChange = !onOpenNetworksClick; + + return ( + <> +
+ Filter by network + + + {chain ? chain.nameI18nKey ? : chain.name : 'All Networks'} + +
+ +
+ } + onClick={() => handleFilterKindChange(null)} + first + /> + + } + onClick={() => handleFilterKindChange('send')} + /> + + } + onClick={() => handleFilterKindChange('receive')} + /> + + } + onClick={() => handleFilterKindChange('approve')} + /> + + } + onClick={() => handleFilterKindChange('transfer')} + /> + + } + onClick={() => handleFilterKindChange('bundle')} + /> +
+ + ); + } +); diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index 0f63f41064..0c80fd589a 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { AxiosError } from 'axios'; @@ -15,13 +15,15 @@ import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; +import { FilterKind, getEvmActivityFilterKind } from './utils'; interface Props { chainId: number; assetSlug?: string; + filterKind: FilterKind; } -export const EvmActivityList: FC = ({ chainId, assetSlug }) => { +export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) => { const network = useEvmChainByChainId(chainId); const accountAddress = useAccountAddressForEvm(); @@ -71,19 +73,24 @@ export const EvmActivityList: FC = ({ chainId, assetSlug }) => { () => setNextPage(undefined) ); - if (activities.length === 0 && !isLoading && reachedTheEnd) { + const displayActivities = useMemo( + () => (filterKind ? activities.filter(a => getEvmActivityFilterKind(a) === filterKind) : activities), + [activities, filterKind] + ); + + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } return ( - {activities.map(activity => ( + {displayActivities.map(activity => ( ))} diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 80cb19a992..af714f3827 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -25,8 +25,13 @@ import { import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; +import { FilterKind, getEvmActivityFilterKind, getTezosPreActivityFilterKind, isEvmActivity } from './utils'; -export const MultichainActivityList = memo(() => { +interface Props { + filterKind: FilterKind; +} + +export const MultichainActivityList = memo(({ filterKind }) => { useLoadPartnersPromo(); const tezosChains = useEnabledTezosChains(); @@ -116,10 +121,17 @@ export const MultichainActivityList = memo(() => { [tezosLoaders, evmLoaders] ); - const displayActivities = useMemo( - () => activities.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)), - [activities] - ); + const displayActivities = useMemo(() => { + const filtered = filterKind + ? activities.filter(act => { + if (isEvmActivity(act)) return getEvmActivityFilterKind(act) === filterKind; + + return getTezosPreActivityFilterKind(act, tezAccAddress!) === filterKind; + }) + : activities; + + return filtered.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)); + }, [activities, filterKind, tezAccAddress]); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index 35ee81bd72..b7dece8eb4 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; @@ -12,13 +12,15 @@ import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front' import { TezosActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; +import { FilterKind, getTezosPreActivityFilterKind } from './utils'; interface Props { tezosChainId: string; assetSlug?: string; + filterKind: FilterKind; } -export const TezosActivityList = memo(({ tezosChainId, assetSlug }) => { +export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterKind }) => { const network = useTezosChainByChainId(tezosChainId); const accountAddress = useAccountAddressForTezos(); @@ -65,19 +67,25 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug }) => { isKnownChainId(chainId) ); - if (activities.length === 0 && !isLoading && reachedTheEnd) { + const displayActivities = useMemo( + () => + filterKind ? activities.filter(a => getTezosPreActivityFilterKind(a, accountAddress) === filterKind) : activities, + [activities, filterKind, accountAddress] + ); + + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } return ( - {activities.map(activity => ( + {displayActivities.map(activity => ( ( loadActivities(true, abortAndRenewSignal()); }, resetDeps); - return { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext }; + return { + activities, + isLoading, + reachedTheEnd, + setActivities, + setIsLoading, + setReachedTheEnd, + loadNext + }; } diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts new file mode 100644 index 0000000000..63334dc785 --- /dev/null +++ b/src/app/templates/activity/utils.ts @@ -0,0 +1,41 @@ +import { ActivityOperKindEnum, Activity, EvmActivity, parseTezosPreActivityOperation } from 'lib/activity'; +import { TezosPreActivity } from 'lib/activity/tezos/types'; + +export function isEvmActivity(activity: Activity | TezosPreActivity): activity is EvmActivity { + return typeof activity.chainId === 'number'; +} + +export type FilterKind = 'send' | 'receive' | 'approve' | 'transfer' | 'bundle' | null; + +export function getEvmActivityFaceKind({ operations, operationsCount }: EvmActivity) { + return operationsCount === 1 ? operations.at(0)?.kind ?? ActivityOperKindEnum.interaction : 'batch'; +} + +const FILTER_KINDS: Record = { + [ActivityOperKindEnum.approve]: 'approve', + [ActivityOperKindEnum.transferFrom]: 'transfer', + [ActivityOperKindEnum.transferFrom_ToAccount]: 'send', + [ActivityOperKindEnum.transferTo]: 'transfer', + [ActivityOperKindEnum.transferTo_FromAccount]: 'receive', + // + [ActivityOperKindEnum.interaction]: null, + [ActivityOperKindEnum.swap]: null +}; + +export function getEvmActivityFilterKind({ operations, operationsCount }: EvmActivity): FilterKind { + if (operationsCount !== 1) return 'bundle'; + + const kind = operations.at(0)?.kind ?? ActivityOperKindEnum.interaction; + + return FILTER_KINDS[kind]; +} + +export function getTezosPreActivityFilterKind({ operations }: TezosPreActivity, address: string): FilterKind { + if (operations.length !== 1) return 'bundle'; + + const operation = operations.at(0)!; + + const kind = parseTezosPreActivityOperation(operation, address)?.kind; + + return FILTER_KINDS[kind]; +} diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index ad76f44691..57c542c817 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -10,7 +10,6 @@ export enum ActivityOperKindEnum { approve } -// @ts-prune-ignore-next export type Activity = TezosActivity | EvmActivity; interface ChainActivityBase { diff --git a/src/lib/ui/hooks/useTimeout.ts b/src/lib/ui/hooks/useTimeout.ts index 5736c5b846..465c958d7c 100644 --- a/src/lib/ui/hooks/useTimeout.ts +++ b/src/lib/ui/hooks/useTimeout.ts @@ -1,17 +1,18 @@ import { useEffect } from 'react'; +import { useUpdatableRef } from './useUpdatableRef'; + const DEFAULT_DEPS: unknown[] = []; -/** - * @arg callback // Must be memoized - */ export const useTimeout = (callback: EmptyFn, timeout: number, condition = true, deps = DEFAULT_DEPS) => { + const callbackRef = useUpdatableRef(callback); + useEffect(() => { if (!condition) return; - const timeoutId = setTimeout(callback, timeout); + const timeoutId = setTimeout(() => void callbackRef.current(), timeout); return () => void clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [condition, timeout, callback, ...deps]); + }, [condition, timeout, ...deps]); }; diff --git a/yarn.lock b/yarn.lock index d04fbfcd5a..20d924580b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12653,10 +12653,10 @@ use-composed-ref@^1.0.0: dependencies: ts-essentials "^2.0.3" -use-debounce@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-7.0.1.tgz#380e6191cc13ad29f8e2149a12b5c37cc2891190" - integrity sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q== +use-debounce@^10: + version "10.0.3" + resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.3.tgz#636094a37f7aa2bcc77b26b961481a0b571bf7ea" + integrity sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg== use-force-update@1.0.7: version "1.0.7" From 951cea374423b2ebefcc670895832564b87c35f0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 4 Oct 2024 18:56:14 +0300 Subject: [PATCH 37/74] TW-1479: [EVM] Transactions history. Refactor --- .../activity/ActivityItem/EvmActivity.tsx | 6 +- .../activity/ActivityItem/TezosActivity.tsx | 95 ++---- .../ActivityItem/TezosActivityOperation.tsx | 49 ++- .../templates/activity/EvmActivityList.tsx | 4 +- src/app/templates/activity/MultichainList.tsx | 32 +- .../templates/activity/TezosActivityList.tsx | 23 +- src/app/templates/activity/utils.ts | 14 +- src/lib/activity/evm/parse.ts | 312 ------------------ src/lib/activity/evm/parse/gas.ts | 46 +++ src/lib/activity/evm/parse/gr-v2.ts | 83 +++++ src/lib/activity/evm/parse/gr-v3.ts | 215 ++++++++++++ src/lib/activity/evm/parse/index.ts | 2 + src/lib/activity/index.ts | 2 - src/lib/activity/tezos/index.ts | 47 ++- src/lib/activity/types.ts | 20 +- src/lib/apis/temple/endpoints/evm/index.ts | 25 +- .../types/{erc20-transfers.ts => gr-v2.ts} | 10 +- .../evm/types/{transactions.ts => gr-v3.ts} | 9 +- 18 files changed, 501 insertions(+), 493 deletions(-) delete mode 100644 src/lib/activity/evm/parse.ts create mode 100644 src/lib/activity/evm/parse/gas.ts create mode 100644 src/lib/activity/evm/parse/gr-v2.ts create mode 100644 src/lib/activity/evm/parse/gr-v3.ts create mode 100644 src/lib/activity/evm/parse/index.ts rename src/lib/apis/temple/endpoints/evm/types/{erc20-transfers.ts => gr-v2.ts} (94%) rename src/lib/apis/temple/endpoints/evm/types/{transactions.ts => gr-v3.ts} (98%) diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 76df60fcbb..7ee5fe672c 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -26,11 +26,9 @@ interface Props { export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) => { const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - const { hash, blockExplorerUrl } = activity; + const { hash, operations, operationsCount, blockExplorerUrl } = activity; - const operations = activity.operations; - - if (activity.operationsCount === 1) { + if (operationsCount === 1) { const operation = operations.at(0); return ( diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index c2eab6a7fb..47545edf94 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -5,47 +5,46 @@ import clsx from 'clsx'; import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { TezosActivityAsset, parseTezosPreActivityOperation } from 'lib/activity'; -import { TezosPreActivity } from 'lib/activity/tezos/types'; +import { TezosActivity } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; -import { toTezosAssetSlug } from 'lib/assets/utils'; import { t } from 'lib/i18n'; import { useGetChainTokenOrGasMetadata } from 'lib/metadata'; -import { useBooleanState, useMemoWithCompare } from 'lib/ui/hooks'; +import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; import { useExplorerHref } from 'temple/front/block-explorers'; import { TezosChain } from 'temple/front/chains'; -import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { ActivityOperationBaseComponent } from './ActivityOperationBase'; import { InteractionsConnector } from './InteractionsConnector'; -import { TezosActivityOperationComponent } from './TezosActivityOperation'; +import { TezosActivityOperationComponent, buildTezosOperationAsset } from './TezosActivityOperation'; interface Props { - activity: TezosPreActivity; + activity: TezosActivity; chain: TezosChain; - accountAddress: string; assetSlug?: string; } -export const TezosActivityComponent = memo(({ activity, chain, accountAddress, assetSlug }) => { +export const TezosActivityComponent = memo(({ activity, chain, assetSlug }) => { const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - const { hash, operations } = activity; + const { hash, operations, operationsCount } = activity; const blockExplorerUrl = useExplorerHref(chain.chainId, hash) ?? undefined; - if (operations.length === 1) + if (operationsCount === 1) { + const operation = operations.at(0); + return ( ); + } return ( (({ activity, chain, accountAdd chainId={chain.chainId} assetSlug={assetSlug} blockExplorerUrl={blockExplorerUrl} - accountAddress={accountAddress} networkName={networkName} /> ); }); interface BatchProps { - activity: TezosPreActivity; + activity: TezosActivity; chainId: string; assetSlug?: string; blockExplorerUrl?: string; - accountAddress: string; networkName: string; } const TezosActivityBatchComponent = memo( - ({ activity, chainId, assetSlug, blockExplorerUrl, accountAddress, networkName }) => { + ({ activity, chainId, assetSlug, blockExplorerUrl, networkName }) => { const [expanded, , , toggleExpanded] = useBooleanState(false); - const { hash } = activity; - - const preOperations = activity.operations; + const { hash, operations } = activity; const getMetadata = useGetChainTokenOrGasMetadata(chainId); - const operations = useMemoWithCompare( - () => - preOperations.map(o => { - const slug = o.contract ? toTezosAssetSlug(o.contract, o.tokenId) : undefined; - return parseTezosPreActivityOperation(o, accountAddress, slug ? getMetadata(slug) : undefined); - }), - [preOperations, getMetadata, accountAddress] - ); - - const faceSlug = useMemo(() => { - if (assetSlug) return assetSlug; - - for (const { kind, asset } of operations) { - if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) - return toTezosAssetSlug(asset.contract, asset.tokenId); - } - - return; - }, [operations, assetSlug]); - const batchAsset = useMemo(() => { + const faceSlug = + assetSlug || + operations.find( + ({ kind, assetSlug, amountSigned }) => + assetSlug && + amountSigned && + Number(amountSigned) !== 0 && + isTransferActivityOperKind(kind) && + getMetadata(assetSlug) + )?.assetSlug; + if (!faceSlug) return; - let faceAsset: TezosActivityAsset | undefined; let faceAmount = ZERO; - for (const { kind, asset } of operations) { - if ( - typeof asset?.amount === 'string' && - toTezosAssetSlug(asset.contract, asset.tokenId) === faceSlug && - isTransferActivityOperKind(kind) - ) { - faceAmount = faceAmount.plus(asset.amount); - if (!faceAsset) faceAsset = asset; - } + for (const { kind, assetSlug, amountSigned } of operations) { + if (assetSlug === faceSlug && amountSigned && isTransferActivityOperKind(kind)) + faceAmount = faceAmount.plus(amountSigned); } - if (!faceAsset) return; - - const batchAsset: ActivityItemBaseAssetProp = { - ...faceAsset, - amount: faceAmount.toFixed() - }; - - return batchAsset; - }, [operations, faceSlug]); + return buildTezosOperationAsset(faceSlug, getMetadata(faceSlug)!, faceAmount.toFixed()); + }, [getMetadata, operations, assetSlug]); return (
@@ -152,12 +124,11 @@ const TezosActivityBatchComponent = memo( {j > 0 && } - diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 9e08df1328..55b4f419a2 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -1,43 +1,60 @@ import React, { memo, useMemo } from 'react'; -import { TezosOperation, parseTezosPreActivityOperation } from 'lib/activity'; -import { TezosPreActivityOperation } from 'lib/activity/tezos/types'; -import { toTezosAssetSlug } from 'lib/assets/utils'; -import { useTezosAssetMetadata } from 'lib/metadata'; +import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; +import { fromAssetSlug } from 'lib/assets'; +import { AssetMetadataBase, getAssetSymbol, useTezosAssetMetadata } from 'lib/metadata'; -import { ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; interface Props { hash: string; - operation: TezosPreActivityOperation; + operation?: TezosOperation; chainId: string; networkName: string; blockExplorerUrl: string | nullish; - accountAddress: string; withoutAssetIcon?: boolean; } export const TezosActivityOperationComponent = memo( - ({ hash, operation: preOperation, chainId, networkName, blockExplorerUrl, accountAddress, withoutAssetIcon }) => { - const assetSlug = preOperation.contract ? toTezosAssetSlug(preOperation.contract, preOperation.tokenId) : ''; + ({ hash, operation, chainId, networkName, blockExplorerUrl, withoutAssetIcon }) => { + const assetSlug = operation?.assetSlug; + const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); - const assetMetadata = useTezosAssetMetadata(assetSlug, chainId); - - const operation = useMemo( - () => parseTezosPreActivityOperation(preOperation, accountAddress, assetMetadata), - [assetMetadata, preOperation, accountAddress] + const asset = useMemo( + () => + assetSlug && assetMetadata + ? buildTezosOperationAsset(assetSlug, assetMetadata, operation.amountSigned) + : undefined, + [assetMetadata, operation, assetSlug] ); return ( ); } ); + +export function buildTezosOperationAsset( + assetSlug: string, + assetMetadata: AssetMetadataBase, + amount: string | nullish +): ActivityItemBaseAssetProp { + const [contract, tokenId] = fromAssetSlug(assetSlug); + + return { + contract, + tokenId, + amount, + decimals: assetMetadata.decimals, + // nft: isTezosCollectibleMetadata(assetMetadata), + symbol: getAssetSymbol(assetMetadata) + }; +} diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index 0c80fd589a..f5552e1aad 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -15,7 +15,7 @@ import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; -import { FilterKind, getEvmActivityFilterKind } from './utils'; +import { FilterKind, getActivityFilterKind } from './utils'; interface Props { chainId: number; @@ -74,7 +74,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = ); const displayActivities = useMemo( - () => (filterKind ? activities.filter(a => getEvmActivityFilterKind(a) === filterKind) : activities), + () => (filterKind ? activities.filter(a => getActivityFilterKind(a) === filterKind) : activities), [activities, filterKind] ); diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index af714f3827..ccf7e3829b 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -5,11 +5,10 @@ import { AxiosError } from 'axios'; import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { EvmActivity } from 'lib/activity'; +import { Activity, EvmActivity, TezosActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; -import { preparseTezosOperationsGroup } from 'lib/activity/tezos'; +import { parseTezosOperationsGroup } from 'lib/activity/tezos'; import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; -import { TezosPreActivity } from 'lib/activity/tezos/types'; import { TzktApiChainId } from 'lib/apis/tzkt'; import { isKnownChainId as isKnownTzktChainId } from 'lib/apis/tzkt/api'; import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; @@ -25,7 +24,7 @@ import { import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; -import { FilterKind, getEvmActivityFilterKind, getTezosPreActivityFilterKind, isEvmActivity } from './utils'; +import { FilterKind, getActivityFilterKind } from './utils'; interface Props { filterKind: FilterKind; @@ -65,7 +64,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { const getEvmMetadata = useGetEvmAssetMetadata(); const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = - useActivitiesLoadingLogic( + useActivitiesLoadingLogic( async (initial, signal) => { if (signal.aborted) return; @@ -122,16 +121,10 @@ export const MultichainActivityList = memo(({ filterKind }) => { ); const displayActivities = useMemo(() => { - const filtered = filterKind - ? activities.filter(act => { - if (isEvmActivity(act)) return getEvmActivityFilterKind(act) === filterKind; - - return getTezosPreActivityFilterKind(act, tezAccAddress!) === filterKind; - }) - : activities; + const filtered = filterKind ? activities.filter(act => getActivityFilterKind(act) === filterKind) : activities; return filtered.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)); - }, [activities, filterKind, tezAccAddress]); + }, [activities, filterKind]); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; @@ -147,12 +140,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { > {displayActivities.map(activity => isTezosActivity(activity) ? ( - + ) : ( ) @@ -161,7 +149,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { ); }); -function isTezosActivity(activity: EvmActivity | TezosPreActivity): activity is TezosPreActivity { +function isTezosActivity(activity: Activity): activity is TezosActivity { return 'oldestTzktOperation' in activity; } @@ -220,7 +208,7 @@ class EvmActivityLoader { } class TezosActivityLoader { - activities: TezosPreActivity[] = []; + activities: TezosActivity[] = []; reachedTheEnd = false; lastError: unknown; @@ -241,7 +229,7 @@ class TezosActivityLoader { if (signal.aborted) return; - const newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); + const newActivities = groups.map(group => parseTezosOperationsGroup(group, chainId, accountAddress)); if (newActivities.length) this.activities = this.activities.concat(newActivities); else this.reachedTheEnd = true; diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index b7dece8eb4..8e985d7e52 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -4,15 +4,15 @@ import { EmptyState } from 'app/atoms/EmptyState'; import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; -import { preparseTezosOperationsGroup } from 'lib/activity/tezos'; +import { TezosActivity } from 'lib/activity'; +import { parseTezosOperationsGroup } from 'lib/activity/tezos'; import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; -import { TezosPreActivity } from 'lib/activity/tezos/types'; import { isKnownChainId } from 'lib/apis/tzkt/api'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { TezosActivityComponent } from './ActivityItem'; import { useActivitiesLoadingLogic } from './loading-logic'; -import { FilterKind, getTezosPreActivityFilterKind } from './utils'; +import { FilterKind, getActivityFilterKind } from './utils'; interface Props { tezosChainId: string; @@ -31,7 +31,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK const { chainId, rpcBaseURL } = network; const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = - useActivitiesLoadingLogic( + useActivitiesLoadingLogic( async (initial, signal) => { if (!isKnownChainId(chainId)) { setIsLoading(false); @@ -50,7 +50,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK if (signal.aborted) return; - const newActivities = groups.map(group => preparseTezosOperationsGroup(group, accountAddress, chainId)); + const newActivities = groups.map(group => parseTezosOperationsGroup(group, chainId, accountAddress)); setActivities(currActivities.concat(newActivities)); if (newActivities.length === 0) setReachedTheEnd(true); @@ -68,9 +68,8 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK ); const displayActivities = useMemo( - () => - filterKind ? activities.filter(a => getTezosPreActivityFilterKind(a, accountAddress) === filterKind) : activities, - [activities, filterKind, accountAddress] + () => (filterKind ? activities.filter(act => getActivityFilterKind(act) === filterKind) : activities), + [activities, filterKind] ); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { @@ -86,13 +85,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK loadMore={loadNext} > {displayActivities.map(activity => ( - + ))} ); diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index 63334dc785..2146be03a3 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -1,4 +1,4 @@ -import { ActivityOperKindEnum, Activity, EvmActivity, parseTezosPreActivityOperation } from 'lib/activity'; +import { ActivityOperKindEnum, Activity, EvmActivity } from 'lib/activity'; import { TezosPreActivity } from 'lib/activity/tezos/types'; export function isEvmActivity(activity: Activity | TezosPreActivity): activity is EvmActivity { @@ -22,20 +22,10 @@ const FILTER_KINDS: Record = { [ActivityOperKindEnum.swap]: null }; -export function getEvmActivityFilterKind({ operations, operationsCount }: EvmActivity): FilterKind { +export function getActivityFilterKind({ operations, operationsCount }: Activity): FilterKind { if (operationsCount !== 1) return 'bundle'; const kind = operations.at(0)?.kind ?? ActivityOperKindEnum.interaction; return FILTER_KINDS[kind]; } - -export function getTezosPreActivityFilterKind({ operations }: TezosPreActivity, address: string): FilterKind { - if (operations.length !== 1) return 'bundle'; - - const operation = operations.at(0)!; - - const kind = parseTezosPreActivityOperation(operation, address)?.kind; - - return FILTER_KINDS[kind]; -} diff --git a/src/lib/activity/evm/parse.ts b/src/lib/activity/evm/parse.ts deleted file mode 100644 index 9835f9910c..0000000000 --- a/src/lib/activity/evm/parse.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { GoldRushERC20Transfer, GoldRushTransaction } from 'lib/apis/temple/endpoints/evm'; -import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; -import { toEvmAssetSlug } from 'lib/assets/utils'; -import { EvmAssetMetadataGetter } from 'lib/metadata'; -import { isTruthy } from 'lib/utils'; -import { getEvmAddressSafe } from 'lib/utils/evm.utils'; -import { TempleChainKind } from 'temple/types'; - -import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../types'; -import { getAssetSymbol } from '../utils'; - -export function parseGoldRushTransaction( - item: GoldRushTransaction, - chainId: number, - accountAddress: string, - getMetadata: EvmAssetMetadataGetter -): EvmActivity { - const logEvents = item.log_events ?? []; - const addedAt = item.block_signed_at as unknown as string; - - const operations = logEvents - .map(logEvent => { - if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; - - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const _fromAddress = getEvmAddressSafe(logEvent.decoded.params[0]?.value); - const _toAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); - let decimals = logEvent.sender_contract_decimals; - const iconURL = logEvent.sender_logo_url ?? undefined; - - if (logEvent.decoded.name === 'Transfer') { - const kind = (() => { - if (_toAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; - } - - if (_fromAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; - } - - return null; - })(); - - if (kind == null || !contractAddress) return { kind: ActivityOperKindEnum.interaction }; - - const amountOrTokenId: string = logEvent.decoded.params[2]?.value ?? '0'; - const nft = logEvent.decoded.params[2]?.indexed ?? false; - const tokenId = nft ? amountOrTokenId : undefined; - - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - - const amount = nft ? '1' : amountOrTokenId; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; - - const asset: EvmActivityAsset = { - contract: contractAddress, - tokenId, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, - decimals, - symbol, - nft, - iconURL - }; - - return { kind, asset }; - } - - if (logEvent.decoded.name === 'TransferSingle') { - const fromAddress = getEvmAddressSafe(logEvent.decoded.params[1]?.value); - const toAddress = getEvmAddressSafe(logEvent.decoded.params[2]?.value); - - const kind = (() => { - if (toAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; - } - - if (fromAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; - } - - return null; - })(); - - if (kind == null || !contractAddress) return null; - - const tokenId = logEvent.decoded.params[3]?.value ?? '0'; - - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - - const amount = '1'; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; - - const asset: EvmActivityAsset = { - contract: contractAddress, - tokenId, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, - decimals, - symbol, - nft: true, - iconURL - }; - - return { kind, asset }; - } - - if (logEvent.decoded.name === 'Approval') { - if (_fromAddress !== accountAddress) return null; - - const kind = ActivityOperKindEnum.approve; - - if (!contractAddress) return { kind }; - - const amountOrTokenIdParam = logEvent.decoded.params.at(2); - const amountOrTokenId: string = amountOrTokenIdParam?.value ?? '0'; - const nft = amountOrTokenIdParam?.indexed ?? false; - - const tokenId = nft ? amountOrTokenId : undefined; - - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; - - const asset: EvmActivityAsset = { - contract: contractAddress, - tokenId, - amount: nft ? '1' : amountOrTokenId, - decimals, - symbol, - nft, - iconURL - }; - - return { kind, asset }; - } - - if (logEvent.decoded.name === 'ApprovalForAll') { - if ( - // @ts-expect-error // `value` is not always `:string` - logEvent.decoded.params[2]?.value !== true || - _fromAddress !== accountAddress - ) - return null; - - const kind = ActivityOperKindEnum.approve; - - if (!contractAddress) return { kind }; - - const asset: EvmActivityAsset = { - contract: contractAddress, - amount: null, - decimals: NaN, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, - nft: true, - iconURL - }; - - return { kind, asset }; - } - - return { kind: ActivityOperKindEnum.interaction }; - }) - .filter(isTruthy); - - const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length), getMetadata); - - if (gasOperation) operations.unshift(gasOperation); - - return { - chain: TempleChainKind.EVM, - chainId, - hash: item.tx_hash!, - blockExplorerUrl: item.explorers?.[0]?.url, - operations, - operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length, - addedAt - }; -} - -export function parseGoldRushERC20Transfer( - item: GoldRushERC20Transfer, - chainId: number, - accountAddress: string, - getMetadata: EvmAssetMetadataGetter -): EvmActivity { - const transfers = item.transfers ?? []; - const addedAt = item.block_signed_at as unknown as string; - - const operations = transfers.map(transfer => { - const kind = (() => { - if (transfer.transfer_type === 'IN') { - return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; - } - - return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; - })(); - - const contractAddress = getEvmAddressSafe(transfer.contract_address); - - if (contractAddress == null) return { kind }; - - const slug = toEvmAssetSlug(contractAddress); - const metadata = getMetadata(slug); - - const decimals = metadata?.decimals ?? transfer.contract_decimals; - - if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; - - const nft = false; - const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; - const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; - - const asset: EvmActivityAsset = { - contract: contractAddress, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, - decimals, - symbol, - nft, - iconURL: transfer.logo_url ?? undefined - }; - - return { kind, asset }; - }); - - const gasOperation = parseGasTransfer(item, accountAddress, Boolean(transfers.length), getMetadata); - - if (gasOperation) operations.unshift(gasOperation); - - return { - chain: TempleChainKind.EVM, - chainId, - hash: item.tx_hash!, - blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, - operations, - operationsCount: gasOperation ? transfers.length + 1 : transfers.length, - addedAt - }; -} - -function parseGasTransfer( - item: GoldRushTransaction | GoldRushERC20Transfer, - accountAddress: string, - /** Only way to suspect transfering to a contract, not an account */ - partOfBatch: boolean, - getMetadata: EvmAssetMetadataGetter -): EvmOperation | null { - const value: string = item.value?.toString() ?? '0'; - - if (value === '0') return null; - - const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) - return partOfBatch ? ActivityOperKindEnum.transferFrom : ActivityOperKindEnum.transferFrom_ToAccount; - if (getEvmAddressSafe(item.to_address) === accountAddress) - return partOfBatch ? ActivityOperKindEnum.transferTo : ActivityOperKindEnum.transferTo_FromAccount; - - return null; - })(); - - if (!kind) return null; - - const metadata = getMetadata(EVM_TOKEN_SLUG); - const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; - - if (decimals == null) return null; - - const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; - - const asset: EvmActivityAsset = { - contract: EVM_TOKEN_SLUG, - amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value, - decimals, - symbol - }; - - return { kind, asset }; -} diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts new file mode 100644 index 0000000000..0a23452c4b --- /dev/null +++ b/src/lib/activity/evm/parse/gas.ts @@ -0,0 +1,46 @@ +import { GoldRushTransaction, GoldRushERC20Transaction } from 'lib/apis/temple/endpoints/evm'; +import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { EvmAssetMetadataGetter } from 'lib/metadata'; +import { getEvmAddressSafe } from 'lib/utils/evm.utils'; + +import { ActivityOperKindEnum, EvmActivityAsset, EvmOperation } from '../../types'; +import { getAssetSymbol } from '../../utils'; + +export function parseGasTransfer( + item: GoldRushTransaction | GoldRushERC20Transaction, + accountAddress: string, + /** Only way to suspect transfering to a contract, not an account */ + partOfBatch: boolean, + getMetadata: EvmAssetMetadataGetter +): EvmOperation | null { + const value: string = item.value?.toString() ?? '0'; + + if (value === '0') return null; + + const kind = (() => { + if (getEvmAddressSafe(item.from_address) === accountAddress) + return partOfBatch ? ActivityOperKindEnum.transferFrom : ActivityOperKindEnum.transferFrom_ToAccount; + if (getEvmAddressSafe(item.to_address) === accountAddress) + return partOfBatch ? ActivityOperKindEnum.transferTo : ActivityOperKindEnum.transferTo_FromAccount; + + return null; + })(); + + if (!kind) return null; + + const metadata = getMetadata(EVM_TOKEN_SLUG); + const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; + + if (decimals == null) return null; + + const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; + + const asset: EvmActivityAsset = { + contract: EVM_TOKEN_SLUG, + amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value, + decimals, + symbol + }; + + return { kind, asset }; +} diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts new file mode 100644 index 0000000000..b178afa436 --- /dev/null +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -0,0 +1,83 @@ +import { GoldRushERC20Transaction, GoldRushERC20TransactionTransfer } from 'lib/apis/temple/endpoints/evm'; +import { toEvmAssetSlug } from 'lib/assets/utils'; +import { EvmAssetMetadataGetter } from 'lib/metadata'; +import { getEvmAddressSafe } from 'lib/utils/evm.utils'; +import { TempleChainKind } from 'temple/types'; + +import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { getAssetSymbol } from '../../utils'; + +import { parseGasTransfer } from './gas'; + +export function parseGoldRushERC20Transfer( + item: GoldRushERC20Transaction, + chainId: number, + accountAddress: string, + getMetadata: EvmAssetMetadataGetter +): EvmActivity { + const transfers = item.transfers ?? []; + const addedAt = item.block_signed_at as unknown as string; + + const operations = transfers.map(transfer => parseTransfer(transfer, item, getMetadata)); + + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(transfers.length), getMetadata); + + if (gasOperation) operations.unshift(gasOperation); + + return { + chain: TempleChainKind.EVM, + chainId, + hash: item.tx_hash!, + blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, + operations, + operationsCount: gasOperation ? transfers.length + 1 : transfers.length, + addedAt + }; +} + +function parseTransfer( + transfer: GoldRushERC20TransactionTransfer, + item: GoldRushERC20Transaction, + getMetadata: EvmAssetMetadataGetter +): EvmOperation { + const kind = (() => { + if (transfer.transfer_type === 'IN') { + return item.to_address === transfer.contract_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + return item.to_address === transfer.contract_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + })(); + + const contractAddress = getEvmAddressSafe(transfer.contract_address); + + if (contractAddress == null) return { kind }; + + const slug = toEvmAssetSlug(contractAddress); + const metadata = getMetadata(slug); + + const decimals = metadata?.decimals ?? transfer.contract_decimals; + + if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; + + const nft = false; + const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; + const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft, + iconURL: transfer.logo_url ?? undefined + }; + + return { kind, asset }; +} diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts new file mode 100644 index 0000000000..0b82349c4f --- /dev/null +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -0,0 +1,215 @@ +import { GoldRushTransaction, GoldRushTransactionLogEvent } from 'lib/apis/temple/endpoints/evm'; +import { toEvmAssetSlug } from 'lib/assets/utils'; +import { EvmAssetMetadataGetter } from 'lib/metadata'; +import { isTruthy } from 'lib/utils'; +import { getEvmAddressSafe } from 'lib/utils/evm.utils'; +import { TempleChainKind } from 'temple/types'; + +import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { getAssetSymbol } from '../../utils'; + +import { parseGasTransfer } from './gas'; + +export function parseGoldRushTransaction( + item: GoldRushTransaction, + chainId: number, + accountAddress: string, + getMetadata: EvmAssetMetadataGetter +): EvmActivity { + const logEvents = item.log_events ?? []; + const addedAt = item.block_signed_at as unknown as string; + + const operations = logEvents + .map(logEvent => parseLogEvent(logEvent, item, accountAddress, getMetadata)) + .filter(isTruthy); + + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length), getMetadata); + + if (gasOperation) operations.unshift(gasOperation); + + return { + chain: TempleChainKind.EVM, + chainId, + hash: item.tx_hash!, + blockExplorerUrl: item.explorers?.at(0)?.url, + operations, + operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length, + addedAt + }; +} + +function parseLogEvent( + logEvent: GoldRushTransactionLogEvent, + item: GoldRushTransaction, + accountAddress: string, + getMetadata: EvmAssetMetadataGetter +): EvmOperation | null { + if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; + + const contractAddress = getEvmAddressSafe(logEvent.sender_address); + const _fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value); + const _toAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value); + let decimals = logEvent.sender_contract_decimals; + const iconURL = logEvent.sender_logo_url ?? undefined; + + if (logEvent.decoded.name === 'Transfer') { + const kind = (() => { + if (_toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + if (_fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + } + + return null; + })(); + + if (kind == null || !contractAddress) return { kind: ActivityOperKindEnum.interaction }; + + const param3 = logEvent.decoded.params.at(2); + const amountOrTokenId: string = param3?.value ?? '0'; + const nft = param3?.indexed ?? false; + const tokenId = nft ? amountOrTokenId : undefined; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; + + const amount = nft ? '1' : amountOrTokenId; + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft, + iconURL + }; + + return { kind, asset }; + } + + if (logEvent.decoded.name === 'TransferSingle') { + const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value); + const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(2)?.value); + + const kind = (() => { + if (toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo; + } + + if (fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperKindEnum.transferFrom_ToAccount + : ActivityOperKindEnum.transferFrom; + } + + return null; + })(); + + if (kind == null || !contractAddress) return null; + + const tokenId = logEvent.decoded.params.at(3)?.value ?? '0'; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; + + const amount = '1'; + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount, + decimals, + symbol, + nft: true, + iconURL + }; + + return { kind, asset }; + } + + if (logEvent.decoded.name === 'Approval') { + if (_fromAddress !== accountAddress) return null; + + const kind = ActivityOperKindEnum.approve; + + if (!contractAddress) return { kind }; + + const amountOrTokenIdParam = logEvent.decoded.params.at(2); + const amountOrTokenId: string = amountOrTokenIdParam?.value ?? '0'; + const nft = amountOrTokenIdParam?.indexed ?? false; + + const tokenId = nft ? amountOrTokenId : undefined; + + const slug = toEvmAssetSlug(contractAddress, tokenId); + const metadata = getMetadata(slug); + + decimals = metadata?.decimals ?? decimals; + + if (decimals == null) return { kind }; + + const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const asset: EvmActivityAsset = { + contract: contractAddress, + tokenId, + amount: nft ? '1' : amountOrTokenId, + decimals, + symbol, + nft, + iconURL + }; + + return { kind, asset }; + } + + if (logEvent.decoded.name === 'ApprovalForAll') { + if ( + // @ts-expect-error // `value` is not always `:string` + logEvent.decoded.params.at(2).value !== true || + _fromAddress !== accountAddress + ) + return null; + + const kind = ActivityOperKindEnum.approve; + + if (!contractAddress) return { kind }; + + const asset: EvmActivityAsset = { + contract: contractAddress, + amount: null, + decimals: NaN, + symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + nft: true, + iconURL + }; + + return { kind, asset }; + } + + return { kind: ActivityOperKindEnum.interaction }; +} diff --git a/src/lib/activity/evm/parse/index.ts b/src/lib/activity/evm/parse/index.ts new file mode 100644 index 0000000000..859d59a652 --- /dev/null +++ b/src/lib/activity/evm/parse/index.ts @@ -0,0 +1,2 @@ +export { parseGoldRushTransaction } from './gr-v3'; +export { parseGoldRushERC20Transfer } from './gr-v2'; diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 64f57bb648..abe131d52c 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,5 +1,3 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivityAsset, EvmOperation } from './types'; export { ActivityOperKindEnum } from './types'; - -export { parseTezosPreActivityOperation } from './tezos'; diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index e8e026ffa2..812adc344d 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -1,35 +1,36 @@ -import { TezosPreActivity, TezosPreActivityOperation } from 'lib/activity/tezos/types'; -import { AssetMetadataBase, isTezosCollectibleMetadata } from 'lib/metadata'; +import { TempleTzktOperationsGroup, TezosPreActivityOperation } from 'lib/activity/tezos/types'; +import { toTezosAssetSlug } from 'lib/assets/utils'; import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; -import { TezosActivity, ActivityOperKindEnum, TezosOperation, TezosActivityAsset } from '../types'; -import { getAssetSymbol, isTransferActivityOperKind } from '../utils'; +import { TezosActivity, ActivityOperKindEnum, TezosOperation } from '../types'; +import { isTransferActivityOperKind } from '../utils'; -export { preparseTezosOperationsGroup } from './pre-parse'; +import { preparseTezosOperationsGroup } from './pre-parse'; -export function formatLegacyTezosActivity( - _activity: TezosPreActivity, +export function parseTezosOperationsGroup( + operationsGroup: TempleTzktOperationsGroup, chainId: string, address: string ): TezosActivity { - const hash = _activity.hash; + const preActivity = preparseTezosOperationsGroup(operationsGroup, address, chainId); + + const { hash, addedAt, operations: preOperations, oldestTzktOperation } = preActivity; + + const operations = preOperations.map(oper => parseTezosPreActivityOperation(oper, address)); return { hash, chain: TempleChainKind.Tezos, chainId, - operations: _activity.operations.map(oper => parseTezosPreActivityOperation(oper, address)), - operationsCount: _activity.operations.length, - addedAt: _activity.addedAt + operations, + operationsCount: preOperations.length, + addedAt, + oldestTzktOperation }; } -export function parseTezosPreActivityOperation( - preOperation: TezosPreActivityOperation, - address: string, - assetMetadata?: AssetMetadataBase -): TezosOperation { +function parseTezosPreActivityOperation(preOperation: TezosPreActivityOperation, address: string): TezosOperation { let tokenId: string | undefined; const operationBase: TezosOperation = (() => { @@ -79,19 +80,11 @@ export function parseTezosPreActivityOperation( }; })(); - if (!assetMetadata || !preOperation.contract) return operationBase; + if (!preOperation.contract) return operationBase; if (isTransferActivityOperKind(operationBase.kind) || operationBase.kind === ActivityOperKindEnum.approve) { - const asset: TezosActivityAsset = { - contract: preOperation.contract, - tokenId, - amount: preOperation.amountSigned, - decimals: assetMetadata.decimals, - nft: isTezosCollectibleMetadata(assetMetadata), - symbol: getAssetSymbol(assetMetadata) - }; - - operationBase.asset = asset; + operationBase.assetSlug = toTezosAssetSlug(preOperation.contract, tokenId); + operationBase.amountSigned = preOperation.amountSigned; } return operationBase; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 57c542c817..93f092a3ee 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -1,5 +1,7 @@ import { TempleChainKind } from 'temple/types'; +import { TezosActivityOlderThan } from './tezos/types'; + export enum ActivityOperKindEnum { interaction, transferFrom, @@ -21,16 +23,21 @@ interface ChainActivityBase { addedAt: string; } -export interface TezosActivity extends ChainActivityBase { +interface OperationBase { + kind: ActivityOperKindEnum; + /** `null` for 'unlimited' amount */ + amountSigned?: string | null; +} + +export interface TezosActivity extends ChainActivityBase, TezosActivityOlderThan { chain: TempleChainKind.Tezos; chainId: string; blockExplorerUrl?: string; operations: TezosOperation[]; } -export interface TezosOperation { - kind: ActivityOperKindEnum; - asset?: TezosActivityAsset; +export interface TezosOperation extends OperationBase { + assetSlug?: string; } export interface TezosActivityAsset extends ActivityAssetBase {} @@ -42,9 +49,8 @@ export interface EvmActivity extends ChainActivityBase { operations: EvmOperation[]; } -export interface EvmOperation { - kind: ActivityOperKindEnum; - asset?: EvmActivityAsset; +export interface EvmOperation extends OperationBase { + asset?: EvmActivityAsset; // TODO: Same as for Tezos } export interface EvmActivityAsset extends ActivityAssetBase { diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index 87c547c0a0..a96a2bd288 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -1,10 +1,19 @@ import { templeWalletApi } from '../templewallet.api'; import { BalancesResponse, ChainID, NftAddressBalanceNftResponse } from './api.interfaces'; -import { Erc20TransfersResponse, GoldRushERC20Transfer } from './types/erc20-transfers'; -import { GoldRushTransaction } from './types/transactions'; +import { + GoldRushERC20TransactionsResponse, + GoldRushERC20Transaction, + GoldRushERC20TransactionTransfer +} from './types/gr-v2'; +import { GoldRushTransaction, GoldRushTransactionLogEvent, GoldRushTransactionsResponse } from './types/gr-v3'; -export type { GoldRushTransaction, GoldRushERC20Transfer }; +export type { + GoldRushTransaction, + GoldRushTransactionLogEvent, + GoldRushERC20Transaction, + GoldRushERC20TransactionTransfer +}; export const getEvmBalances = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/balances', walletAddress, chainId); @@ -16,8 +25,9 @@ export const getEvmTokensMetadata = (walletAddress: string, chainId: ChainID) => export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/collectibles-metadata', walletAddress, chainId); +/** Calls to GoldRush v3 endpoints */ export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number, signal?: AbortSignal) => - buildEvmRequest<{ items: GoldRushTransaction[]; current_page: number }>( + buildEvmRequest( '/transactions', walletAddress, chainId, @@ -27,10 +37,11 @@ export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page signal ).then(({ items, current_page }) => ({ items, - /** null | \> 0 */ + /** null | > 0 */ nextPage: current_page > 1 ? current_page - 1 : null })); +/** Calls to GoldRush v2 endpoints */ export const getEvmERC20Transfers = ( walletAddress: string, chainId: ChainID, @@ -38,7 +49,7 @@ export const getEvmERC20Transfers = ( page?: number, signal?: AbortSignal ) => - buildEvmRequest( + buildEvmRequest( '/erc20-transfers', walletAddress, chainId, @@ -52,7 +63,7 @@ export const getEvmERC20Transfers = ( return { items: items ?? [], - /** null | \> 0 */ + /** null | > 0 */ nextPage: withoutNextPage ? null : pagination?.page_number ?? 0 + 1 }; }); diff --git a/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts b/src/lib/apis/temple/endpoints/evm/types/gr-v2.ts similarity index 94% rename from src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts rename to src/lib/apis/temple/endpoints/evm/types/gr-v2.ts index 89484bc469..d559ac1cbc 100644 --- a/src/lib/apis/temple/endpoints/evm/types/erc20-transfers.ts +++ b/src/lib/apis/temple/endpoints/evm/types/gr-v2.ts @@ -1,11 +1,13 @@ import { Nullable } from './utils'; -export type Erc20TransfersResponse = Nullable<{ - items: GoldRushERC20Transfer[]; +export type GoldRushERC20TransactionsResponse = Nullable<{ + items: GoldRushERC20Transaction[]; pagination: Pagination; }>; -export type GoldRushERC20Transfer = Nullable; +export type GoldRushERC20Transaction = Nullable; + +export type GoldRushERC20TransactionTransfer = Nullable; interface Pagination { /** * True is there is another page. */ @@ -64,7 +66,7 @@ interface BlockTransactionWithContractTransfers { pretty_gas_quote: string; /** * The native gas exchange rate for the requested `quote-currency`. */ gas_quote_rate: number; - transfers: Nullable[]; + transfers: GoldRushERC20TransactionTransfer[]; } interface ContractMetadata { diff --git a/src/lib/apis/temple/endpoints/evm/types/transactions.ts b/src/lib/apis/temple/endpoints/evm/types/gr-v3.ts similarity index 98% rename from src/lib/apis/temple/endpoints/evm/types/transactions.ts rename to src/lib/apis/temple/endpoints/evm/types/gr-v3.ts index 7c85ca195f..ea929fda48 100644 --- a/src/lib/apis/temple/endpoints/evm/types/transactions.ts +++ b/src/lib/apis/temple/endpoints/evm/types/gr-v3.ts @@ -1,7 +1,14 @@ import { Nullable } from './utils'; +export interface GoldRushTransactionsResponse { + items: GoldRushTransaction[]; + current_page: number; +} + export type GoldRushTransaction = Nullable; +export type GoldRushTransactionLogEvent = Nullable; + interface Transaction { /** * The block signed timestamp in UTC. */ block_signed_at: Date; @@ -55,7 +62,7 @@ interface Transaction { /** * The details for the lending protocol transaction. */ lending_details: Nullable[]; /** * The log events. */ - log_events: Nullable[]; + log_events: GoldRushTransactionLogEvent[]; /** * The details related to the safe transaction. */ safe_details: Nullable[]; } From 8f1776bac627b0666c6a5228eeb17e5f6b43ed03 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 4 Oct 2024 22:08:20 +0300 Subject: [PATCH 38/74] TW-1479: [EVM] Transactions history. Refactor --- TODO.md | 3 +- .../activity/ActivityItem/EvmActivity.tsx | 67 ++++++++++++------- .../ActivityItem/EvmActivityOperation.tsx | 45 ++++++++++--- .../activity/ActivityItem/TezosActivity.tsx | 2 +- .../ActivityItem/TezosActivityOperation.tsx | 21 +++--- .../templates/activity/EvmActivityList.tsx | 4 -- src/app/templates/activity/MultichainList.tsx | 13 +--- src/lib/activity/evm/fetch.ts | 6 +- src/lib/activity/evm/parse/gas.ts | 14 ++-- src/lib/activity/evm/parse/gr-v2.ts | 35 ++++------ src/lib/activity/evm/parse/gr-v3.ts | 67 ++++++------------- src/lib/activity/index.ts | 2 +- src/lib/activity/types.ts | 15 ++--- 13 files changed, 140 insertions(+), 154 deletions(-) diff --git a/TODO.md b/TODO.md index 0a6d0c4caf..fc729caaef 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,6 @@ - ### FOR PRE-REVIEW -- Filters by kind - Filter by network dropdown - Grouping by date - Bundle modal @@ -34,3 +33,5 @@ - - Design - - Click on bundle item - `const HIDE_INTERACTIONS = true;` +- Ad on Multichain +- diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 7ee5fe672c..a70e0672e1 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -5,16 +5,18 @@ import clsx from 'clsx'; import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; -import { ActivityOperKindEnum, EvmActivity } from 'lib/activity'; +import { EvmActivity } from 'lib/activity'; import { EvmActivityAsset } from 'lib/activity/types'; -import { isTransferActivityOperKind } from 'lib/activity/utils'; -import { toEvmAssetSlug } from 'lib/assets/utils'; +import { getAssetSymbol, isTransferActivityOperKind } from 'lib/activity/utils'; +import { fromAssetSlug, toEvmAssetSlug } from 'lib/assets/utils'; import { t } from 'lib/i18n'; +import { useGetEvmChainAssetMetadata } from 'lib/metadata'; import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; import { EvmChain } from 'temple/front/chains'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { EvmActivityOperationComponent } from './EvmActivityOperation'; import { InteractionsConnector } from './InteractionsConnector'; interface Props { @@ -32,12 +34,11 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) const operation = operations.at(0); return ( - @@ -69,43 +70,60 @@ const EvmActivityBatchComponent = memo( const { hash, operations } = activity; + const getMetadata = useGetEvmChainAssetMetadata(chainId); + const faceSlug = useMemo(() => { if (assetSlug) return assetSlug; for (const { kind, asset } of operations) { - if (typeof asset?.amount === 'string' && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) - return toEvmAssetSlug(asset.contract, asset.tokenId); + if (asset?.amount && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) { + const slug = toEvmAssetSlug(asset.contract, asset.tokenId); + + const decimals = getMetadata(slug)?.decimals ?? asset.decimals; + + if (decimals != null) return slug; + } } return; - }, [operations, assetSlug]); + }, [getMetadata, operations, assetSlug]); - const batchAsset = useMemo(() => { + const batchAsset = useMemo(() => { if (!faceSlug) return; - let faceAsset: EvmActivityAsset | undefined; + let faceAssetBase: EvmActivityAsset | undefined; let faceAmount = ZERO; for (const { kind, asset } of operations) { if ( - typeof asset?.amount === 'string' && - toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug && - isTransferActivityOperKind(kind) + isTransferActivityOperKind(kind) && + asset?.amount && + toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug ) { faceAmount = faceAmount.plus(asset.amount); - if (!faceAsset) faceAsset = asset; + if (!faceAssetBase) faceAssetBase = asset; } } - if (!faceAsset) return; + const assetMetadata = getMetadata(faceSlug); - const batchAsset: ActivityItemBaseAssetProp = { - ...faceAsset, - amount: faceAmount.toFixed() - }; + const decimals = assetMetadata?.decimals ?? faceAssetBase?.decimals; - return batchAsset; - }, [operations, faceSlug]); + if (decimals == null) return; + + const symbol = getAssetSymbol(assetMetadata) || faceAssetBase?.symbol; + + const [contract, tokenId] = fromAssetSlug(faceSlug); + + return { + ...faceAssetBase, + contract, + tokenId, + amount: faceAmount.toFixed(), + decimals, + symbol + }; + }, [getMetadata, operations, faceSlug]); return (
@@ -134,12 +152,11 @@ const EvmActivityBatchComponent = memo( {j > 0 && } - diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 4fb38f0fcd..070004b3d6 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -1,28 +1,53 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; -import type { EvmOperation } from 'lib/activity'; -import { EvmChain } from 'temple/front'; +import { ActivityOperKindEnum, type EvmOperation } from 'lib/activity'; +import { getAssetSymbol } from 'lib/activity/utils'; +import { toEvmAssetSlug } from 'lib/assets/utils'; +import { useEvmAssetMetadata } from 'lib/metadata'; -import { ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; interface Props { hash: string; - operation: EvmOperation; - chain: EvmChain; + operation?: EvmOperation; + chainId: number; networkName: string; blockExplorerUrl?: string; withoutAssetIcon?: boolean; } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chain, networkName, blockExplorerUrl, withoutAssetIcon }) => { + ({ hash, operation, chainId, networkName, blockExplorerUrl, withoutAssetIcon }) => { + const assetBase = operation?.asset; + const assetSlug = assetBase?.contract ? toEvmAssetSlug(assetBase.contract) : undefined; + + const assetMetadata = useEvmAssetMetadata(assetSlug ?? '', chainId); + + const asset = useMemo(() => { + if (!assetBase) return; + + const decimals = assetBase.amount === null ? NaN : assetMetadata?.decimals ?? assetBase.decimals; + + if (decimals == null) return; + + const symbol = getAssetSymbol(assetMetadata) || assetBase.symbol; + + const asset: ActivityItemBaseAssetProp = { + ...assetBase, + decimals, + symbol + }; + + return asset; + }, [assetMetadata, assetBase]); + return ( diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 47545edf94..cfdd5163b0 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -94,7 +94,7 @@ const TezosActivityBatchComponent = memo( faceAmount = faceAmount.plus(amountSigned); } - return buildTezosOperationAsset(faceSlug, getMetadata(faceSlug)!, faceAmount.toFixed()); + return buildTezosOperationAsset(faceSlug, getMetadata(faceSlug), faceAmount.toFixed()); }, [getMetadata, operations, assetSlug]); return ( diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 55b4f419a2..4f8d80d607 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -1,8 +1,9 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; +import { getAssetSymbol } from 'lib/activity/utils'; import { fromAssetSlug } from 'lib/assets'; -import { AssetMetadataBase, getAssetSymbol, useTezosAssetMetadata } from 'lib/metadata'; +import { AssetMetadataBase, useTezosAssetMetadata } from 'lib/metadata'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; @@ -21,10 +22,7 @@ export const TezosActivityOperationComponent = memo( const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); const asset = useMemo( - () => - assetSlug && assetMetadata - ? buildTezosOperationAsset(assetSlug, assetMetadata, operation.amountSigned) - : undefined, + () => (assetSlug ? buildTezosOperationAsset(assetSlug, assetMetadata, operation.amountSigned) : undefined), [assetMetadata, operation, assetSlug] ); @@ -44,16 +42,19 @@ export const TezosActivityOperationComponent = memo( export function buildTezosOperationAsset( assetSlug: string, - assetMetadata: AssetMetadataBase, - amount: string | nullish -): ActivityItemBaseAssetProp { + assetMetadata: AssetMetadataBase | undefined, + amountSigned: string | nullish +): ActivityItemBaseAssetProp | undefined { const [contract, tokenId] = fromAssetSlug(assetSlug); + const decimals = amountSigned === null ? NaN : assetMetadata?.decimals; + if (decimals == null) return; + return { contract, tokenId, - amount, - decimals: assetMetadata.decimals, + amount: amountSigned, + decimals, // nft: isTezosCollectibleMetadata(assetMetadata), symbol: getAssetSymbol(assetMetadata) }; diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index f5552e1aad..e48d499488 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -8,7 +8,6 @@ import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { EvmActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; -import { useGetEvmChainAssetMetadata } from 'lib/metadata'; import { useSafeState } from 'lib/ui/hooks'; import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; @@ -33,8 +32,6 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = const [nextPage, setNextPage] = useSafeState(undefined); - const getMetadata = useGetEvmChainAssetMetadata(chainId); - const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = useActivitiesLoadingLogic( async (initial, signal) => { @@ -49,7 +46,6 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = const { activities: newActivities, nextPage: newNextPage } = await getEvmAssetTransactions( accountAddress, chainId, - getMetadata, assetSlug, page, signal diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index ccf7e3829b..e0e05bd263 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -11,7 +11,6 @@ import { parseTezosOperationsGroup } from 'lib/activity/tezos'; import fetchTezosOperationsGroups from 'lib/activity/tezos/fetch'; import { TzktApiChainId } from 'lib/apis/tzkt'; import { isKnownChainId as isKnownTzktChainId } from 'lib/apis/tzkt/api'; -import { EvmAssetMetadataGetter, useGetEvmAssetMetadata } from 'lib/metadata'; import { isTruthy } from 'lib/utils'; import { useAccountAddressForEvm, @@ -61,8 +60,6 @@ export const MultichainActivityList = memo(({ filterKind }) => { [evmChains, evmAccAddress] ); - const getEvmMetadata = useGetEvmAssetMetadata(); - const { activities, isLoading, reachedTheEnd, setActivities, setIsLoading, setReachedTheEnd, loadNext } = useActivitiesLoadingLogic( async (initial, signal) => { @@ -77,7 +74,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { await Promise.allSettled( evmLoaders - .map(l => l.loadNext((slug: string) => getEvmMetadata(slug, l.chainId), lastEdgeDate, signal)) + .map(l => l.loadNext(lastEdgeDate, signal)) .concat(tezosLoaders.map(l => l.loadNext(lastEdgeDate, signal))) ); @@ -164,12 +161,7 @@ class EvmActivityLoader { return this.nextPage === null; } - async loadNext( - getMetadata: EvmAssetMetadataGetter, - edgeDate: string | undefined, - signal: AbortSignal, - assetSlug?: string - ) { + async loadNext(edgeDate: string | undefined, signal: AbortSignal, assetSlug?: string) { if (edgeDate) { const lastAct = this.activities.at(-1); if (lastAct && lastAct.addedAt > edgeDate) return; @@ -183,7 +175,6 @@ class EvmActivityLoader { const { nextPage: newNextPage, activities: newActivities } = await getEvmAssetTransactions( accountAddress, chainId, - getMetadata, assetSlug, nextPage, signal diff --git a/src/lib/activity/evm/fetch.ts b/src/lib/activity/evm/fetch.ts index 345eb5f36d..b8c0afae3f 100644 --- a/src/lib/activity/evm/fetch.ts +++ b/src/lib/activity/evm/fetch.ts @@ -1,7 +1,6 @@ import { getEvmERC20Transfers, getEvmTransactions } from 'lib/apis/temple/endpoints/evm'; import { fromAssetSlug } from 'lib/assets'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; -import { EvmAssetMetadataGetter } from 'lib/metadata'; import { EvmActivity } from '../types'; @@ -10,7 +9,6 @@ import { parseGoldRushTransaction, parseGoldRushERC20Transfer } from './parse'; export async function getEvmAssetTransactions( walletAddress: string, chainId: number, - getMetadata: EvmAssetMetadataGetter, assetSlug?: string, page?: number, signal?: AbortSignal @@ -21,7 +19,7 @@ export async function getEvmAssetTransactions( signal?.throwIfAborted(); return { - activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress, getMetadata)), + activities: items.map(item => parseGoldRushTransaction(item, chainId, walletAddress)), nextPage }; } @@ -55,7 +53,7 @@ export async function getEvmAssetTransactions( signal?.throwIfAborted(); return { - activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress, getMetadata)), + activities: items.map(item => parseGoldRushERC20Transfer(item, chainId, walletAddress)), nextPage }; } diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index 0a23452c4b..5fbc3e3a60 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -1,17 +1,14 @@ import { GoldRushTransaction, GoldRushERC20Transaction } from 'lib/apis/temple/endpoints/evm'; import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; -import { EvmAssetMetadataGetter } from 'lib/metadata'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { ActivityOperKindEnum, EvmActivityAsset, EvmOperation } from '../../types'; -import { getAssetSymbol } from '../../utils'; export function parseGasTransfer( item: GoldRushTransaction | GoldRushERC20Transaction, accountAddress: string, /** Only way to suspect transfering to a contract, not an account */ - partOfBatch: boolean, - getMetadata: EvmAssetMetadataGetter + partOfBatch: boolean ): EvmOperation | null { const value: string = item.value?.toString() ?? '0'; @@ -28,16 +25,15 @@ export function parseGasTransfer( if (!kind) return null; - const metadata = getMetadata(EVM_TOKEN_SLUG); - const decimals = metadata?.decimals ?? item.gas_metadata?.contract_decimals; + const decimals = item.gas_metadata?.contract_decimals; - if (decimals == null) return null; + const amountSigned = kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value; - const symbol = getAssetSymbol(metadata) || item.gas_metadata?.contract_ticker_symbol; + const symbol = item.gas_metadata?.contract_ticker_symbol; const asset: EvmActivityAsset = { contract: EVM_TOKEN_SLUG, - amount: kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value, + amount: amountSigned, decimals, symbol }; diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index b178afa436..278fa280c3 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -1,26 +1,22 @@ import { GoldRushERC20Transaction, GoldRushERC20TransactionTransfer } from 'lib/apis/temple/endpoints/evm'; -import { toEvmAssetSlug } from 'lib/assets/utils'; -import { EvmAssetMetadataGetter } from 'lib/metadata'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; -import { getAssetSymbol } from '../../utils'; import { parseGasTransfer } from './gas'; export function parseGoldRushERC20Transfer( item: GoldRushERC20Transaction, chainId: number, - accountAddress: string, - getMetadata: EvmAssetMetadataGetter + accountAddress: string ): EvmActivity { const transfers = item.transfers ?? []; const addedAt = item.block_signed_at as unknown as string; - const operations = transfers.map(transfer => parseTransfer(transfer, item, getMetadata)); + const operations = transfers.map(transfer => parseTransfer(transfer, item)); - const gasOperation = parseGasTransfer(item, accountAddress, Boolean(transfers.length), getMetadata); + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(transfers.length)); if (gasOperation) operations.unshift(gasOperation); @@ -35,11 +31,7 @@ export function parseGoldRushERC20Transfer( }; } -function parseTransfer( - transfer: GoldRushERC20TransactionTransfer, - item: GoldRushERC20Transaction, - getMetadata: EvmAssetMetadataGetter -): EvmOperation { +function parseTransfer(transfer: GoldRushERC20TransactionTransfer, item: GoldRushERC20Transaction): EvmOperation { const kind = (() => { if (transfer.transfer_type === 'IN') { return item.to_address === transfer.contract_address @@ -56,23 +48,20 @@ function parseTransfer( if (contractAddress == null) return { kind }; - const slug = toEvmAssetSlug(contractAddress); - const metadata = getMetadata(slug); - - const decimals = metadata?.decimals ?? transfer.contract_decimals; - - if (decimals == null) return { kind: ActivityOperKindEnum.interaction }; + const decimals = transfer.contract_decimals ?? undefined; const nft = false; const amount = nft ? '1' : transfer.delta?.toString() ?? '0'; - const symbol = getAssetSymbol(metadata) || transfer.contract_ticker_symbol || undefined; + const symbol = transfer.contract_ticker_symbol || undefined; + + const amountSigned = + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount; const asset: EvmActivityAsset = { contract: contractAddress, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, + amount: amountSigned, decimals, symbol, nft, diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index 0b82349c4f..95848cb980 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -1,29 +1,25 @@ import { GoldRushTransaction, GoldRushTransactionLogEvent } from 'lib/apis/temple/endpoints/evm'; -import { toEvmAssetSlug } from 'lib/assets/utils'; -import { EvmAssetMetadataGetter } from 'lib/metadata'; import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; -import { getAssetSymbol } from '../../utils'; import { parseGasTransfer } from './gas'; export function parseGoldRushTransaction( item: GoldRushTransaction, chainId: number, - accountAddress: string, - getMetadata: EvmAssetMetadataGetter + accountAddress: string ): EvmActivity { const logEvents = item.log_events ?? []; const addedAt = item.block_signed_at as unknown as string; const operations = logEvents - .map(logEvent => parseLogEvent(logEvent, item, accountAddress, getMetadata)) + .map(logEvent => parseLogEvent(logEvent, item, accountAddress)) .filter(isTruthy); - const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length), getMetadata); + const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length)); if (gasOperation) operations.unshift(gasOperation); @@ -41,15 +37,15 @@ export function parseGoldRushTransaction( function parseLogEvent( logEvent: GoldRushTransactionLogEvent, item: GoldRushTransaction, - accountAddress: string, - getMetadata: EvmAssetMetadataGetter + accountAddress: string ): EvmOperation | null { if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; const contractAddress = getEvmAddressSafe(logEvent.sender_address); const _fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value); const _toAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value); - let decimals = logEvent.sender_contract_decimals; + const decimals = logEvent.sender_contract_decimals ?? undefined; + const symbol = logEvent.sender_contract_ticker_symbol || undefined; const iconURL = logEvent.sender_logo_url ?? undefined; if (logEvent.decoded.name === 'Transfer') { @@ -76,23 +72,17 @@ function parseLogEvent( const nft = param3?.indexed ?? false; const tokenId = nft ? amountOrTokenId : undefined; - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - const amount = nft ? '1' : amountOrTokenId; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const amountSigned = + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount; const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, + amount: amountSigned, decimals, symbol, nft, @@ -126,23 +116,17 @@ function parseLogEvent( const tokenId = logEvent.decoded.params.at(3)?.value ?? '0'; - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - const amount = '1'; - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + + const amountSigned = + kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + ? `-${amount}` + : amount; const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount - ? `-${amount}` - : amount, + amount: amountSigned, decimals, symbol, nft: true, @@ -165,19 +149,12 @@ function parseLogEvent( const tokenId = nft ? amountOrTokenId : undefined; - const slug = toEvmAssetSlug(contractAddress, tokenId); - const metadata = getMetadata(slug); - - decimals = metadata?.decimals ?? decimals; - - if (decimals == null) return { kind }; - - const symbol = getAssetSymbol(metadata) || logEvent.sender_contract_ticker_symbol || undefined; + const amountSigned = nft ? '1' : amountOrTokenId; const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: nft ? '1' : amountOrTokenId, + amount: amountSigned, decimals, symbol, nft, @@ -202,8 +179,8 @@ function parseLogEvent( const asset: EvmActivityAsset = { contract: contractAddress, amount: null, - decimals: NaN, - symbol: logEvent.sender_contract_ticker_symbol ?? undefined, + // decimals: NaN, // We are not supposed to use these in this case (of 'Unlimited' amount) + symbol, nft: true, iconURL }; diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index abe131d52c..94e861ce12 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,3 +1,3 @@ -export type { Activity, TezosActivity, EvmActivity, TezosOperation, TezosActivityAsset, EvmOperation } from './types'; +export type { Activity, TezosActivity, EvmActivity, TezosOperation, EvmOperation } from './types'; export { ActivityOperKindEnum } from './types'; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 93f092a3ee..55ac438b7a 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -25,8 +25,6 @@ interface ChainActivityBase { interface OperationBase { kind: ActivityOperKindEnum; - /** `null` for 'unlimited' amount */ - amountSigned?: string | null; } export interface TezosActivity extends ChainActivityBase, TezosActivityOlderThan { @@ -38,10 +36,10 @@ export interface TezosActivity extends ChainActivityBase, TezosActivityOlderThan export interface TezosOperation extends OperationBase { assetSlug?: string; + /** `null` for 'unlimited' amount */ + amountSigned?: string | null; } -export interface TezosActivityAsset extends ActivityAssetBase {} - export interface EvmActivity extends ChainActivityBase { chain: TempleChainKind.EVM; chainId: number; @@ -53,18 +51,15 @@ export interface EvmOperation extends OperationBase { asset?: EvmActivityAsset; // TODO: Same as for Tezos } -export interface EvmActivityAsset extends ActivityAssetBase { - iconURL?: string; -} - -interface ActivityAssetBase { +export interface EvmActivityAsset { contract: string; tokenId?: string; /** Signed (with `-` if applicable). `null` for 'unlimited' amount */ amount?: string | null; - decimals: number; + decimals?: number; nft?: boolean; symbol?: string; + iconURL?: string; } export interface OperationMember { From 442be6655f9560cde047947b1a86b9ab19f06227 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 4 Oct 2024 22:34:20 +0300 Subject: [PATCH 39/74] TW-1479: [EVM] Transactions history. Refactor --- public/_locales/de/messages.json | 4 ---- .../ActivityItem/ActivityOperationBase.tsx | 11 +++++------ .../activity/ActivityItem/EvmActivity.tsx | 10 +++++----- .../ActivityItem/EvmActivityOperation.tsx | 5 ++--- .../ActivityItem/TezosActivityOperation.tsx | 3 +-- .../templates/activity/EvmActivityList.tsx | 2 +- .../templates/activity/TezosActivityList.tsx | 2 +- src/app/templates/activity/utils.ts | 19 ++++++++++--------- src/lib/activity/evm/parse/gas.ts | 2 +- src/lib/activity/evm/parse/gr-v2.ts | 2 +- src/lib/activity/evm/parse/gr-v3.ts | 10 +++++----- src/lib/activity/types.ts | 4 ++-- src/lib/activity/utils.ts | 4 ---- src/lib/metadata/index.ts | 2 +- src/lib/metadata/utils.ts | 8 ++------ src/temple/networks.ts | 11 ----------- 16 files changed, 37 insertions(+), 62 deletions(-) diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index aa432b03de..76d95877e9 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -469,10 +469,6 @@ "message": "Tezos Mainnet", "description": "Mainnet = main network" }, - "marigoldMainnet": { - "message": "Marigold Mainnet", - "description": "Mainnet = main network" - }, "templeWalletOptions": { "message": "Temple Wallet | Optionen" }, diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 67f4c691d7..17c2141265 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -7,19 +7,18 @@ import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; +import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { InFiat } from 'app/templates/InFiat'; import { ActivityOperKindEnum } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; -import { EvmAssetIcon, TezosAssetIcon } from '../../AssetIcon'; - -type Kind = ActivityOperKindEnum | 'bundle'; +import { FaceKind } from '../utils'; interface Props { chainId: string | number; - kind: Kind; + kind: FaceKind; hash: string; networkName: string; asset?: ActivityItemBaseAssetProp; @@ -174,7 +173,7 @@ export const ActivityOperationBaseComponent: FC = ({ ); }; -const ActivityKindTitle: Record = { +const ActivityKindTitle: Record = { bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', [ActivityOperKindEnum.transferFrom_ToAccount]: 'Send', @@ -185,7 +184,7 @@ const ActivityKindTitle: Record = { [ActivityOperKindEnum.approve]: 'Approve' }; -const ActivityKindIconSvg: Record = { +const ActivityKindIconSvg: Record = { bundle: DocumentsSvg, [ActivityOperKindEnum.interaction]: DocumentsSvg, [ActivityOperKindEnum.transferFrom_ToAccount]: SendSvg, diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index a70e0672e1..9a5076528b 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -7,7 +7,7 @@ import { PageModal } from 'app/atoms/PageModal'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { EvmActivity } from 'lib/activity'; import { EvmActivityAsset } from 'lib/activity/types'; -import { getAssetSymbol, isTransferActivityOperKind } from 'lib/activity/utils'; +import { isTransferActivityOperKind } from 'lib/activity/utils'; import { fromAssetSlug, toEvmAssetSlug } from 'lib/assets/utils'; import { t } from 'lib/i18n'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; @@ -76,7 +76,7 @@ const EvmActivityBatchComponent = memo( if (assetSlug) return assetSlug; for (const { kind, asset } of operations) { - if (asset?.amount && Number(asset.amount) !== 0 && isTransferActivityOperKind(kind)) { + if (asset?.amountSigned && Number(asset.amountSigned) !== 0 && isTransferActivityOperKind(kind)) { const slug = toEvmAssetSlug(asset.contract, asset.tokenId); const decimals = getMetadata(slug)?.decimals ?? asset.decimals; @@ -97,10 +97,10 @@ const EvmActivityBatchComponent = memo( for (const { kind, asset } of operations) { if ( isTransferActivityOperKind(kind) && - asset?.amount && + asset?.amountSigned && toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug ) { - faceAmount = faceAmount.plus(asset.amount); + faceAmount = faceAmount.plus(asset.amountSigned); if (!faceAssetBase) faceAssetBase = asset; } } @@ -111,7 +111,7 @@ const EvmActivityBatchComponent = memo( if (decimals == null) return; - const symbol = getAssetSymbol(assetMetadata) || faceAssetBase?.symbol; + const symbol = assetMetadata?.symbol || faceAssetBase?.symbol; const [contract, tokenId] = fromAssetSlug(faceSlug); diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 070004b3d6..8cba00ff39 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, type EvmOperation } from 'lib/activity'; -import { getAssetSymbol } from 'lib/activity/utils'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; @@ -26,11 +25,11 @@ export const EvmActivityOperationComponent = memo( const asset = useMemo(() => { if (!assetBase) return; - const decimals = assetBase.amount === null ? NaN : assetMetadata?.decimals ?? assetBase.decimals; + const decimals = assetBase.amountSigned === null ? NaN : assetMetadata?.decimals ?? assetBase.decimals; if (decimals == null) return; - const symbol = getAssetSymbol(assetMetadata) || assetBase.symbol; + const symbol = assetMetadata?.symbol || assetBase.symbol; const asset: ActivityItemBaseAssetProp = { ...assetBase, diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 4f8d80d607..bb3e6d00f8 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; -import { getAssetSymbol } from 'lib/activity/utils'; import { fromAssetSlug } from 'lib/assets'; import { AssetMetadataBase, useTezosAssetMetadata } from 'lib/metadata'; @@ -56,6 +55,6 @@ export function buildTezosOperationAsset( amount: amountSigned, decimals, // nft: isTezosCollectibleMetadata(assetMetadata), - symbol: getAssetSymbol(assetMetadata) + symbol: assetMetadata?.symbol }; } diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index e48d499488..f1944bb843 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -19,7 +19,7 @@ import { FilterKind, getActivityFilterKind } from './utils'; interface Props { chainId: number; assetSlug?: string; - filterKind: FilterKind; + filterKind?: FilterKind; } export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) => { diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index 8e985d7e52..a754b61370 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -17,7 +17,7 @@ import { FilterKind, getActivityFilterKind } from './utils'; interface Props { tezosChainId: string; assetSlug?: string; - filterKind: FilterKind; + filterKind?: FilterKind; } export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterKind }) => { diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index 2146be03a3..22d5616805 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -5,13 +5,12 @@ export function isEvmActivity(activity: Activity | TezosPreActivity): activity i return typeof activity.chainId === 'number'; } -export type FilterKind = 'send' | 'receive' | 'approve' | 'transfer' | 'bundle' | null; +export type FaceKind = ActivityOperKindEnum | 'bundle'; -export function getEvmActivityFaceKind({ operations, operationsCount }: EvmActivity) { - return operationsCount === 1 ? operations.at(0)?.kind ?? ActivityOperKindEnum.interaction : 'batch'; -} +export type FilterKind = 'send' | 'receive' | 'approve' | 'transfer' | 'bundle' | null; -const FILTER_KINDS: Record = { +const KINDS_MAP: Record = { + bundle: 'bundle', [ActivityOperKindEnum.approve]: 'approve', [ActivityOperKindEnum.transferFrom]: 'transfer', [ActivityOperKindEnum.transferFrom_ToAccount]: 'send', @@ -22,10 +21,12 @@ const FILTER_KINDS: Record = { [ActivityOperKindEnum.swap]: null }; -export function getActivityFilterKind({ operations, operationsCount }: Activity): FilterKind { - if (operationsCount !== 1) return 'bundle'; +export function getActivityFilterKind(activity: Activity): FilterKind { + const faceKind = getActivityFaceKind(activity); - const kind = operations.at(0)?.kind ?? ActivityOperKindEnum.interaction; + return KINDS_MAP[faceKind]; +} - return FILTER_KINDS[kind]; +function getActivityFaceKind({ operations, operationsCount }: Activity): FaceKind { + return operationsCount === 1 ? operations.at(0)?.kind ?? ActivityOperKindEnum.interaction : 'bundle'; } diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index 5fbc3e3a60..690cc227b3 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -33,7 +33,7 @@ export function parseGasTransfer( const asset: EvmActivityAsset = { contract: EVM_TOKEN_SLUG, - amount: amountSigned, + amountSigned, decimals, symbol }; diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index 278fa280c3..b237d90a51 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -61,7 +61,7 @@ function parseTransfer(transfer: GoldRushERC20TransactionTransfer, item: GoldRus const asset: EvmActivityAsset = { contract: contractAddress, - amount: amountSigned, + amountSigned, decimals, symbol, nft, diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index 95848cb980..4543acd0cf 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -82,7 +82,7 @@ function parseLogEvent( const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: amountSigned, + amountSigned, decimals, symbol, nft, @@ -126,7 +126,7 @@ function parseLogEvent( const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: amountSigned, + amountSigned, decimals, symbol, nft: true, @@ -154,7 +154,7 @@ function parseLogEvent( const asset: EvmActivityAsset = { contract: contractAddress, tokenId, - amount: amountSigned, + amountSigned, decimals, symbol, nft, @@ -166,7 +166,7 @@ function parseLogEvent( if (logEvent.decoded.name === 'ApprovalForAll') { if ( - // @ts-expect-error // `value` is not always `:string` + // @ts-expect-error // `.value` is not always `:string` logEvent.decoded.params.at(2).value !== true || _fromAddress !== accountAddress ) @@ -178,7 +178,7 @@ function parseLogEvent( const asset: EvmActivityAsset = { contract: contractAddress, - amount: null, + amountSigned: null, // decimals: NaN, // We are not supposed to use these in this case (of 'Unlimited' amount) symbol, nft: true, diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 55ac438b7a..9968192d72 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -54,8 +54,8 @@ export interface EvmOperation extends OperationBase { export interface EvmActivityAsset { contract: string; tokenId?: string; - /** Signed (with `-` if applicable). `null` for 'unlimited' amount */ - amount?: string | null; + /** `null` for 'unlimited' amount */ + amountSigned?: string | null; decimals?: number; nft?: boolean; symbol?: string; diff --git a/src/lib/activity/utils.ts b/src/lib/activity/utils.ts index 86540f37ea..1892279a59 100644 --- a/src/lib/activity/utils.ts +++ b/src/lib/activity/utils.ts @@ -1,5 +1,3 @@ -import { getAssetSymbol as getAssetSymbolFromMeta } from 'lib/metadata'; - import { ActivityOperKindEnum } from './types'; export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { @@ -10,5 +8,3 @@ export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { kind === ActivityOperKindEnum.transferTo ); } - -export const getAssetSymbol: typeof getAssetSymbolFromMeta = metadata => getAssetSymbolFromMeta(metadata, false, null); diff --git a/src/lib/metadata/index.ts b/src/lib/metadata/index.ts index bc24e2779a..f51de337e1 100644 --- a/src/lib/metadata/index.ts +++ b/src/lib/metadata/index.ts @@ -94,7 +94,7 @@ export const useGetEvmAssetMetadata = () => { ); }; -export type EvmAssetMetadataGetter = ( +type EvmAssetMetadataGetter = ( slug: string ) => EvmNativeTokenMetadata | EvmTokenMetadata | EvmCollectibleMetadata | undefined; diff --git a/src/lib/metadata/utils.ts b/src/lib/metadata/utils.ts index c9f90eae39..4cd54e115c 100644 --- a/src/lib/metadata/utils.ts +++ b/src/lib/metadata/utils.ts @@ -12,12 +12,8 @@ import { EvmAssetMetadataBase } from './types'; -export function getAssetSymbol( - metadata: EvmAssetMetadataBase | AssetMetadataBase | nullish, - short = false, - fallback?: string | null -) { - if (!metadata?.symbol) return fallback === null ? undefined : fallback ?? '???'; +export function getAssetSymbol(metadata: EvmAssetMetadataBase | AssetMetadataBase | nullish, short = false) { + if (!metadata?.symbol) return '???'; if (!short) return metadata.symbol; diff --git a/src/temple/networks.ts b/src/temple/networks.ts index b895a6f1ce..2dc8adcdb4 100644 --- a/src/temple/networks.ts +++ b/src/temple/networks.ts @@ -78,17 +78,6 @@ export const TEZOS_DEFAULT_NETWORKS: NonEmptyArray = [ color: '#83b300', default: true }, - { - id: 'marigold-mainnet', - name: 'Marigold Mainnet', - nameI18nKey: 'marigoldMainnet', - chain: TempleChainKind.Tezos, - chainId: TempleTezosChainId.Mainnet, - rpcBaseURL: 'https://mainnet.tezos.marigold.dev', - description: 'Marigold mainnet', - color: '#48bb78', - default: true - }, { id: 'smartpy-mainnet', name: 'SmartPy Mainnet', From 90d8c76996abad26e47131559dfef303c79d9c65 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Oct 2024 20:39:54 +0300 Subject: [PATCH 40/74] TW-1479: [EVM] Transactions history. Filters. Networks dropdown --- TODO.md | 4 +- src/app/atoms/AccountName.tsx | 1 - src/app/atoms/ActionListItem.tsx | 61 +++++---- src/app/atoms/NetworkLogo.tsx | 23 +++- src/app/atoms/NetworkSelectButton.tsx | 4 +- .../pages/Activity/FilterChainDropdown.tsx | 93 +++++++++++++ src/app/pages/Activity/index.tsx | 126 +++++++----------- .../components/CollectibleItem.tsx | 20 +-- src/app/templates/AssetIcon.tsx | 4 +- src/app/templates/NetworkSelectModal.tsx | 9 +- src/app/templates/SearchField.tsx | 2 +- .../ActivityItem/ActivityOperationBase.tsx | 4 +- 12 files changed, 213 insertions(+), 138 deletions(-) create mode 100644 src/app/pages/Activity/FilterChainDropdown.tsx diff --git a/TODO.md b/TODO.md index fc729caaef..c988652d64 100644 --- a/TODO.md +++ b/TODO.md @@ -27,11 +27,13 @@ ### FOR PRE-REVIEW - Filter by network dropdown -- Grouping by date - Bundle modal - - Optimise render - - Design - - Click on bundle item +- Grouping by date - `const HIDE_INTERACTIONS = true;` - Ad on Multichain +- `-- (({ chain, address, onCopy }) => ( { window.navigator.clipboard.writeText(address); onCopy(); diff --git a/src/app/atoms/ActionListItem.tsx b/src/app/atoms/ActionListItem.tsx index ca9f79902d..afeaea4f28 100644 --- a/src/app/atoms/ActionListItem.tsx +++ b/src/app/atoms/ActionListItem.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { FC } from 'react'; import clsx from 'clsx'; @@ -14,33 +14,42 @@ export interface ActionListItemProps extends PropsWithChildren { /** Pass it, if you want it to be called with `false` on click too */ setOpened?: SyncFn; testID?: string; + active?: boolean; danger?: boolean; } -export const ActionListItem = memo( - ({ Icon, linkTo, className, onClick, setOpened, testID, danger, children }) => { - const baseProps = { - testID, - className: clsx( - 'flex items-center py-1.5 px-2 gap-x-1 rounded-md text-font-description', - 'hover:bg-secondary-low', - className - ), - onClick: setOpened - ? () => { - setOpened(false); - onClick?.(); - } - : onClick, - children: ( - <> - {Icon && } +export const ActionListItem: FC = ({ + Icon, + linkTo, + className, + onClick, + setOpened, + testID, + active, + danger, + children +}) => { + const baseProps = { + testID, + className: clsx( + 'flex items-center py-1.5 px-2 gap-x-1 rounded-md text-font-description', + active ? 'bg-grey-4' : danger ? 'hover:bg-error-low' : 'hover:bg-secondary-low', + className + ), + onClick: setOpened + ? () => { + setOpened(false); + onClick?.(); + } + : onClick, + children: ( + <> + {Icon && } - {typeof children === 'string' ? {children} : children} - - ) - }; + {typeof children === 'string' ? {children} : children} + + ) + }; - return linkTo ? :
@@ -220,11 +216,7 @@ export const TezosCollectibleItem = memo( {network && ( - + )}
@@ -339,11 +331,7 @@ export const EvmCollectibleItem = memo( className="absolute bottom-0.5 right-0.5" placement="bottom" > - + )}
@@ -392,7 +380,7 @@ export const EvmCollectibleItem = memo( {network && ( - + )}
diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index 9f3677bd4b..9710d14efe 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -42,7 +42,7 @@ export const TezosTokenIconWithNetwork = memo(({ tezosChai {network && ( - + )}
@@ -75,7 +75,7 @@ export const EvmTokenIconWithNetwork = memo(({ evmChainId, c {network && ( - + )}
diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 8f46cb7bf8..7d9949e640 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -84,7 +84,7 @@ export const NetworkSelectModalContent = memo(({ opened, selectedN const filteredNetworks = useMemo( () => - searchValueDebounced.length + searchValueDebounced ? searchAndFilterNetworksByName(networks, searchValueDebounced) : networks, [searchValueDebounced, networks] @@ -156,13 +156,10 @@ export const Network: FC = ({ const Icon = useMemo(() => { if (isAllNetworks) return ; - if (network.kind === TempleChainKind.Tezos) - return ; + if (network.kind === TempleChainKind.Tezos) return ; if (network.kind === TempleChainKind.EVM) - return ( - - ); + return ; return null; }, [isAllNetworks, network, iconSize]); diff --git a/src/app/templates/SearchField.tsx b/src/app/templates/SearchField.tsx index 59dfd5a20e..274b79948b 100644 --- a/src/app/templates/SearchField.tsx +++ b/src/app/templates/SearchField.tsx @@ -104,13 +104,13 @@ export const SearchBarField = memo( diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 17c2141265..76cb1c2b2f 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -141,9 +141,9 @@ export const ActivityOperationBaseComponent: FC = ({ {typeof chainId === 'number' ? ( - + ) : ( - + )}
From fbe168b3e054cd6187f7931370640874bcfd6302 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Oct 2024 21:36:46 +0300 Subject: [PATCH 41/74] TW-1479: [EVM] Transactions history. Refactor NetworkIcon tooltips --- TODO.md | 1 - src/app/atoms/NetworkLogo.tsx | 115 +++++++----- .../components/CollectibleItem.tsx | 46 +++-- src/app/templates/AssetIcon.tsx | 20 ++- .../ActivityItem/ActivityOperationBase.tsx | 16 +- .../activity/ActivityItem/EvmActivity.tsx | 170 +++++++++--------- .../ActivityItem/EvmActivityOperation.tsx | 4 +- .../activity/ActivityItem/TezosActivity.tsx | 134 +++++++------- .../ActivityItem/TezosActivityOperation.tsx | 4 +- 9 files changed, 263 insertions(+), 247 deletions(-) diff --git a/TODO.md b/TODO.md index c988652d64..40d78f6910 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,6 @@ - ### FOR PRE-REVIEW -- Filter by network dropdown - Bundle modal - - Optimise render - - Design diff --git a/src/app/atoms/NetworkLogo.tsx b/src/app/atoms/NetworkLogo.tsx index 0e651efcd4..8e3cd90356 100644 --- a/src/app/atoms/NetworkLogo.tsx +++ b/src/app/atoms/NetworkLogo.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, FC, memo, useMemo } from 'react'; +import React, { FC, memo, useMemo } from 'react'; import clsx from 'clsx'; import type { Placement } from 'tippy.js'; @@ -27,54 +27,79 @@ const logosRecord: Record = { interface TezosNetworkLogoProps { chainId: string; size?: number; + className?: string; + withTooltip?: boolean; + tooltipPlacement?: Placement; } -export const TezosNetworkLogo = memo(({ chainId, size = 24 }) => - chainId === TEZOS_MAINNET_CHAIN_ID ? ( - - ) : ( - - ) +export const TezosNetworkLogo = memo( + ({ chainId, size = 24, className, withTooltip, tooltipPlacement }) => { + const chain = useTezosChainByChainId(chainId); + const networkName = useMemo(() => (chain?.nameI18nKey ? t(chain.nameI18nKey) : chain?.name), [chain]); + + const withoutTooltipClassName = withTooltip ? undefined : className; + + const logoJsx = + chainId === TEZOS_MAINNET_CHAIN_ID ? ( + + ) : ( + + ); + + return withTooltip ? ( + + {logoJsx} + + ) : ( + logoJsx + ); + } ); -export const TezosNetworkLogoFallback = memo(({ chainId, size = 24 }) => { - const chain = useTezosChainByChainId(chainId); - const networkName = useMemo(() => (chain?.nameI18nKey ? t(chain.nameI18nKey) : chain?.name || ''), [chain]); - - return ; -}); - interface EvmNetworkLogoProps { chainId: number; size?: number; + className?: string; imgClassName?: string; - style?: CSSProperties; + withTooltip?: boolean; + tooltipPlacement?: Placement; } -export const EvmNetworkLogo = memo(({ chainId, size = 24, imgClassName, style }) => { - const source = useMemo(() => logosRecord[chainId] || getEvmNativeAssetIcon(chainId, size * 2), [chainId, size]); - - const chain = useEvmChainByChainId(chainId); - const networkName = useMemo(() => (chain?.nameI18nKey ? t(chain.nameI18nKey) : chain?.name || ''), [chain]); - - return source ? ( - {networkName} - ) : ( - - ); -}); +export const EvmNetworkLogo = memo( + ({ chainId, size = 24, className, imgClassName, withTooltip, tooltipPlacement }) => { + const source = useMemo(() => logosRecord[chainId] || getEvmNativeAssetIcon(chainId, size * 2), [chainId, size]); + + const chain = useEvmChainByChainId(chainId); + const networkName = useMemo(() => (chain?.nameI18nKey ? t(chain.nameI18nKey) : chain?.name), [chain]); + + const withoutTooltipClassName = withTooltip ? undefined : className; + + const logoJsx = source ? ( + {networkName} + ) : ( + + ); + + return withTooltip ? ( + + {logoJsx} + + ) : ( + logoJsx + ); + } +); const IDENTICON_OPTS = { chars: 1 }; interface NetworkLogoFallbackProps { - networkName: string; + networkName?: string; size?: number; className?: string; } @@ -85,20 +110,26 @@ const NetworkLogoFallback = memo(({ networkName, size className={clsx('p-px border border-grey-4 bg-white rounded-full overflow-hidden', className)} >
- +
)); -interface NetworkLogoTooltipWrapProps { - networkName: string; +interface WithTooltipWrapProps { + title?: string; className?: string; placement?: Placement; } -export const NetworkLogoTooltipWrap: FC> = ({ +const WithTooltipWrap: FC> = ({ className, - networkName, + title, placement = 'bottom-start', children }) => { @@ -106,11 +137,11 @@ export const NetworkLogoTooltipWrap: FC ({ trigger: 'mouseenter', hideOnClick: false, - content: networkName ?? 'Unknown Network', + content: title ?? 'Unknown Network', animation: 'shift-away-subtle', placement }), - [networkName, placement] + [title, placement] ); const networkIconRef = useTippy(tippyProps); diff --git a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx index df4d28705a..4260684317 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx'; import { IconBase, ToggleSwitch } from 'app/atoms'; import Money from 'app/atoms/Money'; -import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as DeleteIcon } from 'app/icons/base/delete.svg'; import { dispatch } from 'app/store'; import { setEvmCollectibleStatusAction } from 'app/store/evm/assets/actions'; @@ -152,13 +152,13 @@ export const TezosCollectibleItem = memo( /> {network && ( - - - + withTooltip + tooltipPlacement="bottom" + /> )}
@@ -215,9 +215,13 @@ export const TezosCollectibleItem = memo( )} {network && ( - - - + )}
@@ -326,13 +330,13 @@ export const EvmCollectibleItem = memo( {metadata && } {network && ( - - - + withTooltip + tooltipPlacement="bottom" + /> )}
@@ -379,9 +383,13 @@ export const EvmCollectibleItem = memo( )} {network && ( - - - + )}
diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index 9710d14efe..097d864cae 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -3,7 +3,7 @@ import React, { memo } from 'react'; import clsx from 'clsx'; import { Identicon } from 'app/atoms'; -import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as CollectiblePlaceholderSvg } from 'app/icons/collectible-placeholder.svg'; import { AssetMetadataBase } from 'lib/metadata'; import { EvmAssetMetadataBase } from 'lib/metadata/types'; @@ -41,9 +41,12 @@ export const TezosTokenIconWithNetwork = memo(({ tezosChai {network && ( - - - + )}
); @@ -74,9 +77,12 @@ export const EvmTokenIconWithNetwork = memo(({ evmChainId, c {network && ( - - - + )}
); diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 76cb1c2b2f..d645e28fed 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,7 +1,7 @@ import React, { FC, ReactNode, useCallback, useMemo } from 'react'; import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; -import { EvmNetworkLogo, NetworkLogoTooltipWrap, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; @@ -20,7 +20,6 @@ interface Props { chainId: string | number; kind: FaceKind; hash: string; - networkName: string; asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; withoutAssetIcon?: boolean; @@ -39,7 +38,6 @@ export const ActivityOperationBaseComponent: FC = ({ kind, hash, chainId, - networkName, asset, blockExplorerUrl, withoutAssetIcon @@ -139,13 +137,11 @@ export const ActivityOperationBaseComponent: FC = ({ /> )} - - {typeof chainId === 'number' ? ( - - ) : ( - - )} - + {typeof chainId === 'number' ? ( + + ) : ( + + )}
diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 9a5076528b..1984540a5f 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -9,7 +9,6 @@ import { EvmActivity } from 'lib/activity'; import { EvmActivityAsset } from 'lib/activity/types'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { fromAssetSlug, toEvmAssetSlug } from 'lib/assets/utils'; -import { t } from 'lib/i18n'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; @@ -26,8 +25,6 @@ interface Props { } export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) => { - const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - const { hash, operations, operationsCount, blockExplorerUrl } = activity; if (operationsCount === 1) { @@ -38,7 +35,6 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) hash={hash} operation={operation} chainId={chain.chainId} - networkName={networkName} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={Boolean(assetSlug)} /> @@ -51,7 +47,6 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) chainId={chain.chainId} assetSlug={assetSlug} blockExplorerUrl={blockExplorerUrl} - networkName={networkName} /> ); }); @@ -61,109 +56,104 @@ interface BatchProps { chainId: number; assetSlug?: string; blockExplorerUrl?: string; - networkName: string; } -const EvmActivityBatchComponent = memo( - ({ activity, chainId, assetSlug, blockExplorerUrl, networkName }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); +const EvmActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); - const { hash, operations } = activity; + const { hash, operations } = activity; - const getMetadata = useGetEvmChainAssetMetadata(chainId); + const getMetadata = useGetEvmChainAssetMetadata(chainId); - const faceSlug = useMemo(() => { - if (assetSlug) return assetSlug; + const faceSlug = useMemo(() => { + if (assetSlug) return assetSlug; - for (const { kind, asset } of operations) { - if (asset?.amountSigned && Number(asset.amountSigned) !== 0 && isTransferActivityOperKind(kind)) { - const slug = toEvmAssetSlug(asset.contract, asset.tokenId); + for (const { kind, asset } of operations) { + if (asset?.amountSigned && Number(asset.amountSigned) !== 0 && isTransferActivityOperKind(kind)) { + const slug = toEvmAssetSlug(asset.contract, asset.tokenId); - const decimals = getMetadata(slug)?.decimals ?? asset.decimals; + const decimals = getMetadata(slug)?.decimals ?? asset.decimals; - if (decimals != null) return slug; - } + if (decimals != null) return slug; } + } - return; - }, [getMetadata, operations, assetSlug]); + return; + }, [getMetadata, operations, assetSlug]); - const batchAsset = useMemo(() => { - if (!faceSlug) return; + const batchAsset = useMemo(() => { + if (!faceSlug) return; - let faceAssetBase: EvmActivityAsset | undefined; - let faceAmount = ZERO; + let faceAssetBase: EvmActivityAsset | undefined; + let faceAmount = ZERO; - for (const { kind, asset } of operations) { - if ( - isTransferActivityOperKind(kind) && - asset?.amountSigned && - toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug - ) { - faceAmount = faceAmount.plus(asset.amountSigned); - if (!faceAssetBase) faceAssetBase = asset; - } + for (const { kind, asset } of operations) { + if ( + isTransferActivityOperKind(kind) && + asset?.amountSigned && + toEvmAssetSlug(asset.contract, asset.tokenId) === faceSlug + ) { + faceAmount = faceAmount.plus(asset.amountSigned); + if (!faceAssetBase) faceAssetBase = asset; } + } - const assetMetadata = getMetadata(faceSlug); + const assetMetadata = getMetadata(faceSlug); - const decimals = assetMetadata?.decimals ?? faceAssetBase?.decimals; + const decimals = assetMetadata?.decimals ?? faceAssetBase?.decimals; - if (decimals == null) return; + if (decimals == null) return; - const symbol = assetMetadata?.symbol || faceAssetBase?.symbol; + const symbol = assetMetadata?.symbol || faceAssetBase?.symbol; - const [contract, tokenId] = fromAssetSlug(faceSlug); + const [contract, tokenId] = fromAssetSlug(faceSlug); - return { - ...faceAssetBase, - contract, - tokenId, - amount: faceAmount.toFixed(), - decimals, - symbol - }; - }, [getMetadata, operations, faceSlug]); + return { + ...faceAssetBase, + contract, + tokenId, + amount: faceAmount.toFixed(), + decimals, + symbol + }; + }, [getMetadata, operations, faceSlug]); - return ( -
- - - - - -
- {operations.map((operation, j) => ( - - {j > 0 && } - - - - ))} -
-
-
- ); - } -); + return ( +
+ + + + + +
+ {operations.map((operation, j) => ( + + {j > 0 && } + + + + ))} +
+
+
+ ); +}); diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 8cba00ff39..504381a080 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -10,13 +10,12 @@ interface Props { hash: string; operation?: EvmOperation; chainId: number; - networkName: string; blockExplorerUrl?: string; withoutAssetIcon?: boolean; } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chainId, networkName, blockExplorerUrl, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, withoutAssetIcon }) => { const assetBase = operation?.asset; const assetSlug = assetBase?.contract ? toEvmAssetSlug(assetBase.contract) : undefined; @@ -45,7 +44,6 @@ export const EvmActivityOperationComponent = memo( kind={operation?.kind ?? ActivityOperKindEnum.interaction} hash={hash} chainId={chainId} - networkName={networkName} asset={asset} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={withoutAssetIcon} diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index cfdd5163b0..d341e175a3 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -7,7 +7,6 @@ import { PageModal } from 'app/atoms/PageModal'; import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { TezosActivity } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; -import { t } from 'lib/i18n'; import { useGetChainTokenOrGasMetadata } from 'lib/metadata'; import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; @@ -25,8 +24,6 @@ interface Props { } export const TezosActivityComponent = memo(({ activity, chain, assetSlug }) => { - const networkName = chain.nameI18nKey ? t(chain.nameI18nKey) : chain.name; - const { hash, operations, operationsCount } = activity; const blockExplorerUrl = useExplorerHref(chain.chainId, hash) ?? undefined; @@ -39,7 +36,6 @@ export const TezosActivityComponent = memo(({ activity, chain, assetSlug hash={hash} operation={operation} chainId={chain.chainId} - networkName={networkName} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={Boolean(assetSlug)} /> @@ -52,7 +48,6 @@ export const TezosActivityComponent = memo(({ activity, chain, assetSlug chainId={chain.chainId} assetSlug={assetSlug} blockExplorerUrl={blockExplorerUrl} - networkName={networkName} /> ); }); @@ -62,80 +57,75 @@ interface BatchProps { chainId: string; assetSlug?: string; blockExplorerUrl?: string; - networkName: string; } -const TezosActivityBatchComponent = memo( - ({ activity, chainId, assetSlug, blockExplorerUrl, networkName }) => { - const [expanded, , , toggleExpanded] = useBooleanState(false); +const TezosActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { + const [expanded, , , toggleExpanded] = useBooleanState(false); - const { hash, operations } = activity; + const { hash, operations } = activity; - const getMetadata = useGetChainTokenOrGasMetadata(chainId); + const getMetadata = useGetChainTokenOrGasMetadata(chainId); - const batchAsset = useMemo(() => { - const faceSlug = - assetSlug || - operations.find( - ({ kind, assetSlug, amountSigned }) => - assetSlug && - amountSigned && - Number(amountSigned) !== 0 && - isTransferActivityOperKind(kind) && - getMetadata(assetSlug) - )?.assetSlug; + const batchAsset = useMemo(() => { + const faceSlug = + assetSlug || + operations.find( + ({ kind, assetSlug, amountSigned }) => + assetSlug && + amountSigned && + Number(amountSigned) !== 0 && + isTransferActivityOperKind(kind) && + getMetadata(assetSlug) + )?.assetSlug; - if (!faceSlug) return; + if (!faceSlug) return; - let faceAmount = ZERO; + let faceAmount = ZERO; - for (const { kind, assetSlug, amountSigned } of operations) { - if (assetSlug === faceSlug && amountSigned && isTransferActivityOperKind(kind)) - faceAmount = faceAmount.plus(amountSigned); - } + for (const { kind, assetSlug, amountSigned } of operations) { + if (assetSlug === faceSlug && amountSigned && isTransferActivityOperKind(kind)) + faceAmount = faceAmount.plus(amountSigned); + } - return buildTezosOperationAsset(faceSlug, getMetadata(faceSlug), faceAmount.toFixed()); - }, [getMetadata, operations, assetSlug]); + return buildTezosOperationAsset(faceSlug, getMetadata(faceSlug), faceAmount.toFixed()); + }, [getMetadata, operations, assetSlug]); - return ( -
- - - - - -
- {operations.map((operation, j) => ( - - {j > 0 && } - - - - ))} -
-
-
- ); - } -); + return ( +
+ + + + + +
+ {operations.map((operation, j) => ( + + {j > 0 && } + + + + ))} +
+
+
+ ); +}); diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index bb3e6d00f8..e0d74b5171 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -10,13 +10,12 @@ interface Props { hash: string; operation?: TezosOperation; chainId: string; - networkName: string; blockExplorerUrl: string | nullish; withoutAssetIcon?: boolean; } export const TezosActivityOperationComponent = memo( - ({ hash, operation, chainId, networkName, blockExplorerUrl, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, withoutAssetIcon }) => { const assetSlug = operation?.assetSlug; const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); @@ -30,7 +29,6 @@ export const TezosActivityOperationComponent = memo( kind={operation?.kind ?? ActivityOperKindEnum.interaction} hash={hash} chainId={chainId} - networkName={networkName} asset={asset} blockExplorerUrl={blockExplorerUrl ?? undefined} withoutAssetIcon={withoutAssetIcon} From c1522867f48c39e794b7ab5227cbb9a3aaf19c79 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2024 04:10:14 +0300 Subject: [PATCH 42/74] TW-1479: [EVM] Transactions history. Bundle modal. ++ --- TODO.md | 4 +- public/_locales/en/messages.json | 1 + src/app/atoms/Anchor.tsx | 4 +- .../atoms/PageModal/actions-buttons-box.tsx | 87 +++--- src/app/atoms/PageModal/index.tsx | 119 ++++---- src/app/atoms/ScrollView.tsx | 66 +++++ src/app/atoms/StyledButton.tsx | 17 +- .../ActivityItem/ActivityOperationBase.tsx | 256 +++++++++--------- .../activity/ActivityItem/BundleModal.tsx | 24 ++ .../activity/ActivityItem/EvmActivity.tsx | 49 ++-- .../activity/ActivityItem/TezosActivity.tsx | 49 ++-- src/lib/ui/button-like-styles.ts | 16 +- tailwind.config.js | 1 + 13 files changed, 402 insertions(+), 291 deletions(-) create mode 100644 src/app/atoms/ScrollView.tsx create mode 100644 src/app/templates/activity/ActivityItem/BundleModal.tsx diff --git a/TODO.md b/TODO.md index 40d78f6910..14016d8456 100644 --- a/TODO.md +++ b/TODO.md @@ -27,12 +27,10 @@ ### FOR PRE-REVIEW - Bundle modal -- - Optimise render - - Design -- - Click on bundle item - Grouping by date - `const HIDE_INTERACTIONS = true;` - Ad on Multichain -- `-- ` - Covalent SDK - diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index d42ceeeac4..0bf92b1a4c 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -2306,6 +2306,7 @@ "blockExplorer": { "message": "Blockchain explorer" }, + "viewOnExplorer": { "message": "View on explorer" }, "viewOnBlockExplorer": { "message": "View on block explorer" }, "specifyTokenId": { "message": "Specify token ID" diff --git a/src/app/atoms/Anchor.tsx b/src/app/atoms/Anchor.tsx index bde9c9d734..0371045edc 100644 --- a/src/app/atoms/Anchor.tsx +++ b/src/app/atoms/Anchor.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; import { AnalyticsEventCategory, setTestID, TestIDProps, useAnalytics } from 'lib/analytics'; -interface Props +export interface AnchorProps extends React.PropsWithRef, HTMLAnchorElement>>, TestIDProps { treatAsButton?: boolean; } -export const Anchor = React.forwardRef( +export const Anchor = React.forwardRef( ( { target = '_blank', rel = 'noopener noreferrer', testID, testIDProperties, treatAsButton, onClick, ...props }, ref diff --git a/src/app/atoms/PageModal/actions-buttons-box.tsx b/src/app/atoms/PageModal/actions-buttons-box.tsx index b3cd812099..b237d13ecb 100644 --- a/src/app/atoms/PageModal/actions-buttons-box.tsx +++ b/src/app/atoms/PageModal/actions-buttons-box.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes, memo, useCallback, useEffect, useMemo } from 'react'; +import React, { FC, HTMLAttributes, useCallback, useMemo } from 'react'; import clsx from 'clsx'; import { throttle } from 'lodash'; @@ -6,57 +6,56 @@ import { useDispatch } from 'react-redux'; import { useAppEnv } from 'app/env'; import { setToastsContainerBottomShiftAction } from 'app/store/settings/actions'; +import { useWillUnmount } from 'lib/ui/hooks/useWillUnmount'; -interface ActionsButtonsBoxProps extends HTMLAttributes { +interface Props extends HTMLAttributes { shouldCastShadow?: boolean; - flexDirection?: 'row' | 'col'; } -export const ActionsButtonsBox = memo( - ({ className, shouldCastShadow, flexDirection = 'col', ...restProps }) => { - const dispatch = useDispatch(); - const { popup } = useAppEnv(); +export const ActionsButtonsBox: FC = ({ className, shouldCastShadow, ...restProps }) => { + const dispatch = useDispatch(); + const { popup } = useAppEnv(); - useEffect(() => { - return () => void dispatch(setToastsContainerBottomShiftAction(0)); - }, []); + useWillUnmount(() => void dispatch(setToastsContainerBottomShiftAction(0))); - const handleResize = useMemo( - () => + const resizeObserver = useMemo( + () => + new ResizeObserver( throttle(entries => { - const borderBoxSize = entries.map(entry => entry.borderBoxSize?.[0]).filter(Boolean)[0]; + const borderBoxSize = entries.find(entry => entry.borderBoxSize?.[0])?.borderBoxSize?.[0]; if (borderBoxSize) { dispatch(setToastsContainerBottomShiftAction(borderBoxSize.blockSize - (popup ? 16 : 0))); } - }, 100), - [dispatch, popup] - ); - - const resizeObserver = useMemo(() => new ResizeObserver(handleResize), [handleResize]); - const rootRef = useCallback( - (node: HTMLDivElement | null) => { - resizeObserver.disconnect(); - if (node) { - resizeObserver.observe(node); - const { height } = node.getBoundingClientRect(); - dispatch(setToastsContainerBottomShiftAction(height)); - } - }, - [dispatch, resizeObserver] - ); - - return ( -
- ); - } -); + }, 100) + ), + [popup, dispatch] + ); + + const rootRef = useCallback( + (node: HTMLDivElement | null) => { + resizeObserver.disconnect(); + + if (node) { + resizeObserver.observe(node); + + const { height } = node.getBoundingClientRect(); + + dispatch(setToastsContainerBottomShiftAction(height)); + } + }, + [resizeObserver, dispatch] + ); + + return ( +
+ ); +}; diff --git a/src/app/atoms/PageModal/index.tsx b/src/app/atoms/PageModal/index.tsx index d11dde790e..42912b8a4a 100644 --- a/src/app/atoms/PageModal/index.tsx +++ b/src/app/atoms/PageModal/index.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, memo } from 'react'; +import React, { FC, ReactElement, ReactNode } from 'react'; import clsx from 'clsx'; import Modal from 'react-modal'; @@ -15,6 +15,8 @@ import { SuspenseContainer } from '../SuspenseContainer'; import ModStyles from './styles.module.css'; +export { ActionsButtonsBox } from './actions-buttons-box'; + interface Props extends TestIDProps { title: string; opened: boolean; @@ -23,66 +25,69 @@ interface Props extends TestIDProps { onGoBack?: EmptyFn; animated?: boolean; contentPadding?: boolean; + children: ReactNode | (() => ReactElement); } -export const PageModal = memo>( - ({ - title, - opened, - shouldShowBackButton, - onRequestClose, - onGoBack, - children, - testID, - animated = true, - contentPadding = false - }) => { - const { fullPage } = useAppEnv(); - - return ( - -
-
- {shouldShowBackButton && ( - - )} -
+export const PageModal: FC = ({ + title, + opened, + shouldShowBackButton, + onRequestClose, + onGoBack, + children, + testID, + animated = true, + contentPadding = false +}) => { + const { fullPage } = useAppEnv(); -
{title}
+ // const isHidden = useDebounce(opened, 300); -
- -
+ return ( + +
+
+ {shouldShowBackButton && ( + + )}
-
- {children} +
{title}
+ +
+
- - ); - } -); +
+ +
+ + {typeof children === 'function' ? (opened ? children() : null) : children} + +
+ + ); +}; diff --git a/src/app/atoms/ScrollView.tsx b/src/app/atoms/ScrollView.tsx new file mode 100644 index 0000000000..f6b29f65c2 --- /dev/null +++ b/src/app/atoms/ScrollView.tsx @@ -0,0 +1,66 @@ +import React, { FC, UIEventHandler, useCallback, useMemo, useRef } from 'react'; + +import clsx from 'clsx'; +import { throttle } from 'lodash'; + +import { useSafeState } from 'lib/ui/hooks'; + +interface Props extends PropsWithChildren { + className?: string; +} + +export const ScrollView: FC = ({ className, children }) => { + const [contentHiding, setContentHiding] = useSafeState(false); + + const ref = useRef(); + + const setContentHidingThrottled = useMemo(() => throttle((value: boolean) => setContentHiding(value), 300), []); + + const onScroll = useMemo>( + () => event => { + const node = event.currentTarget; + + const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; + + setContentHidingThrottled(node.scrollHeight > node.clientHeight && scrollBottom > 0); + }, + [] + ); + + const resizeObserver = useMemo( + () => + new ResizeObserver( + throttle(() => { + const node = ref.current; + + if (!node) return; + + const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; + + setContentHidingThrottled(node.scrollHeight > node.clientHeight && scrollBottom > 0); + }, 300) + ), + [] + ); + + const refFn = useCallback((node: HTMLDivElement | null) => { + ref.current = node; + if (!node) return void setContentHiding(false); + + resizeObserver.observe(node); + + const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; + + setContentHiding(node.scrollHeight > node.clientHeight && scrollBottom > 0); + }, []); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/app/atoms/StyledButton.tsx b/src/app/atoms/StyledButton.tsx index 285a1fab26..702c6c95c9 100644 --- a/src/app/atoms/StyledButton.tsx +++ b/src/app/atoms/StyledButton.tsx @@ -1,7 +1,14 @@ import React from 'react'; -import { ButtonLikeStylingProps, useStyledButtonOrLinkProps } from 'lib/ui/button-like-styles'; +import clsx from 'clsx'; +import { + ButtonLikeStylingProps, + useStyledButtonClassName, + useStyledButtonOrLinkProps +} from 'lib/ui/button-like-styles'; + +import { Anchor, AnchorProps } from './Anchor'; import { Button, ButtonProps } from './Button'; export interface StyledButtonProps extends Omit, ButtonLikeStylingProps {} @@ -11,3 +18,11 @@ export const StyledButton = React.forwardRef; }); + +export const StyledButtonAnchor = React.forwardRef( + ({ size, color, active, disabled, className: classNameProp, ...props }, ref) => { + const className = useStyledButtonClassName({ size, color, active, disabled }, clsx('text-center', classNameProp)); + + return ; + } +); diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index d645e28fed..2bcb367fd8 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,4 +1,6 @@ -import React, { FC, ReactNode, useCallback, useMemo } from 'react'; +import React, { FC, memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; + +import clsx from 'clsx'; import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; @@ -23,6 +25,7 @@ interface Props { asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; withoutAssetIcon?: boolean; + onClick?: EmptyFn; } export interface ActivityItemBaseAssetProp { @@ -34,140 +37,149 @@ export interface ActivityItemBaseAssetProp { iconURL?: string; } -export const ActivityOperationBaseComponent: FC = ({ - kind, - hash, - chainId, - asset, - blockExplorerUrl, - withoutAssetIcon -}) => { - const assetSlug = asset - ? typeof chainId === 'number' - ? toEvmAssetSlug(asset.contract, asset.tokenId) - : toTezosAssetSlug(asset.contract, asset.tokenId) - : null; - - const amountJsx = useMemo(() => { - if (!asset) return null; - - const symbol = asset.symbol || (kind === ActivityOperKindEnum.approve ? '---' : ''); - const symbolStr = symbol.length > 6 ? `${symbol.slice(0, 6)}...` : symbol; - - return ( -
- {kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( - - {atomsToTokens(asset.amount, asset.decimals)} - - ) : null} - - {symbolStr ? {symbolStr} : null} -
+export const ActivityOperationBaseComponent = memo( + ({ kind, hash, chainId, asset, blockExplorerUrl, withoutAssetIcon, onClick }) => { + const assetSlug = asset + ? typeof chainId === 'number' + ? toEvmAssetSlug(asset.contract, asset.tokenId) + : toTezosAssetSlug(asset.contract, asset.tokenId) + : null; + + const amountJsx = useMemo(() => { + if (!asset) return null; + + const symbol = asset.symbol || (kind === ActivityOperKindEnum.approve ? '---' : ''); + const symbolStr = symbol.length > 6 ? `${symbol.slice(0, 6)}...` : symbol; + + return ( +
+ {kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( + + {atomsToTokens(asset.amount, asset.decimals)} + + ) : null} + + {symbolStr ? {symbolStr} : null} +
+ ); + }, [asset, kind]); + + const fiatJsx = useMemo(() => { + if (!asset) return null; + + if (!asset.amount) return asset.amount === null ? 'Unlimited' : null; + + if (kind === ActivityOperKindEnum.approve) + return {atomsToTokens(asset.amount, asset.decimals)}; + + if (!assetSlug) return null; + + const amountForFiat = + kind === 'bundle' || isTransferActivityOperKind(kind) ? atomsToTokens(asset.amount, asset.decimals) : null; + + if (!amountForFiat) return null; + + return ( + + {({ balance, symbol, noPrice }) => + noPrice ? ( + No value + ) : ( + <> + {balance} + {symbol} + + ) + } + + ); + }, [asset, kind, assetSlug, chainId]); + + const handleClick = useCallback>( + event => { + if (!onClick) return; + + if (event.target instanceof HTMLAnchorElement) return; + + onClick(); + }, + [onClick] ); - }, [asset, kind]); - - const fiatJsx = useMemo(() => { - if (!asset) return null; - if (!asset.amount) return asset.amount === null ? 'Unlimited' : null; - - if (kind === ActivityOperKindEnum.approve) - return {atomsToTokens(asset.amount, asset.decimals)}; - - if (!assetSlug) return null; - - const amountForFiat = - kind === 'bundle' || isTransferActivityOperKind(kind) ? atomsToTokens(asset.amount, asset.decimals) : null; - - if (!amountForFiat) return null; + const IconFallback = useCallback( + () => ( +
+ +
+ ), + [kind] + ); return ( - - {({ balance, symbol, noPrice }) => - noPrice ? ( - No value +
+ {withoutAssetIcon || !assetSlug ? ( + + ) : typeof chainId === 'number' ? ( + ) : ( - <> - {balance} - {symbol} - - ) - } - - ); - }, [asset, kind, assetSlug, chainId]); - - const IconFallback = useCallback( - () => ( -
- -
- ), - [kind] - ); - - return ( -
-
- {withoutAssetIcon || !assetSlug ? ( - - ) : typeof chainId === 'number' ? ( - - ) : ( - - )} - - {typeof chainId === 'number' ? ( - - ) : ( - - )} -
+ + )} + + {typeof chainId === 'number' ? ( + + ) : ( + + )} +
-
-
-
{ActivityKindTitle[kind]}
+
+
+
{ActivityKindTitle[kind]}
- {amountJsx} -
+ {amountJsx} +
-
- - +
+ + - - + + -
{fiatJsx}
+
{fiatJsx}
+
-
- ); -}; + ); + } +); const ActivityKindTitle: Record = { bundle: 'Bundle', diff --git a/src/app/templates/activity/ActivityItem/BundleModal.tsx b/src/app/templates/activity/ActivityItem/BundleModal.tsx new file mode 100644 index 0000000000..002c9b53b5 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/BundleModal.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; + +import { ActionsButtonsBox } from 'app/atoms/PageModal'; +import { ScrollView } from 'app/atoms/ScrollView'; +import { StyledButtonAnchor } from 'app/atoms/StyledButton'; +import { T } from 'lib/i18n'; + +interface Props extends PropsWithChildren { + blockExplorerUrl?: string; +} + +export const BundleModalContent: FC = ({ blockExplorerUrl, children }) => { + return ( + <> + {children} + + + + + + + + ); +}; diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 1984540a5f..6eaf8b7a57 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -1,10 +1,6 @@ import React, { memo, useMemo } from 'react'; -import clsx from 'clsx'; - -import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; -import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { EvmActivity } from 'lib/activity'; import { EvmActivityAsset } from 'lib/activity/types'; import { isTransferActivityOperKind } from 'lib/activity/utils'; @@ -15,6 +11,7 @@ import { ZERO } from 'lib/utils/numbers'; import { EvmChain } from 'temple/front/chains'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { BundleModalContent } from './BundleModal'; import { EvmActivityOperationComponent } from './EvmActivityOperation'; import { InteractionsConnector } from './InteractionsConnector'; @@ -119,7 +116,7 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl }, [getMetadata, operations, faceSlug]); return ( -
+ <> (({ activity, chainId, assetSl asset={batchAsset} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={Boolean(assetSlug)} - /> - - + /> -
- {operations.map((operation, j) => ( - - {j > 0 && } - - - - ))} -
+ {() => ( + + {operations.map((operation, j) => ( + + {j > 0 && } + + + + ))} + + )}
-
+ ); }); diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index d341e175a3..489222979d 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -1,10 +1,6 @@ import React, { memo, useMemo } from 'react'; -import clsx from 'clsx'; - -import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; -import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { TezosActivity } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { useGetChainTokenOrGasMetadata } from 'lib/metadata'; @@ -14,6 +10,7 @@ import { useExplorerHref } from 'temple/front/block-explorers'; import { TezosChain } from 'temple/front/chains'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { BundleModalContent } from './BundleModal'; import { InteractionsConnector } from './InteractionsConnector'; import { TezosActivityOperationComponent, buildTezosOperationAsset } from './TezosActivityOperation'; @@ -91,7 +88,7 @@ const TezosActivityBatchComponent = memo(({ activity, chainId, asset }, [getMetadata, operations, assetSlug]); return ( -
+ <> (({ activity, chainId, asset asset={batchAsset} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={Boolean(assetSlug)} - /> - - + /> -
- {operations.map((operation, j) => ( - - {j > 0 && } - - - - ))} -
+ {() => ( + + {operations.map((operation, j) => ( + + {j > 0 && } + + + + ))} + + )}
-
+ ); }); diff --git a/src/lib/ui/button-like-styles.ts b/src/lib/ui/button-like-styles.ts index e5e3336500..2d66ee8ff1 100644 --- a/src/lib/ui/button-like-styles.ts +++ b/src/lib/ui/button-like-styles.ts @@ -13,6 +13,7 @@ export interface ButtonLikeStylingProps { size: Size; color: StyledButtonColor; active?: boolean; + disabled?: boolean; } export const ACTIVE_STYLED_BUTTON_COLORS_CLASSNAME = 'bg-grey-4 text-grey-1'; @@ -43,6 +44,16 @@ const SIZE_CLASSNAME: Record = { S: 'py-1 px-2 rounded-lg text-font-description-bold' }; +export function useStyledButtonClassName( + { size, color, active, disabled }: ButtonLikeStylingProps, + className?: string +) { + return useMemo( + () => clsx(SIZE_CLASSNAME[size], getStyledButtonColorsClassNames(color, active, disabled), className), + [active, className, color, disabled, size] + ); +} + export function useStyledButtonOrLinkProps(inputProps: ButtonProps & ButtonLikeStylingProps): ButtonProps; export function useStyledButtonOrLinkProps(inputProps: LinkProps & ButtonLikeStylingProps): LinkProps; export function useStyledButtonOrLinkProps({ @@ -55,10 +66,7 @@ export function useStyledButtonOrLinkProps({ const isLink = 'to' in restProps; const disabled = 'disabled' in restProps && restProps.disabled; - const className = useMemo( - () => clsx(SIZE_CLASSNAME[size], getStyledButtonColorsClassNames(color, active, disabled), classNameProp), - [active, classNameProp, color, disabled, size] - ); + const className = useStyledButtonClassName({ size, color, active, disabled }, classNameProp); return isLink ? { ...restProps, className } : { ...restProps, className, disabled }; } diff --git a/tailwind.config.js b/tailwind.config.js index f2d5cd37ca..ea613a0980 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -37,6 +37,7 @@ module.exports = { xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', + 'inner-bottom': 'inset 0 -2px 4px 0 rgba(0, 0, 0, 0.06)', outline: '0 0 0 3px rgba(237, 137, 54, 0.5)', none: 'none', // From 74544343df8c6bfc4c495d3dab74c7a76ada3583 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2024 04:16:22 +0300 Subject: [PATCH 43/74] TW-1479: [EVM] Transactions history. Multichain list. + Ad --- TODO.md | 1 - src/app/pages/Activity/index.tsx | 5 +++-- src/app/templates/activity/ActivityListContainer.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 14016d8456..a60b56fef7 100644 --- a/TODO.md +++ b/TODO.md @@ -30,7 +30,6 @@ - - Design - Grouping by date - `const HIDE_INTERACTIONS = true;` -- Ad on Multichain - `` - Covalent SDK - diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index 21be5eff7a..ddcd492cee 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -18,7 +18,6 @@ import { MultichainActivityList, FilterKind } from 'app/templates/activity'; -import { NetworkSelectModalContent } from 'app/templates/NetworkSelectModal'; import { T } from 'lib/i18n'; import { useBooleanState } from 'lib/ui/hooks'; import Popper from 'lib/ui/Popper'; @@ -82,7 +81,9 @@ export const ActivityPage = memo(() => { )} ) : ( - + + + )} ); diff --git a/src/app/templates/activity/ActivityListContainer.tsx b/src/app/templates/activity/ActivityListContainer.tsx index ab634c659d..90d308588a 100644 --- a/src/app/templates/activity/ActivityListContainer.tsx +++ b/src/app/templates/activity/ActivityListContainer.tsx @@ -9,7 +9,7 @@ import { t } from 'lib/i18n/react'; import { ReactivateAdsBanner } from './ReactivateAdsBanner'; interface Props extends PropsWithChildren { - chainId: string | number; + chainId?: string | number; assetSlug?: string; } @@ -20,7 +20,7 @@ export const ActivityListContainer: FC = ({ children, chainId, assetSlug if (shouldShowPartnersPromo) return ( Date: Tue, 8 Oct 2024 04:36:47 +0300 Subject: [PATCH 44/74] TW-1479: [EVM] Transactions history. + @covalenthq/client-sdk --- package.json | 1 + src/lib/activity/evm/parse/gas.ts | 5 +- src/lib/activity/evm/parse/gr-v2.ts | 7 +- src/lib/activity/evm/parse/gr-v3.ts | 15 +- src/lib/apis/temple/endpoints/evm/index.ts | 23 +- .../apis/temple/endpoints/evm/types/gr-v2.ts | 141 -------- .../apis/temple/endpoints/evm/types/gr-v3.ts | 336 ------------------ .../apis/temple/endpoints/evm/types/utils.ts | 3 - yarn.lock | 12 + 9 files changed, 30 insertions(+), 513 deletions(-) delete mode 100644 src/lib/apis/temple/endpoints/evm/types/gr-v2.ts delete mode 100644 src/lib/apis/temple/endpoints/evm/types/gr-v3.ts delete mode 100644 src/lib/apis/temple/endpoints/evm/types/utils.ts diff --git a/package.json b/package.json index 45df51af89..ad3a862cdc 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@apollo/client": "^3.7.2", + "@covalenthq/client-sdk": "^2", "@dicebear/bottts-neutral": "^8.0.1", "@dicebear/core": "^8.0.1", "@firebase/app": "^0.10.1", diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index 690cc227b3..d497387cbd 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -1,11 +1,12 @@ -import { GoldRushTransaction, GoldRushERC20Transaction } from 'lib/apis/temple/endpoints/evm'; +import type { Transaction, BlockTransactionWithContractTransfers } from '@covalenthq/client-sdk'; + import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { ActivityOperKindEnum, EvmActivityAsset, EvmOperation } from '../../types'; export function parseGasTransfer( - item: GoldRushTransaction | GoldRushERC20Transaction, + item: Transaction | BlockTransactionWithContractTransfers, accountAddress: string, /** Only way to suspect transfering to a contract, not an account */ partOfBatch: boolean diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index b237d90a51..5ade1b4503 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -1,4 +1,5 @@ -import { GoldRushERC20Transaction, GoldRushERC20TransactionTransfer } from 'lib/apis/temple/endpoints/evm'; +import type { BlockTransactionWithContractTransfers, TokenTransferItem } from '@covalenthq/client-sdk'; + import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; @@ -7,7 +8,7 @@ import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } fro import { parseGasTransfer } from './gas'; export function parseGoldRushERC20Transfer( - item: GoldRushERC20Transaction, + item: BlockTransactionWithContractTransfers, chainId: number, accountAddress: string ): EvmActivity { @@ -31,7 +32,7 @@ export function parseGoldRushERC20Transfer( }; } -function parseTransfer(transfer: GoldRushERC20TransactionTransfer, item: GoldRushERC20Transaction): EvmOperation { +function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithContractTransfers): EvmOperation { const kind = (() => { if (transfer.transfer_type === 'IN') { return item.to_address === transfer.contract_address diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index 4543acd0cf..c750d94748 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -1,4 +1,5 @@ -import { GoldRushTransaction, GoldRushTransactionLogEvent } from 'lib/apis/temple/endpoints/evm'; +import type { Transaction, LogEvent } from '@covalenthq/client-sdk'; + import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; @@ -7,11 +8,7 @@ import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } fro import { parseGasTransfer } from './gas'; -export function parseGoldRushTransaction( - item: GoldRushTransaction, - chainId: number, - accountAddress: string -): EvmActivity { +export function parseGoldRushTransaction(item: Transaction, chainId: number, accountAddress: string): EvmActivity { const logEvents = item.log_events ?? []; const addedAt = item.block_signed_at as unknown as string; @@ -34,11 +31,7 @@ export function parseGoldRushTransaction( }; } -function parseLogEvent( - logEvent: GoldRushTransactionLogEvent, - item: GoldRushTransaction, - accountAddress: string -): EvmOperation | null { +function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: string): EvmOperation | null { if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; const contractAddress = getEvmAddressSafe(logEvent.sender_address); diff --git a/src/lib/apis/temple/endpoints/evm/index.ts b/src/lib/apis/temple/endpoints/evm/index.ts index a96a2bd288..5b3796870e 100644 --- a/src/lib/apis/temple/endpoints/evm/index.ts +++ b/src/lib/apis/temple/endpoints/evm/index.ts @@ -1,19 +1,8 @@ +import type { RecentTransactionsResponse, TransactionsResponse, Erc20TransfersResponse } from '@covalenthq/client-sdk'; + import { templeWalletApi } from '../templewallet.api'; import { BalancesResponse, ChainID, NftAddressBalanceNftResponse } from './api.interfaces'; -import { - GoldRushERC20TransactionsResponse, - GoldRushERC20Transaction, - GoldRushERC20TransactionTransfer -} from './types/gr-v2'; -import { GoldRushTransaction, GoldRushTransactionLogEvent, GoldRushTransactionsResponse } from './types/gr-v3'; - -export type { - GoldRushTransaction, - GoldRushTransactionLogEvent, - GoldRushERC20Transaction, - GoldRushERC20TransactionTransfer -}; export const getEvmBalances = (walletAddress: string, chainId: ChainID) => buildEvmRequest('/balances', walletAddress, chainId); @@ -27,7 +16,7 @@ export const getEvmCollectiblesMetadata = (walletAddress: string, chainId: Chain /** Calls to GoldRush v3 endpoints */ export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page?: number, signal?: AbortSignal) => - buildEvmRequest( + buildEvmRequest( '/transactions', walletAddress, chainId, @@ -36,9 +25,9 @@ export const getEvmTransactions = (walletAddress: string, chainId: ChainID, page }, signal ).then(({ items, current_page }) => ({ - items, + items: items ?? [], /** null | > 0 */ - nextPage: current_page > 1 ? current_page - 1 : null + nextPage: current_page && current_page > 1 ? current_page - 1 : null })); /** Calls to GoldRush v2 endpoints */ @@ -49,7 +38,7 @@ export const getEvmERC20Transfers = ( page?: number, signal?: AbortSignal ) => - buildEvmRequest( + buildEvmRequest( '/erc20-transfers', walletAddress, chainId, diff --git a/src/lib/apis/temple/endpoints/evm/types/gr-v2.ts b/src/lib/apis/temple/endpoints/evm/types/gr-v2.ts deleted file mode 100644 index d559ac1cbc..0000000000 --- a/src/lib/apis/temple/endpoints/evm/types/gr-v2.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Nullable } from './utils'; - -export type GoldRushERC20TransactionsResponse = Nullable<{ - items: GoldRushERC20Transaction[]; - pagination: Pagination; -}>; - -export type GoldRushERC20Transaction = Nullable; - -export type GoldRushERC20TransactionTransfer = Nullable; - -interface Pagination { - /** * True is there is another page. */ - has_more: boolean; - /** * The requested page number. */ - page_number: number; - /** * The requested number of items on the current page. */ - page_size: number; - /** @deprecated // Always null - * The total number of items across all pages for this request. - */ - total_count: number; -} - -interface BlockTransactionWithContractTransfers { - /** * The block signed timestamp in UTC. */ - block_signed_at: Date; - /** * The height of the block. */ - block_height: number; - /** * The hash of the block. Use it to remove transactions from re-org-ed blocks. */ - block_hash: string; - /** * The requested transaction hash. */ - tx_hash: string; - /** * The offset is the position of the tx in the block. */ - tx_offset: number; - /** * Whether or not transaction is successful. */ - successful: boolean; - /** * The address of the miner. */ - miner_address: string; - /** * The sender's wallet address. */ - from_address: string; - /** * The label of `from` address. */ - from_address_label: string; - /** * The receiver's wallet address. */ - to_address: string; - /** * The label of `to` address. */ - to_address_label: string; - /** * The value attached to this tx. */ - value: bigint; - /** * The value attached in `quote-currency` to this tx. */ - value_quote: number; - /** * A prettier version of the quote for rendering purposes. */ - pretty_value_quote: string; - /** * The requested chain native gas token metadata. */ - gas_metadata: ContractMetadata; - gas_offered: number; - /** * The gas spent for this tx. */ - gas_spent: number; - /** * The gas price at the time of this tx. */ - gas_price: number; - /** * The transaction's gas_price * gas_spent, denoted in wei. */ - fees_paid: bigint; - /** * The gas spent in `quote-currency` denomination. */ - gas_quote: number; - /** * A prettier version of the quote for rendering purposes. */ - pretty_gas_quote: string; - /** * The native gas exchange rate for the requested `quote-currency`. */ - gas_quote_rate: number; - transfers: GoldRushERC20TransactionTransfer[]; -} - -interface ContractMetadata { - /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ - contract_decimals: number; - /** * The string returned by the `name()` method. */ - contract_name: string; - /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ - contract_ticker_symbol: string; - /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ - contract_address: string; - /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ - supports_erc: string[]; - /** * The contract logo URL. */ - logo_url: string; -} - -interface TokenTransferItem { - /** * The block signed timestamp in UTC. */ - block_signed_at: Date; - /** * The requested transaction hash. */ - tx_hash: string; - /** * The sender's wallet address. */ - from_address: string; - /** * The label of `from` address. */ - from_address_label: string; - /** * The receiver's wallet address. */ - to_address: string; - /** * The label of `to` address. */ - to_address_label: string; - /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ - contract_decimals: number; - /** * The string returned by the `name()` method. */ - contract_name: string; - /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ - contract_ticker_symbol: string; - /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ - contract_address: string; - /** * The contract logo URL. */ - logo_url: string; - /** * Categorizes token transactions as either `transfer-in` or `transfer-out`, indicating whether tokens are being received or sent from an account. */ - transfer_type: string; - /** * The delta attached to this transfer. */ - delta: bigint; - /** * The asset balance. Use `contract_decimals` to scale this balance for display purposes. */ - balance: bigint; - /** * The exchange rate for the requested quote currency. */ - quote_rate: number; - /** * The current delta converted to fiat in `quote-currency`. */ - delta_quote: number; - /** * A prettier version of the quote for rendering purposes. */ - pretty_delta_quote: string; - /** * The current balance converted to fiat in `quote-currency`. */ - balance_quote: number; - /** * Additional details on which transfer events were invoked. Defaults to `true`. */ - method_calls: Nullable[]; - /** * The explorer links for this transaction. */ - explorers: Explorer[]; -} - -interface MethodCallsForTransfers { - /** * The address of the sender. */ - sender_address: string; - method: string; -} - -interface Explorer { - /** * The name of the explorer. */ - label: string; - /** * The URL of the explorer. */ - url: string; -} diff --git a/src/lib/apis/temple/endpoints/evm/types/gr-v3.ts b/src/lib/apis/temple/endpoints/evm/types/gr-v3.ts deleted file mode 100644 index ea929fda48..0000000000 --- a/src/lib/apis/temple/endpoints/evm/types/gr-v3.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { Nullable } from './utils'; - -export interface GoldRushTransactionsResponse { - items: GoldRushTransaction[]; - current_page: number; -} - -export type GoldRushTransaction = Nullable; - -export type GoldRushTransactionLogEvent = Nullable; - -interface Transaction { - /** * The block signed timestamp in UTC. */ - block_signed_at: Date; - /** * The height of the block. */ - block_height: number; - /** * The hash of the block. Use it to remove transactions from re-org-ed blocks. */ - block_hash: string; - /** * The requested transaction hash. */ - tx_hash: string; - /** * The offset is the position of the tx in the block. */ - tx_offset: number; - /** * Indicates whether a transaction failed or succeeded. */ - successful: boolean; - /** * The sender's wallet address. */ - from_address: string; - /** * The address of the miner. */ - miner_address: string; - /** * The label of `from` address. */ - from_address_label: string; - /** * The receiver's wallet address. */ - to_address: string; - /** * The label of `to` address. */ - to_address_label: string; - /** * The value attached to this tx. */ - value: bigint; - /** * The value attached in `quote-currency` to this tx. */ - value_quote: number; - /** * A prettier version of the quote for rendering purposes. */ - pretty_value_quote: string; - /** * The requested chain native gas token metadata. */ - gas_metadata: ContractMetadata; - gas_offered: number; - /** * The gas spent for this tx. */ - gas_spent: number; - /** * The gas price at the time of this tx. */ - gas_price: number; - /** * The total transaction fees (`gas_price` * `gas_spent`) paid for this tx, denoted in wei. */ - fees_paid: bigint; - /** * The gas spent in `quote-currency` denomination. */ - gas_quote: number; - /** * A prettier version of the quote for rendering purposes. */ - pretty_gas_quote: string; - /** * The native gas exchange rate for the requested `quote-currency`. */ - gas_quote_rate: number; - /** * The explorer links for this transaction. */ - explorers: Explorer[]; - /** * The details for the dex transaction. */ - dex_details: Nullable[]; - /** * The details for the NFT sale transaction. */ - nft_sale_details: Nullable[]; - /** * The details for the lending protocol transaction. */ - lending_details: Nullable[]; - /** * The log events. */ - log_events: GoldRushTransactionLogEvent[]; - /** * The details related to the safe transaction. */ - safe_details: Nullable[]; -} - -interface NftSalesReport { - /** * The offset is the position of the log entry within an event log. */ - log_offset: number; - /** * Stores the topic event hash. All events have a unique topic event hash. */ - topic0: string; - /** * Stores the contract address of the protocol that facilitated the event. */ - protocol_contract_address: string; - /** * Stores the name of the protocol that facilitated the event. */ - protocol_name: string; - /** * The protocol logo URL. */ - protocol_logo_url: string; - /** * Stores the address of the transaction recipient. */ - to: string; - /** * Stores the address of the transaction sender. */ - from: string; - /** * Stores the address selling the NFT. */ - maker: string; - /** * Stores the address buying the NFT. */ - taker: string; - /** * Stores the NFTs token ID. All NFTs have a token ID. Within a collection, these token IDs are unique. If the NFT is transferred to another owner, the token id remains the same, as this number is its identifier within a collection. For example, if a collection has 10K NFTs then an NFT in that collection can have a token ID from 1-10K. */ - token_id: string; - /** * Stores the address of the collection. For example, [Bored Ape Yacht Club](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d) */ - collection_address: string; - /** * Stores the name of the collection. */ - collection_name: string; - /** * Stores the address of the token used to purchase the NFT. */ - token_address: string; - /** * Stores the name of the token used to purchase the NFT. */ - token_name: string; - /** * Stores the ticker symbol of the token used to purchase the NFT. */ - ticker_symbol: string; - /** * Stores the number decimal of the token used to purchase the NFT. */ - num_decimals: number; - contract_quote_rate: number; - /** * The token amount used to purchase the NFT. For example, if the user purchased an NFT for 1 ETH. The `nft_token_price` field will hold `1`. */ - nft_token_price: number; - /** * The USD amount used to purchase the NFT. */ - nft_token_price_usd: number; - pretty_nft_token_price_usd: string; - /** * The price of the NFT denominated in the chains native token. Even if a seller sells their NFT for DAI or MANA, this field denominates the price in the native token (e.g. ETH, AVAX, FTM, etc.) */ - nft_token_price_native: number; - pretty_nft_token_price_native: string; - /** * Stores the number of NFTs involved in the sale. It's quick routine to see multiple NFTs involved in a single sale. */ - token_count: number; - num_token_ids_sold_per_sale: number; - num_token_ids_sold_per_tx: number; - num_collections_sold_per_sale: number; - num_collections_sold_per_tx: number; - trade_type: string; - trade_group_type: string; -} - -interface DexReport { - /** * The offset is the position of the log entry within an event log. */ - log_offset: number; - /** * Stores the name of the protocol that facilitated the event. */ - protocol_name: string; - /** * Stores the contract address of the protocol that facilitated the event. */ - protocol_address: string; - /** * The protocol logo URL. */ - protocol_logo_url: string; - /** * Stores the aggregator responsible for the event. */ - aggregator_name: string; - /** * Stores the contract address of the aggregator responsible for the event. */ - aggregator_address: string; - /** * DEXs often have multiple version - e.g Uniswap V1, V2 and V3. The `version` field allows you to look at a specific version of the DEX. */ - version: number; - /** * Similarly to the `version` field, `fork_version` gives you the version of the forked DEX. For example, most DEXs are a fork of Uniswap V2; therefore, `fork` = `aave` & `fork_version` = `2` */ - fork_version: number; - /** * Many DEXs are a fork of an already established DEX. The fork field allows you to see which DEX has been forked. */ - fork: string; - /** * Stores the event taking place - e.g `swap`, `add_liquidity` and `remove_liquidity`. */ - event: string; - /** * Stores the address of the pair that the user interacts with. */ - pair_address: string; - pair_lp_fee_bps: number; - lp_token_address: string; - lp_token_ticker: string; - lp_token_num_decimals: number; - lp_token_name: string; - lp_token_value: string; - exchange_rate_usd: number; - /** * Stores the address of token 0 in the specific pair. */ - token_0_address: string; - /** * Stores the ticker symbol of token 0 in the specific pair. */ - token_0_ticker: string; - /** * Stores the number of contract decimals of token 0 in the specific pair. */ - token_0_num_decimals: number; - /** * Stores the contract name of token 0 in the specific pair. */ - token_0_name: string; - /** * Stores the address of token 1 in the specific pair. */ - token_1_address: string; - /** * Stores the ticker symbol of token 1 in the specific pair. */ - token_1_ticker: string; - /** * Stores the number of contract decimals of token 1 in the specific pair. */ - token_1_num_decimals: number; - /** * Stores the contract name of token 1 in the specific pair. */ - token_1_name: string; - /** * Stores the amount of token 0 used in the transaction. For example, 1 ETH, 100 USDC, 30 UNI, etc. */ - token_0_amount: string; - token_0_quote_rate: number; - token_0_usd_quote: number; - pretty_token_0_usd_quote: string; - token_0_logo_url: string; - /** * Stores the amount of token 1 used in the transaction. For example, 1 ETH, 100 USDC, 30 UNI, etc. */ - token_1_amount: string; - token_1_quote_rate: number; - token_1_usd_quote: number; - pretty_token_1_usd_quote: string; - token_1_logo_url: string; - /** * Stores the wallet address that initiated the transaction (i.e the wallet paying the gas fee). */ - sender: string; - /** * Stores the recipient of the transaction - recipients can be other wallets or smart contracts. For example, if you want to Swap tokens on Uniswap, the Uniswap router would typically be the recipient of the transaction. */ - recipient: string; -} - -interface ContractMetadata { - /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ - contract_decimals: number; - /** * The string returned by the `name()` method. */ - contract_name: string; - /** * The ticker symbol for this contract. This field is set by a developer and non-unique across a network. */ - contract_ticker_symbol: string; - /** * Use the relevant `contract_address` to lookup prices, logos, token transfers, etc. */ - contract_address: string; - /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ - supports_erc: string[]; - /** * The contract logo URL. */ - logo_url: string; -} - -interface Explorer { - /** * The name of the explorer. */ - label: string; - /** * The URL of the explorer. */ - url: string; -} - -interface LendingReport { - /** * The offset is the position of the log entry within an event log. */ - log_offset: number; - /** * Stores the name of the lending protocol that facilitated the event. */ - protocol_name: string; - /** * Stores the contract address of the lending protocol that facilitated the event. */ - protocol_address: string; - /** * The protocol logo URL. */ - protocol_logo_url: string; - /** * Lending protocols often have multiple version (e.g. Aave V1, V2 and V3). The `version` field allows you to look at a specific version of the Lending protocol. */ - version: string; - /** * Many lending protocols are a fork of an already established protocol. The fork column allows you to see which lending protocol has been forked. */ - fork: string; - /** * Similarly to the `version` column, `fork_version` gives you the version of the forked lending protocol. For example, most lending protocols in the space are a fork of Aave V2; therefore, `fork` = `aave` & `fork_version` = `2` */ - fork_version: string; - /** * Stores the event taking place - e.g `borrow`, `deposit`, `liquidation`, 'repay' and 'withdraw'. */ - event: string; - /** * Stores the name of the LP token issued by the lending protocol. LP tokens can be debt or interest bearing tokens. */ - lp_token_name: string; - /** * Stores the number decimal of the LP token. */ - lp_decimals: number; - /** * Stores the ticker symbol of the LP token. */ - lp_ticker_symbol: string; - /** * Stores the token address of the LP token. */ - lp_token_address: string; - /** * Stores the amount of LP token used in the event (e.g. 1 aETH, 100 cUSDC, etc). */ - lp_token_amount: number; - /** * Stores the total USD amount of all the LP Token used in the event. */ - lp_token_price: number; - /** * Stores the exchange rate between the LP and underlying token. */ - exchange_rate: number; - /** * Stores the USD price of the LP Token used in the event. */ - exchange_rate_usd: number; - /** * Stores the name of the token going into the lending protocol (e.g the token getting deposited). */ - token_name_in: string; - /** * Stores the number decimal of the token going into the lending protocol. */ - token_decimal_in: number; - /** * Stores the address of the token going into the lending protocol. */ - token_address_in: string; - /** * Stores the ticker symbol of the token going into the lending protocol. */ - token_ticker_in: string; - /** * Stores the logo URL of the token going into the lending protocol. */ - token_logo_in: string; - /** * Stores the amount of tokens going into the lending protocol (e.g 1 ETH, 100 USDC, etc). */ - token_amount_in: number; - /** * Stores the total USD amount of all tokens going into the lending protocol. */ - amount_in_usd: number; - pretty_amount_in_usd: string; - /** * Stores the name of the token going out of the lending protocol (e.g the token getting deposited). */ - token_name_out: string; - /** * Stores the number decimal of the token going out of the lending protocol. */ - token_decimals_out: number; - /** * Stores the address of the token going out of the lending protocol. */ - token_address_out: string; - /** * Stores the ticker symbol of the token going out of the lending protocol. */ - token_ticker_out: string; - /** * Stores the logo URL of the token going out of the lending protocol. */ - token_logo_out: string; - /** * Stores the amount of tokens going out of the lending protocol (e.g 1 ETH, 100 USDC, etc). */ - token_amount_out: number; - /** * Stores the total USD amount of all tokens going out of the lending protocol. */ - amount_out_usd: number; - pretty_amount_out_usd: string; - /** * Stores the type of loan the user is taking out. Lending protocols enable you to take out a stable or variable loan. Only relevant to borrow events. */ - borrow_rate_mode: number; - /** * Stores the interest rate of the loan. Only relevant to borrow events. */ - borrow_rate: number; - on_behalf_of: string; - /** * Stores the wallet address liquidating the loan. Only relevant to liquidation events. */ - liquidator: string; - /** * Stores the wallet address of the user initiating the event. */ - user: string; -} - -interface LogEvent { - /** * The block signed timestamp in UTC. */ - block_signed_at: Date; - /** * The height of the block. */ - block_height: number; - /** * The offset is the position of the tx in the block. */ - tx_offset: number; - /** * The offset is the position of the log entry within an event log. */ - log_offset: number; - /** * The requested transaction hash. */ - tx_hash: string; - /** * The log topics in raw data. */ - raw_log_topics: string[]; - /** * Use contract decimals to format the token balance for display purposes - divide the balance by `10^{contract_decimals}`. */ - sender_contract_decimals: number; - /** * The name of the sender. */ - sender_name: string; - sender_contract_ticker_symbol: string; - /** * The address of the sender. */ - sender_address: string; - /** * The label of the sender address. */ - sender_address_label: string; - /** * The contract logo URL. */ - sender_logo_url: string; - /** * A list of supported standard ERC interfaces, eg: `ERC20` and `ERC721`. */ - supports_erc: string[]; - /** * The address of the deployed UniswapV2 like factory contract for this DEX. */ - sender_factory_address: string; - /** * The log events in raw. */ - raw_log_data: string; - /** * The decoded item. */ - decoded: DecodedItem; -} - -interface SafeDetails { - /** * The address that signed the safe transaction. */ - owner_address: string; - /** * The signature of the owner for the safe transaction. */ - signature: string; - /** * The type of safe signature used. */ - signature_type: string; -} - -interface DecodedItem { - name: string; - signature: string; - params: Param[]; -} -interface Param { - name: string; - type: string; - indexed: boolean; - decoded: boolean; - value: string; -} diff --git a/src/lib/apis/temple/endpoints/evm/types/utils.ts b/src/lib/apis/temple/endpoints/evm/types/utils.ts deleted file mode 100644 index 72c965e63b..0000000000 --- a/src/lib/apis/temple/endpoints/evm/types/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Nullable = { - [P in keyof T]: T[P] | null; -}; diff --git a/yarn.lock b/yarn.lock index 20d924580b..7986a04008 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1303,6 +1303,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@covalenthq/client-sdk@^2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@covalenthq/client-sdk/-/client-sdk-2.1.1.tgz#d49a70f67f9f03747acbe28455240ee38b9ecc90" + integrity sha512-wrtb6sn5cUOTOTD+GbE1Xi92b2Q2Wd76roK2wwUMdbOQ591ucahgSPfaJEgZY29nocLc3wDV2vhR11fNX8nZxA== + dependencies: + big.js "^6.2.1" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -4958,6 +4965,11 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +big.js@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" + integrity sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ== + bignumber.js@9.1.0, bignumber.js@^9.1.0, bignumber.js@^9.1.2: version "9.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" From d22af19b8c437a89f6a41e0b04855b1e4c3d6ceb Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2024 04:39:56 +0300 Subject: [PATCH 45/74] TW-1479: [EVM] Transactions history. Refactor --- TODO.md | 35 ------------------------ src/app/templates/NetworkSelectModal.tsx | 2 +- 2 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a60b56fef7..0000000000 --- a/TODO.md +++ /dev/null @@ -1,35 +0,0 @@ -- Remove `lib/temple/activity` - -- Prepare activities from TZKT in one parse -- NFTs -- `FAILED` // Look into `successful": false` -- Block explorers href -- New loader -- - -### IDEAS -- Fees, paid in gas -- - Especially, when tx is a gas transfer -- Clickable asset symbol -- - Take user to asset page -- - -### ISSUES -- NFTs are not obvious -- Approve amount -- - Unsetting approval (false in ApprovalForAll for ERC721) -- Getting contract address & asset ID -- - Maybe make asset symbol clickable -- Classification for operations. Same? -- 'Show more/less' button logic -- `TransferSingle` of ERC-1155 -- - -### FOR PRE-REVIEW -- Bundle modal -- - Design -- Grouping by date -- `const HIDE_INTERACTIONS = true;` -- `` -- Covalent SDK -- diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 7d9949e640..59844d594f 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -65,7 +65,7 @@ interface ContentProps { handleNetworkSelect: (chain: OneOfChains | null) => void; } -export const NetworkSelectModalContent = memo(({ opened, selectedNetwork, handleNetworkSelect }) => { +const NetworkSelectModalContent = memo(({ opened, selectedNetwork, handleNetworkSelect }) => { const accountTezAddress = useAccountAddressForTezos(); const accountEvmAddress = useAccountAddressForEvm(); From d8cf707c0c78eb344b6da2bd24c89bbe76fe1039 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Oct 2024 14:24:40 +0300 Subject: [PATCH 46/74] TW-1479: [EVM] Transactions history. + Grouping by Date --- src/app/atoms/ScrollView.tsx | 20 +++++----- .../ActivityItem/ActivityOperationBase.tsx | 7 +++- .../templates/activity/EvmActivityList.tsx | 19 +++++++-- src/app/templates/activity/MultichainList.tsx | 31 ++++++++++---- .../templates/activity/TezosActivityList.tsx | 19 +++++++-- .../templates/activity/grouping-by-date.tsx | 40 +++++++++++++++++++ 6 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 src/app/templates/activity/grouping-by-date.tsx diff --git a/src/app/atoms/ScrollView.tsx b/src/app/atoms/ScrollView.tsx index f6b29f65c2..1e608a4f88 100644 --- a/src/app/atoms/ScrollView.tsx +++ b/src/app/atoms/ScrollView.tsx @@ -20,9 +20,7 @@ export const ScrollView: FC = ({ className, children }) => { () => event => { const node = event.currentTarget; - const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; - - setContentHidingThrottled(node.scrollHeight > node.clientHeight && scrollBottom > 0); + setContentHidingThrottled(isContentHidingBelow(node)); }, [] ); @@ -33,11 +31,7 @@ export const ScrollView: FC = ({ className, children }) => { throttle(() => { const node = ref.current; - if (!node) return; - - const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; - - setContentHidingThrottled(node.scrollHeight > node.clientHeight && scrollBottom > 0); + if (node) setContentHidingThrottled(isContentHidingBelow(node)); }, 300) ), [] @@ -49,9 +43,7 @@ export const ScrollView: FC = ({ className, children }) => { resizeObserver.observe(node); - const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; - - setContentHiding(node.scrollHeight > node.clientHeight && scrollBottom > 0); + setContentHiding(isContentHidingBelow(node)); }, []); return ( @@ -64,3 +56,9 @@ export const ScrollView: FC = ({ className, children }) => {
); }; + +function isContentHidingBelow(node: HTMLDivElement) { + const scrollBottom = node.scrollHeight - node.clientHeight - node.scrollTop; + + return node.scrollHeight > node.clientHeight && scrollBottom > 0; +} diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 2bcb367fd8..7003fcb24a 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -52,7 +52,12 @@ export const ActivityOperationBaseComponent = memo( const symbolStr = symbol.length > 6 ? `${symbol.slice(0, 6)}...` : symbol; return ( -
+
0 && 'text-success' + )} + > {kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( {atomsToTokens(asset.amount, asset.decimals)} diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index f1944bb843..f95856d480 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -13,6 +13,7 @@ import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; +import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -74,6 +75,20 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = [activities, filterKind] ); + const groupedActivities = useGroupingByDate(displayActivities); + + const contentJsx = useMemo( + () => + groupedActivities.map(([dateStr, activities]) => ( + + {activities.map(activity => ( + + ))} + + )), + [groupedActivities, network, assetSlug] + ); + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } @@ -86,9 +101,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = retryInitialLoad={loadNext} loadMore={loadNext} > - {displayActivities.map(activity => ( - - ))} + {contentJsx} ); }; diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index e0e05bd263..81327afda6 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -22,6 +22,7 @@ import { } from 'temple/front'; import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; +import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -123,6 +124,28 @@ export const MultichainActivityList = memo(({ filterKind }) => { return filtered.toSorted((a, b) => (a.addedAt < b.addedAt ? 1 : -1)); }, [activities, filterKind]); + const groupedActivities = useGroupingByDate(displayActivities); + + const contentJsx = useMemo( + () => + groupedActivities.map(([dateStr, activities]) => ( + + {activities.map(activity => + isTezosActivity(activity) ? ( + + ) : ( + + ) + )} + + )), + [groupedActivities, allTezosChains, allEvmChains] + ); + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } @@ -135,13 +158,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { retryInitialLoad={loadNext} loadMore={loadNext} > - {displayActivities.map(activity => - isTezosActivity(activity) ? ( - - ) : ( - - ) - )} + {contentJsx} ); }); diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index a754b61370..fa4bb9cd62 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -11,6 +11,7 @@ import { isKnownChainId } from 'lib/apis/tzkt/api'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { TezosActivityComponent } from './ActivityItem'; +import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -72,6 +73,20 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK [activities, filterKind] ); + const groupedActivities = useGroupingByDate(displayActivities); + + const contentJsx = useMemo( + () => + groupedActivities.map(([dateStr, activities]) => ( + + {activities.map(activity => ( + + ))} + + )), + [groupedActivities, network, assetSlug] + ); + if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { return ; } @@ -84,9 +99,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK retryInitialLoad={loadNext} loadMore={loadNext} > - {displayActivities.map(activity => ( - - ))} + {contentJsx} ); }); diff --git a/src/app/templates/activity/grouping-by-date.tsx b/src/app/templates/activity/grouping-by-date.tsx new file mode 100644 index 0000000000..441db80358 --- /dev/null +++ b/src/app/templates/activity/grouping-by-date.tsx @@ -0,0 +1,40 @@ +import React, { FC, useMemo } from 'react'; + +import { formatDate } from 'lib/i18n'; + +interface AddedAt { + /** ISO string */ + addedAt: string; +} + +export function useGroupingByDate(sorted: T[]) { + return useMemo(() => { + const groupedItems = new Map(); + + for (const item of sorted) { + const dateStr = formatDate(item.addedAt, 'PP'); + + const group = groupedItems.get(dateStr) ?? []; + + if (!groupedItems.has(dateStr)) groupedItems.set(dateStr, group); + + group.push(item); + } + + return Array.from(groupedItems); + }, [sorted]); +} + +interface ActivitiesDateGroupProps extends PropsWithChildren { + title: string; +} + +export const ActivitiesDateGroup: FC = ({ title, children }) => { + return ( + <> +
{title}
+ + {children} + + ); +}; From 83ca8dc730bbe1bb2e44b0278cc8e74b1268f626 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Oct 2024 14:29:50 +0300 Subject: [PATCH 47/74] TW-1479: [EVM] Transactions history. Fix amountSigned. + Green positive amounts --- .../ActivityItem/ActivityOperationBase.tsx | 17 ++++++++++------- .../activity/ActivityItem/EvmActivity.tsx | 2 +- .../ActivityItem/TezosActivityOperation.tsx | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 7003fcb24a..61653b06a5 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -31,7 +31,8 @@ interface Props { export interface ActivityItemBaseAssetProp { contract: string; tokenId?: string; - amount?: string | null; + /** `null` for 'unlimited' amount */ + amountSigned?: string | null; decimals: number; symbol?: string; iconURL?: string; @@ -55,12 +56,12 @@ export const ActivityOperationBaseComponent = memo(
0 && 'text-success' + asset.amountSigned && Number(asset.amountSigned) > 0 && 'text-success' )} > - {kind === ActivityOperKindEnum.approve ? null : asset.amount ? ( + {kind === ActivityOperKindEnum.approve ? null : asset.amountSigned ? ( - {atomsToTokens(asset.amount, asset.decimals)} + {atomsToTokens(asset.amountSigned, asset.decimals)} ) : null} @@ -72,15 +73,17 @@ export const ActivityOperationBaseComponent = memo( const fiatJsx = useMemo(() => { if (!asset) return null; - if (!asset.amount) return asset.amount === null ? 'Unlimited' : null; + if (!asset.amountSigned) return asset.amountSigned === null ? 'Unlimited' : null; if (kind === ActivityOperKindEnum.approve) - return {atomsToTokens(asset.amount, asset.decimals)}; + return {atomsToTokens(asset.amountSigned, asset.decimals)}; if (!assetSlug) return null; const amountForFiat = - kind === 'bundle' || isTransferActivityOperKind(kind) ? atomsToTokens(asset.amount, asset.decimals) : null; + kind === 'bundle' || isTransferActivityOperKind(kind) + ? atomsToTokens(asset.amountSigned, asset.decimals) + : null; if (!amountForFiat) return null; diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 6eaf8b7a57..3426038d5b 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -109,7 +109,7 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl ...faceAssetBase, contract, tokenId, - amount: faceAmount.toFixed(), + amountSigned: faceAmount.toFixed(), decimals, symbol }; diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index e0d74b5171..9483220a57 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -50,7 +50,7 @@ export function buildTezosOperationAsset( return { contract, tokenId, - amount: amountSigned, + amountSigned, decimals, // nft: isTezosCollectibleMetadata(assetMetadata), symbol: assetMetadata?.symbol From 946302d88ee16b106437e18ec5d3a023fb14cc44 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Oct 2024 22:36:48 +0300 Subject: [PATCH 48/74] TW-1479: [EVM] Transactions history. + Bundle icons stack. + NFTs rounding --- src/app/atoms/Identicon.tsx | 37 +++++--- src/app/atoms/NetworkLogo.tsx | 15 +--- src/app/icons/collectible-placeholder.svg | 2 +- .../Home/OtherComponents/AssetBanner.tsx | 6 +- src/app/templates/AssetIcon.tsx | 64 ++++++------- .../AssetImage/AssetImageStacked.tsx | 4 +- src/app/templates/SendForm/AssetSelect.tsx | 6 +- .../SwapFormInput/AssetsMenu/AssetOption.tsx | 4 +- .../SwapForm/SwapFormInput/SwapFormInput.tsx | 4 +- .../SwapRoute/SwapRouteItem/hop-item.tsx | 6 +- .../ActivityItem/ActivityOperationBase.tsx | 90 +++++++++++++------ .../ActivityItem/TezosActivityOperation.tsx | 5 +- src/lib/{temple/front => }/identicon.ts | 59 ++++++------ .../components/DexTypeIcon/DexTypeIcon.tsx | 4 +- src/lib/temple/front/index.ts | 2 - 15 files changed, 171 insertions(+), 137 deletions(-) rename src/lib/{temple/front => }/identicon.ts (56%) diff --git a/src/app/atoms/Identicon.tsx b/src/app/atoms/Identicon.tsx index 13175b06a9..7e5950ebe3 100644 --- a/src/app/atoms/Identicon.tsx +++ b/src/app/atoms/Identicon.tsx @@ -1,18 +1,23 @@ -import React, { HTMLAttributes, useMemo } from 'react'; +import React, { FC, HTMLAttributes, ImgHTMLAttributes, useMemo } from 'react'; import clsx from 'clsx'; -import { IdenticonType, getIdenticonUri } from 'lib/temple/front'; -import { IdenticonOptions } from 'lib/temple/front/identicon'; +import { + IdenticonImgType, + ImageIdenticonOptions, + InitialsIdenticonOptions, + buildImageIdenticonUri, + buildInitialsIdenticonUri +} from 'lib/identicon'; -type IdenticonProps = HTMLAttributes & { +interface IdenticonProps extends HTMLAttributes { type: T; hash: string; size?: number; - options?: IdenticonOptions; -}; + options?: ImageIdenticonOptions; +} -export const Identicon = ({ +export const Identicon = ({ type, hash, size = 100, @@ -21,14 +26,13 @@ export const Identicon = ({ options, ...rest }: IdenticonProps) => { - const backgroundImage = useMemo(() => getIdenticonUri(hash, size, type, options), [hash, options, size, type]); + const backgroundImage = useMemo(() => buildImageIdenticonUri(hash, size, type, options), [hash, options, size, type]); return (
({
); }; + +interface IdenticonInitialsProps extends ImgHTMLAttributes { + value: string; + options?: InitialsIdenticonOptions; +} + +export const IdenticonInitials: FC = ({ value, options, ...props }) => { + const src = useMemo(() => buildInitialsIdenticonUri(value, options), [options, value]); + + return ; +}; diff --git a/src/app/atoms/NetworkLogo.tsx b/src/app/atoms/NetworkLogo.tsx index 8e3cd90356..2b632efa70 100644 --- a/src/app/atoms/NetworkLogo.tsx +++ b/src/app/atoms/NetworkLogo.tsx @@ -14,7 +14,7 @@ import useTippy, { UseTippyOptions } from 'lib/ui/useTippy'; import { useTezosChainByChainId } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; -import { Identicon } from './Identicon'; +import { IdenticonInitials } from './Identicon'; import { TezNetworkLogo } from './NetworksLogos'; const logosRecord: Record = { @@ -95,9 +95,6 @@ export const EvmNetworkLogo = memo( ); } ); - -const IDENTICON_OPTS = { chars: 1 }; - interface NetworkLogoFallbackProps { networkName?: string; size?: number; @@ -109,15 +106,7 @@ const NetworkLogoFallback = memo(({ networkName, size style={{ width: size, height: size }} className={clsx('p-px border border-grey-4 bg-white rounded-full overflow-hidden', className)} > -
- -
+
)); diff --git a/src/app/icons/collectible-placeholder.svg b/src/app/icons/collectible-placeholder.svg index 7040272efc..3a364133c8 100644 --- a/src/app/icons/collectible-placeholder.svg +++ b/src/app/icons/collectible-placeholder.svg @@ -1,4 +1,4 @@ - + diff --git a/src/app/pages/Home/OtherComponents/AssetBanner.tsx b/src/app/pages/Home/OtherComponents/AssetBanner.tsx index 85e16b5f9c..e72f300d4b 100644 --- a/src/app/pages/Home/OtherComponents/AssetBanner.tsx +++ b/src/app/pages/Home/OtherComponents/AssetBanner.tsx @@ -4,7 +4,7 @@ import Money from 'app/atoms/Money'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; import AddressChip from 'app/templates/AddressChip'; -import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; +import { EvmTokenIcon, TezosTokenIcon } from 'app/templates/AssetIcon'; import { EvmBalance, TezosBalance } from 'app/templates/Balance'; import InFiat from 'app/templates/InFiat'; import { setAnotherSelector, setTestID } from 'lib/analytics'; @@ -48,7 +48,7 @@ const TezosAssetBanner = memo(({ tezosChainId, assetSlug return ( <>
- +
(({ evmChainId, assetSlug }) => <>
- +
(({ className, style, ...props }) => ( +export const TezosAssetIcon = memo(props => ( + +)); + +export const TezosTokenIcon = memo(({ className, style, ...props }) => (
- +
)); -const TezosAssetIconPlaceholder: TezosAssetImageProps['Fallback'] = memo(({ metadata, size }) => ( - -)); +const TezosAssetIconPlaceholder: TezosAssetImageProps['Fallback'] = memo(({ metadata, className, style }) => + metadata && isTezosCollectibleMetadata(metadata) ? ( + + ) : ( + + ) +); const ICON_DEFAULT_SIZE = 40; const ASSET_IMAGE_DEFAULT_SIZE = 30; @@ -38,7 +40,7 @@ export const TezosTokenIconWithNetwork = memo(({ tezosChai className={clsx('flex items-center justify-center relative', className)} style={{ width: ICON_DEFAULT_SIZE, height: ICON_DEFAULT_SIZE, ...style }} > - + {network && ( (({ tezosChai ); }); -export const EvmAssetIcon = memo(({ className, style, ...props }) => ( +export const EvmAssetIcon = memo(props => ( + +)); + +export const EvmTokenIcon = memo(({ className, style, ...props }) => (
- +
)); -const EvmAssetIconPlaceholder: EvmAssetImageProps['Fallback'] = memo(({ metadata, size }) => ( - -)); +const EvmAssetIconPlaceholder: EvmAssetImageProps['Fallback'] = memo(({ metadata, className, style }) => + metadata && isEvmCollectibleMetadata(metadata) ? ( + + ) : ( + + ) +); export const EvmTokenIconWithNetwork = memo(({ evmChainId, className, style, ...props }) => { const network = useEvmChainByChainId(evmChainId); @@ -74,7 +80,7 @@ export const EvmTokenIconWithNetwork = memo(({ evmChainId, c className={clsx('flex items-center justify-center relative', className)} style={{ width: ICON_DEFAULT_SIZE, height: ICON_DEFAULT_SIZE, ...style }} > - + {network && ( (({ evmChainId, c
); }); - -const AssetIconPlaceholder = memo<{ - isCollectible?: boolean; - metadata: AssetMetadataBase | EvmAssetMetadataBase | nullish; - size?: number; -}>(({ isCollectible, metadata, size }) => - isCollectible ? ( - - ) : ( - - ) -); diff --git a/src/app/templates/AssetImage/AssetImageStacked.tsx b/src/app/templates/AssetImage/AssetImageStacked.tsx index 266a02a727..8a686e4ceb 100644 --- a/src/app/templates/AssetImage/AssetImageStacked.tsx +++ b/src/app/templates/AssetImage/AssetImageStacked.tsx @@ -77,8 +77,7 @@ const AssetImageStacked: FC = ({ loader, fallback, onStackLoaded, - onStackFailed, - ...rest + onStackFailed }) => { const styleMemo: React.CSSProperties = useMemo( () => ({ @@ -102,7 +101,6 @@ const AssetImageStacked: FC = ({ width={size} onStackLoaded={onStackLoaded} onStackFailed={onStackFailed} - {...rest} /> ); }; diff --git a/src/app/templates/SendForm/AssetSelect.tsx b/src/app/templates/SendForm/AssetSelect.tsx index a623fb747e..e04f6a6e12 100644 --- a/src/app/templates/SendForm/AssetSelect.tsx +++ b/src/app/templates/SendForm/AssetSelect.tsx @@ -4,7 +4,7 @@ import classNames from 'clsx'; import { useDebounce } from 'use-debounce'; import Money from 'app/atoms/Money'; -import { TezosAssetIcon } from 'app/templates/AssetIcon'; +import { TezosTokenIcon } from 'app/templates/AssetIcon'; import { TezosBalance } from 'app/templates/Balance'; import InFiat from 'app/templates/InFiat'; import { setTestID, setAnotherSelector, TestIDProperty } from 'lib/analytics'; @@ -113,7 +113,7 @@ const AssetFieldContent = memo(({ network, slug, publicK return (
- + {balance => ( @@ -154,7 +154,7 @@ const AssetOptionContent = memo(({ network, accountPkh, {...setTestID(SendFormSelectors.assetDropDownItem)} {...setAnotherSelector('slug', slug)} > - +
diff --git a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx index 5e24dc1996..8e71f2b6eb 100644 --- a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import classNames from 'clsx'; -import { TezosAssetIcon } from 'app/templates/AssetIcon'; +import { TezosTokenIcon } from 'app/templates/AssetIcon'; import { AssetItemContent } from 'app/templates/AssetItemContent'; import { setAnotherSelector, setTestID } from 'lib/analytics'; import { useTezosAssetMetadata } from 'lib/metadata'; @@ -32,7 +32,7 @@ export const AssetOption: FC = ({ network, assetSlug, selected, accountPk {...setTestID(AssetsMenuSelectors.assetsMenuAssetItem)} {...setAnotherSelector('slug', assetSlug)} > - +
diff --git a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx index 6694980ab9..9723c8217d 100644 --- a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx @@ -7,7 +7,7 @@ import classNames from 'clsx'; import AssetField from 'app/atoms/AssetField'; import Money from 'app/atoms/Money'; import { useTokensListingLogicForSwapInput } from 'app/hooks/use-tokens-listing-logic-for-swap-input'; -import { TezosAssetIcon } from 'app/templates/AssetIcon'; +import { TezosTokenIcon } from 'app/templates/AssetIcon'; import { DropdownSelect } from 'app/templates/DropdownSelect/DropdownSelect'; import InFiat from 'app/templates/InFiat'; import { InputContainer } from 'app/templates/InputContainer/InputContainer'; @@ -221,7 +221,7 @@ const SwapDropdownFace: FC = ({ tezosChainId, testId, selectedAs
{selectedAssetSlug ? (
- + {selectedAssetMetadata.symbol} diff --git a/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx b/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx index 0f9b7194da..eaf5080d9e 100644 --- a/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx +++ b/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx @@ -4,7 +4,7 @@ import { isDefined } from '@rnw-community/shared'; import classNames from 'clsx'; import { useAppEnv } from 'app/env'; -import { TezosAssetIcon } from 'app/templates/AssetIcon'; +import { TezosTokenIcon } from 'app/templates/AssetIcon'; import { Route3Dex } from 'lib/apis/route3/fetch-route3-dexes'; import { Route3Token } from 'lib/apis/route3/fetch-route3-tokens'; import { toTokenSlug, TEZ_TOKEN_SLUG } from 'lib/assets'; @@ -50,11 +50,11 @@ export const HopItem: FC = ({ dex, aToken, bToken, className }) => {
- +
- +
diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 61653b06a5..31d55c5e9b 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; +import React, { memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import clsx from 'clsx'; @@ -36,6 +36,7 @@ export interface ActivityItemBaseAssetProp { decimals: number; symbol?: string; iconURL?: string; + nft?: boolean; } export const ActivityOperationBaseComponent = memo( @@ -121,13 +122,30 @@ export const ActivityOperationBaseComponent = memo( [onClick] ); - const IconFallback = useCallback( - () => ( -
- -
- ), - [kind] + const isNFT = Boolean(asset?.nft); + + const faceIconJsx = useMemo( + () => + withoutAssetIcon || !assetSlug ? ( +
+ +
+ ) : typeof chainId === 'number' ? ( + + ) : ( + + ), + [chainId, withoutAssetIcon, kind, asset?.iconURL, assetSlug] ); return ( @@ -135,28 +153,14 @@ export const ActivityOperationBaseComponent = memo( className={clsx('z-1 group flex gap-x-2 p-2 rounded-lg hover:bg-secondary-low', onClick && 'cursor-pointer')} onClick={handleClick} > -
- {withoutAssetIcon || !assetSlug ? ( - - ) : typeof chainId === 'number' ? ( - +
+ {kind === 'bundle' ? ( + {faceIconJsx} ) : ( - +
{faceIconJsx}
)} - {typeof chainId === 'number' ? ( + {withoutAssetIcon ? null : typeof chainId === 'number' ? ( ) : ( @@ -189,6 +193,38 @@ export const ActivityOperationBaseComponent = memo( } ); +const MEDALION_CLASS_NAME = 'border border-lines bg-white'; + +const BundleIconsStack = memo>(({ isNFT, children }) => { + return ( + <> +
+ +
+ +
+
+ {children} +
+
+ + ); +}); + const ActivityKindTitle: Record = { bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 9483220a57..36dce995a2 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -2,7 +2,7 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; import { fromAssetSlug } from 'lib/assets'; -import { AssetMetadataBase, useTezosAssetMetadata } from 'lib/metadata'; +import { AssetMetadataBase, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; @@ -53,6 +53,7 @@ export function buildTezosOperationAsset( amountSigned, decimals, // nft: isTezosCollectibleMetadata(assetMetadata), - symbol: assetMetadata?.symbol + symbol: assetMetadata?.symbol, + nft: assetMetadata ? isTezosCollectibleMetadata(assetMetadata) : undefined }; } diff --git a/src/lib/temple/front/identicon.ts b/src/lib/identicon.ts similarity index 56% rename from src/lib/temple/front/identicon.ts rename to src/lib/identicon.ts index 7175489609..2f46f2ce0a 100644 --- a/src/lib/temple/front/identicon.ts +++ b/src/lib/identicon.ts @@ -5,44 +5,47 @@ import memoizee from 'memoizee'; import * as firstLetters from 'lib/first-letters'; -export type IdenticonType = 'jdenticon' | 'botttsneutral' | 'initials'; +export type IdenticonImgType = 'jdenticon' | 'botttsneutral'; -export type IdenticonOptions = T extends 'jdenticon' +export type ImageIdenticonOptions = T extends 'jdenticon' ? Omit - : T extends 'botttsneutral' - ? Omit - : Omit; + : Omit; -const MAX_INITIALS_LENGTH = 5; -const DEFAULT_FONT_SIZE = 50; +export const buildImageIdenticonUri = memoizee(buildImageIdenticonUriLocal, { + max: 1024, + normalizer: ([hash, size, type, options]) => JSON.stringify([hash, size, type, options]) +}); -function internalGetIdenticonUri( +function buildImageIdenticonUriLocal( hash: string, size: number, type: T, - options?: IdenticonOptions + options?: ImageIdenticonOptions ) { - switch (type) { - case 'jdenticon': - // TODO: implement options interpretation for jdenticon - return `data:image/svg+xml,${encodeURIComponent(jdenticon.toSvg(hash, size))}`; - case 'botttsneutral': - return createAvatar(botttsNeutral, { seed: hash, size, ...options }).toDataUriSync(); - default: - return createAvatar(firstLetters, { - seed: hash, - size, - fontFamily: ['Menlo', 'Monaco', 'monospace'], - fontSize: estimateOptimalFontSize(hash.length), - ...options - }).toDataUriSync(); - } + if (type === 'botttsneutral') return createAvatar(botttsNeutral, { seed: hash, size, ...options }).toDataUriSync(); + + // TODO: implement options interpretation for jdenticon + return `data:image/svg+xml,${encodeURIComponent(jdenticon.toSvg(hash, size))}`; } -export const getIdenticonUri = memoizee(internalGetIdenticonUri, { - max: 1024, - normalizer: ([hash, size, type, options]) => JSON.stringify([hash, size, type, options]) -}); +export type InitialsIdenticonOptions = Omit; + +export const buildInitialsIdenticonUri = memoizee( + (seed: string, options?: InitialsIdenticonOptions) => + createAvatar(firstLetters, { + ...options, + seed, + fontFamily: ['Menlo', 'Monaco', 'monospace'], + fontSize: estimateOptimalFontSize(seed.length) // (!) Doesn't account for options.chars + }).toDataUriSync(), + { + max: 1024, + normalizer: ([seed, options]) => JSON.stringify([seed, options]) + } +); + +const MAX_INITIALS_LENGTH = 5; +const DEFAULT_FONT_SIZE = 50; /** * Dicebear renders letters in a viewbox of 100x100 with a font size that is returned by this function. Let's ensure diff --git a/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx b/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx index 54ab248615..fa13f203d5 100644 --- a/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx +++ b/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { TezosAssetIcon } from 'app/templates/AssetIcon'; +import { TezosTokenIcon } from 'app/templates/AssetIcon'; import { Route3DexTypeEnum } from 'lib/apis/route3/fetch-route3-dexes'; import { ReactComponent as CtezIcon } from './icons/ctez-icon.svg'; @@ -48,6 +48,6 @@ export const DexTypeIcon: FC = ({ dexType }) => { return Dexter logo; default: - return ; + return ; } }; diff --git a/src/lib/temple/front/index.ts b/src/lib/temple/front/index.ts index ffb7351b4b..3becd6e889 100644 --- a/src/lib/temple/front/index.ts +++ b/src/lib/temple/front/index.ts @@ -19,5 +19,3 @@ export { validateDelegate } from './validate-delegate'; export { validateRecipient } from './validate-recipient'; export { useFilteredContacts } from './use-filtered-contacts.hook'; - -export { type IdenticonType, getIdenticonUri } from './identicon'; From 638838dedf75b41229ea7a8d2967bf0e0877719f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Oct 2024 01:47:08 +0300 Subject: [PATCH 49/74] TW-1479: [EVM] Transactions history. ++ Asset Images --- src/app/icons/base/ok.svg | 6 ++ .../Collectibles/CollectiblePage/index.tsx | 2 +- .../components/CollectibleItem.tsx | 6 +- .../components/CollectibleItemImage.tsx | 57 ++++++++------- .../Home/OtherComponents/AssetBanner.tsx | 8 +-- src/app/templates/AssetIcon.tsx | 40 +++++------ .../AssetImage/AssetImageStacked.tsx | 60 +++------------- src/app/templates/AssetImage/index.tsx | 2 +- src/app/templates/SendForm/AssetSelect.tsx | 16 ++++- .../SwapFormInput/AssetsMenu/AssetOption.tsx | 4 +- .../SwapForm/SwapFormInput/SwapFormInput.tsx | 4 +- .../SwapRoute/SwapRouteItem/hop-item.tsx | 6 +- .../SwapForm/SwapRoute/lb-pool-part.tsx | 2 +- .../ActivityItem/ActivityOperationBase.tsx | 70 ++++++++++--------- .../components/DexTypeIcon/DexTypeIcon.tsx | 4 +- src/lib/ui/ImageStacked.tsx | 10 ++- 16 files changed, 143 insertions(+), 154 deletions(-) create mode 100644 src/app/icons/base/ok.svg diff --git a/src/app/icons/base/ok.svg b/src/app/icons/base/ok.svg new file mode 100644 index 0000000000..4036a9e2d0 --- /dev/null +++ b/src/app/icons/base/ok.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/app/pages/Collectibles/CollectiblePage/index.tsx b/src/app/pages/Collectibles/CollectiblePage/index.tsx index 8e78d663e2..8c8f51b518 100644 --- a/src/app/pages/Collectibles/CollectiblePage/index.tsx +++ b/src/app/pages/Collectibles/CollectiblePage/index.tsx @@ -290,7 +290,7 @@ const TezosCollectiblePage = memo(({ tezosChainId, as objktArtifactUri={details?.objktArtifactUri} isAdultContent={details?.isAdultContent} mime={details?.mime} - className="h-full w-full" + className="h-full w-full object-contain" />
diff --git a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx index 4260684317..051a12ec44 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItem.tsx @@ -149,6 +149,7 @@ export const TezosCollectibleItem = memo( areDetailsLoading={areDetailsLoading && details === undefined} mime={details?.mime} containerElemRef={wrapperElemRef} + className="object-cover" /> {network && ( @@ -206,6 +207,7 @@ export const TezosCollectibleItem = memo( areDetailsLoading={areDetailsLoading && details === undefined} mime={details?.mime} containerElemRef={wrapperElemRef} + className="object-contain" /> {areDetailsShown && balance && ( @@ -327,7 +329,7 @@ export const EvmCollectibleItem = memo( )} style={manageImgStyle} > - {metadata && } + {metadata && } {network && ( ( )} style={ImgStyle} > - {metadata && } + {metadata && } {showDetails && (
diff --git a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItemImage.tsx b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItemImage.tsx index d12c504555..fd1a31c4cc 100644 --- a/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItemImage.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab/components/CollectibleItemImage.tsx @@ -1,6 +1,7 @@ import React, { memo, useMemo } from 'react'; import { isDefined } from '@rnw-community/shared'; +import clsx from 'clsx'; import { useCollectibleIsAdultSelector } from 'app/store/tezos/collectibles/selectors'; import { buildCollectibleImagesStack, buildEvmCollectibleIconSources } from 'lib/images-uri'; @@ -19,48 +20,52 @@ interface Props { areDetailsLoading: boolean; mime?: string | null; containerElemRef: React.RefObject; + className?: string; } -export const CollectibleItemImage = memo(({ assetSlug, metadata, adultBlur, areDetailsLoading, mime }) => { - const isAdultContent = useCollectibleIsAdultSelector(assetSlug); - const isAdultFlagLoading = areDetailsLoading && !isDefined(isAdultContent); - const shouldShowBlur = isAdultContent && adultBlur; +export const CollectibleItemImage = memo( + ({ assetSlug, metadata, adultBlur, areDetailsLoading, mime, className }) => { + const isAdultContent = useCollectibleIsAdultSelector(assetSlug); + const isAdultFlagLoading = areDetailsLoading && !isDefined(isAdultContent); + const shouldShowBlur = isAdultContent && adultBlur; - const sources = useMemo(() => (metadata ? buildCollectibleImagesStack(metadata) : []), [metadata]); + const sources = useMemo(() => (metadata ? buildCollectibleImagesStack(metadata) : []), [metadata]); - const isAudioCollectible = useMemo(() => Boolean(mime && mime.startsWith('audio')), [mime]); + const isAudioCollectible = useMemo(() => Boolean(mime && mime.startsWith('audio')), [mime]); - return ( - <> - {isAdultFlagLoading ? ( - - ) : shouldShowBlur ? ( - - ) : ( - } - fallback={} - /> - )} - - ); -}); + return ( + <> + {isAdultFlagLoading ? ( + + ) : shouldShowBlur ? ( + + ) : ( + } + fallback={} + /> + )} + + ); + } +); interface EvmCollectibleItemImageProps { metadata: EvmCollectibleMetadata; + className?: string; } -export const EvmCollectibleItemImage = memo(({ metadata }) => { +export const EvmCollectibleItemImage = memo(({ metadata, className }) => { const sources = useMemo(() => buildEvmCollectibleIconSources(metadata), [metadata]); return ( } fallback={} /> diff --git a/src/app/pages/Home/OtherComponents/AssetBanner.tsx b/src/app/pages/Home/OtherComponents/AssetBanner.tsx index e72f300d4b..aeeb2d3032 100644 --- a/src/app/pages/Home/OtherComponents/AssetBanner.tsx +++ b/src/app/pages/Home/OtherComponents/AssetBanner.tsx @@ -4,7 +4,7 @@ import Money from 'app/atoms/Money'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useEvmTokenMetadataSelector } from 'app/store/evm/tokens-metadata/selectors'; import AddressChip from 'app/templates/AddressChip'; -import { EvmTokenIcon, TezosTokenIcon } from 'app/templates/AssetIcon'; +import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { EvmBalance, TezosBalance } from 'app/templates/Balance'; import InFiat from 'app/templates/InFiat'; import { setAnotherSelector, setTestID } from 'lib/analytics'; @@ -48,7 +48,7 @@ const TezosAssetBanner = memo(({ tezosChainId, assetSlug return ( <>
- +
(({ evmChainId, assetSlug }) => return ( <>
-
- -
+
(props => ( )); -export const TezosTokenIcon = memo(({ className, style, ...props }) => ( -
- -
-)); - -const TezosAssetIconPlaceholder: TezosAssetImageProps['Fallback'] = memo(({ metadata, className, style }) => +const TezosAssetIconPlaceholder: TezosAssetImageProps['Fallback'] = memo(({ metadata, size, className, style }) => metadata && isTezosCollectibleMetadata(metadata) ? ( - + ) : ( - + ) ); @@ -40,7 +40,7 @@ export const TezosTokenIconWithNetwork = memo(({ tezosChai className={clsx('flex items-center justify-center relative', className)} style={{ width: ICON_DEFAULT_SIZE, height: ICON_DEFAULT_SIZE, ...style }} > - + {network && ( (props => ( )); -export const EvmTokenIcon = memo(({ className, style, ...props }) => ( -
- -
-)); - -const EvmAssetIconPlaceholder: EvmAssetImageProps['Fallback'] = memo(({ metadata, className, style }) => +const EvmAssetIconPlaceholder: EvmAssetImageProps['Fallback'] = memo(({ metadata, size, className, style }) => metadata && isEvmCollectibleMetadata(metadata) ? ( - + ) : ( - + ) ); @@ -80,7 +80,7 @@ export const EvmTokenIconWithNetwork = memo(({ evmChainId, c className={clsx('flex items-center justify-center relative', className)} style={{ width: ICON_DEFAULT_SIZE, height: ICON_DEFAULT_SIZE, ...style }} > - + {network && ( { +interface AssetImageStackedPropsBase extends Omit { + extraSrc?: string; +} + +export interface TezosAssetImageStackedProps extends AssetImageStackedPropsBase { metadata?: AssetMetadataBase; fullViewCollectible?: boolean; - extraSrc?: string; } export const TezosAssetImageStacked: FC = ({ @@ -34,13 +37,12 @@ export const TezosAssetImageStacked: FC = ({ return sources; }, [metadata, fullViewCollectible, extraSrc]); - return ; + return ; }; -export interface EvmAssetImageStackedProps extends Omit { +export interface EvmAssetImageStackedProps extends AssetImageStackedPropsBase { metadata?: EvmAssetMetadataBase; evmChainId: number; - extraSrc?: string; } export const EvmAssetImageStacked: FC = ({ evmChainId, metadata, extraSrc, ...rest }) => { @@ -56,51 +58,5 @@ export const EvmAssetImageStacked: FC = ({ evmChainId return sources; }, [evmChainId, metadata, extraSrc]); - return ; -}; - -interface AssetImageStackedProps - extends Pick< - ImageStackedProps, - 'loader' | 'fallback' | 'className' | 'style' | 'onStackLoaded' | 'onStackFailed' | 'alt' - > { - sources: string[]; - size?: number; -} - -const AssetImageStacked: FC = ({ - sources, - className, - size, - style, - alt, - loader, - fallback, - onStackLoaded, - onStackFailed -}) => { - const styleMemo: React.CSSProperties = useMemo( - () => ({ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', - ...style - }), - [style] - ); - - return ( - - ); + return ; }; diff --git a/src/app/templates/AssetImage/index.tsx b/src/app/templates/AssetImage/index.tsx index 15dbd55cc4..b171b7d6f9 100644 --- a/src/app/templates/AssetImage/index.tsx +++ b/src/app/templates/AssetImage/index.tsx @@ -11,7 +11,7 @@ import { } from './AssetImageStacked'; export type { TezosAssetImageStackedProps, EvmAssetImageStackedProps }; -export { TezosAssetImageStacked }; +export { TezosAssetImageStacked, EvmAssetImageStacked }; export interface TezosAssetImageProps extends Omit { diff --git a/src/app/templates/SendForm/AssetSelect.tsx b/src/app/templates/SendForm/AssetSelect.tsx index e04f6a6e12..db50363627 100644 --- a/src/app/templates/SendForm/AssetSelect.tsx +++ b/src/app/templates/SendForm/AssetSelect.tsx @@ -4,7 +4,7 @@ import classNames from 'clsx'; import { useDebounce } from 'use-debounce'; import Money from 'app/atoms/Money'; -import { TezosTokenIcon } from 'app/templates/AssetIcon'; +import { TezosAssetIcon } from 'app/templates/AssetIcon'; import { TezosBalance } from 'app/templates/Balance'; import InFiat from 'app/templates/InFiat'; import { setTestID, setAnotherSelector, TestIDProperty } from 'lib/analytics'; @@ -113,7 +113,12 @@ const AssetFieldContent = memo(({ network, slug, publicK return (
- + {balance => ( @@ -154,7 +159,12 @@ const AssetOptionContent = memo(({ network, accountPkh, {...setTestID(SendFormSelectors.assetDropDownItem)} {...setAnotherSelector('slug', slug)} > - +
diff --git a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx index 8e71f2b6eb..5e24dc1996 100644 --- a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetOption.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import classNames from 'clsx'; -import { TezosTokenIcon } from 'app/templates/AssetIcon'; +import { TezosAssetIcon } from 'app/templates/AssetIcon'; import { AssetItemContent } from 'app/templates/AssetItemContent'; import { setAnotherSelector, setTestID } from 'lib/analytics'; import { useTezosAssetMetadata } from 'lib/metadata'; @@ -32,7 +32,7 @@ export const AssetOption: FC = ({ network, assetSlug, selected, accountPk {...setTestID(AssetsMenuSelectors.assetsMenuAssetItem)} {...setAnotherSelector('slug', assetSlug)} > - +
diff --git a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx index 9723c8217d..3d79a0852e 100644 --- a/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/SwapFormInput.tsx @@ -7,7 +7,7 @@ import classNames from 'clsx'; import AssetField from 'app/atoms/AssetField'; import Money from 'app/atoms/Money'; import { useTokensListingLogicForSwapInput } from 'app/hooks/use-tokens-listing-logic-for-swap-input'; -import { TezosTokenIcon } from 'app/templates/AssetIcon'; +import { TezosAssetIcon } from 'app/templates/AssetIcon'; import { DropdownSelect } from 'app/templates/DropdownSelect/DropdownSelect'; import InFiat from 'app/templates/InFiat'; import { InputContainer } from 'app/templates/InputContainer/InputContainer'; @@ -221,7 +221,7 @@ const SwapDropdownFace: FC = ({ tezosChainId, testId, selectedAs
{selectedAssetSlug ? (
- + {selectedAssetMetadata.symbol} diff --git a/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx b/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx index eaf5080d9e..0f9b7194da 100644 --- a/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx +++ b/src/app/templates/SwapForm/SwapRoute/SwapRouteItem/hop-item.tsx @@ -4,7 +4,7 @@ import { isDefined } from '@rnw-community/shared'; import classNames from 'clsx'; import { useAppEnv } from 'app/env'; -import { TezosTokenIcon } from 'app/templates/AssetIcon'; +import { TezosAssetIcon } from 'app/templates/AssetIcon'; import { Route3Dex } from 'lib/apis/route3/fetch-route3-dexes'; import { Route3Token } from 'lib/apis/route3/fetch-route3-tokens'; import { toTokenSlug, TEZ_TOKEN_SLUG } from 'lib/assets'; @@ -50,11 +50,11 @@ export const HopItem: FC = ({ dex, aToken, bToken, className }) => {
- +
- +
diff --git a/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx b/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx index 1ed6a3db6c..cef1c6fb54 100644 --- a/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx +++ b/src/app/templates/SwapForm/SwapRoute/lb-pool-part.tsx @@ -53,7 +53,7 @@ export const LbPoolPart: FC = ({ amount, isLbOutput, totalChains }) => { style={advancedLbPoolItemStyles} >
- +
diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 31d55c5e9b..aef8d1147e 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -6,6 +6,7 @@ import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; +import { ReactComponent as OkSvg } from 'app/icons/base/ok.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; @@ -134,14 +135,14 @@ export const ActivityOperationBaseComponent = memo( ) : ( ), @@ -153,9 +154,11 @@ export const ActivityOperationBaseComponent = memo( className={clsx('z-1 group flex gap-x-2 p-2 rounded-lg hover:bg-secondary-low', onClick && 'cursor-pointer')} onClick={handleClick} > -
+
{kind === 'bundle' ? ( - {faceIconJsx} + + {faceIconJsx} + ) : (
{faceIconJsx}
)} @@ -193,37 +196,40 @@ export const ActivityOperationBaseComponent = memo( } ); -const MEDALION_CLASS_NAME = 'border border-lines bg-white'; +const MEDALION_CLASS_NAME = 'absolute border border-lines'; -const BundleIconsStack = memo>(({ isNFT, children }) => { - return ( - <> -
+const BundleIconsStack = memo>( + ({ withoutAssetIcon, isNFT, children }) => { + return ( + <> +
-
+
-
-
- {children} +
+
+ {children} +
-
- - ); -}); + + ); + } +); const ActivityKindTitle: Record = { bundle: 'Bundle', @@ -244,5 +250,5 @@ const ActivityKindIconSvg: Record = { [ActivityOperKindEnum.transferFrom]: DocumentsSvg, [ActivityOperKindEnum.transferTo]: DocumentsSvg, [ActivityOperKindEnum.swap]: SwapSvg, - [ActivityOperKindEnum.approve]: DocumentsSvg + [ActivityOperKindEnum.approve]: OkSvg }; diff --git a/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx b/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx index fa13f203d5..54ab248615 100644 --- a/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx +++ b/src/lib/swap-router/components/DexTypeIcon/DexTypeIcon.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { TezosTokenIcon } from 'app/templates/AssetIcon'; +import { TezosAssetIcon } from 'app/templates/AssetIcon'; import { Route3DexTypeEnum } from 'lib/apis/route3/fetch-route3-dexes'; import { ReactComponent as CtezIcon } from './icons/ctez-icon.svg'; @@ -48,6 +48,6 @@ export const DexTypeIcon: FC = ({ dexType }) => { return Dexter logo; default: - return ; + return ; } }; diff --git a/src/lib/ui/ImageStacked.tsx b/src/lib/ui/ImageStacked.tsx index 327a7c71df..a2462dd804 100644 --- a/src/lib/ui/ImageStacked.tsx +++ b/src/lib/ui/ImageStacked.tsx @@ -9,6 +9,7 @@ export interface ImageStackedProps extends React.ImgHTMLAttributes = ({ sources, + size, loader, fallback, style, @@ -36,8 +38,12 @@ export const ImageStacked: FC = ({ width: 0, height: 0 } - : style, - [style, isLoading] + : { + width: size, + height: size, + ...style + }, + [style, isLoading, size] ); const onStackLoadedRef = useRef(onStackLoaded); From 18e7f3da238ec06c412c882c18f975b510ef68a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Oct 2024 11:20:17 +0300 Subject: [PATCH 50/74] TW-1479: [EVM] Transactions history. Fix pipeline --- src/app/templates/AssetImage/index.tsx | 3 +-- src/app/templates/activity/utils.ts | 7 +------ src/lib/i18n/core.ts | 2 +- src/lib/i18n/index.ts | 2 +- src/lib/metadata/index.ts | 1 + 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/app/templates/AssetImage/index.tsx b/src/app/templates/AssetImage/index.tsx index b171b7d6f9..a4aa5f3318 100644 --- a/src/app/templates/AssetImage/index.tsx +++ b/src/app/templates/AssetImage/index.tsx @@ -10,8 +10,7 @@ import { EvmAssetImageStacked } from './AssetImageStacked'; -export type { TezosAssetImageStackedProps, EvmAssetImageStackedProps }; -export { TezosAssetImageStacked, EvmAssetImageStacked }; +export { TezosAssetImageStacked }; export interface TezosAssetImageProps extends Omit { diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index 22d5616805..4dcb0a2803 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -1,9 +1,4 @@ -import { ActivityOperKindEnum, Activity, EvmActivity } from 'lib/activity'; -import { TezosPreActivity } from 'lib/activity/tezos/types'; - -export function isEvmActivity(activity: Activity | TezosPreActivity): activity is EvmActivity { - return typeof activity.chainId === 'number'; -} +import { ActivityOperKindEnum, Activity } from 'lib/activity'; export type FaceKind = ActivityOperKindEnum | 'bundle'; diff --git a/src/lib/i18n/core.ts b/src/lib/i18n/core.ts index dd868ec336..4d1cf998c0 100644 --- a/src/lib/i18n/core.ts +++ b/src/lib/i18n/core.ts @@ -61,7 +61,7 @@ export function getMessage(messageName: string, substitutions?: Substitutions) { : browser.i18n.getMessage(messageName, substitutions) ?? ''; } -export function getDateFnsLocale() { +function getDateFnsLocale() { return dateFnsLocales[getCurrentLocale()] || enUS; } diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index ae18de2bb0..1485935e85 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -1,6 +1,6 @@ export type { TID } from './types'; -export { getMessage, getCurrentLocale, getDateFnsLocale, getNumberSymbols, formatDate } from './core'; +export { getMessage, getCurrentLocale, getNumberSymbols, formatDate } from './core'; export { updateLocale, onInited } from './loading'; diff --git a/src/lib/metadata/index.ts b/src/lib/metadata/index.ts index f51de337e1..09b5fca59f 100644 --- a/src/lib/metadata/index.ts +++ b/src/lib/metadata/index.ts @@ -79,6 +79,7 @@ export const useGetEvmChainAssetMetadata = (chainId: number) => { ); }; +// @ ts-prune-ignore-next export const useGetEvmAssetMetadata = () => { const allEvmChains = useAllEvmChains(); const tokensMetadatas = useEvmTokensMetadataRecordSelector(); From aefd6ae685a9a30222a995f112a507c7689a1fa6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Oct 2024 11:54:55 +0300 Subject: [PATCH 51/74] TW-1479: [EVM] Transactions history. Trigger pipeline --- src/app/templates/SettingsGeneral/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/templates/SettingsGeneral/index.tsx b/src/app/templates/SettingsGeneral/index.tsx index c0501d3181..79bc7cbe87 100644 --- a/src/app/templates/SettingsGeneral/index.tsx +++ b/src/app/templates/SettingsGeneral/index.tsx @@ -2,11 +2,11 @@ import React, { memo } from 'react'; import { NotificationsSettings } from 'lib/notifications/components'; -import AnalyticsSettings from './components/AnalyticsSettings'; -import FiatCurrencySelect from './components/FiatCurrencySelect'; -import LocaleSelect from './components/LocaleSelect'; -import LockUpSettings from './components/LockUpSettings'; -import PopupSettings from './components/PopupSettings'; +import AnalyticsSettings from './Components/AnalyticsSettings'; +import FiatCurrencySelect from './Components/FiatCurrencySelect'; +import LocaleSelect from './Components/LocaleSelect'; +import LockUpSettings from './Components/LockUpSettings'; +import PopupSettings from './Components/PopupSettings'; const GeneralSettings = memo(() => (
From e94a76ec55cd6960e2528eee8f250ca33db077f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Oct 2024 11:59:50 +0300 Subject: [PATCH 52/74] TW-1479: [EVM] Transactions history. Trigger pipeline --- src/app/templates/SettingsGeneral/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/templates/SettingsGeneral/index.tsx b/src/app/templates/SettingsGeneral/index.tsx index 79bc7cbe87..c0501d3181 100644 --- a/src/app/templates/SettingsGeneral/index.tsx +++ b/src/app/templates/SettingsGeneral/index.tsx @@ -2,11 +2,11 @@ import React, { memo } from 'react'; import { NotificationsSettings } from 'lib/notifications/components'; -import AnalyticsSettings from './Components/AnalyticsSettings'; -import FiatCurrencySelect from './Components/FiatCurrencySelect'; -import LocaleSelect from './Components/LocaleSelect'; -import LockUpSettings from './Components/LockUpSettings'; -import PopupSettings from './Components/PopupSettings'; +import AnalyticsSettings from './components/AnalyticsSettings'; +import FiatCurrencySelect from './components/FiatCurrencySelect'; +import LocaleSelect from './components/LocaleSelect'; +import LockUpSettings from './components/LockUpSettings'; +import PopupSettings from './components/PopupSettings'; const GeneralSettings = memo(() => (
From d8480418ca629ae9a74aadc1eb942ae0f1ca520b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 11 Oct 2024 03:26:42 +0300 Subject: [PATCH 53/74] TW-1479: [EVM] Transactions history. Minor design fixes --- .../pages/Activity/FilterChainDropdown.tsx | 52 ++++++++++--------- .../ActivityItem/ActivityOperationBase.tsx | 11 +++- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/app/pages/Activity/FilterChainDropdown.tsx b/src/app/pages/Activity/FilterChainDropdown.tsx index f89a79dc2f..4a05c241a6 100644 --- a/src/app/pages/Activity/FilterChainDropdown.tsx +++ b/src/app/pages/Activity/FilterChainDropdown.tsx @@ -44,35 +44,37 @@ export const FilterChainDropdown = memo(({ filterChain, setFilterChain, o ); return ( - - + + - {!inSearch && ( - onChainClick(null)}> - - - +
+ {!inSearch && ( + onChainClick(null)}> + + + - - - )} + + + )} - {searchedNetworks.map(chain => ( - onChainClick(chain)} - > - {chain.nameI18nKey ? : chain.name} + {searchedNetworks.map(chain => ( + onChainClick(chain)} + > + {chain.nameI18nKey ? : chain.name} - {chain.kind === TempleChainKind.Tezos ? ( - - ) : ( - - )} - - ))} + {chain.kind === TempleChainKind.Tezos ? ( + + ) : ( + + )} + + ))} +
); }); diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index aef8d1147e..d3e6410b6d 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -116,7 +116,8 @@ export const ActivityOperationBaseComponent = memo( event => { if (!onClick) return; - if (event.target instanceof HTMLAnchorElement) return; + // Case of click on link inside this component's element + if (event.target instanceof Element && event.target.closest(`.${CLICK_DETECTION_ATTR} a`)) return; onClick(); }, @@ -151,7 +152,11 @@ export const ActivityOperationBaseComponent = memo( return (
@@ -231,6 +236,8 @@ const BundleIconsStack = memo = { bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', From 40fe4c9077bfc121f62019e49afe56e62ea6821e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 11 Oct 2024 04:56:55 +0300 Subject: [PATCH 54/74] TW-1479: [EVM] Transactions history. + Activity status tag elem --- .../ActivityItem/ActivityOperationBase.tsx | 30 +++++++++++++++++-- .../activity/ActivityItem/EvmActivity.tsx | 6 ++-- .../ActivityItem/EvmActivityOperation.tsx | 5 +++- .../activity/ActivityItem/TezosActivity.tsx | 6 ++-- .../ActivityItem/TezosActivityOperation.tsx | 5 +++- .../activity/ActivityItem/pending-spin.svg | 10 +++++++ src/lib/activity/evm/parse/gas.ts | 2 +- src/lib/activity/evm/parse/gr-v2.ts | 5 ++-- src/lib/activity/evm/parse/gr-v3.ts | 5 ++-- src/lib/activity/tezos/index.ts | 12 ++++++-- src/lib/activity/types.ts | 7 +++++ 11 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 src/app/templates/activity/ActivityItem/pending-spin.svg diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index d3e6410b6d..2407507a42 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,4 +1,4 @@ -import React, { memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; +import React, { FC, memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import clsx from 'clsx'; @@ -13,18 +13,22 @@ import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { InFiat } from 'app/templates/InFiat'; import { ActivityOperKindEnum } from 'lib/activity'; +import { ActivityStatus } from 'lib/activity/types'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; import { FaceKind } from '../utils'; +import { ReactComponent as PendingSpinSvg } from './pending-spin.svg'; + interface Props { chainId: string | number; kind: FaceKind; hash: string; asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; + status?: ActivityStatus; withoutAssetIcon?: boolean; onClick?: EmptyFn; } @@ -41,7 +45,7 @@ export interface ActivityItemBaseAssetProp { } export const ActivityOperationBaseComponent = memo( - ({ kind, hash, chainId, asset, blockExplorerUrl, withoutAssetIcon, onClick }) => { + ({ kind, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick }) => { const assetSlug = asset ? typeof chainId === 'number' ? toEvmAssetSlug(asset.contract, asset.tokenId) @@ -177,7 +181,11 @@ export const ActivityOperationBaseComponent = memo(
-
{ActivityKindTitle[kind]}
+
+ {ActivityKindTitle[kind]} + + +
{amountJsx}
@@ -238,6 +246,22 @@ const BundleIconsStack = memo = ({ status }) => { + if (status === ActivityStatus.failed) return StatusTagFailed; + + if (status === ActivityStatus.pending) return StatusTagPending; + + return null; +}; + +const StatusTagFailed = ( +
+ FAILED +
+); + +const StatusTagPending = ; + const ActivityKindTitle: Record = { bundle: 'Bundle', [ActivityOperKindEnum.interaction]: 'Interaction', diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 3426038d5b..ca1bd866fc 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -22,7 +22,7 @@ interface Props { } export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) => { - const { hash, operations, operationsCount, blockExplorerUrl } = activity; + const { hash, operations, operationsCount, blockExplorerUrl, status } = activity; if (operationsCount === 1) { const operation = operations.at(0); @@ -33,6 +33,7 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) operation={operation} chainId={chain.chainId} blockExplorerUrl={blockExplorerUrl} + status={status} withoutAssetIcon={Boolean(assetSlug)} /> ); @@ -58,7 +59,7 @@ interface BatchProps { const EvmActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { const [expanded, , , toggleExpanded] = useBooleanState(false); - const { hash, operations } = activity; + const { hash, operations, status } = activity; const getMetadata = useGetEvmChainAssetMetadata(chainId); @@ -123,6 +124,7 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl chainId={chainId} asset={batchAsset} blockExplorerUrl={blockExplorerUrl} + status={status} withoutAssetIcon={Boolean(assetSlug)} onClick={toggleExpanded} /> diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 504381a080..ece7a8b200 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -1,6 +1,7 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, type EvmOperation } from 'lib/activity'; +import { ActivityStatus } from 'lib/activity/types'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; @@ -11,11 +12,12 @@ interface Props { operation?: EvmOperation; chainId: number; blockExplorerUrl?: string; + status?: ActivityStatus; withoutAssetIcon?: boolean; } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon }) => { const assetBase = operation?.asset; const assetSlug = assetBase?.contract ? toEvmAssetSlug(assetBase.contract) : undefined; @@ -46,6 +48,7 @@ export const EvmActivityOperationComponent = memo( chainId={chainId} asset={asset} blockExplorerUrl={blockExplorerUrl} + status={status} withoutAssetIcon={withoutAssetIcon} /> ); diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 489222979d..778dac38cc 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -21,7 +21,7 @@ interface Props { } export const TezosActivityComponent = memo(({ activity, chain, assetSlug }) => { - const { hash, operations, operationsCount } = activity; + const { hash, operations, status, operationsCount } = activity; const blockExplorerUrl = useExplorerHref(chain.chainId, hash) ?? undefined; @@ -34,6 +34,7 @@ export const TezosActivityComponent = memo(({ activity, chain, assetSlug operation={operation} chainId={chain.chainId} blockExplorerUrl={blockExplorerUrl} + status={status} withoutAssetIcon={Boolean(assetSlug)} /> ); @@ -59,7 +60,7 @@ interface BatchProps { const TezosActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { const [expanded, , , toggleExpanded] = useBooleanState(false); - const { hash, operations } = activity; + const { hash, operations, status } = activity; const getMetadata = useGetChainTokenOrGasMetadata(chainId); @@ -94,6 +95,7 @@ const TezosActivityBatchComponent = memo(({ activity, chainId, asset hash={hash} chainId={chainId} asset={batchAsset} + status={status} blockExplorerUrl={blockExplorerUrl} withoutAssetIcon={Boolean(assetSlug)} onClick={toggleExpanded} diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 36dce995a2..4076e42673 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -1,6 +1,7 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; +import { ActivityStatus } from 'lib/activity/types'; import { fromAssetSlug } from 'lib/assets'; import { AssetMetadataBase, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; @@ -10,12 +11,13 @@ interface Props { hash: string; operation?: TezosOperation; chainId: string; + status?: ActivityStatus; blockExplorerUrl: string | nullish; withoutAssetIcon?: boolean; } export const TezosActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon }) => { const assetSlug = operation?.assetSlug; const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); @@ -30,6 +32,7 @@ export const TezosActivityOperationComponent = memo( hash={hash} chainId={chainId} asset={asset} + status={status} blockExplorerUrl={blockExplorerUrl ?? undefined} withoutAssetIcon={withoutAssetIcon} /> diff --git a/src/app/templates/activity/ActivityItem/pending-spin.svg b/src/app/templates/activity/ActivityItem/pending-spin.svg new file mode 100644 index 0000000000..07b3be96da --- /dev/null +++ b/src/app/templates/activity/ActivityItem/pending-spin.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index d497387cbd..740531ee42 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -13,7 +13,7 @@ export function parseGasTransfer( ): EvmOperation | null { const value: string = item.value?.toString() ?? '0'; - if (value === '0') return null; + if (value === '0' && partOfBatch) return null; const kind = (() => { if (getEvmAddressSafe(item.from_address) === accountAddress) diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index 5ade1b4503..740eb8d0e3 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -3,7 +3,7 @@ import type { BlockTransactionWithContractTransfers, TokenTransferItem } from '@ import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { ActivityOperKindEnum, ActivityStatus, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; import { parseGasTransfer } from './gas'; @@ -28,7 +28,8 @@ export function parseGoldRushERC20Transfer( blockExplorerUrl: item.transfers?.[0].explorers?.[0]?.url, operations, operationsCount: gasOperation ? transfers.length + 1 : transfers.length, - addedAt + addedAt, + status: item.successful ? ActivityStatus.applied : ActivityStatus.failed }; } diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index c750d94748..309878649c 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -4,7 +4,7 @@ import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { ActivityOperKindEnum, ActivityStatus, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; import { parseGasTransfer } from './gas'; @@ -27,7 +27,8 @@ export function parseGoldRushTransaction(item: Transaction, chainId: number, acc blockExplorerUrl: item.explorers?.at(0)?.url, operations, operationsCount: gasOperation ? logEvents.length + 1 : logEvents.length, - addedAt + addedAt, + status: item.successful ? ActivityStatus.applied : ActivityStatus.failed }; } diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 812adc344d..900694e3d3 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -3,7 +3,7 @@ import { toTezosAssetSlug } from 'lib/assets/utils'; import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; -import { TezosActivity, ActivityOperKindEnum, TezosOperation } from '../types'; +import { TezosActivity, ActivityOperKindEnum, TezosOperation, ActivityStatus } from '../types'; import { isTransferActivityOperKind } from '../utils'; import { preparseTezosOperationsGroup } from './pre-parse'; @@ -15,7 +15,7 @@ export function parseTezosOperationsGroup( ): TezosActivity { const preActivity = preparseTezosOperationsGroup(operationsGroup, address, chainId); - const { hash, addedAt, operations: preOperations, oldestTzktOperation } = preActivity; + const { hash, addedAt, operations: preOperations, oldestTzktOperation, status } = preActivity; const operations = preOperations.map(oper => parseTezosPreActivityOperation(oper, address)); @@ -26,7 +26,13 @@ export function parseTezosOperationsGroup( operations, operationsCount: preOperations.length, addedAt, - oldestTzktOperation + oldestTzktOperation, + status: + status === 'applied' + ? ActivityStatus.applied + : status === 'pending' + ? ActivityStatus.pending + : ActivityStatus.failed }; } diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 9968192d72..144534b8f6 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -21,6 +21,13 @@ interface ChainActivityBase { operationsCount: number; /** ISO string */ addedAt: string; + status: ActivityStatus; +} + +export enum ActivityStatus { + applied, + pending, + failed } interface OperationBase { From a4944043d97e14f46110d747ac801ee8ebb8303c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Oct 2024 19:01:00 +0300 Subject: [PATCH 55/74] TW-1479: [EVM] Transactions history. + Address chips in Bundle modal --- src/app/layouts/PageLayout/utils.ts | 0 .../ActivityItem/ActivityOperationBase.tsx | 33 ++++++--- .../activity/ActivityItem/AddressChip.tsx | 51 ++++++++++++++ .../activity/ActivityItem/EvmActivity.tsx | 1 + .../ActivityItem/EvmActivityOperation.tsx | 10 ++- .../activity/ActivityItem/TezosActivity.tsx | 1 + .../ActivityItem/TezosActivityOperation.tsx | 10 ++- src/lib/activity/evm/parse/gas.ts | 9 ++- src/lib/activity/evm/parse/gr-v2.ts | 34 +++++++--- src/lib/activity/evm/parse/gr-v3.ts | 64 ++++++++++-------- src/lib/activity/tezos/index.ts | 30 ++++++--- src/lib/activity/types.ts | 67 +++++++++++++++++-- 12 files changed, 242 insertions(+), 68 deletions(-) delete mode 100644 src/app/layouts/PageLayout/utils.ts create mode 100644 src/app/templates/activity/ActivityItem/AddressChip.tsx diff --git a/src/app/layouts/PageLayout/utils.ts b/src/app/layouts/PageLayout/utils.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 2407507a42..2b992cdadd 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo, MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; +import React, { FC, memo, MouseEventHandler, ReactElement, ReactNode, useCallback, useMemo } from 'react'; import clsx from 'clsx'; @@ -31,6 +31,7 @@ interface Props { status?: ActivityStatus; withoutAssetIcon?: boolean; onClick?: EmptyFn; + addressChip?: ReactElement | null; } export interface ActivityItemBaseAssetProp { @@ -45,7 +46,7 @@ export interface ActivityItemBaseAssetProp { } export const ActivityOperationBaseComponent = memo( - ({ kind, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick }) => { + ({ kind, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick, addressChip }) => { const assetSlug = asset ? typeof chainId === 'number' ? toEvmAssetSlug(asset.contract, asset.tokenId) @@ -116,6 +117,24 @@ export const ActivityOperationBaseComponent = memo( ); }, [asset, kind, assetSlug, chainId]); + const chipJsx = useMemo( + () => + addressChip ?? ( + + + + + + + + ), + [addressChip, hash, blockExplorerUrl] + ); + const handleClick = useCallback>( event => { if (!onClick) return; @@ -191,15 +210,7 @@ export const ActivityOperationBaseComponent = memo(
- - - - - + {chipJsx}
{fiatJsx}
diff --git a/src/app/templates/activity/ActivityItem/AddressChip.tsx b/src/app/templates/activity/ActivityItem/AddressChip.tsx new file mode 100644 index 0000000000..c6e7e01e72 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/AddressChip.tsx @@ -0,0 +1,51 @@ +import React, { FC, useMemo } from 'react'; + +import { HashShortView, IconBase } from 'app/atoms'; +import { CopyButton } from 'app/atoms/CopyButton'; +import { ReactComponent as CopySvg } from 'app/icons/base/copy.svg'; +import { ActivityOperKindEnum, EvmOperation, TezosOperation } from 'lib/activity'; + +interface Props { + operation: TezosOperation | EvmOperation; +} + +export const OperAddressChip: FC = ({ operation }) => { + const info = useMemo(() => { + if (operation.kind === ActivityOperKindEnum.approve) return { title: 'For', address: operation.spenderAddress }; + + if (operation.kind === ActivityOperKindEnum.transferFrom_ToAccount) + return { title: 'To', address: operation.toAddress }; + + if (operation.kind === ActivityOperKindEnum.transferTo_FromAccount) + return { title: 'From', address: operation.fromAddress }; + + if (operation.kind === ActivityOperKindEnum.transferFrom) return { title: 'With', address: operation.toAddress }; + + if (operation.kind === ActivityOperKindEnum.transferTo) return { title: 'With', address: operation.fromAddress }; + + if (operation.kind === ActivityOperKindEnum.interaction && operation.withAddress) + return { title: 'With', address: operation.withAddress }; + + return; + }, [operation]); + + if (!info) return null; + + return ( +
+ {info.title}: + + + + + + + + +
+ ); +}; diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index ca1bd866fc..9d1b338749 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -141,6 +141,7 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl operation={operation} chainId={chainId} blockExplorerUrl={blockExplorerUrl} + withoutOperHashChip /> ))} diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index ece7a8b200..26076f7281 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -6,6 +6,7 @@ import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { OperAddressChip } from './AddressChip'; interface Props { hash: string; @@ -14,10 +15,11 @@ interface Props { blockExplorerUrl?: string; status?: ActivityStatus; withoutAssetIcon?: boolean; + withoutOperHashChip?: boolean; } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { const assetBase = operation?.asset; const assetSlug = assetBase?.contract ? toEvmAssetSlug(assetBase.contract) : undefined; @@ -41,6 +43,11 @@ export const EvmActivityOperationComponent = memo( return asset; }, [assetMetadata, assetBase]); + const addressChip = useMemo( + () => (withoutOperHashChip && operation ? : null), + [operation, withoutOperHashChip] + ); + return ( ( blockExplorerUrl={blockExplorerUrl} status={status} withoutAssetIcon={withoutAssetIcon} + addressChip={addressChip} /> ); } diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 778dac38cc..4bcbb55f93 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -113,6 +113,7 @@ const TezosActivityBatchComponent = memo(({ activity, chainId, asset operation={operation} chainId={chainId} blockExplorerUrl={blockExplorerUrl} + withoutOperHashChip /> ))} diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 4076e42673..24fd777324 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -6,6 +6,7 @@ import { fromAssetSlug } from 'lib/assets'; import { AssetMetadataBase, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; +import { OperAddressChip } from './AddressChip'; interface Props { hash: string; @@ -14,10 +15,11 @@ interface Props { status?: ActivityStatus; blockExplorerUrl: string | nullish; withoutAssetIcon?: boolean; + withoutOperHashChip?: boolean; } export const TezosActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon }) => { + ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { const assetSlug = operation?.assetSlug; const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); @@ -26,6 +28,11 @@ export const TezosActivityOperationComponent = memo( [assetMetadata, operation, assetSlug] ); + const addressChip = useMemo( + () => (withoutOperHashChip && operation ? : null), + [operation, withoutOperHashChip] + ); + return ( ( status={status} blockExplorerUrl={blockExplorerUrl ?? undefined} withoutAssetIcon={withoutAssetIcon} + addressChip={addressChip} /> ); } diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index 740531ee42..907bc53a81 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -15,10 +15,13 @@ export function parseGasTransfer( if (value === '0' && partOfBatch) return null; + const fromAddress = getEvmAddressSafe(item.from_address)!; + const toAddress = getEvmAddressSafe(item.to_address)!; + const kind = (() => { - if (getEvmAddressSafe(item.from_address) === accountAddress) + if (fromAddress === accountAddress) return partOfBatch ? ActivityOperKindEnum.transferFrom : ActivityOperKindEnum.transferFrom_ToAccount; - if (getEvmAddressSafe(item.to_address) === accountAddress) + if (toAddress === accountAddress) return partOfBatch ? ActivityOperKindEnum.transferTo : ActivityOperKindEnum.transferTo_FromAccount; return null; @@ -39,5 +42,5 @@ export function parseGasTransfer( symbol }; - return { kind, asset }; + return { kind, fromAddress, toAddress, asset }; } diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index 740eb8d0e3..67a5566d59 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -3,7 +3,14 @@ import type { BlockTransactionWithContractTransfers, TokenTransferItem } from '@ import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, ActivityStatus, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { + ActivityOperKindEnum, + ActivityOperTransferKinds, + ActivityStatus, + EvmActivity, + EvmActivityAsset, + EvmOperation +} from '../../types'; import { parseGasTransfer } from './gas'; @@ -34,21 +41,26 @@ export function parseGoldRushERC20Transfer( } function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithContractTransfers): EvmOperation { - const kind = (() => { + const fromAddress = getEvmAddressSafe(transfer.from_address)!; + const toAddress = getEvmAddressSafe(transfer.to_address)!; + + const kind: ActivityOperTransferKinds = (() => { if (transfer.transfer_type === 'IN') { - return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; + if (item.to_address === transfer.contract_address) return ActivityOperKindEnum.transferTo_FromAccount; + + return ActivityOperKindEnum.transferTo; } - return item.to_address === transfer.contract_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; + if (item.to_address === transfer.contract_address) return ActivityOperKindEnum.transferFrom_ToAccount; + + return ActivityOperKindEnum.transferFrom; })(); + const operBase = { kind, fromAddress, toAddress }; + const contractAddress = getEvmAddressSafe(transfer.contract_address); - if (contractAddress == null) return { kind }; + if (contractAddress == null) return operBase; const decimals = transfer.contract_decimals ?? undefined; @@ -57,7 +69,7 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo const symbol = transfer.contract_ticker_symbol || undefined; const amountSigned = - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + operBase.kind === ActivityOperKindEnum.transferFrom || operBase.kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${amount}` : amount; @@ -70,5 +82,5 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo iconURL: transfer.logo_url ?? undefined }; - return { kind, asset }; + return { ...operBase, asset }; } diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index 309878649c..39bf5b00bc 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -1,6 +1,5 @@ import type { Transaction, LogEvent } from '@covalenthq/client-sdk'; -import { isTruthy } from 'lib/utils'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; @@ -12,9 +11,7 @@ export function parseGoldRushTransaction(item: Transaction, chainId: number, acc const logEvents = item.log_events ?? []; const addedAt = item.block_signed_at as unknown as string; - const operations = logEvents - .map(logEvent => parseLogEvent(logEvent, item, accountAddress)) - .filter(isTruthy); + const operations = logEvents.map(logEvent => parseLogEvent(logEvent, item, accountAddress)); const gasOperation = parseGasTransfer(item, accountAddress, Boolean(logEvents.length)); @@ -32,25 +29,27 @@ export function parseGoldRushTransaction(item: Transaction, chainId: number, acc }; } -function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: string): EvmOperation | null { - if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction }; +function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: string): EvmOperation { + const contractAddress = getEvmAddressSafe(logEvent.sender_address) ?? undefined; + + if (!logEvent.decoded?.params) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; - const contractAddress = getEvmAddressSafe(logEvent.sender_address); - const _fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value); - const _toAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value); const decimals = logEvent.sender_contract_decimals ?? undefined; const symbol = logEvent.sender_contract_ticker_symbol || undefined; const iconURL = logEvent.sender_logo_url ?? undefined; if (logEvent.decoded.name === 'Transfer') { + const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value)!; + const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value)!; + const kind = (() => { - if (_toAddress === accountAddress) { + if (toAddress === accountAddress) { return item.to_address === logEvent.sender_address ? ActivityOperKindEnum.transferTo_FromAccount : ActivityOperKindEnum.transferTo; } - if (_fromAddress === accountAddress) { + if (fromAddress === accountAddress) { return item.to_address === logEvent.sender_address ? ActivityOperKindEnum.transferFrom_ToAccount : ActivityOperKindEnum.transferFrom; @@ -59,7 +58,9 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st return null; })(); - if (kind == null || !contractAddress) return { kind: ActivityOperKindEnum.interaction }; + if (kind == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; + + if (!contractAddress) return { kind, fromAddress, toAddress }; const param3 = logEvent.decoded.params.at(2); const amountOrTokenId: string = param3?.value ?? '0'; @@ -83,12 +84,12 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, asset }; + return { kind, fromAddress, toAddress, asset }; } if (logEvent.decoded.name === 'TransferSingle') { - const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value); - const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(2)?.value); + const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value)!; + const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(2)?.value)!; const kind = (() => { if (toAddress === accountAddress) { @@ -106,7 +107,9 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st return null; })(); - if (kind == null || !contractAddress) return null; + if (kind == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; + + if (!contractAddress) return { kind, fromAddress, toAddress }; const tokenId = logEvent.decoded.params.at(3)?.value ?? '0'; @@ -127,15 +130,18 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, asset }; + return { kind, fromAddress, toAddress, asset }; } if (logEvent.decoded.name === 'Approval') { - if (_fromAddress !== accountAddress) return null; + const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value); + if (fromAddress !== accountAddress) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; const kind = ActivityOperKindEnum.approve; - if (!contractAddress) return { kind }; + const spenderAddress = logEvent.decoded.params.at(1)!.value; + + if (!contractAddress) return { kind, spenderAddress }; const amountOrTokenIdParam = logEvent.decoded.params.at(2); const amountOrTokenId: string = amountOrTokenIdParam?.value ?? '0'; @@ -155,20 +161,20 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, asset }; + return { kind, spenderAddress, asset }; } if (logEvent.decoded.name === 'ApprovalForAll') { - if ( - // @ts-expect-error // `.value` is not always `:string` - logEvent.decoded.params.at(2).value !== true || - _fromAddress !== accountAddress - ) - return null; + const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value); + + if ((logEvent.decoded.params.at(2)!.value as unknown as boolean) !== true || fromAddress !== accountAddress) + return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; const kind = ActivityOperKindEnum.approve; - if (!contractAddress) return { kind }; + const spenderAddress = logEvent.decoded.params.at(1)!.value; + + if (!contractAddress) return { kind, spenderAddress }; const asset: EvmActivityAsset = { contract: contractAddress, @@ -179,8 +185,8 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, asset }; + return { kind, spenderAddress, asset }; } - return { kind: ActivityOperKindEnum.interaction }; + return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; } diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 900694e3d3..0dd226b88c 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -43,45 +43,59 @@ function parseTezosPreActivityOperation(preOperation: TezosPreActivityOperation, if (preOperation.type === 'transaction') { tokenId = preOperation.tokenId; - if (preOperation.subtype === 'approve') return { kind: ActivityOperKindEnum.approve }; + if (preOperation.subtype === 'approve') + return { + kind: ActivityOperKindEnum.approve, + spenderAddress: preOperation.to.at(0)!.address + }; if (preOperation.subtype !== 'transfer' || isZero(preOperation.amountSigned)) return { - kind: ActivityOperKindEnum.interaction - // with: oper.destination.address, + kind: ActivityOperKindEnum.interaction, + withAddress: preOperation.destination.address // entrypoint: oper.entrypoint }; + const fromAddress = preOperation.from.address; + const toAddress = preOperation.to.at(0)!.address; + if (preOperation.from.address === address) return { kind: preOperation.to.length === 1 && !isTezosContractAddress(preOperation.to[0].address) ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom + : ActivityOperKindEnum.transferFrom, + fromAddress, + toAddress }; if (preOperation.to.some(member => member.address === address)) return { kind: isTezosContractAddress(preOperation.from.address) ? ActivityOperKindEnum.transferTo - : ActivityOperKindEnum.transferTo_FromAccount + : ActivityOperKindEnum.transferTo_FromAccount, + fromAddress, + toAddress }; return { - kind: ActivityOperKindEnum.interaction + kind: ActivityOperKindEnum.interaction, + withAddress: preOperation.destination.address }; } if (preOperation.type === 'delegation' && preOperation.sender.address === address && preOperation.destination) { return { - kind: ActivityOperKindEnum.interaction + kind: ActivityOperKindEnum.interaction, + withAddress: preOperation.destination.address // subkind: ActivitySubKindEnum.Delegation // to: oper.destination.address }; } return { - kind: ActivityOperKindEnum.interaction + kind: ActivityOperKindEnum.interaction, + withAddress: preOperation.destination?.address // subkind: OperStackItemTypeEnum.Other }; })(); diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index 144534b8f6..b908c91690 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -37,16 +37,44 @@ interface OperationBase { export interface TezosActivity extends ChainActivityBase, TezosActivityOlderThan { chain: TempleChainKind.Tezos; chainId: string; - blockExplorerUrl?: string; operations: TezosOperation[]; } -export interface TezosOperation extends OperationBase { +interface TezosOperationBase extends OperationBase { assetSlug?: string; /** `null` for 'unlimited' amount */ amountSigned?: string | null; } +interface TezosApproveOperation extends TezosOperationBase { + kind: ActivityOperKindEnum.approve; + spenderAddress: string; +} + +interface TezosTransferOperation extends TezosOperationBase { + kind: ActivityOperTransferKinds; + fromAddress: string; + toAddress: string; +} + +interface TezosInteractionOperation extends TezosOperationBase { + kind: ActivityOperKindEnum.interaction; + withAddress?: string; +} + +interface TezosOtherOperation extends TezosOperationBase { + kind: Exclude< + ActivityOperKindEnum, + ActivityOperKindEnum.approve | ActivityOperTransferKinds | ActivityOperKindEnum.interaction + >; +} + +export type TezosOperation = + | TezosApproveOperation + | TezosTransferOperation + | TezosInteractionOperation + | TezosOtherOperation; + export interface EvmActivity extends ChainActivityBase { chain: TempleChainKind.EVM; chainId: number; @@ -54,10 +82,41 @@ export interface EvmActivity extends ChainActivityBase { operations: EvmOperation[]; } -export interface EvmOperation extends OperationBase { - asset?: EvmActivityAsset; // TODO: Same as for Tezos +interface EvmOperationBase extends OperationBase { + asset?: EvmActivityAsset; } +interface EvmApproveOperation extends EvmOperationBase { + kind: ActivityOperKindEnum.approve; + spenderAddress: string; +} + +export type ActivityOperTransferKinds = + | ActivityOperKindEnum.transferFrom + | ActivityOperKindEnum.transferFrom_ToAccount + | ActivityOperKindEnum.transferTo + | ActivityOperKindEnum.transferTo_FromAccount; + +interface EvmTransferOperation extends EvmOperationBase { + kind: ActivityOperTransferKinds; + toAddress: string; + fromAddress: string; +} + +interface EvmInteractionOperation extends EvmOperationBase { + kind: ActivityOperKindEnum.interaction; + withAddress?: string; +} + +interface EvmOtherOperation extends EvmOperationBase { + kind: Exclude< + ActivityOperKindEnum, + ActivityOperKindEnum.approve | ActivityOperTransferKinds | ActivityOperKindEnum.interaction + >; +} + +export type EvmOperation = EvmApproveOperation | EvmTransferOperation | EvmInteractionOperation | EvmOtherOperation; + export interface EvmActivityAsset { contract: string; tokenId?: string; From df056be8a49190b01c0ec46b371e83fe0c29f831 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Oct 2024 19:54:59 +0300 Subject: [PATCH 56/74] TW-1479: [EVM] Transactions history. Refactor. + Transfer types --- .../ActivityItem/ActivityOperationBase.tsx | 60 +++++++++++-------- .../activity/ActivityItem/AddressChip.tsx | 15 ++--- .../ActivityItem/EvmActivityOperation.tsx | 3 + .../ActivityItem/TezosActivityOperation.tsx | 3 + src/app/templates/activity/utils.ts | 43 +++++++------ src/lib/activity/evm/parse/gas.ts | 17 +++--- src/lib/activity/evm/parse/gr-v2.ts | 16 ++--- src/lib/activity/evm/parse/gr-v3.ts | 49 +++++++++------ src/lib/activity/index.ts | 2 +- src/lib/activity/tezos/index.ts | 22 ++++--- src/lib/activity/types.ts | 47 +++++---------- src/lib/activity/utils.ts | 7 +-- 12 files changed, 155 insertions(+), 129 deletions(-) diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 2b992cdadd..8359d1dfe0 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -9,11 +9,10 @@ import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OkSvg } from 'app/icons/base/ok.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; -import { ReactComponent as SwapSvg } from 'app/icons/base/swap.svg'; import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { InFiat } from 'app/templates/InFiat'; import { ActivityOperKindEnum } from 'lib/activity'; -import { ActivityStatus } from 'lib/activity/types'; +import { ActivityOperTransferType, ActivityStatus } from 'lib/activity/types'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -25,6 +24,7 @@ import { ReactComponent as PendingSpinSvg } from './pending-spin.svg'; interface Props { chainId: string | number; kind: FaceKind; + transferType?: ActivityOperTransferType; hash: string; asset?: ActivityItemBaseAssetProp; blockExplorerUrl?: string; @@ -46,7 +46,7 @@ export interface ActivityItemBaseAssetProp { } export const ActivityOperationBaseComponent = memo( - ({ kind, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick, addressChip }) => { + ({ kind, transferType, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick, addressChip }) => { const assetSlug = asset ? typeof chainId === 'number' ? toEvmAssetSlug(asset.contract, asset.tokenId) @@ -153,7 +153,7 @@ export const ActivityOperationBaseComponent = memo( () => withoutAssetIcon || !assetSlug ? (
- +
) : typeof chainId === 'number' ? ( ( extraSrc={asset?.iconURL} /> ), - [chainId, withoutAssetIcon, kind, asset?.iconURL, assetSlug] + [chainId, withoutAssetIcon, kind, transferType, asset?.iconURL, assetSlug] ); return ( @@ -201,7 +201,7 @@ export const ActivityOperationBaseComponent = memo(
- {ActivityKindTitle[kind]} + {getTitleByKind(kind, transferType)}
@@ -273,24 +273,36 @@ const StatusTagFailed = ( const StatusTagPending = ; -const ActivityKindTitle: Record = { - bundle: 'Bundle', - [ActivityOperKindEnum.interaction]: 'Interaction', - [ActivityOperKindEnum.transferFrom_ToAccount]: 'Send', - [ActivityOperKindEnum.transferTo_FromAccount]: 'Receive', - [ActivityOperKindEnum.transferFrom]: 'Transfer', - [ActivityOperKindEnum.transferTo]: 'Transfer', - [ActivityOperKindEnum.swap]: 'Swap', - [ActivityOperKindEnum.approve]: 'Approve' +function getTitleByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { + if (kind === 'bundle') return 'Bundle'; + + if (kind === ActivityOperKindEnum.interaction) return 'Interaction'; + + if (kind === ActivityOperKindEnum.approve) return 'Approve'; + + return transferType == null ? 'Interaction' : TransferTypeTitle[transferType]; +} + +const TransferTypeTitle: Record = { + [ActivityOperTransferType.fromUsToAccount]: 'Send', + [ActivityOperTransferType.toUsFromAccount]: 'Receive', + [ActivityOperTransferType.fromUs]: 'Transfer', + [ActivityOperTransferType.toUs]: 'Transfer' }; -const ActivityKindIconSvg: Record = { - bundle: DocumentsSvg, - [ActivityOperKindEnum.interaction]: DocumentsSvg, - [ActivityOperKindEnum.transferFrom_ToAccount]: SendSvg, - [ActivityOperKindEnum.transferTo_FromAccount]: IncomeSvg, - [ActivityOperKindEnum.transferFrom]: DocumentsSvg, - [ActivityOperKindEnum.transferTo]: DocumentsSvg, - [ActivityOperKindEnum.swap]: SwapSvg, - [ActivityOperKindEnum.approve]: OkSvg +function getIconByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { + if (kind === 'bundle') return DocumentsSvg; + + if (kind === ActivityOperKindEnum.interaction) return DocumentsSvg; + + if (kind === ActivityOperKindEnum.approve) return OkSvg; + + return transferType == null ? DocumentsSvg : TransferTypeIconSvg[transferType]; +} + +const TransferTypeIconSvg: Record = { + [ActivityOperTransferType.fromUsToAccount]: SendSvg, + [ActivityOperTransferType.toUsFromAccount]: IncomeSvg, + [ActivityOperTransferType.fromUs]: DocumentsSvg, + [ActivityOperTransferType.toUs]: DocumentsSvg }; diff --git a/src/app/templates/activity/ActivityItem/AddressChip.tsx b/src/app/templates/activity/ActivityItem/AddressChip.tsx index c6e7e01e72..c2ea3099c8 100644 --- a/src/app/templates/activity/ActivityItem/AddressChip.tsx +++ b/src/app/templates/activity/ActivityItem/AddressChip.tsx @@ -4,6 +4,7 @@ import { HashShortView, IconBase } from 'app/atoms'; import { CopyButton } from 'app/atoms/CopyButton'; import { ReactComponent as CopySvg } from 'app/icons/base/copy.svg'; import { ActivityOperKindEnum, EvmOperation, TezosOperation } from 'lib/activity'; +import { ActivityOperTransferType } from 'lib/activity/types'; interface Props { operation: TezosOperation | EvmOperation; @@ -13,18 +14,18 @@ export const OperAddressChip: FC = ({ operation }) => { const info = useMemo(() => { if (operation.kind === ActivityOperKindEnum.approve) return { title: 'For', address: operation.spenderAddress }; - if (operation.kind === ActivityOperKindEnum.transferFrom_ToAccount) + if (operation.kind === ActivityOperKindEnum.interaction) + return operation.withAddress ? { title: 'With', address: operation.withAddress } : undefined; + + if (operation.type === ActivityOperTransferType.fromUsToAccount) return { title: 'To', address: operation.toAddress }; - if (operation.kind === ActivityOperKindEnum.transferTo_FromAccount) + if (operation.type === ActivityOperTransferType.toUsFromAccount) return { title: 'From', address: operation.fromAddress }; - if (operation.kind === ActivityOperKindEnum.transferFrom) return { title: 'With', address: operation.toAddress }; - - if (operation.kind === ActivityOperKindEnum.transferTo) return { title: 'With', address: operation.fromAddress }; + if (operation.type === ActivityOperTransferType.fromUs) return { title: 'With', address: operation.toAddress }; - if (operation.kind === ActivityOperKindEnum.interaction && operation.withAddress) - return { title: 'With', address: operation.withAddress }; + if (operation.type === ActivityOperTransferType.toUs) return { title: 'With', address: operation.fromAddress }; return; }, [operation]); diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 26076f7281..39d3de29c4 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -5,6 +5,8 @@ import { ActivityStatus } from 'lib/activity/types'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; +import { getActivityOperTransferType } from '../utils'; + import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; import { OperAddressChip } from './AddressChip'; @@ -51,6 +53,7 @@ export const EvmActivityOperationComponent = memo( return ( ( return ( = { - bundle: 'bundle', - [ActivityOperKindEnum.approve]: 'approve', - [ActivityOperKindEnum.transferFrom]: 'transfer', - [ActivityOperKindEnum.transferFrom_ToAccount]: 'send', - [ActivityOperKindEnum.transferTo]: 'transfer', - [ActivityOperKindEnum.transferTo_FromAccount]: 'receive', - // - [ActivityOperKindEnum.interaction]: null, - [ActivityOperKindEnum.swap]: null -}; +export function getActivityOperTransferType(operation?: TezosOperation | EvmOperation) { + if (operation?.kind !== ActivityOperKindEnum.transfer) return; + + return operation.type; +} export function getActivityFilterKind(activity: Activity): FilterKind { - const faceKind = getActivityFaceKind(activity); + const { operations, operationsCount } = activity; - return KINDS_MAP[faceKind]; -} + if (operationsCount !== 1) return 'bundle'; + + const operation = operations.at(0); + + if (!operation) return null; + + const kind = operation.kind; + + if (kind === ActivityOperKindEnum.interaction) return null; + if (kind === ActivityOperKindEnum.approve) return 'approve'; + + const type = operation.type; + + if (type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.toUs) return 'transfer'; + + if (type === ActivityOperTransferType.fromUsToAccount) return 'send'; + + if (type === ActivityOperTransferType.toUsFromAccount) return 'receive'; -function getActivityFaceKind({ operations, operationsCount }: Activity): FaceKind { - return operationsCount === 1 ? operations.at(0)?.kind ?? ActivityOperKindEnum.interaction : 'bundle'; + return null; } diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index 907bc53a81..ba9ca348c8 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -3,7 +3,7 @@ import type { Transaction, BlockTransactionWithContractTransfers } from '@covale import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; -import { ActivityOperKindEnum, EvmActivityAsset, EvmOperation } from '../../types'; +import { ActivityOperKindEnum, ActivityOperTransferType, EvmActivityAsset, EvmOperation } from '../../types'; export function parseGasTransfer( item: Transaction | BlockTransactionWithContractTransfers, @@ -18,20 +18,23 @@ export function parseGasTransfer( const fromAddress = getEvmAddressSafe(item.from_address)!; const toAddress = getEvmAddressSafe(item.to_address)!; - const kind = (() => { + const type = (() => { if (fromAddress === accountAddress) - return partOfBatch ? ActivityOperKindEnum.transferFrom : ActivityOperKindEnum.transferFrom_ToAccount; + return partOfBatch ? ActivityOperTransferType.fromUs : ActivityOperTransferType.fromUsToAccount; if (toAddress === accountAddress) - return partOfBatch ? ActivityOperKindEnum.transferTo : ActivityOperKindEnum.transferTo_FromAccount; + return partOfBatch ? ActivityOperTransferType.toUs : ActivityOperTransferType.toUsFromAccount; return null; })(); - if (!kind) return null; + if (type == null) return null; + + const kind = ActivityOperKindEnum.transfer; const decimals = item.gas_metadata?.contract_decimals; - const amountSigned = kind === ActivityOperKindEnum.transferFrom_ToAccount ? `-${value}` : value; + const amountSigned = + type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount ? `-${value}` : value; const symbol = item.gas_metadata?.contract_ticker_symbol; @@ -42,5 +45,5 @@ export function parseGasTransfer( symbol }; - return { kind, fromAddress, toAddress, asset }; + return { kind, type, fromAddress, toAddress, asset }; } diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index 67a5566d59..7301c05656 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -5,7 +5,7 @@ import { TempleChainKind } from 'temple/types'; import { ActivityOperKindEnum, - ActivityOperTransferKinds, + ActivityOperTransferType, ActivityStatus, EvmActivity, EvmActivityAsset, @@ -44,19 +44,19 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo const fromAddress = getEvmAddressSafe(transfer.from_address)!; const toAddress = getEvmAddressSafe(transfer.to_address)!; - const kind: ActivityOperTransferKinds = (() => { + const type: ActivityOperTransferType = (() => { if (transfer.transfer_type === 'IN') { - if (item.to_address === transfer.contract_address) return ActivityOperKindEnum.transferTo_FromAccount; + if (item.to_address === transfer.contract_address) return ActivityOperTransferType.toUsFromAccount; - return ActivityOperKindEnum.transferTo; + return ActivityOperTransferType.toUs; } - if (item.to_address === transfer.contract_address) return ActivityOperKindEnum.transferFrom_ToAccount; + if (item.to_address === transfer.contract_address) return ActivityOperTransferType.fromUsToAccount; - return ActivityOperKindEnum.transferFrom; + return ActivityOperTransferType.fromUs; })(); - const operBase = { kind, fromAddress, toAddress }; + const operBase = { kind: ActivityOperKindEnum.transfer as const, type, fromAddress, toAddress }; const contractAddress = getEvmAddressSafe(transfer.contract_address); @@ -69,7 +69,7 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo const symbol = transfer.contract_ticker_symbol || undefined; const amountSigned = - operBase.kind === ActivityOperKindEnum.transferFrom || operBase.kind === ActivityOperKindEnum.transferFrom_ToAccount + type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount ? `-${amount}` : amount; diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index 39bf5b00bc..a862716209 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -3,7 +3,14 @@ import type { Transaction, LogEvent } from '@covalenthq/client-sdk'; import { getEvmAddressSafe } from 'lib/utils/evm.utils'; import { TempleChainKind } from 'temple/types'; -import { ActivityOperKindEnum, ActivityStatus, EvmActivity, EvmActivityAsset, EvmOperation } from '../../types'; +import { + ActivityOperKindEnum, + ActivityOperTransferType, + ActivityStatus, + EvmActivity, + EvmActivityAsset, + EvmOperation +} from '../../types'; import { parseGasTransfer } from './gas'; @@ -42,25 +49,27 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(0)?.value)!; const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value)!; - const kind = (() => { + const type = (() => { if (toAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; + ? ActivityOperTransferType.toUsFromAccount + : ActivityOperTransferType.toUs; } if (fromAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; + ? ActivityOperTransferType.fromUsToAccount + : ActivityOperTransferType.fromUs; } return null; })(); - if (kind == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; + if (type == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; - if (!contractAddress) return { kind, fromAddress, toAddress }; + const kind = ActivityOperKindEnum.transfer; + + if (!contractAddress) return { kind, type, fromAddress, toAddress }; const param3 = logEvent.decoded.params.at(2); const amountOrTokenId: string = param3?.value ?? '0'; @@ -70,7 +79,7 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const amount = nft ? '1' : amountOrTokenId; const amountSigned = - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount ? `-${amount}` : amount; @@ -84,39 +93,41 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, fromAddress, toAddress, asset }; + return { kind, type, fromAddress, toAddress, asset }; } if (logEvent.decoded.name === 'TransferSingle') { const fromAddress = getEvmAddressSafe(logEvent.decoded.params.at(1)?.value)!; const toAddress = getEvmAddressSafe(logEvent.decoded.params.at(2)?.value)!; - const kind = (() => { + const type = (() => { if (toAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferTo_FromAccount - : ActivityOperKindEnum.transferTo; + ? ActivityOperTransferType.toUsFromAccount + : ActivityOperTransferType.toUs; } if (fromAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom; + ? ActivityOperTransferType.fromUsToAccount + : ActivityOperTransferType.fromUs; } return null; })(); - if (kind == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; + if (type == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; + + const kind = ActivityOperKindEnum.transfer; - if (!contractAddress) return { kind, fromAddress, toAddress }; + if (!contractAddress) return { kind, type, fromAddress, toAddress }; const tokenId = logEvent.decoded.params.at(3)?.value ?? '0'; const amount = '1'; const amountSigned = - kind === ActivityOperKindEnum.transferFrom || kind === ActivityOperKindEnum.transferFrom_ToAccount + type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount ? `-${amount}` : amount; @@ -130,7 +141,7 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st iconURL }; - return { kind, fromAddress, toAddress, asset }; + return { kind, type, fromAddress, toAddress, asset }; } if (logEvent.decoded.name === 'Approval') { diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 94e861ce12..7117896afb 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,3 +1,3 @@ export type { Activity, TezosActivity, EvmActivity, TezosOperation, EvmOperation } from './types'; -export { ActivityOperKindEnum } from './types'; +export { ActivityOperKindEnum, ActivityOperTransferType } from './types'; diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index 0dd226b88c..ec56dd0dce 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -3,7 +3,13 @@ import { toTezosAssetSlug } from 'lib/assets/utils'; import { isTezosContractAddress } from 'lib/tezos'; import { TempleChainKind } from 'temple/types'; -import { TezosActivity, ActivityOperKindEnum, TezosOperation, ActivityStatus } from '../types'; +import { + TezosActivity, + ActivityOperKindEnum, + TezosOperation, + ActivityStatus, + ActivityOperTransferType +} from '../types'; import { isTransferActivityOperKind } from '../utils'; import { preparseTezosOperationsGroup } from './pre-parse'; @@ -61,19 +67,21 @@ function parseTezosPreActivityOperation(preOperation: TezosPreActivityOperation, if (preOperation.from.address === address) return { - kind: + kind: ActivityOperKindEnum.transfer, + type: preOperation.to.length === 1 && !isTezosContractAddress(preOperation.to[0].address) - ? ActivityOperKindEnum.transferFrom_ToAccount - : ActivityOperKindEnum.transferFrom, + ? ActivityOperTransferType.fromUsToAccount + : ActivityOperTransferType.fromUs, fromAddress, toAddress }; if (preOperation.to.some(member => member.address === address)) return { - kind: isTezosContractAddress(preOperation.from.address) - ? ActivityOperKindEnum.transferTo - : ActivityOperKindEnum.transferTo_FromAccount, + kind: ActivityOperKindEnum.transfer, + type: isTezosContractAddress(preOperation.from.address) + ? ActivityOperTransferType.toUs + : ActivityOperTransferType.toUsFromAccount, fromAddress, toAddress }; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index b908c91690..fe2f69e5a8 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -4,11 +4,7 @@ import { TezosActivityOlderThan } from './tezos/types'; export enum ActivityOperKindEnum { interaction, - transferFrom, - transferTo, - transferFrom_ToAccount, - transferTo_FromAccount, - swap, + transfer, approve } @@ -51,8 +47,16 @@ interface TezosApproveOperation extends TezosOperationBase { spenderAddress: string; } +export enum ActivityOperTransferType { + fromUs, + toUs, + fromUsToAccount, + toUsFromAccount +} + interface TezosTransferOperation extends TezosOperationBase { - kind: ActivityOperTransferKinds; + kind: ActivityOperKindEnum.transfer; + type: ActivityOperTransferType; fromAddress: string; toAddress: string; } @@ -62,18 +66,7 @@ interface TezosInteractionOperation extends TezosOperationBase { withAddress?: string; } -interface TezosOtherOperation extends TezosOperationBase { - kind: Exclude< - ActivityOperKindEnum, - ActivityOperKindEnum.approve | ActivityOperTransferKinds | ActivityOperKindEnum.interaction - >; -} - -export type TezosOperation = - | TezosApproveOperation - | TezosTransferOperation - | TezosInteractionOperation - | TezosOtherOperation; +export type TezosOperation = TezosApproveOperation | TezosTransferOperation | TezosInteractionOperation; export interface EvmActivity extends ChainActivityBase { chain: TempleChainKind.EVM; @@ -91,14 +84,9 @@ interface EvmApproveOperation extends EvmOperationBase { spenderAddress: string; } -export type ActivityOperTransferKinds = - | ActivityOperKindEnum.transferFrom - | ActivityOperKindEnum.transferFrom_ToAccount - | ActivityOperKindEnum.transferTo - | ActivityOperKindEnum.transferTo_FromAccount; - interface EvmTransferOperation extends EvmOperationBase { - kind: ActivityOperTransferKinds; + kind: ActivityOperKindEnum.transfer; + type: ActivityOperTransferType; toAddress: string; fromAddress: string; } @@ -108,14 +96,7 @@ interface EvmInteractionOperation extends EvmOperationBase { withAddress?: string; } -interface EvmOtherOperation extends EvmOperationBase { - kind: Exclude< - ActivityOperKindEnum, - ActivityOperKindEnum.approve | ActivityOperTransferKinds | ActivityOperKindEnum.interaction - >; -} - -export type EvmOperation = EvmApproveOperation | EvmTransferOperation | EvmInteractionOperation | EvmOtherOperation; +export type EvmOperation = EvmApproveOperation | EvmTransferOperation | EvmInteractionOperation; export interface EvmActivityAsset { contract: string; diff --git a/src/lib/activity/utils.ts b/src/lib/activity/utils.ts index 1892279a59..f033b4334c 100644 --- a/src/lib/activity/utils.ts +++ b/src/lib/activity/utils.ts @@ -1,10 +1,5 @@ import { ActivityOperKindEnum } from './types'; export function isTransferActivityOperKind(kind: ActivityOperKindEnum) { - return ( - kind === ActivityOperKindEnum.transferTo_FromAccount || - kind === ActivityOperKindEnum.transferFrom_ToAccount || - kind === ActivityOperKindEnum.transferFrom || - kind === ActivityOperKindEnum.transferTo - ); + return kind === ActivityOperKindEnum.transfer; } From f50c8e18600feacac65aebc45079e2c006158353 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Oct 2024 20:00:20 +0300 Subject: [PATCH 57/74] TW-1479: [EVM] Transactions history. Refactor. Transfer types --- .../ActivityItem/ActivityOperationBase.tsx | 19 +++++++-------- .../activity/ActivityItem/AddressChip.tsx | 12 ++++------ .../activity/ActivityItem/EvmActivity.tsx | 3 +-- .../ActivityItem/EvmActivityOperation.tsx | 3 +-- .../ActivityItem/TezosActivityOperation.tsx | 3 +-- src/app/templates/activity/utils.ts | 6 ++--- src/lib/activity/evm/parse/gas.ts | 6 ++--- src/lib/activity/evm/parse/gr-v2.ts | 12 ++++------ src/lib/activity/evm/parse/gr-v3.ts | 24 ++++++++----------- src/lib/activity/index.ts | 4 ++-- src/lib/activity/tezos/index.ts | 8 +++---- src/lib/activity/types.ts | 8 +++---- 12 files changed, 48 insertions(+), 60 deletions(-) diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index 8359d1dfe0..d7707bfc1e 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -11,8 +11,7 @@ import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; import { InFiat } from 'app/templates/InFiat'; -import { ActivityOperKindEnum } from 'lib/activity'; -import { ActivityOperTransferType, ActivityStatus } from 'lib/activity/types'; +import { ActivityOperKindEnum, ActivityOperTransferType, ActivityStatus } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -284,10 +283,10 @@ function getTitleByKind(kind: FaceKind, transferType?: ActivityOperTransferType) } const TransferTypeTitle: Record = { - [ActivityOperTransferType.fromUsToAccount]: 'Send', - [ActivityOperTransferType.toUsFromAccount]: 'Receive', - [ActivityOperTransferType.fromUs]: 'Transfer', - [ActivityOperTransferType.toUs]: 'Transfer' + [ActivityOperTransferType.sendToAccount]: 'Send', + [ActivityOperTransferType.receiveFromAccount]: 'Receive', + [ActivityOperTransferType.send]: 'Transfer', + [ActivityOperTransferType.receive]: 'Transfer' }; function getIconByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { @@ -301,8 +300,8 @@ function getIconByKind(kind: FaceKind, transferType?: ActivityOperTransferType) } const TransferTypeIconSvg: Record = { - [ActivityOperTransferType.fromUsToAccount]: SendSvg, - [ActivityOperTransferType.toUsFromAccount]: IncomeSvg, - [ActivityOperTransferType.fromUs]: DocumentsSvg, - [ActivityOperTransferType.toUs]: DocumentsSvg + [ActivityOperTransferType.sendToAccount]: SendSvg, + [ActivityOperTransferType.receiveFromAccount]: IncomeSvg, + [ActivityOperTransferType.send]: DocumentsSvg, + [ActivityOperTransferType.receive]: DocumentsSvg }; diff --git a/src/app/templates/activity/ActivityItem/AddressChip.tsx b/src/app/templates/activity/ActivityItem/AddressChip.tsx index c2ea3099c8..079a07c611 100644 --- a/src/app/templates/activity/ActivityItem/AddressChip.tsx +++ b/src/app/templates/activity/ActivityItem/AddressChip.tsx @@ -3,8 +3,7 @@ import React, { FC, useMemo } from 'react'; import { HashShortView, IconBase } from 'app/atoms'; import { CopyButton } from 'app/atoms/CopyButton'; import { ReactComponent as CopySvg } from 'app/icons/base/copy.svg'; -import { ActivityOperKindEnum, EvmOperation, TezosOperation } from 'lib/activity'; -import { ActivityOperTransferType } from 'lib/activity/types'; +import { ActivityOperKindEnum, EvmOperation, TezosOperation, ActivityOperTransferType } from 'lib/activity'; interface Props { operation: TezosOperation | EvmOperation; @@ -17,15 +16,14 @@ export const OperAddressChip: FC = ({ operation }) => { if (operation.kind === ActivityOperKindEnum.interaction) return operation.withAddress ? { title: 'With', address: operation.withAddress } : undefined; - if (operation.type === ActivityOperTransferType.fromUsToAccount) - return { title: 'To', address: operation.toAddress }; + if (operation.type === ActivityOperTransferType.sendToAccount) return { title: 'To', address: operation.toAddress }; - if (operation.type === ActivityOperTransferType.toUsFromAccount) + if (operation.type === ActivityOperTransferType.receiveFromAccount) return { title: 'From', address: operation.fromAddress }; - if (operation.type === ActivityOperTransferType.fromUs) return { title: 'With', address: operation.toAddress }; + if (operation.type === ActivityOperTransferType.send) return { title: 'With', address: operation.toAddress }; - if (operation.type === ActivityOperTransferType.toUs) return { title: 'With', address: operation.fromAddress }; + if (operation.type === ActivityOperTransferType.receive) return { title: 'With', address: operation.fromAddress }; return; }, [operation]); diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 9d1b338749..7a558992fc 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -1,8 +1,7 @@ import React, { memo, useMemo } from 'react'; import { PageModal } from 'app/atoms/PageModal'; -import { EvmActivity } from 'lib/activity'; -import { EvmActivityAsset } from 'lib/activity/types'; +import { EvmActivity, EvmActivityAsset } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { fromAssetSlug, toEvmAssetSlug } from 'lib/assets/utils'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index 39d3de29c4..c164db9926 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo } from 'react'; -import { ActivityOperKindEnum, type EvmOperation } from 'lib/activity'; -import { ActivityStatus } from 'lib/activity/types'; +import { ActivityOperKindEnum, EvmOperation, ActivityStatus } from 'lib/activity'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index ffb4ebbdae..08b4e5640f 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo } from 'react'; -import { ActivityOperKindEnum, TezosOperation } from 'lib/activity'; -import { ActivityStatus } from 'lib/activity/types'; +import { ActivityOperKindEnum, TezosOperation, ActivityStatus } from 'lib/activity'; import { fromAssetSlug } from 'lib/assets'; import { AssetMetadataBase, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index a5e7fa2b31..4f773092c7 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -26,11 +26,11 @@ export function getActivityFilterKind(activity: Activity): FilterKind { const type = operation.type; - if (type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.toUs) return 'transfer'; + if (type === ActivityOperTransferType.send || type === ActivityOperTransferType.receive) return 'transfer'; - if (type === ActivityOperTransferType.fromUsToAccount) return 'send'; + if (type === ActivityOperTransferType.sendToAccount) return 'send'; - if (type === ActivityOperTransferType.toUsFromAccount) return 'receive'; + if (type === ActivityOperTransferType.receiveFromAccount) return 'receive'; return null; } diff --git a/src/lib/activity/evm/parse/gas.ts b/src/lib/activity/evm/parse/gas.ts index ba9ca348c8..4cbeabd95c 100644 --- a/src/lib/activity/evm/parse/gas.ts +++ b/src/lib/activity/evm/parse/gas.ts @@ -20,9 +20,9 @@ export function parseGasTransfer( const type = (() => { if (fromAddress === accountAddress) - return partOfBatch ? ActivityOperTransferType.fromUs : ActivityOperTransferType.fromUsToAccount; + return partOfBatch ? ActivityOperTransferType.send : ActivityOperTransferType.sendToAccount; if (toAddress === accountAddress) - return partOfBatch ? ActivityOperTransferType.toUs : ActivityOperTransferType.toUsFromAccount; + return partOfBatch ? ActivityOperTransferType.receive : ActivityOperTransferType.receiveFromAccount; return null; })(); @@ -34,7 +34,7 @@ export function parseGasTransfer( const decimals = item.gas_metadata?.contract_decimals; const amountSigned = - type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount ? `-${value}` : value; + type === ActivityOperTransferType.send || type === ActivityOperTransferType.sendToAccount ? `-${value}` : value; const symbol = item.gas_metadata?.contract_ticker_symbol; diff --git a/src/lib/activity/evm/parse/gr-v2.ts b/src/lib/activity/evm/parse/gr-v2.ts index 7301c05656..409536b263 100644 --- a/src/lib/activity/evm/parse/gr-v2.ts +++ b/src/lib/activity/evm/parse/gr-v2.ts @@ -46,14 +46,14 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo const type: ActivityOperTransferType = (() => { if (transfer.transfer_type === 'IN') { - if (item.to_address === transfer.contract_address) return ActivityOperTransferType.toUsFromAccount; + if (item.to_address === transfer.contract_address) return ActivityOperTransferType.receiveFromAccount; - return ActivityOperTransferType.toUs; + return ActivityOperTransferType.receive; } - if (item.to_address === transfer.contract_address) return ActivityOperTransferType.fromUsToAccount; + if (item.to_address === transfer.contract_address) return ActivityOperTransferType.sendToAccount; - return ActivityOperTransferType.fromUs; + return ActivityOperTransferType.send; })(); const operBase = { kind: ActivityOperKindEnum.transfer as const, type, fromAddress, toAddress }; @@ -69,9 +69,7 @@ function parseTransfer(transfer: TokenTransferItem, item: BlockTransactionWithCo const symbol = transfer.contract_ticker_symbol || undefined; const amountSigned = - type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount - ? `-${amount}` - : amount; + type === ActivityOperTransferType.send || type === ActivityOperTransferType.sendToAccount ? `-${amount}` : amount; const asset: EvmActivityAsset = { contract: contractAddress, diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index a862716209..9659d0db4e 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -52,14 +52,14 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const type = (() => { if (toAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.toUsFromAccount - : ActivityOperTransferType.toUs; + ? ActivityOperTransferType.receiveFromAccount + : ActivityOperTransferType.receive; } if (fromAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.fromUsToAccount - : ActivityOperTransferType.fromUs; + ? ActivityOperTransferType.sendToAccount + : ActivityOperTransferType.send; } return null; @@ -79,9 +79,7 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const amount = nft ? '1' : amountOrTokenId; const amountSigned = - type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount - ? `-${amount}` - : amount; + type === ActivityOperTransferType.send || type === ActivityOperTransferType.sendToAccount ? `-${amount}` : amount; const asset: EvmActivityAsset = { contract: contractAddress, @@ -103,14 +101,14 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const type = (() => { if (toAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.toUsFromAccount - : ActivityOperTransferType.toUs; + ? ActivityOperTransferType.receiveFromAccount + : ActivityOperTransferType.receive; } if (fromAddress === accountAddress) { return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.fromUsToAccount - : ActivityOperTransferType.fromUs; + ? ActivityOperTransferType.sendToAccount + : ActivityOperTransferType.send; } return null; @@ -127,9 +125,7 @@ function parseLogEvent(logEvent: LogEvent, item: Transaction, accountAddress: st const amount = '1'; const amountSigned = - type === ActivityOperTransferType.fromUs || type === ActivityOperTransferType.fromUsToAccount - ? `-${amount}` - : amount; + type === ActivityOperTransferType.send || type === ActivityOperTransferType.sendToAccount ? `-${amount}` : amount; const asset: EvmActivityAsset = { contract: contractAddress, diff --git a/src/lib/activity/index.ts b/src/lib/activity/index.ts index 7117896afb..f76626617f 100644 --- a/src/lib/activity/index.ts +++ b/src/lib/activity/index.ts @@ -1,3 +1,3 @@ -export type { Activity, TezosActivity, EvmActivity, TezosOperation, EvmOperation } from './types'; +export type { Activity, TezosActivity, EvmActivity, TezosOperation, EvmOperation, EvmActivityAsset } from './types'; -export { ActivityOperKindEnum, ActivityOperTransferType } from './types'; +export { ActivityOperKindEnum, ActivityOperTransferType, ActivityStatus } from './types'; diff --git a/src/lib/activity/tezos/index.ts b/src/lib/activity/tezos/index.ts index ec56dd0dce..58f28c0dcd 100644 --- a/src/lib/activity/tezos/index.ts +++ b/src/lib/activity/tezos/index.ts @@ -70,8 +70,8 @@ function parseTezosPreActivityOperation(preOperation: TezosPreActivityOperation, kind: ActivityOperKindEnum.transfer, type: preOperation.to.length === 1 && !isTezosContractAddress(preOperation.to[0].address) - ? ActivityOperTransferType.fromUsToAccount - : ActivityOperTransferType.fromUs, + ? ActivityOperTransferType.sendToAccount + : ActivityOperTransferType.send, fromAddress, toAddress }; @@ -80,8 +80,8 @@ function parseTezosPreActivityOperation(preOperation: TezosPreActivityOperation, return { kind: ActivityOperKindEnum.transfer, type: isTezosContractAddress(preOperation.from.address) - ? ActivityOperTransferType.toUs - : ActivityOperTransferType.toUsFromAccount, + ? ActivityOperTransferType.receive + : ActivityOperTransferType.receiveFromAccount, fromAddress, toAddress }; diff --git a/src/lib/activity/types.ts b/src/lib/activity/types.ts index fe2f69e5a8..7e9200239c 100644 --- a/src/lib/activity/types.ts +++ b/src/lib/activity/types.ts @@ -48,10 +48,10 @@ interface TezosApproveOperation extends TezosOperationBase { } export enum ActivityOperTransferType { - fromUs, - toUs, - fromUsToAccount, - toUsFromAccount + send, + receive, + sendToAccount, + receiveFromAccount } interface TezosTransferOperation extends TezosOperationBase { From 8575d5cb46d440cc81884a73f49efed2f4d44167 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Oct 2024 22:13:17 +0300 Subject: [PATCH 58/74] TW-1479: [EVM] Transactions history. Minor fixes --- src/app/atoms/EmptyState.tsx | 7 ++- src/app/pages/Activity/index.tsx | 1 + src/app/templates/NetworkSelectModal.tsx | 6 +-- src/app/templates/SettingsGeneral/index.tsx | 10 ++-- .../ActivityItem/ActivityOperationBase.tsx | 49 ++++++++++--------- .../activity/ActivityItem/BundleModal.tsx | 15 ++++-- .../activity/ActivityItem/EvmActivity.tsx | 22 ++++----- .../ActivityItem/InteractionsConnector.tsx | 9 ---- .../activity/ActivityItem/TezosActivity.tsx | 22 ++++----- .../ActivityItem/interactions-connector.svg | 6 --- .../activity/ActivityListContainer.tsx | 2 +- .../templates/activity/EvmActivityList.tsx | 2 +- src/app/templates/activity/MultichainList.tsx | 2 +- .../templates/activity/TezosActivityList.tsx | 2 +- 14 files changed, 72 insertions(+), 83 deletions(-) delete mode 100644 src/app/templates/activity/ActivityItem/InteractionsConnector.tsx delete mode 100644 src/app/templates/activity/ActivityItem/interactions-connector.svg diff --git a/src/app/atoms/EmptyState.tsx b/src/app/atoms/EmptyState.tsx index 6670da7c35..4e29018a11 100644 --- a/src/app/atoms/EmptyState.tsx +++ b/src/app/atoms/EmptyState.tsx @@ -9,10 +9,13 @@ import { T } from 'lib/i18n'; interface EmptyStateProps { className?: string; variant?: 'tokenSearch' | 'universal' | 'searchUniversal'; + stretch?: boolean; } -export const EmptyState = memo(({ className, variant = 'universal' }) => ( -
+export const EmptyState = memo(({ className, variant = 'universal', stretch }) => ( +
{variant === 'universal' ? : } diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index ddcd492cee..1bf0c387aa 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -59,6 +59,7 @@ export const ActivityPage = memo(() => { return ( } diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 59844d594f..2a6c372867 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -49,7 +49,7 @@ export const NetworkSelectModal = memo(({ opened, selectedNetwork, onRequ ); return ( - + (({ opened, selectedNetwork, return ( <> -
+
navigate('settings/networks')} />
-
+
{filteredNetworks.length === 0 && } {filteredNetworks.map(network => ( diff --git a/src/app/templates/SettingsGeneral/index.tsx b/src/app/templates/SettingsGeneral/index.tsx index c0501d3181..79bc7cbe87 100644 --- a/src/app/templates/SettingsGeneral/index.tsx +++ b/src/app/templates/SettingsGeneral/index.tsx @@ -2,11 +2,11 @@ import React, { memo } from 'react'; import { NotificationsSettings } from 'lib/notifications/components'; -import AnalyticsSettings from './components/AnalyticsSettings'; -import FiatCurrencySelect from './components/FiatCurrencySelect'; -import LocaleSelect from './components/LocaleSelect'; -import LockUpSettings from './components/LockUpSettings'; -import PopupSettings from './components/PopupSettings'; +import AnalyticsSettings from './Components/AnalyticsSettings'; +import FiatCurrencySelect from './Components/FiatCurrencySelect'; +import LocaleSelect from './Components/LocaleSelect'; +import LockUpSettings from './Components/LockUpSettings'; +import PopupSettings from './Components/PopupSettings'; const GeneralSettings = memo(() => (
diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx index d7707bfc1e..97f5c43eb0 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx @@ -1,9 +1,10 @@ -import React, { FC, memo, MouseEventHandler, ReactElement, ReactNode, useCallback, useMemo } from 'react'; +import React, { FC, memo, ReactElement, ReactNode, useMemo } from 'react'; import clsx from 'clsx'; -import { Anchor, HashShortView, IconBase, Money } from 'app/atoms'; +import { Anchor, Button, HashShortView, IconBase, Money } from 'app/atoms'; import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { ReactComponent as ChevronRightSvg } from 'app/icons/base/chevron_right.svg'; import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; import { ReactComponent as OkSvg } from 'app/icons/base/ok.svg'; @@ -61,8 +62,9 @@ export const ActivityOperationBaseComponent = memo( return (
0 && 'text-success' + 'max-w-40 flex text-font-num-14 overflow-hidden', + asset.amountSigned && Number(asset.amountSigned) > 0 && 'text-success', + onClick && 'group-hover:hidden' )} > {kind === ActivityOperKindEnum.approve ? null : asset.amountSigned ? ( @@ -74,7 +76,7 @@ export const ActivityOperationBaseComponent = memo( {symbolStr ? {symbolStr} : null}
); - }, [asset, kind]); + }, [asset, kind, onClick]); const fiatJsx = useMemo(() => { if (!asset) return null; @@ -134,18 +136,6 @@ export const ActivityOperationBaseComponent = memo( [addressChip, hash, blockExplorerUrl] ); - const handleClick = useCallback>( - event => { - if (!onClick) return; - - // Case of click on link inside this component's element - if (event.target instanceof Element && event.target.closest(`.${CLICK_DETECTION_ATTR} a`)) return; - - onClick(); - }, - [onClick] - ); - const isNFT = Boolean(asset?.nft); const faceIconJsx = useMemo( @@ -175,11 +165,9 @@ export const ActivityOperationBaseComponent = memo( return (
{kind === 'bundle' ? ( @@ -211,9 +199,24 @@ export const ActivityOperationBaseComponent = memo(
{chipJsx} -
{fiatJsx}
+
{fiatJsx}
+ + {onClick && ( + + )}
); } @@ -254,8 +257,6 @@ const BundleIconsStack = memo = ({ status }) => { if (status === ActivityStatus.failed) return StatusTagFailed; diff --git a/src/app/templates/activity/ActivityItem/BundleModal.tsx b/src/app/templates/activity/ActivityItem/BundleModal.tsx index 002c9b53b5..59900fffff 100644 --- a/src/app/templates/activity/ActivityItem/BundleModal.tsx +++ b/src/app/templates/activity/ActivityItem/BundleModal.tsx @@ -1,18 +1,25 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { ActionsButtonsBox } from 'app/atoms/PageModal'; import { ScrollView } from 'app/atoms/ScrollView'; import { StyledButtonAnchor } from 'app/atoms/StyledButton'; -import { T } from 'lib/i18n'; +import { T, formatDate } from 'lib/i18n'; interface Props extends PropsWithChildren { + addedAt: string; blockExplorerUrl?: string; } -export const BundleModalContent: FC = ({ blockExplorerUrl, children }) => { +export const BundleModalContent: FC = ({ addedAt, blockExplorerUrl, children }) => { + const title = useMemo(() => formatDate(addedAt, 'PP'), [addedAt]); + return ( <> - {children} + +
{title}
+ + {children} +
diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index 7a558992fc..edfb8373dd 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -12,7 +12,6 @@ import { EvmChain } from 'temple/front/chains'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; import { BundleModalContent } from './BundleModal'; import { EvmActivityOperationComponent } from './EvmActivityOperation'; -import { InteractionsConnector } from './InteractionsConnector'; interface Props { activity: EvmActivity; @@ -130,19 +129,16 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl {() => ( - + {operations.map((operation, j) => ( - - {j > 0 && } - - - + ))} )} diff --git a/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx b/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx deleted file mode 100644 index 86af11a920..0000000000 --- a/src/app/templates/activity/ActivityItem/InteractionsConnector.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { memo } from 'react'; - -import { ReactComponent as InteractionsConnectorSvg } from './interactions-connector.svg'; - -export const InteractionsConnector = memo(() => ( -
- -
-)); diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index 4bcbb55f93..fb89d79e18 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -11,7 +11,6 @@ import { TezosChain } from 'temple/front/chains'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; import { BundleModalContent } from './BundleModal'; -import { InteractionsConnector } from './InteractionsConnector'; import { TezosActivityOperationComponent, buildTezosOperationAsset } from './TezosActivityOperation'; interface Props { @@ -103,19 +102,16 @@ const TezosActivityBatchComponent = memo(({ activity, chainId, asset {() => ( - + {operations.map((operation, j) => ( - - {j > 0 && } - - - + ))} )} diff --git a/src/app/templates/activity/ActivityItem/interactions-connector.svg b/src/app/templates/activity/ActivityItem/interactions-connector.svg deleted file mode 100644 index 064f90a8b5..0000000000 --- a/src/app/templates/activity/ActivityItem/interactions-connector.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/app/templates/activity/ActivityListContainer.tsx b/src/app/templates/activity/ActivityListContainer.tsx index 90d308588a..f6f66d1017 100644 --- a/src/app/templates/activity/ActivityListContainer.tsx +++ b/src/app/templates/activity/ActivityListContainer.tsx @@ -33,7 +33,7 @@ export const ActivityListContainer: FC = ({ children, chainId, assetSlug return ( -
+
{promotion} {children} diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index f95856d480..426906785d 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -90,7 +90,7 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = ); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; + return ; } return ( diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 81327afda6..fbdb46420a 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -147,7 +147,7 @@ export const MultichainActivityList = memo(({ filterKind }) => { ); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; + return ; } return ( diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index fa4bb9cd62..a8cf3b2c8a 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -88,7 +88,7 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK ); if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; + return ; } return ( From 7df286ca4bb77d06590939d7674b1c1efbfdf5e0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 02:15:07 +0300 Subject: [PATCH 59/74] TW-1479: [EVM] Transactions history. Multichain. + Fallback icons --- src/app/templates/AssetImage/index.tsx | 6 +- .../index.tsx} | 146 ++++-------------- .../ActivityOperationBase/nft.svg | 18 +++ .../ActivityOperationBase/token.svg | 11 ++ .../ActivityOperationBase/utils.tsx | 117 ++++++++++++++ src/lib/identicon.ts | 2 +- 6 files changed, 182 insertions(+), 118 deletions(-) rename src/app/templates/activity/ActivityItem/{ActivityOperationBase.tsx => ActivityOperationBase/index.tsx} (61%) create mode 100644 src/app/templates/activity/ActivityItem/ActivityOperationBase/nft.svg create mode 100644 src/app/templates/activity/ActivityItem/ActivityOperationBase/token.svg create mode 100644 src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx diff --git a/src/app/templates/AssetImage/index.tsx b/src/app/templates/AssetImage/index.tsx index a4aa5f3318..66da1c05dd 100644 --- a/src/app/templates/AssetImage/index.tsx +++ b/src/app/templates/AssetImage/index.tsx @@ -12,8 +12,7 @@ import { export { TezosAssetImageStacked }; -export interface TezosAssetImageProps - extends Omit { +export interface TezosAssetImageProps extends Omit { tezosChainId: string; assetSlug: string; Loader?: Placeholder; @@ -35,8 +34,7 @@ export const TezosAssetImage = memo(({ Loader, Fallback, . ); }); -export interface EvmAssetImageProps - extends Omit { +export interface EvmAssetImageProps extends Omit { evmChainId: number; assetSlug: string; Loader?: Placeholder; diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx similarity index 61% rename from src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx rename to src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx index 97f5c43eb0..753c0dbfbb 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx @@ -1,25 +1,21 @@ -import React, { FC, memo, ReactElement, ReactNode, useMemo } from 'react'; +import React, { memo, ReactElement, ReactNode, useMemo } from 'react'; import clsx from 'clsx'; import { Anchor, Button, HashShortView, IconBase, Money } from 'app/atoms'; import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { ReactComponent as ChevronRightSvg } from 'app/icons/base/chevron_right.svg'; -import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; -import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; -import { ReactComponent as OkSvg } from 'app/icons/base/ok.svg'; import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; -import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; -import { EvmAssetIcon, TezosAssetIcon } from 'app/templates/AssetIcon'; +import { EvmAssetImage, TezosAssetImage } from 'app/templates/AssetImage'; import { InFiat } from 'app/templates/InFiat'; import { ActivityOperKindEnum, ActivityOperTransferType, ActivityStatus } from 'lib/activity'; import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; -import { FaceKind } from '../utils'; +import { FaceKind } from '../../utils'; -import { ReactComponent as PendingSpinSvg } from './pending-spin.svg'; +import { AssetIconPlaceholder, BundleIconsStack, getIconByKind, getTitleByKind, StatusTag } from './utils'; interface Props { chainId: string | number; @@ -138,29 +134,38 @@ export const ActivityOperationBaseComponent = memo( const isNFT = Boolean(asset?.nft); - const faceIconJsx = useMemo( - () => - withoutAssetIcon || !assetSlug ? ( + const faceIconJsx = useMemo(() => { + if (withoutAssetIcon || !assetSlug) + return (
- ) : typeof chainId === 'number' ? ( - - ) : ( - - ), - [chainId, withoutAssetIcon, kind, transferType, asset?.iconURL, assetSlug] - ); + ); + + const className = 'w-full h-full object-cover'; + + const placeholderJsx = ; + + return typeof chainId === 'number' ? ( + + ) : ( + + ); + }, [chainId, withoutAssetIcon, kind, transferType, asset?.iconURL, asset?.symbol, isNFT, assetSlug]); return (
( ); } ); - -const MEDALION_CLASS_NAME = 'absolute border border-lines'; - -const BundleIconsStack = memo>( - ({ withoutAssetIcon, isNFT, children }) => { - return ( - <> -
- -
- -
-
- {children} -
-
- - ); - } -); - -const StatusTag: FC<{ status?: ActivityStatus }> = ({ status }) => { - if (status === ActivityStatus.failed) return StatusTagFailed; - - if (status === ActivityStatus.pending) return StatusTagPending; - - return null; -}; - -const StatusTagFailed = ( -
- FAILED -
-); - -const StatusTagPending = ; - -function getTitleByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { - if (kind === 'bundle') return 'Bundle'; - - if (kind === ActivityOperKindEnum.interaction) return 'Interaction'; - - if (kind === ActivityOperKindEnum.approve) return 'Approve'; - - return transferType == null ? 'Interaction' : TransferTypeTitle[transferType]; -} - -const TransferTypeTitle: Record = { - [ActivityOperTransferType.sendToAccount]: 'Send', - [ActivityOperTransferType.receiveFromAccount]: 'Receive', - [ActivityOperTransferType.send]: 'Transfer', - [ActivityOperTransferType.receive]: 'Transfer' -}; - -function getIconByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { - if (kind === 'bundle') return DocumentsSvg; - - if (kind === ActivityOperKindEnum.interaction) return DocumentsSvg; - - if (kind === ActivityOperKindEnum.approve) return OkSvg; - - return transferType == null ? DocumentsSvg : TransferTypeIconSvg[transferType]; -} - -const TransferTypeIconSvg: Record = { - [ActivityOperTransferType.sendToAccount]: SendSvg, - [ActivityOperTransferType.receiveFromAccount]: IncomeSvg, - [ActivityOperTransferType.send]: DocumentsSvg, - [ActivityOperTransferType.receive]: DocumentsSvg -}; diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase/nft.svg b/src/app/templates/activity/ActivityItem/ActivityOperationBase/nft.svg new file mode 100644 index 0000000000..25e8d29562 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/nft.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase/token.svg b/src/app/templates/activity/ActivityItem/ActivityOperationBase/token.svg new file mode 100644 index 0000000000..9a2c57c4ea --- /dev/null +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/token.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx new file mode 100644 index 0000000000..8fa1c9c7e4 --- /dev/null +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx @@ -0,0 +1,117 @@ +import React, { FC, memo } from 'react'; + +import clsx from 'clsx'; + +import { IdenticonInitials } from 'app/atoms/Identicon'; +import { ReactComponent as DocumentsSvg } from 'app/icons/base/documents.svg'; +import { ReactComponent as IncomeSvg } from 'app/icons/base/income.svg'; +import { ReactComponent as OkSvg } from 'app/icons/base/ok.svg'; +import { ReactComponent as SendSvg } from 'app/icons/base/send.svg'; +import { ActivityOperKindEnum, ActivityOperTransferType, ActivityStatus } from 'lib/activity'; + +import { FaceKind } from '../../utils'; +import { ReactComponent as PendingSpinSvg } from '../pending-spin.svg'; + +import { ReactComponent as NftPlaceholderSvg } from './nft.svg'; +import { ReactComponent as TokenPlaceholderSvg } from './token.svg'; + +const MEDALION_CLASS_NAME = 'absolute border border-lines'; + +export const BundleIconsStack = memo>( + ({ withoutAssetIcon, isNFT, children }) => { + const bgClassName = withoutAssetIcon ? 'bg-grey-4' : 'bg-white'; + + return ( + <> +
+ +
+ +
+
+ {children} +
+
+ + ); + } +); + +export const StatusTag: FC<{ status?: ActivityStatus }> = ({ status }) => { + if (status === ActivityStatus.failed) return StatusTagFailed; + + if (status === ActivityStatus.pending) return StatusTagPending; + + return null; +}; + +const StatusTagFailed = ( +
+ FAILED +
+); + +const StatusTagPending = ; + +export function getTitleByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { + if (kind === 'bundle') return 'Bundle'; + + if (kind === ActivityOperKindEnum.interaction) return 'Interaction'; + + if (kind === ActivityOperKindEnum.approve) return 'Approve'; + + return transferType == null ? 'Interaction' : TransferTypeTitle[transferType]; +} + +const TransferTypeTitle: Record = { + [ActivityOperTransferType.sendToAccount]: 'Send', + [ActivityOperTransferType.receiveFromAccount]: 'Receive', + [ActivityOperTransferType.send]: 'Transfer', + [ActivityOperTransferType.receive]: 'Transfer' +}; + +export function getIconByKind(kind: FaceKind, transferType?: ActivityOperTransferType) { + if (kind === 'bundle') return DocumentsSvg; + + if (kind === ActivityOperKindEnum.interaction) return DocumentsSvg; + + if (kind === ActivityOperKindEnum.approve) return OkSvg; + + return transferType == null ? DocumentsSvg : TransferTypeIconSvg[transferType]; +} + +const TransferTypeIconSvg: Record = { + [ActivityOperTransferType.sendToAccount]: SendSvg, + [ActivityOperTransferType.receiveFromAccount]: IncomeSvg, + [ActivityOperTransferType.send]: DocumentsSvg, + [ActivityOperTransferType.receive]: DocumentsSvg +}; + +export const AssetIconPlaceholder: FC<{ isNFT: boolean; symbol?: string; className?: string }> = ({ + isNFT, + symbol, + className +}) => { + if (isNFT) return ; + + return symbol ? ( + + ) : ( + + ); +}; diff --git a/src/lib/identicon.ts b/src/lib/identicon.ts index 2f46f2ce0a..cf4b848831 100644 --- a/src/lib/identicon.ts +++ b/src/lib/identicon.ts @@ -36,7 +36,7 @@ export const buildInitialsIdenticonUri = memoizee( ...options, seed, fontFamily: ['Menlo', 'Monaco', 'monospace'], - fontSize: estimateOptimalFontSize(seed.length) // (!) Doesn't account for options.chars + fontSize: estimateOptimalFontSize(options?.chars ?? 2) }).toDataUriSync(), { max: 1024, From 9d0fe6e5fc15f4c5546e55fead02ffe6ee6b691b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 04:01:04 +0300 Subject: [PATCH 60/74] TW-1479: [EVM] Transactions history. + New loader --- src/app/atoms/Loader/index.tsx | 49 +++++++++++++++++++ .../templates/activity/ActivityListView.tsx | 38 ++++++++++++++ .../templates/activity/EvmActivityList.tsx | 16 ++---- src/app/templates/activity/MultichainList.tsx | 16 ++---- .../templates/activity/TezosActivityList.tsx | 16 ++---- 5 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 src/app/atoms/Loader/index.tsx create mode 100644 src/app/templates/activity/ActivityListView.tsx diff --git a/src/app/atoms/Loader/index.tsx b/src/app/atoms/Loader/index.tsx new file mode 100644 index 0000000000..fc1262ad95 --- /dev/null +++ b/src/app/atoms/Loader/index.tsx @@ -0,0 +1,49 @@ +import React, { FC, memo } from 'react'; + +import clsx from 'clsx'; + +type Color = 'primary' | 'secondary' | 'white'; + +interface Props { + color: Color; + small?: boolean; + className?: string; +} + +export const Loader = memo(({ color, small, className }) => ( + + + + + +)); + +interface PageLoaderProps { + color?: Color; + text?: string; + stretch?: boolean; +} + +export const PageLoader: FC = ({ color = 'secondary', text = 'Content is Loading...', stretch }) => ( +
+
+ +
+ +
+ {text} +
+
+); diff --git a/src/app/templates/activity/ActivityListView.tsx b/src/app/templates/activity/ActivityListView.tsx new file mode 100644 index 0000000000..3b21f864bf --- /dev/null +++ b/src/app/templates/activity/ActivityListView.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; + +import { EmptyState } from 'app/atoms/EmptyState'; +import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; +import { Loader, PageLoader } from 'app/atoms/Loader'; + +interface Props { + activitiesNumber: number; + isSyncing: boolean; + reachedTheEnd: boolean; + loadNext: EmptyFn; +} + +export const ActivityListView: FC> = ({ + activitiesNumber, + isSyncing, + reachedTheEnd, + loadNext, + children +}) => { + if (activitiesNumber === 0) { + if (isSyncing) return ; + else if (reachedTheEnd) return ; + } + + return ( + } + > + {children} + + ); +}; diff --git a/src/app/templates/activity/EvmActivityList.tsx b/src/app/templates/activity/EvmActivityList.tsx index 426906785d..74f3b1c5f9 100644 --- a/src/app/templates/activity/EvmActivityList.tsx +++ b/src/app/templates/activity/EvmActivityList.tsx @@ -2,8 +2,6 @@ import React, { FC, useMemo } from 'react'; import { AxiosError } from 'axios'; -import { EmptyState } from 'app/atoms/EmptyState'; -import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { EvmActivity } from 'lib/activity'; @@ -13,6 +11,7 @@ import { useAccountAddressForEvm } from 'temple/front'; import { useEvmChainByChainId } from 'temple/front/chains'; import { EvmActivityComponent } from './ActivityItem'; +import { ActivityListView } from './ActivityListView'; import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -89,19 +88,14 @@ export const EvmActivityList: FC = ({ chainId, assetSlug, filterKind }) = [groupedActivities, network, assetSlug] ); - if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; - } - return ( - {contentJsx} - + ); }; diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index fbdb46420a..7c187ddf8d 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -2,8 +2,6 @@ import React, { memo, useMemo } from 'react'; import { AxiosError } from 'axios'; -import { EmptyState } from 'app/atoms/EmptyState'; -import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { Activity, EvmActivity, TezosActivity } from 'lib/activity'; import { getEvmAssetTransactions } from 'lib/activity/evm'; @@ -22,6 +20,7 @@ import { } from 'temple/front'; import { EvmActivityComponent, TezosActivityComponent } from './ActivityItem'; +import { ActivityListView } from './ActivityListView'; import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -146,20 +145,15 @@ export const MultichainActivityList = memo(({ filterKind }) => { [groupedActivities, allTezosChains, allEvmChains] ); - if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; - } - return ( - {contentJsx} - + ); }); diff --git a/src/app/templates/activity/TezosActivityList.tsx b/src/app/templates/activity/TezosActivityList.tsx index a8cf3b2c8a..db1babbd4d 100644 --- a/src/app/templates/activity/TezosActivityList.tsx +++ b/src/app/templates/activity/TezosActivityList.tsx @@ -1,7 +1,5 @@ import React, { memo, useMemo } from 'react'; -import { EmptyState } from 'app/atoms/EmptyState'; -import { InfiniteScroll } from 'app/atoms/InfiniteScroll'; import { DeadEndBoundaryError } from 'app/ErrorBoundary'; import { useLoadPartnersPromo } from 'app/hooks/use-load-partners-promo'; import { TezosActivity } from 'lib/activity'; @@ -11,6 +9,7 @@ import { isKnownChainId } from 'lib/apis/tzkt/api'; import { useAccountAddressForTezos, useTezosChainByChainId } from 'temple/front'; import { TezosActivityComponent } from './ActivityItem'; +import { ActivityListView } from './ActivityListView'; import { ActivitiesDateGroup, useGroupingByDate } from './grouping-by-date'; import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; @@ -87,19 +86,14 @@ export const TezosActivityList = memo(({ tezosChainId, assetSlug, filterK [groupedActivities, network, assetSlug] ); - if (displayActivities.length === 0 && !isLoading && reachedTheEnd) { - return ; - } - return ( - {contentJsx} - + ); }); From 28926eda5dffa06b1227c57b434ac08fbf7b271d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 04:37:52 +0300 Subject: [PATCH 61/74] TW-1479: [EVM] Transactions history. Multichain. -- Filters by kind --- .../pages/Activity/FilterChainDropdown.tsx | 95 ---------- src/app/pages/Activity/index.tsx | 167 +++--------------- src/app/templates/NetworkSelectModal.tsx | 64 ++++--- src/app/templates/activity/MultichainList.tsx | 2 +- 4 files changed, 66 insertions(+), 262 deletions(-) delete mode 100644 src/app/pages/Activity/FilterChainDropdown.tsx diff --git a/src/app/pages/Activity/FilterChainDropdown.tsx b/src/app/pages/Activity/FilterChainDropdown.tsx deleted file mode 100644 index 4a05c241a6..0000000000 --- a/src/app/pages/Activity/FilterChainDropdown.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { memo, useCallback, useMemo, useState } from 'react'; - -import { useDebounce } from 'use-debounce'; - -import { IconBase } from 'app/atoms'; -import { ActionListItem } from 'app/atoms/ActionListItem'; -import { ActionsDropdownPopup } from 'app/atoms/ActionsDropdown'; -import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; -import { ReactComponent as BrowseSvg } from 'app/icons/base/browse.svg'; -import { FilterChain } from 'app/store/assets-filter-options/state'; -import { SearchBarField } from 'app/templates/SearchField'; -import { T, t } from 'lib/i18n'; -import { PopperRenderProps } from 'lib/ui/Popper'; -import { isSearchStringApplicable, searchAndFilterItems } from 'lib/utils/search-items'; -import { OneOfChains, useEnabledEvmChains, useEnabledTezosChains } from 'temple/front'; -import { TempleChainKind } from 'temple/types'; - -interface Props extends PopperRenderProps { - filterChain: FilterChain; - setFilterChain: (chain: OneOfChains | null) => void; -} - -export const FilterChainDropdown = memo(({ filterChain, setFilterChain, opened, setOpened }) => { - const tezosChains = useEnabledTezosChains(); - const evmChains = useEnabledEvmChains(); - - const [searchValue, setSearchValue] = useState(''); - const [searchValueDebounced] = useDebounce(searchValue, 300); - const inSearch = isSearchStringApplicable(searchValueDebounced); - - const networks = useMemo(() => [...tezosChains, ...evmChains], [tezosChains, evmChains]); - - const searchedNetworks = useMemo( - () => (inSearch ? searchAndFilterChains(networks, searchValueDebounced) : networks), - [inSearch, searchValueDebounced, networks] - ); - - const onChainClick = useCallback( - (chain: OneOfChains | null) => { - setFilterChain(chain); - setOpened(false); - }, - [setFilterChain, setOpened] - ); - - return ( - - - -
- {!inSearch && ( - onChainClick(null)}> - - - - - - - )} - - {searchedNetworks.map(chain => ( - onChainClick(chain)} - > - {chain.nameI18nKey ? : chain.name} - - {chain.kind === TempleChainKind.Tezos ? ( - - ) : ( - - )} - - ))} -
-
- ); -}); - -function searchAndFilterChains(networks: OneOfChains[], searchValue: string) { - return searchAndFilterItems( - networks, - searchValue.trim(), - [ - { name: 'name', weight: 1 }, - { name: 'nameI18n', weight: 1 } - ], - ({ name, nameI18nKey }) => ({ - name, - nameI18n: nameI18nKey ? t(nameI18nKey) : undefined - }) - ); -} diff --git a/src/app/pages/Activity/index.tsx b/src/app/pages/Activity/index.tsx index 1bf0c387aa..98f4e9c62a 100644 --- a/src/app/pages/Activity/index.tsx +++ b/src/app/pages/Activity/index.tsx @@ -1,13 +1,9 @@ -import React, { memo, useCallback, useMemo, useState } from 'react'; - -import { noop } from 'lodash'; +import React, { memo, useCallback, useState } from 'react'; import { IconBase } from 'app/atoms'; import { PageModal } from 'app/atoms/PageModal'; -import { RadioButton } from 'app/atoms/RadioButton'; -import { TextButton } from 'app/atoms/TextButton'; -import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg'; import { ReactComponent as FilterOffIcon } from 'app/icons/base/filteroff.svg'; +import { ReactComponent as FilterOnIcon } from 'app/icons/base/filteron.svg'; import PageLayout from 'app/layouts/PageLayout'; import { useAssetsFilterOptionsSelector } from 'app/store/assets-filter-options/selectors'; import { FilterChain } from 'app/store/assets-filter-options/state'; @@ -15,45 +11,25 @@ import { ActivityListContainer, EvmActivityList, TezosActivityList, - MultichainActivityList, - FilterKind + MultichainActivityList } from 'app/templates/activity'; -import { T } from 'lib/i18n'; +import { NetworkSelectModalContent } from 'app/templates/NetworkSelectModal'; import { useBooleanState } from 'lib/ui/hooks'; -import Popper from 'lib/ui/Popper'; -import { OneOfChains, useAllEvmChains, useAllTezosChains } from 'temple/front'; -import { TempleChainKind } from 'temple/types'; - -import { SettingsCell } from '../../atoms/SettingsCell'; - -import { FilterChainDropdown } from './FilterChainDropdown'; +import { OneOfChains } from 'temple/front'; export const ActivityPage = memo(() => { const { filterChain: initFilterChain } = useAssetsFilterOptionsSelector(); const [filterChain, setFilterChain] = useState(initFilterChain); - const [filterKind, setFilterKind] = useState(null); - const [filtersModalOpen, setFiltersModalOpen, setFiltersModalClosed] = useBooleanState(false); - const handleFilterChainSelect = useMemo( - () => - initFilterChain - ? undefined - : (chain: OneOfChains | null) => { - setFilterChain(chain); - setFiltersModalClosed(); - }, - [setFiltersModalClosed] - ); - - const handleFilterKindSelect = useCallback( - (kind: FilterKind) => { - setFilterKind(kind); - if (kind !== filterKind) setFiltersModalClosed(); + const handleFilterChainSelect = useCallback( + (chain: OneOfChains | null) => { + setFilterChain(chain); + setFiltersModalClosed(); }, - [filterKind, setFiltersModalClosed] + [setFiltersModalClosed] ); return ( @@ -61,125 +37,36 @@ export const ActivityPage = memo(() => { pageTitle="Activity" contentPadding={false} headerRightElem={ - + } > - + + {() => ( + + )} + {filterChain ? ( {filterChain.kind === 'tezos' ? ( - + ) : ( - + )} ) : ( - + )} ); }); - -interface FiltersModalProps { - open: boolean; - filterKind: FilterKind; - filterChain: FilterChain; - handleFilterChainSelect?: SyncFn; - handleFilterKindSelect: SyncFn; - onRequestClose: EmptyFn; -} - -const FiltersModal = memo( - ({ open, filterKind, filterChain, handleFilterChainSelect, handleFilterKindSelect, onRequestClose }) => { - const tezosChains = useAllTezosChains(); - const evmChains = useAllEvmChains(); - - const chain = useMemo(() => { - if (!filterChain) return null; - - if (filterChain.kind === TempleChainKind.Tezos) return tezosChains[filterChain.chainId]; - - return evmChains[filterChain.chainId]; - }, [filterChain, evmChains, tezosChains]); - - return ( - -
- Filter by network - - ( - - )} - > - {({ ref, toggleOpened }) => ( - - {chain ? chain.nameI18nKey ? : chain.name : 'All Networks'} - - )} - -
- -
- } - onClick={() => handleFilterKindSelect(null)} - first - /> - - } - onClick={() => handleFilterKindSelect('send')} - /> - - } - onClick={() => handleFilterKindSelect('receive')} - /> - - } - onClick={() => handleFilterKindSelect('approve')} - /> - - } - onClick={() => handleFilterKindSelect('transfer')} - /> - - } - onClick={() => handleFilterKindSelect('bundle')} - /> -
-
- ); - } -); diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 2a6c372867..6b36fcbfd8 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -16,13 +16,12 @@ import { dispatch } from 'app/store'; import { setAssetsFilterChain } from 'app/store/assets-filter-options/actions'; import { FilterChain } from 'app/store/assets-filter-options/state'; import { SearchBarField } from 'app/templates/SearchField'; -import { T } from 'lib/i18n'; +import { T, t } from 'lib/i18n'; import { useScrollIntoViewOnMount } from 'lib/ui/use-scroll-into-view'; +import { isSearchStringApplicable, searchAndFilterItems } from 'lib/utils/search-items'; import { navigate } from 'lib/woozie'; import { - EvmChain, OneOfChains, - TezosChain, useAccount, useAccountAddressForEvm, useAccountAddressForTezos, @@ -65,7 +64,7 @@ interface ContentProps { handleNetworkSelect: (chain: OneOfChains | null) => void; } -const NetworkSelectModalContent = memo(({ opened, selectedNetwork, handleNetworkSelect }) => { +export const NetworkSelectModalContent = memo(({ opened, selectedNetwork, handleNetworkSelect }) => { const accountTezAddress = useAccountAddressForTezos(); const accountEvmAddress = useAccountAddressForEvm(); @@ -73,21 +72,19 @@ const NetworkSelectModalContent = memo(({ opened, selectedNetwork, const evmChains = useEnabledEvmChains(); const networks = useMemo( - () => [ALL_NETWORKS, ...(accountTezAddress ? tezosChains : []), ...(accountEvmAddress ? evmChains : [])], + () => [...(accountTezAddress ? tezosChains : []), ...(accountEvmAddress ? evmChains : [])], [accountEvmAddress, accountTezAddress, evmChains, tezosChains] ); const [searchValue, setSearchValue] = useState(''); const [searchValueDebounced] = useDebounce(searchValue, 300); + const inSearch = isSearchStringApplicable(searchValueDebounced); const [attractSelectedNetwork, setAttractSelectedNetwork] = useState(true); - const filteredNetworks = useMemo( - () => - searchValueDebounced - ? searchAndFilterNetworksByName(networks, searchValueDebounced) - : networks, - [searchValueDebounced, networks] + const searchedNetworks = useMemo( + () => (inSearch ? searchAndFilterChains(networks, searchValueDebounced) : networks), + [inSearch, searchValueDebounced, networks] ); useEffect(() => { @@ -111,11 +108,23 @@ const NetworkSelectModalContent = memo(({ opened, selectedNetwork,
- {filteredNetworks.length === 0 && } - - {filteredNetworks.map(network => ( + {searchedNetworks.length === 0 ? ( + + ) : ( + !inSearch && ( + + ) + )} + + {searchedNetworks.map(network => ( = ({ ); }; -type SearchNetwork = string | { name: string }; - -const searchAndFilterNetworksByName = (networks: T[], searchValue: string) => { - const preparedSearchValue = searchValue.trim().toLowerCase(); - - return networks.filter(network => { - if (typeof network === 'string') return network.toLowerCase().includes(preparedSearchValue); - - return network.name.toLowerCase().includes(preparedSearchValue); - }); -}; +function searchAndFilterChains(networks: OneOfChains[], searchValue: string) { + return searchAndFilterItems( + networks, + searchValue.trim(), + [ + { name: 'name', weight: 1 }, + { name: 'nameI18n', weight: 1 } + ], + ({ name, nameI18nKey }) => ({ + name, + nameI18n: nameI18nKey ? t(nameI18nKey) : undefined + }) + ); +} diff --git a/src/app/templates/activity/MultichainList.tsx b/src/app/templates/activity/MultichainList.tsx index 7c187ddf8d..f5b7db17d0 100644 --- a/src/app/templates/activity/MultichainList.tsx +++ b/src/app/templates/activity/MultichainList.tsx @@ -26,7 +26,7 @@ import { useActivitiesLoadingLogic } from './loading-logic'; import { FilterKind, getActivityFilterKind } from './utils'; interface Props { - filterKind: FilterKind; + filterKind?: FilterKind; } export const MultichainActivityList = memo(({ filterKind }) => { From b5c529bb28dfa217b29b592d07ad9b90be923b06 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 04:40:59 +0300 Subject: [PATCH 62/74] TW-1479: [EVM] Transactions history. Fix merge --- src/app/atoms/SettingsCell.tsx | 1 + .../components/select-with-modal/index.tsx | 6 +++--- .../select-with-modal/select-modal-option.tsx | 6 +++--- src/app/templates/activity/index.ts | 2 -- src/app/templates/enabling-setting.tsx | 10 +++++----- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app/atoms/SettingsCell.tsx b/src/app/atoms/SettingsCell.tsx index ba4131e68a..129fb1c5fc 100644 --- a/src/app/atoms/SettingsCell.tsx +++ b/src/app/atoms/SettingsCell.tsx @@ -60,6 +60,7 @@ interface Props { onClick: EmptyFn; } +// @ts-prune-ignore-next export const SettingsCell: FC = ({ title, first, onClick, icon }) => { return (
diff --git a/src/app/atoms/PageModal/index.tsx b/src/app/atoms/PageModal/index.tsx index 42912b8a4a..09af0896a4 100644 --- a/src/app/atoms/PageModal/index.tsx +++ b/src/app/atoms/PageModal/index.tsx @@ -41,8 +41,6 @@ export const PageModal: FC = ({ }) => { const { fullPage } = useAppEnv(); - // const isHidden = useDebounce(opened, 300); - return ( = ({ className, children }) => { const setContentHidingThrottled = useMemo(() => throttle((value: boolean) => setContentHiding(value), 300), []); - const onScroll = useMemo>( - () => event => { - const node = event.currentTarget; + const onScroll = useCallback>(event => { + const node = event.currentTarget; - setContentHidingThrottled(isContentHidingBelow(node)); - }, - [] - ); + setContentHidingThrottled(isContentHidingBelow(node)); + }, []); const resizeObserver = useMemo( () => - new ResizeObserver( - throttle(() => { - const node = ref.current; + new ResizeObserver(() => { + const node = ref.current; - if (node) setContentHidingThrottled(isContentHidingBelow(node)); - }, 300) - ), + if (node) setContentHidingThrottled(isContentHidingBelow(node)); + }), [] ); + useWillUnmount(() => void resizeObserver.disconnect()); + const refFn = useCallback((node: HTMLDivElement | null) => { ref.current = node; if (!node) return void setContentHiding(false); diff --git a/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx b/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx index 310ba99f6a..ee8c0f4a4b 100644 --- a/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx +++ b/src/app/layouts/PageLayout/BackupMnemonicOverlay/backup-options-modal.tsx @@ -31,7 +31,7 @@ export const BackupOptionsModal = memo(({ onSelect }) = onClick={onSelect} testID={BackupOptionsModalSelectors.manualBackupButton} > - + {t('backupManually')}
diff --git a/src/app/layouts/PageLayout/index.tsx b/src/app/layouts/PageLayout/index.tsx index 113a2d75ad..5bb7e6a40d 100644 --- a/src/app/layouts/PageLayout/index.tsx +++ b/src/app/layouts/PageLayout/index.tsx @@ -43,7 +43,6 @@ export interface PageLayoutProps extends DefaultHeaderProps, ScrollEdgesVisibili Header?: ComponentType; contentPadding?: boolean; dimBg?: boolean; - paperClassName?: string; headerChildren?: ReactNode; } diff --git a/src/app/pages/Home/OtherComponents/AssetBanner.tsx b/src/app/pages/Home/OtherComponents/AssetBanner.tsx index aeeb2d3032..5e579d1c96 100644 --- a/src/app/pages/Home/OtherComponents/AssetBanner.tsx +++ b/src/app/pages/Home/OtherComponents/AssetBanner.tsx @@ -114,7 +114,7 @@ const EvmAssetBanner = memo(({ evmChainId, assetSlug }) => return ( <>
- +
(({ tabSlug }) => { const [extensionModalOpened, openResetExtensionModal, closeResetExtensionModal] = useBooleanState(false); return ( - } headerChildren={headerChildren} dimBg> + } headerChildren={headerChildren}> {extensionModalOpened && } {activeTab ? ( diff --git a/src/app/pages/Welcome/Welcome.tsx b/src/app/pages/Welcome/Welcome.tsx index 49097253d7..f297d4579f 100644 --- a/src/app/pages/Welcome/Welcome.tsx +++ b/src/app/pages/Welcome/Welcome.tsx @@ -57,7 +57,7 @@ const Welcome = memo(() => { ); return ( - + >( ({ withoutAssetIcon, isNFT, children }) => { @@ -24,18 +24,18 @@ export const BundleIconsStack = memo
Date: Tue, 15 Oct 2024 17:24:24 +0300 Subject: [PATCH 64/74] TW-1479: [EVM] Transactions history. Refactor --- public/_locales/uk/messages.json | 3 +++ src/app/PageRouter.tsx | 2 +- src/app/atoms/SettingsCell.tsx | 20 +++++++++---------- src/app/templates/NetworkSelectModal.tsx | 2 +- .../ActivityItem/TezosActivityOperation.tsx | 1 - src/lib/activity/tezos/pre-parse.ts | 7 +++++-- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index f7ef48c6d4..0f3297c25b 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -1963,5 +1963,8 @@ }, "zarName": { "message": "Південноафриканський ранд" + }, + "allNetworks": { + "message": "Всі мережі" } } diff --git a/src/app/PageRouter.tsx b/src/app/PageRouter.tsx index 7f5ffa2e8f..bd03e944be 100644 --- a/src/app/PageRouter.tsx +++ b/src/app/PageRouter.tsx @@ -75,7 +75,7 @@ const ROUTE_MAP = Woozie.createMap([ )) ], - ['/activity/:chainKind?/:chainId?', onlyReady(() => )], + ['/activity', onlyReady(() => )], ['/connect-ledger', onlyReady(onlyInFullPage(() => ))], ['/receive', onlyReady(() => )], [ diff --git a/src/app/atoms/SettingsCell.tsx b/src/app/atoms/SettingsCell.tsx index 129fb1c5fc..82589fc181 100644 --- a/src/app/atoms/SettingsCell.tsx +++ b/src/app/atoms/SettingsCell.tsx @@ -61,15 +61,13 @@ interface Props { } // @ts-prune-ignore-next -export const SettingsCell: FC = ({ title, first, onClick, icon }) => { - return ( - - ); -}; + {icon} + +); diff --git a/src/app/templates/NetworkSelectModal.tsx b/src/app/templates/NetworkSelectModal.tsx index 6b36fcbfd8..51b622bd9e 100644 --- a/src/app/templates/NetworkSelectModal.tsx +++ b/src/app/templates/NetworkSelectModal.tsx @@ -185,7 +185,7 @@ export const Network: FC = ({ {Icon}
- {isAllNetworks ? ALL_NETWORKS : network.name} + {isAllNetworks ? : network.name} {showBalance && ( diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index 08b4e5640f..c6c9c0517b 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -65,7 +65,6 @@ export function buildTezosOperationAsset( tokenId, amountSigned, decimals, - // nft: isTezosCollectibleMetadata(assetMetadata), symbol: assetMetadata?.symbol, nft: assetMetadata ? isTezosCollectibleMetadata(assetMetadata) : undefined }; diff --git a/src/lib/activity/tezos/pre-parse.ts b/src/lib/activity/tezos/pre-parse.ts index 00e7bcc6c4..d419b8008b 100644 --- a/src/lib/activity/tezos/pre-parse.ts +++ b/src/lib/activity/tezos/pre-parse.ts @@ -127,7 +127,7 @@ function reduceOneTzktTransactionOperation( return _buildReturn({ amount, from, to, contract, subtype: 'transfer' }); } else if (isTzktOperParam_Fa2_transfer(parameter)) { const values = reduceParameterFa2TransferValues(parameter.value, address); - const firstVal = values[0]; + const firstVal = values.at(0); // (!) Here we abandon other but 1st non-zero-amount values if (firstVal == null) return null; @@ -216,12 +216,15 @@ function reduceParameterFa2TransferValues(values: ParameterFa2Transfer['value'], const result: ReducedParameterFa2Values[] = []; for (const val of values) { + const firstTx = val.txs.at(0); + if (firstTx == null) continue; + /* We assume, that all `val.txs` items have same `token_id` value. Visit https://tezos.b9lab.com/fa2 - There is a link to code in Smartpy IDE. Fa2 token-standard/smartcontract literally has it in its code. */ - const tokenId = val.txs[0]!.token_id; + const tokenId = firstTx.token_id; const fromAddress = val.from_; From a80bc78a24e60cee4a9fb1eeb48362d73f2cb4ff Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 17:30:49 +0300 Subject: [PATCH 65/74] TW-1479: [EVM] Transactions history. Multichain. Refactor --- src/app/atoms/Avatar.tsx | 4 ++-- src/app/atoms/SimpleSegmentControl.tsx | 2 +- src/app/atoms/ToggleSwitch.tsx | 2 +- src/app/pages/Market/BuyPageOption.tsx | 2 +- .../PaymentProviderTag.tsx | 2 +- .../ActivityOperationBase/utils.tsx | 23 ++++++++++++------- .../hypelab-image-promotion.tsx | 2 +- .../components/text-promotion-view.tsx | 2 +- .../templates/partners-promotion/index.tsx | 2 +- tailwind.config.js | 8 +++++-- 10 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/app/atoms/Avatar.tsx b/src/app/atoms/Avatar.tsx index 35f1a6ed50..bbb5240e16 100644 --- a/src/app/atoms/Avatar.tsx +++ b/src/app/atoms/Avatar.tsx @@ -24,11 +24,11 @@ export type AvatarProps = AvatarDivProps | AvatarButtonProps; const sizeButtonClassNames = { 24: 'w-6 h-6 rounded border p-px', 32: 'w-8 h-8 rounded-md border p-px', - 60: 'w-15 h-15 rounded-2.5 border-1.5 p-[0.5px]' + 60: 'w-15 h-15 rounded-10 border-1.5 p-[0.5px]' }; const sizeContainerClassNames = { - 24: 'rounded-0.75', + 24: 'rounded-3', 32: 'rounded', 60: 'rounded-lg border border-white' }; diff --git a/src/app/atoms/SimpleSegmentControl.tsx b/src/app/atoms/SimpleSegmentControl.tsx index fd343563f9..0992bd0aae 100644 --- a/src/app/atoms/SimpleSegmentControl.tsx +++ b/src/app/atoms/SimpleSegmentControl.tsx @@ -21,7 +21,7 @@ export const SimpleSegmentControl = memo( {/* Slider */}
diff --git a/src/app/atoms/ToggleSwitch.tsx b/src/app/atoms/ToggleSwitch.tsx index 279829d29f..b1c293dff3 100644 --- a/src/app/atoms/ToggleSwitch.tsx +++ b/src/app/atoms/ToggleSwitch.tsx @@ -38,7 +38,7 @@ export const ToggleSwitch = forwardRef((props, ref) => () => clsx( 'absolute h-full shadow-drop duration-300 ease-out', - small ? 'w-4 rounded-1.25' : 'w-6 rounded-md', + small ? 'w-4 rounded-5' : 'w-6 rounded-md', disabled ? 'bg-lines' : 'bg-white', (() => { if (localChecked) return small ? 'left-3.5 right-0' : 'left-5 right-0'; diff --git a/src/app/pages/Market/BuyPageOption.tsx b/src/app/pages/Market/BuyPageOption.tsx index 2b6517fb6c..94e48ba962 100644 --- a/src/app/pages/Market/BuyPageOption.tsx +++ b/src/app/pages/Market/BuyPageOption.tsx @@ -13,7 +13,7 @@ interface BuyPageOptionProps extends TestIDProps { export const BuyPageOption: FC = ({ Icon, title, to, ...testIDProps }) => ( diff --git a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/PaymentProviderTag.tsx b/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/PaymentProviderTag.tsx index 2731c7a279..0921728f61 100644 --- a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/PaymentProviderTag.tsx +++ b/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/PaymentProviderTag.tsx @@ -6,7 +6,7 @@ export interface PaymentProviderTagProps { } export const PaymentProviderTag: FC = ({ bgColor, text }) => ( -
+
{text}
); diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx index 3070d7732e..2f7bee85e4 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/utils.tsx @@ -24,13 +24,22 @@ export const BundleIconsStack = memo
-
- {children} -
+
{children}
); diff --git a/src/app/templates/partners-promotion/components/hypelab-promotion/hypelab-image-promotion.tsx b/src/app/templates/partners-promotion/components/hypelab-promotion/hypelab-image-promotion.tsx index 61972725d2..e4c412775c 100644 --- a/src/app/templates/partners-promotion/components/hypelab-promotion/hypelab-image-promotion.tsx +++ b/src/app/templates/partners-promotion/components/hypelab-promotion/hypelab-image-promotion.tsx @@ -93,7 +93,7 @@ export const HypelabImagePromotion: FC diff --git a/src/app/templates/partners-promotion/components/text-promotion-view.tsx b/src/app/templates/partners-promotion/components/text-promotion-view.tsx index f53d7d1c75..a4e5771b50 100644 --- a/src/app/templates/partners-promotion/components/text-promotion-view.tsx +++ b/src/app/templates/partners-promotion/components/text-promotion-view.tsx @@ -60,7 +60,7 @@ export const TextPromotionView = memo( (
diff --git a/tailwind.config.js b/tailwind.config.js index ea613a0980..b3362cfde4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -286,9 +286,13 @@ module.exports = { gap: theme => theme('spacing'), borderRadius: { - 0.75: '0.1875rem', // 3px - 1.25: '0.3125rem', // 5px 2.5: '0.625rem', // 10px + 3: 3, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 10: 10, circle: '50%', inherit: 'inherit' }, From b02d81b07e6cf5e3318666c9af024d6d6fc84056 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Oct 2024 13:28:59 +0300 Subject: [PATCH 66/74] TW-1479: [EVM] Transactions history. Refactor --- tailwind.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index b3362cfde4..211ab66ee4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -286,7 +286,6 @@ module.exports = { gap: theme => theme('spacing'), borderRadius: { - 2.5: '0.625rem', // 10px 3: 3, 5: 5, 6: 6, From bac34e1833ffbb7ee5d97d8492b1098362275a10 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Oct 2024 18:13:48 +0300 Subject: [PATCH 67/74] TW-1479: [EVM] Transactions history. Refactor --- src/app/store/assets-filter-options/state.ts | 14 ++------ .../ActivityOperationBase/index.tsx | 30 +++++++++-------- .../activity/ActivityItem/AddressChip.tsx | 32 ++++++++++--------- .../activity/ActivityItem/EvmActivity.tsx | 18 +++++------ .../ActivityItem/EvmActivityOperation.tsx | 9 +++--- .../activity/ActivityItem/TezosActivity.tsx | 18 +++++------ .../ActivityItem/TezosActivityOperation.tsx | 9 +++--- src/app/templates/activity/utils.ts | 28 +++++++++------- src/temple/front/chains.ts | 20 ++++++++---- 9 files changed, 94 insertions(+), 84 deletions(-) diff --git a/src/app/store/assets-filter-options/state.ts b/src/app/store/assets-filter-options/state.ts index 97118a952d..fae6fa1235 100644 --- a/src/app/store/assets-filter-options/state.ts +++ b/src/app/store/assets-filter-options/state.ts @@ -1,16 +1,6 @@ -import { TempleChainKind } from 'temple/types'; +import { BasicChain } from 'temple/front/chains'; -interface EvmChain { - kind: TempleChainKind.EVM; - chainId: number; -} - -interface TezosChain { - kind: TempleChainKind.Tezos; - chainId: string; -} - -export type FilterChain = EvmChain | TezosChain | null; +export type FilterChain = BasicChain | null; export interface AssetsFilterOptionsStateInterface { filterChain: FilterChain; diff --git a/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx b/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx index 753c0dbfbb..cc1dd25e1d 100644 --- a/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx +++ b/src/app/templates/activity/ActivityItem/ActivityOperationBase/index.tsx @@ -12,13 +12,15 @@ import { ActivityOperKindEnum, ActivityOperTransferType, ActivityStatus } from ' import { isTransferActivityOperKind } from 'lib/activity/utils'; import { toEvmAssetSlug, toTezosAssetSlug } from 'lib/assets/utils'; import { atomsToTokens } from 'lib/temple/helpers'; +import { BasicChain } from 'temple/front/chains'; +import { TempleChainKind } from 'temple/types'; import { FaceKind } from '../../utils'; import { AssetIconPlaceholder, BundleIconsStack, getIconByKind, getTitleByKind, StatusTag } from './utils'; interface Props { - chainId: string | number; + chain: BasicChain; kind: FaceKind; transferType?: ActivityOperTransferType; hash: string; @@ -42,9 +44,11 @@ export interface ActivityItemBaseAssetProp { } export const ActivityOperationBaseComponent = memo( - ({ kind, transferType, hash, chainId, asset, blockExplorerUrl, status, withoutAssetIcon, onClick, addressChip }) => { + ({ kind, transferType, hash, chain, asset, blockExplorerUrl, status, withoutAssetIcon, onClick, addressChip }) => { + const isForEvm = chain.kind === TempleChainKind.EVM; + const assetSlug = asset - ? typeof chainId === 'number' + ? isForEvm ? toEvmAssetSlug(asset.contract, asset.tokenId) : toTezosAssetSlug(asset.contract, asset.tokenId) : null; @@ -93,8 +97,8 @@ export const ActivityOperationBaseComponent = memo( return ( ( } ); - }, [asset, kind, assetSlug, chainId]); + }, [asset, kind, assetSlug, chain.chainId, isForEvm]); const chipJsx = useMemo( () => @@ -146,9 +150,9 @@ export const ActivityOperationBaseComponent = memo( const placeholderJsx = ; - return typeof chainId === 'number' ? ( + return isForEvm ? ( ( /> ) : ( ( fallback={placeholderJsx} /> ); - }, [chainId, withoutAssetIcon, kind, transferType, asset?.iconURL, asset?.symbol, isNFT, assetSlug]); + }, [chain, isForEvm, withoutAssetIcon, kind, transferType, asset?.iconURL, asset?.symbol, isNFT, assetSlug]); return (
(
{faceIconJsx}
)} - {withoutAssetIcon ? null : typeof chainId === 'number' ? ( - + {withoutAssetIcon ? null : isForEvm ? ( + ) : ( - + )}
diff --git a/src/app/templates/activity/ActivityItem/AddressChip.tsx b/src/app/templates/activity/ActivityItem/AddressChip.tsx index 079a07c611..d6da8ca3ab 100644 --- a/src/app/templates/activity/ActivityItem/AddressChip.tsx +++ b/src/app/templates/activity/ActivityItem/AddressChip.tsx @@ -11,21 +11,23 @@ interface Props { export const OperAddressChip: FC = ({ operation }) => { const info = useMemo(() => { - if (operation.kind === ActivityOperKindEnum.approve) return { title: 'For', address: operation.spenderAddress }; - - if (operation.kind === ActivityOperKindEnum.interaction) - return operation.withAddress ? { title: 'With', address: operation.withAddress } : undefined; - - if (operation.type === ActivityOperTransferType.sendToAccount) return { title: 'To', address: operation.toAddress }; - - if (operation.type === ActivityOperTransferType.receiveFromAccount) - return { title: 'From', address: operation.fromAddress }; - - if (operation.type === ActivityOperTransferType.send) return { title: 'With', address: operation.toAddress }; - - if (operation.type === ActivityOperTransferType.receive) return { title: 'With', address: operation.fromAddress }; - - return; + switch (operation.kind) { + case ActivityOperKindEnum.approve: + return { title: 'For', address: operation.spenderAddress }; + case ActivityOperKindEnum.interaction: + return operation.withAddress ? { title: 'With', address: operation.withAddress } : undefined; + } + + switch (operation.type) { + case ActivityOperTransferType.sendToAccount: + return { title: 'To', address: operation.toAddress }; + case ActivityOperTransferType.receiveFromAccount: + return { title: 'From', address: operation.fromAddress }; + case ActivityOperTransferType.send: + return { title: 'With', address: operation.toAddress }; + case ActivityOperTransferType.receive: + return { title: 'With', address: operation.fromAddress }; + } }, [operation]); if (!info) return null; diff --git a/src/app/templates/activity/ActivityItem/EvmActivity.tsx b/src/app/templates/activity/ActivityItem/EvmActivity.tsx index edfb8373dd..75821b5265 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivity.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivity.tsx @@ -7,7 +7,7 @@ import { fromAssetSlug, toEvmAssetSlug } from 'lib/assets/utils'; import { useGetEvmChainAssetMetadata } from 'lib/metadata'; import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; -import { EvmChain } from 'temple/front/chains'; +import { BasicEvmChain } from 'temple/front/chains'; import { ActivityItemBaseAssetProp, ActivityOperationBaseComponent } from './ActivityOperationBase'; import { BundleModalContent } from './BundleModal'; @@ -15,7 +15,7 @@ import { EvmActivityOperationComponent } from './EvmActivityOperation'; interface Props { activity: EvmActivity; - chain: EvmChain; + chain: BasicEvmChain; assetSlug?: string; } @@ -29,7 +29,7 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) (({ activity, chain, assetSlug }) return ( @@ -49,17 +49,17 @@ export const EvmActivityComponent = memo(({ activity, chain, assetSlug }) interface BatchProps { activity: EvmActivity; - chainId: number; + chain: BasicEvmChain; assetSlug?: string; blockExplorerUrl?: string; } -const EvmActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { +const EvmActivityBatchComponent = memo(({ activity, chain, assetSlug, blockExplorerUrl }) => { const [expanded, , , toggleExpanded] = useBooleanState(false); const { hash, operations, status } = activity; - const getMetadata = useGetEvmChainAssetMetadata(chainId); + const getMetadata = useGetEvmChainAssetMetadata(chain.chainId); const faceSlug = useMemo(() => { if (assetSlug) return assetSlug; @@ -119,7 +119,7 @@ const EvmActivityBatchComponent = memo(({ activity, chainId, assetSl (({ activity, chainId, assetSl key={`${hash}-${j}`} hash={hash} operation={operation} - chainId={chainId} + chain={chain} blockExplorerUrl={blockExplorerUrl} withoutOperHashChip /> diff --git a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx index c164db9926..3cf9a91543 100644 --- a/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/EvmActivityOperation.tsx @@ -3,6 +3,7 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, EvmOperation, ActivityStatus } from 'lib/activity'; import { toEvmAssetSlug } from 'lib/assets/utils'; import { useEvmAssetMetadata } from 'lib/metadata'; +import { BasicEvmChain } from 'temple/front/chains'; import { getActivityOperTransferType } from '../utils'; @@ -12,7 +13,7 @@ import { OperAddressChip } from './AddressChip'; interface Props { hash: string; operation?: EvmOperation; - chainId: number; + chain: BasicEvmChain; blockExplorerUrl?: string; status?: ActivityStatus; withoutAssetIcon?: boolean; @@ -20,11 +21,11 @@ interface Props { } export const EvmActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { + ({ hash, operation, chain, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { const assetBase = operation?.asset; const assetSlug = assetBase?.contract ? toEvmAssetSlug(assetBase.contract) : undefined; - const assetMetadata = useEvmAssetMetadata(assetSlug ?? '', chainId); + const assetMetadata = useEvmAssetMetadata(assetSlug ?? '', chain.chainId); const asset = useMemo(() => { if (!assetBase) return; @@ -54,7 +55,7 @@ export const EvmActivityOperationComponent = memo( kind={operation?.kind ?? ActivityOperKindEnum.interaction} transferType={getActivityOperTransferType(operation)} hash={hash} - chainId={chainId} + chain={chain} asset={asset} blockExplorerUrl={blockExplorerUrl} status={status} diff --git a/src/app/templates/activity/ActivityItem/TezosActivity.tsx b/src/app/templates/activity/ActivityItem/TezosActivity.tsx index fb89d79e18..43748a9fe7 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivity.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivity.tsx @@ -7,7 +7,7 @@ import { useGetChainTokenOrGasMetadata } from 'lib/metadata'; import { useBooleanState } from 'lib/ui/hooks'; import { ZERO } from 'lib/utils/numbers'; import { useExplorerHref } from 'temple/front/block-explorers'; -import { TezosChain } from 'temple/front/chains'; +import { BasicTezosChain } from 'temple/front/chains'; import { ActivityOperationBaseComponent } from './ActivityOperationBase'; import { BundleModalContent } from './BundleModal'; @@ -15,7 +15,7 @@ import { TezosActivityOperationComponent, buildTezosOperationAsset } from './Tez interface Props { activity: TezosActivity; - chain: TezosChain; + chain: BasicTezosChain; assetSlug?: string; } @@ -31,7 +31,7 @@ export const TezosActivityComponent = memo(({ activity, chain, assetSlug (({ activity, chain, assetSlug return ( @@ -51,17 +51,17 @@ export const TezosActivityComponent = memo(({ activity, chain, assetSlug interface BatchProps { activity: TezosActivity; - chainId: string; + chain: BasicTezosChain; assetSlug?: string; blockExplorerUrl?: string; } -const TezosActivityBatchComponent = memo(({ activity, chainId, assetSlug, blockExplorerUrl }) => { +const TezosActivityBatchComponent = memo(({ activity, chain, assetSlug, blockExplorerUrl }) => { const [expanded, , , toggleExpanded] = useBooleanState(false); const { hash, operations, status } = activity; - const getMetadata = useGetChainTokenOrGasMetadata(chainId); + const getMetadata = useGetChainTokenOrGasMetadata(chain.chainId); const batchAsset = useMemo(() => { const faceSlug = @@ -92,7 +92,7 @@ const TezosActivityBatchComponent = memo(({ activity, chainId, asset (({ activity, chainId, asset key={`${hash}-${j}`} hash={hash} operation={operation} - chainId={chainId} + chain={chain} blockExplorerUrl={blockExplorerUrl} withoutOperHashChip /> diff --git a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx index c6c9c0517b..e44550a40f 100644 --- a/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx +++ b/src/app/templates/activity/ActivityItem/TezosActivityOperation.tsx @@ -3,6 +3,7 @@ import React, { memo, useMemo } from 'react'; import { ActivityOperKindEnum, TezosOperation, ActivityStatus } from 'lib/activity'; import { fromAssetSlug } from 'lib/assets'; import { AssetMetadataBase, isTezosCollectibleMetadata, useTezosAssetMetadata } from 'lib/metadata'; +import { BasicTezosChain } from 'temple/front/chains'; import { getActivityOperTransferType } from '../utils'; @@ -12,7 +13,7 @@ import { OperAddressChip } from './AddressChip'; interface Props { hash: string; operation?: TezosOperation; - chainId: string; + chain: BasicTezosChain; status?: ActivityStatus; blockExplorerUrl: string | nullish; withoutAssetIcon?: boolean; @@ -20,9 +21,9 @@ interface Props { } export const TezosActivityOperationComponent = memo( - ({ hash, operation, chainId, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { + ({ hash, operation, chain, blockExplorerUrl, status, withoutAssetIcon, withoutOperHashChip }) => { const assetSlug = operation?.assetSlug; - const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chainId); + const assetMetadata = useTezosAssetMetadata(assetSlug ?? '', chain.chainId); const asset = useMemo( () => (assetSlug ? buildTezosOperationAsset(assetSlug, assetMetadata, operation.amountSigned) : undefined), @@ -39,7 +40,7 @@ export const TezosActivityOperationComponent = memo( kind={operation?.kind ?? ActivityOperKindEnum.interaction} transferType={getActivityOperTransferType(operation)} hash={hash} - chainId={chainId} + chain={chain} asset={asset} status={status} blockExplorerUrl={blockExplorerUrl ?? undefined} diff --git a/src/app/templates/activity/utils.ts b/src/app/templates/activity/utils.ts index 4f773092c7..d2f40b07f2 100644 --- a/src/app/templates/activity/utils.ts +++ b/src/app/templates/activity/utils.ts @@ -19,18 +19,22 @@ export function getActivityFilterKind(activity: Activity): FilterKind { if (!operation) return null; - const kind = operation.kind; - - if (kind === ActivityOperKindEnum.interaction) return null; - if (kind === ActivityOperKindEnum.approve) return 'approve'; - - const type = operation.type; - - if (type === ActivityOperTransferType.send || type === ActivityOperTransferType.receive) return 'transfer'; - - if (type === ActivityOperTransferType.sendToAccount) return 'send'; - - if (type === ActivityOperTransferType.receiveFromAccount) return 'receive'; + switch (operation.kind) { + case ActivityOperKindEnum.interaction: + return null; + case ActivityOperKindEnum.approve: + return 'approve'; + } + + switch (operation.type) { + case ActivityOperTransferType.send: + case ActivityOperTransferType.receive: + return 'transfer'; + case ActivityOperTransferType.sendToAccount: + return 'send'; + case ActivityOperTransferType.receiveFromAccount: + return 'receive'; + } return null; } diff --git a/src/temple/front/chains.ts b/src/temple/front/chains.ts index 8a736e5d05..da80fe4128 100644 --- a/src/temple/front/chains.ts +++ b/src/temple/front/chains.ts @@ -6,6 +6,18 @@ import type { TempleChainKind } from 'temple/types'; import { useAllTezosChains, useAllEvmChains } from './ready'; +export interface BasicEvmChain { + kind: TempleChainKind.EVM; + chainId: number; +} + +export interface BasicTezosChain { + kind: TempleChainKind.Tezos; + chainId: string; +} + +export type BasicChain = BasicEvmChain | BasicTezosChain; + interface ChainBase { rpcBaseURL: string; name: string; @@ -13,16 +25,12 @@ interface ChainBase { disabled?: boolean; } -export interface TezosChain extends ChainBase { - kind: TempleChainKind.Tezos; - chainId: string; +export interface TezosChain extends BasicTezosChain, ChainBase { rpc: StoredTezosNetwork; allRpcs: StoredTezosNetwork[]; } -export interface EvmChain extends ChainBase { - kind: TempleChainKind.EVM; - chainId: number; +export interface EvmChain extends BasicEvmChain, ChainBase { currency: EvmNativeTokenMetadata; rpc: StoredEvmNetwork; allRpcs: StoredEvmNetwork[]; From f0acb7d316f8ab1e106000cf58a8b365cc2fced6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 28 Oct 2024 12:50:45 +0200 Subject: [PATCH 68/74] Fix audit-deps --- package.json | 1 + yarn.lock | 903 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 849 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index ad3a862cdc..e572d93a68 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,7 @@ "react-dev-utils/fork-ts-checker-webpack-plugin": "^9", "tslib": "^2.4.0", "@types/react": "18.0.15", + "@types/react-dev-utils/@types/webpack-dev-server": "^4", "@taquito/taquito": "20.0.0", "@taquito/utils": "20.0.0", "@taquito/beacon-wallet": "20.0.0", diff --git a/yarn.lock b/yarn.lock index d229793e60..58118562b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2061,6 +2061,26 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz#33ca57ee29d12feef540f2139225597469dec894" + integrity sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.5.0.tgz#6008e35b9d9d8ee27bc4bfaa70c8cbf33a537b4c" + integrity sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA== + "@ledgerhq/devices@^5.34.0", "@ledgerhq/devices@^5.51.1": version "5.51.1" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.51.1.tgz#d741a4a5d8f17c2f9d282fd27147e6fe1999edb7" @@ -2161,6 +2181,11 @@ resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.12.0.tgz#ad903528bf3687a44da435d7b2479d724d374f5d" integrity sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA== +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + "@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" @@ -3338,6 +3363,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + "@types/bs58check@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/bs58check/-/bs58check-2.1.0.tgz#7d25a8b88fe7a9e315d2647335ee3c43c8fdb0c0" @@ -3361,10 +3393,10 @@ "@types/node" "*" source-map "^0.6.0" -"@types/connect-history-api-fallback@*": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" "@types/node" "*" @@ -3430,6 +3462,16 @@ "@types/qs" "*" "@types/range-parser" "*" +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + "@types/express@*": version "4.17.14" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" @@ -3440,6 +3482,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/filesystem@*": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf" @@ -3513,10 +3565,15 @@ "@types/tapable" "^1" "@types/webpack" "^4" -"@types/http-proxy@^1.17.5": - version "1.17.9" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" - integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== dependencies: "@types/node" "*" @@ -3607,11 +3664,23 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + "@types/node-forge@^1.3.1": version "1.3.10" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.10.tgz#62a19d4f75a8b03290578c2b04f294b1a5a71b07" @@ -3741,6 +3810,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -3758,6 +3832,21 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + "@types/serve-static@*": version "1.15.0" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" @@ -3766,6 +3855,15 @@ "@types/mime" "*" "@types/node" "*" +"@types/serve-static@^1.15.5": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + "@types/sha.js@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/sha.js/-/sha.js-2.4.0.tgz#bce682ef860b40f419d024fa08600c3b8d24bb01" @@ -3773,6 +3871,13 @@ dependencies: "@types/node" "*" +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -3827,16 +3932,12 @@ resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.9.1.tgz#fcb5c352e2e461d0287774db89bc326b15b47844" integrity sha512-6aNzPIhqKlAV9t06nwSH3/veAceYE2dS2RVFZI8V1+UXHqsFNB6cRwxNmheiBvEGRc45E/gyZNzH0xAYIC27KA== -"@types/webpack-dev-server@3": - version "3.11.6" - resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz#d8888cfd2f0630203e13d3ed7833a4d11b8a34dc" - integrity sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ== +"@types/webpack-dev-server@3", "@types/webpack-dev-server@^4": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-4.7.2.tgz#a12d9881aa23cdd4cecbb2d31fa784a45c4967e0" + integrity sha512-Y3p0Fmfvp0MHBDoCzo+xFJaWTw0/z37mWIo6P15j+OtmUDLvznJWdZNeD7Q004R+MpQlys12oXbXsrXRmxwg4Q== dependencies: - "@types/connect-history-api-fallback" "*" - "@types/express" "*" - "@types/serve-static" "*" - "@types/webpack" "^4" - http-proxy-middleware "^1.0.0" + webpack-dev-server "*" "@types/webpack-sources@*", "@types/webpack-sources@^3.2.0": version "3.2.0" @@ -3875,6 +3976,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.5.10": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -4365,6 +4473,14 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -4497,6 +4613,11 @@ ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -4619,6 +4740,11 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" @@ -4965,6 +5091,11 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + big.js@^6.2.1: version "6.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" @@ -5030,6 +5161,32 @@ bn.js@^5.0.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -5225,6 +5382,23 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -5308,7 +5482,7 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -chokidar@^3.5.3: +chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -5458,6 +5632,11 @@ colord@^2.9.3: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== +colorette@^2.0.10: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + colorette@^2.0.14: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -5515,11 +5694,36 @@ compress-commons@^5.0.1: normalize-path "^3.0.0" readable-stream "^3.6.0" +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + consola@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" @@ -5535,6 +5739,18 @@ consumable-stream@^2.0.0: resolved "https://registry.yarnpkg.com/consumable-stream/-/consumable-stream-2.0.0.tgz#11d3c7281b747eb9efd31c199b3a8b1711bec654" integrity sha512-I6WA2JVYXs/68rEvi1ie3rZjP6qusTVFEQkbzR+WC+fY56TpwiGTIDJETsrnlxv5CsnmK69ps6CkYvIbpEEqBA== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.4.0, convert-source-map@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -5552,6 +5768,16 @@ cookie-es@^1.0.0: resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.0.0.tgz#4759684af168dfc54365b2c2dda0a8d7ee1e4865" integrity sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ== +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + copy-webpack-plugin@11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" @@ -6005,6 +6231,13 @@ debounce@^1.2.1: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== +debug@2.6.9, debug@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -6012,13 +6245,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -6056,6 +6282,19 @@ deepmerge@^4.3.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -6070,6 +6309,11 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -6123,6 +6367,16 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -6141,6 +6395,11 @@ destr@^2.0.1, destr@^2.0.2: resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.2.tgz#8d3c0ee4ec0a76df54bc8b819bca215592a8c218" integrity sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-browser@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" @@ -6156,6 +6415,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + detect-port-alt@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" @@ -6205,6 +6469,13 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -6348,6 +6619,11 @@ ed25519-hd-key@1.1.2: create-hmac "1.1.7" tweetnacl "1.0.3" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + effector@21.2.0: version "21.2.0" resolved "https://registry.yarnpkg.com/effector/-/effector-21.2.0.tgz#7f5e17bfbd41f9a8fb07f6fe8d6b2a14fc63e451" @@ -6386,6 +6662,16 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -6600,6 +6886,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -6883,6 +7174,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + event-emitter@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -6971,6 +7267,43 @@ expect@^27.4.2: jest-message-util "^27.4.2" jest-regex-util "^27.4.0" +express@^4.19.2: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + ext@^1.1.2: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -7031,6 +7364,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -7102,6 +7442,19 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -7212,11 +7565,21 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + fs-extra@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" @@ -7506,7 +7869,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7563,6 +7926,11 @@ h3@^1.10.1, h3@^1.8.2: uncrypto "^0.1.3" unenv "^1.9.0" +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -7673,6 +8041,16 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -7680,6 +8058,11 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-entities@^2.4.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -7719,6 +8102,37 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -7728,12 +8142,12 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-proxy-middleware@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665" - integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== +http-proxy-middleware@^2.0.3: + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: - "@types/http-proxy" "^1.17.5" + "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" is-glob "^4.0.1" is-plain-obj "^3.0.0" @@ -7771,6 +8185,11 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7854,7 +8273,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7908,6 +8327,16 @@ ioredis@^5.3.2: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + iron-webcrypto@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.0.0.tgz#e3b689c0c61b434a0a4cb82d0aeabbc8b672a867" @@ -8061,6 +8490,11 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -8926,6 +9360,14 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" +launch-editor@^2.6.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -9225,6 +9667,11 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + memfs@^3.4.1: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" @@ -9232,6 +9679,16 @@ memfs@^3.4.1: dependencies: fs-monkey "^1.0.4" +memfs@^4.6.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.14.0.tgz#48d5e85a03ea0b428280003212fbca3063531be3" + integrity sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA== + dependencies: + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.3.0" + tree-dump "^1.0.1" + tslib "^2.0.0" + memoizee@^0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" @@ -9265,6 +9722,11 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -9275,6 +9737,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -9301,6 +9768,11 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + mime-types@^2.1.12: version "2.1.34" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" @@ -9308,13 +9780,18 @@ mime-types@^2.1.12: dependencies: mime-db "1.51.0" -mime-types@^2.1.27: +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -9442,11 +9919,19 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + multiformats@^9.4.2: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -9486,6 +9971,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -9538,7 +10028,7 @@ node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.7.0: dependencies: whatwg-url "^5.0.0" -node-forge@^1.3.1: +node-forge@^1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== @@ -9710,6 +10200,11 @@ object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + ofetch@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" @@ -9729,6 +10224,18 @@ on-exit-leak-free@^0.2.0: resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== +on-finished@2.4.1, on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -9750,6 +10257,16 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +open@^10.0.3: + version "10.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1" + integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -9836,6 +10353,15 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-retry@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.0.tgz#8d6df01af298750009691ce2f9b3ad2d5968f3bd" + integrity sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -9900,6 +10426,11 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascal-case@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" @@ -9961,6 +10492,11 @@ path-scurry@^1.10.1, path-scurry@^1.6.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -10745,6 +11281,14 @@ property-expr@^2.0.5: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -10816,6 +11360,13 @@ qrcode-svg@^1.1.0: resolved "https://registry.yarnpkg.com/qrcode-svg/-/qrcode-svg-1.1.0.tgz#2087549843dc5d0a3f8e8aa046cd9bcf23708cc3" integrity sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@^6.11.1: version "6.11.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" @@ -10873,6 +11424,21 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-base16-styling@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" @@ -11066,10 +11632,10 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@^2.0.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== +readable-stream@^2.0.1, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -11079,10 +11645,10 @@ readable-stream@^2.0.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^2.3.5, readable-stream@~2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== +readable-stream@^2.0.5: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -11092,19 +11658,19 @@ readable-stream@^2.3.5, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +readable-stream@^3.0.6, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -11350,7 +11916,7 @@ response-iterator@^0.2.6: resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw== -retry@0.13.1: +retry@0.13.1, retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== @@ -11387,6 +11953,11 @@ rn-host-detect@^1.2.0: resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0" integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A== +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -11418,12 +11989,12 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -11495,6 +12066,19 @@ scryptsy@2.1.0: resolved "https://registry.yarnpkg.com/scryptsy/-/scryptsy-2.1.0.tgz#8d1e8d0c025b58fdd25b6fa9a0dc905ee8faa790" integrity sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w== +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -11510,6 +12094,25 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -11527,6 +12130,29 @@ seroval@^1.0.3: resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.0.4.tgz#0400d24a48b1f6f44b8a3d55af0476a1c5e8643f" integrity sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg== +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + set-cookie-parser@^2.4.8: version "2.6.0" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" @@ -11559,6 +12185,16 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -11598,7 +12234,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1, shell-quote@^1.7.3: +shell-quote@^1.6.1, shell-quote@^1.7.3, shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== @@ -11664,6 +12300,15 @@ socketcluster-client@^17.1.0: vinyl-buffer "^1.0.1" ws "^8.9.0" +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + solid-js@^1.7.11: version "1.8.14" resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.14.tgz#979aa8000eedede07e7f5a5773a4714ab3eeb113" @@ -11729,6 +12374,29 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" @@ -11756,6 +12424,16 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + std-env@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" @@ -12157,6 +12835,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + thread-stream@^0.15.1: version "0.15.2" resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4" @@ -12187,6 +12870,11 @@ through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + timers-browserify@^2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" @@ -12250,6 +12938,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -12276,6 +12969,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +tree-dump@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" + integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== + "true-myth@^4.1.0": version "4.1.1" resolved "https://registry.yarnpkg.com/true-myth/-/true-myth-4.1.1.tgz#ff4ac9d5130276e34aa338757e2416ec19248ba2" @@ -12436,6 +13134,14 @@ type-fest@^2.19.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -12602,6 +13308,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + unplugin@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" @@ -12739,6 +13450,11 @@ utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -12771,6 +13487,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + viem@^2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/viem/-/viem-2.15.1.tgz#05a9ef5fd74661bd77d865c334477a900e59b436" @@ -12834,6 +13555,13 @@ watchpack@^2.4.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + webcrypto-core@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.2.0.tgz#44fda3f9315ed6effe9a1e47466e0935327733b5" @@ -12889,6 +13617,52 @@ webpack-cli@^5: rechoir "^0.8.0" webpack-merge "^5.7.3" +webpack-dev-middleware@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz#40e265a3d3d26795585cff8207630d3a8ff05877" + integrity sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA== + dependencies: + colorette "^2.0.10" + memfs "^4.6.0" + mime-types "^2.1.31" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@*: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz#8f44147402b4d8ab99bfeb9b6880daa1411064e5" + integrity sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + express "^4.19.2" + graceful-fs "^4.2.6" + html-entities "^2.4.0" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^2.4.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" + webpack-ext-reloader-mv3@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/webpack-ext-reloader-mv3/-/webpack-ext-reloader-mv3-2.1.1.tgz#a96c8be4ab64966c4ac5c556a394c6db29b3360a" @@ -12974,6 +13748,20 @@ webpackbar@^6: std-env "^3.7.0" wrap-ansi "^7.0.0" +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -13143,6 +13931,11 @@ ws@^7.4.5, ws@^7.4.6, ws@^7.5.1, ws@^7.5.2: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" From 0e6d2b27d9099efe37293e723c459b98b580ee4d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 28 Oct 2024 17:48:07 +0200 Subject: [PATCH 69/74] TW-1479: [EVM] Transactions history. Minor fixes --- src/app/atoms/EmptyState.tsx | 30 +++++++++++-------- src/app/atoms/InfiniteScroll.tsx | 8 ++++- .../AddTokenModal/SelectNetworkPage.tsx | 2 +- .../Tokens/components/EmptySection.tsx | 2 +- .../evm/collectibles-metadata/selectors.ts | 4 ++- .../store/evm/tokens-metadata/selectors.ts | 2 +- .../templates/AccountsManagement/index.tsx | 2 +- src/app/templates/AppHeader/AccountsModal.tsx | 2 +- src/app/templates/NetworkSelectModal.tsx | 2 +- .../templates/activity/ActivityListView.tsx | 2 +- .../select-with-modal/select-modal.tsx | 2 +- src/lib/metadata/index.ts | 2 +- 12 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/app/atoms/EmptyState.tsx b/src/app/atoms/EmptyState.tsx index 4e29018a11..a1c6f8269b 100644 --- a/src/app/atoms/EmptyState.tsx +++ b/src/app/atoms/EmptyState.tsx @@ -4,22 +4,26 @@ import clsx from 'clsx'; import { ReactComponent as SadSearchIcon } from 'app/icons/monochrome/sad-search.svg'; import { ReactComponent as SadUniversalIcon } from 'app/icons/monochrome/sad-universal.svg'; -import { T } from 'lib/i18n'; +import { T, TID } from 'lib/i18n'; interface EmptyStateProps { - className?: string; - variant?: 'tokenSearch' | 'universal' | 'searchUniversal'; + forSearch?: boolean; + textI18n?: TID; + text?: string; stretch?: boolean; + className?: string; } -export const EmptyState = memo(({ className, variant = 'universal', stretch }) => ( -
- {variant === 'universal' ? : } +export const EmptyState = memo(({ forSearch = true, textI18n, text, stretch }) => { + const Icon = forSearch ? SadSearchIcon : SadUniversalIcon; + + return ( +
+ - - - -
-)); + + {textI18n ? : text || } + +
+ ); +}); diff --git a/src/app/atoms/InfiniteScroll.tsx b/src/app/atoms/InfiniteScroll.tsx index a5706c00af..5f9ed9af50 100644 --- a/src/app/atoms/InfiniteScroll.tsx +++ b/src/app/atoms/InfiniteScroll.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactElement, useEffect } from 'react'; +import React, { CSSProperties, FC, ReactElement, useEffect } from 'react'; import ReactInfiniteScrollComponent from 'react-infinite-scroll-component'; @@ -49,6 +49,7 @@ export const InfiniteScroll: FC = ({ onScroll={onScroll} loader={null} // Doesn't always show this way scrollableTarget={SCROLL_DOCUMENT ? undefined : APP_CONTENT_PAPER_DOM_ID} + style={STYLE} > {children} @@ -59,6 +60,11 @@ export const InfiniteScroll: FC = ({ ); }; +const STYLE: CSSProperties = { + /** Scrollable element must be an ancestor of this component - document or other. */ + overflow: 'unset' +}; + /** * Build onscroll listener to trigger next loading, when fetching data resulted in error. * `InfiniteScroll.props.next` won't be triggered in this case. diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx index fe10b64825..f30217a988 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/SelectNetworkPage.tsx @@ -66,7 +66,7 @@ export const SelectNetworkPage: FC = ({ selectedNetwork,
- {filteredNetworks.length === 0 && } + {filteredNetworks.length === 0 && } {filteredNetworks.map(network => ( (({ forCollectibles, network }) => { return ( <>
- + -); diff --git a/src/lib/activity/evm/parse/gr-v3.ts b/src/lib/activity/evm/parse/gr-v3.ts index bfe1eaf6c6..752af37283 100644 --- a/src/lib/activity/evm/parse/gr-v3.ts +++ b/src/lib/activity/evm/parse/gr-v3.ts @@ -55,21 +55,7 @@ function parseLogEvent( const fromAddress = logEvent.decoded.params.at(0)!.value; const toAddress = logEvent.decoded.params.at(1)!.value; - const type = (() => { - if (toAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.receiveFromAccount - : ActivityOperTransferType.receive; - } - - if (fromAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.sendToAccount - : ActivityOperTransferType.send; - } - - return null; - })(); + const type = deriveTransferType(accountAddress, fromAddress, toAddress, item, logEvent); if (type == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; @@ -104,21 +90,7 @@ function parseLogEvent( const fromAddress = logEvent.decoded.params.at(1)!.value; const toAddress = logEvent.decoded.params.at(2)!.value; - const type = (() => { - if (toAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.receiveFromAccount - : ActivityOperTransferType.receive; - } - - if (fromAddress === accountAddress) { - return item.to_address === logEvent.sender_address - ? ActivityOperTransferType.sendToAccount - : ActivityOperTransferType.send; - } - - return null; - })(); + const type = deriveTransferType(accountAddress, fromAddress, toAddress, item, logEvent); if (type == null) return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; @@ -203,3 +175,25 @@ function parseLogEvent( return { kind: ActivityOperKindEnum.interaction, withAddress: contractAddress }; } + +function deriveTransferType( + accountAddress: string, + fromAddress: string, + toAddress: string, + item: Transaction, + logEvent: LogEvent +) { + if (toAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperTransferType.receiveFromAccount + : ActivityOperTransferType.receive; + } + + if (fromAddress === accountAddress) { + return item.to_address === logEvent.sender_address + ? ActivityOperTransferType.sendToAccount + : ActivityOperTransferType.send; + } + + return null; +} From 4312004211295273fc5114f788b6ec039170efc5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Nov 2024 16:11:59 +0200 Subject: [PATCH 74/74] fix after merge --- src/app/templates/About/About.tsx | 4 ++-- src/app/templates/About/links-group-item.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/templates/About/About.tsx b/src/app/templates/About/About.tsx index 7c14eac0f4..7ddd8d94ae 100644 --- a/src/app/templates/About/About.tsx +++ b/src/app/templates/About/About.tsx @@ -2,7 +2,7 @@ import React, { memo } from 'react'; import { VerticalLines } from 'app/atoms/Lines'; import { Logo } from 'app/atoms/Logo'; -import { SettingsCell } from 'app/atoms/SettingsCell'; +import { SettingsCellSingle } from 'app/atoms/SettingsCell'; import { SettingsCellGroup } from 'app/atoms/SettingsCellGroup'; import { ReactComponent as DiscordIcon } from 'app/icons/monochrome/discord.svg'; import { ReactComponent as KnowledgeBaseIcon } from 'app/icons/monochrome/knowledge-base.svg'; @@ -93,7 +93,7 @@ export const About = memo(() => { return (
- (({ item, isLast }) => { const { Icon, key, link, testID } = item; return ( - } cellName={t(key)} @@ -30,6 +30,6 @@ export const LinksGroupItem = memo(({ item, isLast }) => { testID={testID} > - + ); });