Skip to content

Commit

Permalink
TW-1387 Implement sending EVM tokens (#1219)
Browse files Browse the repository at this point in the history
* basic layout

* select token for tezos

* select token
modal logic

* dont show tags

* fix select asset modal opening after page reload

* fix e2e ts

* form refactor

* resolving evm address from domain

* max amount calculation

* fix logic for erc20 tokens

* recipient account select main logic

* address copying fixed

* recipient input / select acc state connection + truncation added

* tezos form validation fixes

* evm form validation + react-hook-form v7 used

* add active state for filter network option

* scroll to selected network

* add filter network search

* loader added

* always show converted amount

* show floating assetSymbol in input

* track other networks addresses

* show tezos error toast on form submit

* maxAmount calculation fixes

* fix pipeline

* confirm modal base + segmented control

* confirmation modal layout finished

* Evm / tezos component separation Header, DetailsTab

* fix after-merge conflicts

* fix network icon

* more after-merge fixes

* some more fixes

* some more fixes

* infoIcon

* iconBase memo

* viem update

* send evm transaction

* send evm transaction / add networks support

* fix various ui bugs

* fix fee options calculations

* fix some more bugs

* fix import cycle

* custom transaction params inputs + error handling

* fix ts-prune

* major refactoring and bug fixes

* tezos fee options calculation + ui fixes

* send tezos operations without old confirmation page

* added loading button + proper form reset

* edit gas fee and storageLimit

* show default evm form values

* some ui fixes

* non zero validation

* error tab

* fix ts-prune

* fix fee calculation with custom gas limit

* refactor

* refactor + minor ui fixes

* show default gas fee and storage limit + non zero gas fee validation

* storage limit handling

* fix ts-prune

* fix some after-merge issues

* show tezos raw transaction

* renaming

* raw transaction json view

* tezos submit errors handling

* fix some after-merge issues

* fix some after-merge issues

* fix ts-prune

* refactor

* more refactor

* apply suggestion

* fix audit

* minor bug fixes

* after merge fixes

* fix minor bugs

* some more after-merge fixes

* some more after-merge fixes

* fix info icon size

* TW-1387 Implement sending ERC20 tokens

* TW-1387 Implement sending ERC721 and ERC1155 tokens

* TW-1387 Fix pipeline errors

* TW-1387 Major refactoring

* TW-1387 Fix displaying transaction hex

* TW-1387 Refactoring according to comments

* TW-1387 Refactoring according to comments

* TW-1387 Improve balances update in networks that are not covered by Covalent

* TW-1387 Refactor recent changes

* TW-1387 Fix updating balances on chains that Covalent does not support

* TW-1387 Remove redundant 'break' statement

* TW-1387 Refactor listening to new EVM blocks and transfers events

* TW-1387 Do without passing chain id to listeners

* TW-1387 Minor refactoring

* TW-1387 Fix updating balances to zero if they come from Covalent

---------

Co-authored-by: lendihop <[email protected]>
  • Loading branch information
keshan3262 and lendihop authored Dec 9, 2024
1 parent a3be6fe commit a5882d2
Show file tree
Hide file tree
Showing 69 changed files with 1,858 additions and 342 deletions.
37 changes: 20 additions & 17 deletions src/app/hooks/listing-logic/use-account-tokens-listing-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,26 @@ export const useAccountTokensForListing = (
[enabledEvmChains, enabledTezChains]
);

const enabledChainsSlugsSorted = useMemoWithCompare(() => {
const enabledChainsSlugs = [
...gasChainsSlugs,
...tezTokens
.filter(({ status }) => status === 'enabled')
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.Tezos, chainId, slug)),
...evmTokens
.filter(({ status }) => status === 'enabled')
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.EVM, chainId, slug))
];

const enabledChainsSlugsFiltered = filterZeroBalances
? enabledChainsSlugs.filter(isNonZeroBalance)
: enabledChainsSlugs;

return enabledChainsSlugsFiltered.sort(tokensSortPredicate);
}, [gasChainsSlugs, tezTokens, evmTokens, filterZeroBalances, isNonZeroBalance, tokensSortPredicate]);
const enabledChainsSlugsFiltered = useMemo(() => {
const enabledChainsSlugs = gasChainsSlugs
.concat(
tezTokens
.filter(({ status }) => status === 'enabled')
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.Tezos, chainId, slug))
)
.concat(
evmTokens
.filter(({ status }) => status === 'enabled')
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.EVM, chainId, slug))
);

return filterZeroBalances ? enabledChainsSlugs.filter(isNonZeroBalance) : enabledChainsSlugs;
}, [evmTokens, filterZeroBalances, gasChainsSlugs, isNonZeroBalance, tezTokens]);

const enabledChainsSlugsSorted = useMemoWithCompare(
() => enabledChainsSlugsFiltered.sort(tokensSortPredicate),
[enabledChainsSlugsFiltered, tokensSortPredicate]
);

return {
enabledChainsSlugsSorted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ export const useEvmChainAccountTokensForListing = (
[balances]
);

const enabledSlugsSorted = useMemoWithCompare(() => {
const enabledSlugs = [
EVM_TOKEN_SLUG,
...tokens.filter(({ status }) => status === 'enabled').map(({ slug }) => slug)
];

const enabledSlugsFiltered = filterZeroBalances ? enabledSlugs.filter(isNonZeroBalance) : enabledSlugs;

return enabledSlugsFiltered.sort(tokensSortPredicate);
}, [tokens, isNonZeroBalance, tokensSortPredicate, filterZeroBalances]);
const enabledSlugsFiltered = useMemo(() => {
const gasTokensSlugs: string[] = [EVM_TOKEN_SLUG];
const enabledSlugs = gasTokensSlugs.concat(
tokens.filter(({ status }) => status === 'enabled').map(({ slug }) => slug)
);

return filterZeroBalances ? enabledSlugs.filter(isNonZeroBalance) : enabledSlugs;
}, [filterZeroBalances, isNonZeroBalance, tokens]);

const enabledSlugsSorted = useMemoWithCompare(
() => enabledSlugsFiltered.sort(tokensSortPredicate),
[enabledSlugsFiltered, tokensSortPredicate]
);

return {
enabledSlugsSorted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ export const useTezosAccountTokensForListing = (publicKeyHash: string, filterZer
[balancesRecord, publicKeyHash]
);

const enabledChainsSlugsSorted = useMemoWithCompare(() => {
const enabledSlugsFiltered = useMemo(() => {
const enabledSlugs = gasSlugs.concat(enabledStoredChainSlugs);

const enabledSlugsFiltered = filterZeroBalances ? enabledSlugs.filter(isNonZeroBalance) : enabledSlugs;
return filterZeroBalances ? enabledSlugs.filter(isNonZeroBalance) : enabledSlugs;
}, [enabledStoredChainSlugs, filterZeroBalances, gasSlugs, isNonZeroBalance]);

return enabledSlugsFiltered.sort(tokensSortPredicate);
}, [enabledStoredChainSlugs, isNonZeroBalance, tokensSortPredicate, gasSlugs, filterZeroBalances]);
const enabledChainsSlugsSorted = useMemoWithCompare(
() => enabledSlugsFiltered.sort(tokensSortPredicate),
[enabledSlugsFiltered, tokensSortPredicate]
);

return {
enabledChainsSlugsSorted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@ export const useTezosChainAccountTokensForListing = (publicKeyHash: string, chai
[isNonZeroBalance, leadingAssetsSlugs, filterZeroBalances]
);

const nonLeadingTokenSlugsFilteredSorted = useMemoWithCompare(() => {
const nonLeadingSlugs = [
TEZ_TOKEN_SLUG,
...tokens.filter(({ status }) => status === 'enabled').map(({ slug }) => slug)
];

return (filterZeroBalances ? nonLeadingSlugs.filter(isNonZeroBalance) : nonLeadingSlugs).sort(tokensSortPredicate);
}, [tokens, isNonZeroBalance, tokensSortPredicate, filterZeroBalances]);
const nonLeadingTokensSlugsFiltered = useMemo(() => {
const gasSlugs: string[] = [TEZ_TOKEN_SLUG];
const nonLeadingSlugs = gasSlugs.concat(
tokens.filter(({ status }) => status === 'enabled').map(({ slug }) => slug)
);

return filterZeroBalances ? nonLeadingSlugs.filter(isNonZeroBalance) : nonLeadingSlugs;
}, [tokens, isNonZeroBalance, filterZeroBalances]);

const nonLeadingTokenSlugsFilteredSorted = useMemoWithCompare(
() => nonLeadingTokensSlugsFiltered.sort(tokensSortPredicate),
[nonLeadingTokensSlugsFiltered, tokensSortPredicate]
);

const enabledTokenSlugsSorted = useMemo(
() => Array.from(new Set(leadingAssetsFiltered.concat(nonLeadingTokenSlugsFilteredSorted))),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';

import { isString } from 'lodash';

import { Model3DViewer } from 'app/atoms/Model3DViewer';
import { useCollectiblesListOptionsSelector } from 'app/store/assets-filter-options/selectors';
import { TezosAssetImageStacked } from 'app/templates/AssetImage';
import { isSvgDataUriInUtf8Encoding, buildObjktCollectibleArtifactUri } from 'lib/images-uri';
import { isSvgDataUriInUtf8Encoding, buildObjktCollectibleArtifactUri, buildHttpLinkFromUri } from 'lib/images-uri';
import { TokenMetadata } from 'lib/metadata';
import { EvmCollectibleMetadata } from 'lib/metadata/types';
import { ImageStacked } from 'lib/ui/ImageStacked';
Expand Down Expand Up @@ -103,7 +105,9 @@ interface EvmCollectiblePageImageProps {
}

export const EvmCollectiblePageImage = memo<EvmCollectiblePageImageProps>(({ metadata, className }) => {
const sources = useMemo(() => (metadata.image ? [metadata.image] : []), [metadata.image]);
const { image } = metadata;

const sources = useMemo(() => [buildHttpLinkFromUri(image)].filter(isString), [image]);

return (
<ImageStacked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import React, { memo, useMemo } from 'react';
import BigNumber from 'bignumber.js';

import { OldStyleHashChip, ExternalLinkChip } from 'app/atoms';
import { useRawEvmAssetBalanceSelector } from 'app/store/evm/balances/selectors';
import type { CollectibleDetails } from 'app/store/tezos/collectibles/state';
import { fromFa2TokenSlug } from 'lib/assets/utils';
import { useTezosAssetBalance } from 'lib/balances';
import { useEvmAssetBalance } from 'lib/balances/hooks';
import { formatDate } from 'lib/i18n';
import { EvmCollectibleMetadata } from 'lib/metadata/types';
import { useBlockExplorerHref } from 'temple/front/block-explorers';
import { useEvmChainByChainId } from 'temple/front/chains';
import { TezosNetworkEssentials } from 'temple/networks';
import { TempleChainKind } from 'temple/types';

Expand Down Expand Up @@ -109,15 +110,16 @@ interface EvmPropertiesItemsProps {
}

export const EvmPropertiesItems = memo<EvmPropertiesItemsProps>(({ accountPkh, evmChainId, assetSlug, metadata }) => {
const rawBalance = useRawEvmAssetBalanceSelector(accountPkh, evmChainId, assetSlug);
const chain = useEvmChainByChainId(evmChainId);
const { value: balance } = useEvmAssetBalance(assetSlug, accountPkh, chain!);

if (!metadata) return null;

return (
<>
<div className={itemClassName}>
<h6 className={itemTitleClassName}>Owned</h6>
<span className={itemValueClassName}>{rawBalance ?? '-'}</span>
<span className={itemValueClassName}>{balance?.toFixed() ?? '-'}</span>
</div>

<div className={itemClassName}>
Expand Down
9 changes: 9 additions & 0 deletions src/app/pages/Collectibles/CollectiblePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ const EvmCollectiblePage = memo<EvmCollectiblePageProps>(({ evmChainId, assetSlu
return tab ?? tabs[0]!;
}, [tabs, tabNameInUrl]);

const onSendButtonClick = useCallback(
() => navigate(buildSendPagePath(TempleChainKind.EVM, String(evmChainId), assetSlug)),
[evmChainId, assetSlug]
);

return (
<PageLayout
pageTitle={
Expand Down Expand Up @@ -128,6 +133,10 @@ const EvmCollectiblePage = memo<EvmCollectiblePageProps>(({ evmChainId, assetSlu
</div>
)}

<FormSubmitButton onClick={onSendButtonClick} testID={CollectiblesSelectors.sendButton}>
<T id="send" />
</FormSubmitButton>

<TabsBar tabs={tabs} activeTabName={activeTabName} withOutline />

<div className="grid grid-cols-2 gap-2 text-gray-910">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ReactComponent as DeleteIcon } from 'app/icons/base/delete.svg';
import { dispatch } from 'app/store';
import { setEvmCollectibleStatusAction } from 'app/store/evm/assets/actions';
import { useStoredEvmCollectibleSelector } from 'app/store/evm/assets/selectors';
import { useRawEvmAssetBalanceSelector } from 'app/store/evm/balances/selectors';
import { useEvmCollectibleMetadataSelector } from 'app/store/evm/collectibles-metadata/selectors';
import { setTezosCollectibleStatusAction } from 'app/store/tezos/assets/actions';
import { useStoredTezosCollectibleSelector } from 'app/store/tezos/assets/selectors';
Expand All @@ -24,11 +23,13 @@ import { DeleteAssetModal } from 'app/templates/remove-asset-modal/delete-asset-
import { setAnotherSelector, setTestID } from 'lib/analytics';
import { objktCurrencies } from 'lib/apis/objkt';
import { getAssetStatus } from 'lib/assets/hooks/utils';
import { useEvmAssetBalance } from 'lib/balances/hooks';
import { T } from 'lib/i18n';
import { getTokenName } from 'lib/metadata';
import { getCollectibleName, getCollectionName } from 'lib/metadata/utils';
import { atomsToTokens } from 'lib/temple/helpers';
import { useBooleanState } from 'lib/ui/hooks';
import { ZERO } from 'lib/utils/numbers';
import { Link } from 'lib/woozie';
import { useEvmChainByChainId, useTezosChainByChainId } from 'temple/front/chains';
import { TempleChainKind } from 'temple/types';
Expand Down Expand Up @@ -269,12 +270,13 @@ interface EvmCollectibleItemProps {
export const EvmCollectibleItem = memo<EvmCollectibleItemProps>(
({ assetSlug, evmChainId, accountPkh, showDetails = false, manageActive = false, hideWithoutMeta }) => {
const metadata = useEvmCollectibleMetadataSelector(evmChainId, assetSlug);
const balanceAtomic = useRawEvmAssetBalanceSelector(accountPkh, evmChainId, assetSlug);
const balance = balanceAtomic ?? '0';
const chain = useEvmChainByChainId(evmChainId);
const { value: balance = ZERO } = useEvmAssetBalance(assetSlug, accountPkh, chain!);
const balanceBeforeTruncate = balance.toString();

const storedToken = useStoredEvmCollectibleSelector(accountPkh, evmChainId, assetSlug);

const checked = getAssetStatus(balance, storedToken?.status) === 'enabled';
const checked = getAssetStatus(balanceBeforeTruncate, storedToken?.status) === 'enabled';

const [deleteModalOpened, setDeleteModalOpened, setDeleteModalClosed] = useBooleanState(false);

Expand Down Expand Up @@ -304,7 +306,10 @@ export const EvmCollectibleItem = memo<EvmCollectibleItemProps>(
[checked, assetSlug, evmChainId, accountPkh]
);

const truncatedBalance = useMemo(() => (balance.length > 6 ? `${balance.slice(0, 6)}...` : balance), [balance]);
const truncatedBalance = useMemo(
() => (balanceBeforeTruncate.length > 6 ? `${balanceBeforeTruncate.slice(0, 6)}...` : balanceBeforeTruncate),
[balanceBeforeTruncate]
);

const network = useEvmChainByChainId(evmChainId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { t, T } from 'lib/i18n';
import { isCollectible, TokenMetadata } from 'lib/metadata';
import { fetchOneTokenMetadata } from 'lib/metadata/fetch';
import { TokenMetadataNotFoundError } from 'lib/metadata/on-chain';
import { EvmTokenMetadata } from 'lib/metadata/types';
import { EvmCollectibleMetadata, EvmTokenMetadata } from 'lib/metadata/types';
import { loadContract } from 'lib/temple/contract';
import { useSafeState, useUpdatableRef } from 'lib/ui/hooks';
import { navigate } from 'lib/woozie';
Expand Down Expand Up @@ -69,11 +69,14 @@ interface RequiredTokenMetadataResponse extends TokenMetadataResponse {
symbol: string;
}

interface RequiredEvmTokenMetadata extends EvmTokenMetadata {
interface RequiredMetadata {
name: string;
symbol: string;
}

type RequiredEvmTokenMetadata = EvmTokenMetadata & RequiredMetadata;
type RequiredEvmCollectibleMetadata = EvmCollectibleMetadata & RequiredMetadata;

interface FormData {
address: string;
id?: string;
Expand Down Expand Up @@ -125,7 +128,7 @@ export const AddTokenForm = memo<AddTokenPageProps>(

const attemptRef = useRef(0);
const tezMetadataRef = useRef<RequiredTokenMetadataResponse>();
const evmMetadataRef = useRef<RequiredEvmTokenMetadata>();
const evmMetadataRef = useRef<RequiredEvmTokenMetadata | RequiredEvmCollectibleMetadata>();

const loadMetadataPure = useCallback(async () => {
if (!formValid) return;
Expand Down Expand Up @@ -173,10 +176,10 @@ export const AddTokenForm = memo<AddTokenPageProps>(
? fetchEvmCollectibleMetadataFromChain
: fetchEvmTokenMetadataFromChain)(selectedNetwork, tokenSlug);

if (!metadata || !metadata.name || !metadata.symbol)
if (!metadata || !hasRequiredMetadata(metadata))
throw new TokenMetadataNotFoundError('Failed to load token metadata');

evmMetadataRef.current = metadata as RequiredEvmTokenMetadata;
evmMetadataRef.current = metadata;

stateToSet = { bottomSectionVisible: true };
}
Expand Down Expand Up @@ -282,14 +285,16 @@ export const AddTokenForm = memo<AddTokenPageProps>(
dispatch(
putEvmCollectiblesMetadataAction({
chainId: selectedNetwork.chainId,
records: { [tokenSlug]: { ...evmMetadataRef.current, tokenId: id } }
records: {
[tokenSlug]: { ...(evmMetadataRef.current as RequiredEvmCollectibleMetadata), tokenId: id }
}
})
);
else
dispatch(
putEvmTokensMetadataAction({
chainId: selectedNetwork.chainId,
records: { [tokenSlug]: evmMetadataRef.current }
records: { [tokenSlug]: evmMetadataRef.current as RequiredEvmTokenMetadata }
})
);
}
Expand Down Expand Up @@ -419,6 +424,10 @@ export const AddTokenForm = memo<AddTokenPageProps>(
}
);

const hasRequiredMetadata = (
metadata: EvmTokenMetadata | EvmCollectibleMetadata
): metadata is RequiredEvmCollectibleMetadata | RequiredEvmTokenMetadata => Boolean(metadata.name && metadata.symbol);

const errorHandler = (err: any, contractAddress: string) => {
if (err instanceof ContractNotFoundError) {
toastError(t('referredByTokenContractNotFound', contractAddress));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ const TabContentWithManageActive: FC<TabContentProps> = ({ publicKeyHash, networ
hideZeroBalance
);

const storedSlugs = useMemo(
() => tokens.filter(({ status }) => status !== 'removed').map(({ slug }) => slug),
[tokens]
);

const allStoredSlugsSorted = useMemoWithCompare(
() =>
tokens
.filter(({ status }) => status !== 'removed')
.map(({ slug }) => slug)
.sort(tokensSortPredicate),
[tokens, tokensSortPredicate]
() => storedSlugs.sort(tokensSortPredicate),
[storedSlugs, tokensSortPredicate]
);

const allSlugsSorted = usePreservedOrderSlugsToManage(enabledSlugsSorted, allStoredSlugsSorted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ const TabContentWithManageActive: FC<Props> = ({ publicKeyHash }) => {
hideZeroBalance
);

const allChainsSlugsSorted = useMemoWithCompare(
const allChainsSlugs = useMemo(
() =>
tokens
.filter(({ status }) => status !== 'removed')
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.EVM, chainId, slug))
.sort(tokensSortPredicate),
[tokens, tokensSortPredicate]
.map(({ chainId, slug }) => toChainAssetSlug(TempleChainKind.EVM, chainId, slug)),
[tokens]
);

const allChainsSlugsSorted = useMemoWithCompare(
() => allChainsSlugs.sort(tokensSortPredicate),
[allChainsSlugs, tokensSortPredicate]
);

const allSlugsSorted = usePreservedOrderSlugsToManage(enabledChainSlugsSorted, allChainsSlugsSorted);
Expand Down
Loading

0 comments on commit a5882d2

Please sign in to comment.