From c8f8d566887b5d09417ab9d6eb898de258b587db Mon Sep 17 00:00:00 2001 From: olehkhalin Date: Thu, 11 Aug 2022 13:32:17 -0400 Subject: [PATCH 1/3] Fixed tooltip overflowing --- src/app/components/blocks/overview/NftInfo.tsx | 2 +- src/app/components/elements/Tooltip.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/blocks/overview/NftInfo.tsx b/src/app/components/blocks/overview/NftInfo.tsx index 307210a3e..07bed01a1 100644 --- a/src/app/components/blocks/overview/NftInfo.tsx +++ b/src/app/components/blocks/overview/NftInfo.tsx @@ -161,7 +161,7 @@ const NftInfo: FC = () => { {preparedId && ( = ({ } interactive={interactive ?? size === "large"} appendTo={overflowElement ?? document.body} - maxWidth={maxWidth ?? (size === "large" ? "18rem" : "none")} + maxWidth={maxWidth ?? (size === "large" ? "18rem" : "20rem")} placement={placement ?? (size === "large" ? "right-start" : "bottom")} hideOnClick={hideOnClick} className="pointer-events-auto" @@ -101,6 +101,7 @@ const getSizeClasses = (size: sizeType) => .otherwise(() => classNames( "rounded-md", + "overflow-hidden truncate", "bg-brand-main/20 backdrop-blur-[6px]", IS_FIREFOX && "!bg-[#414356]/[.98]", "py-1 px-3" From 866eab19920c0ddb3f7ae21fa0ba3e7164744ab0 Mon Sep 17 00:00:00 2001 From: pas1ko Date: Thu, 11 Aug 2022 18:47:39 +0100 Subject: [PATCH 2/3] Implement txAction parsing for NFT --- src/app/components/blocks/ActivityModal.tsx | 63 +++++------- src/app/components/blocks/TokenAmount.tsx | 36 ++++++- .../screens/approvals/Transaction.tsx | 17 ++-- src/app/icons/xsymbol.svg | 5 + src/core/back/approve/index.ts | 15 ++- src/core/back/services/txObserver.ts | 28 +++--- src/core/common/tokens.ts | 57 ++++++++++- src/core/common/transaction.ts | 95 ++++++++++++++----- src/core/types/activity.ts | 1 + 9 files changed, 225 insertions(+), 92 deletions(-) create mode 100644 src/app/icons/xsymbol.svg diff --git a/src/app/components/blocks/ActivityModal.tsx b/src/app/components/blocks/ActivityModal.tsx index c7dfa252b..63040f96b 100644 --- a/src/app/components/blocks/ActivityModal.tsx +++ b/src/app/components/blocks/ActivityModal.tsx @@ -25,10 +25,10 @@ import { ActivitySource, ActivityType, TransactionActivity, + TxAction, TxActionType, } from "core/types"; import { rejectAllApprovals } from "core/client"; -import { matchTxAction } from "core/common/transaction"; import { activityModalAtom, @@ -415,7 +415,7 @@ const ActivityCard = memo( {item.type === ActivityType.Transaction && ( @@ -731,49 +731,36 @@ const SectionHeader: FC<{ className?: string }> = memo( type ActivityTokensProps = { source: ActivitySource; - tx: string; + action?: TxAction; accountAddress: string; className?: string; }; -const ActivityTokens: FC = ({ - source, - tx, - accountAddress, - className, -}) => { - const parsedTx = ethers.utils.parseTransaction(tx); - const action = useMemo(() => { - try { - return matchTxAction(parsedTx); - } catch (err) { - console.warn(err); +const ActivityTokens = memo( + ({ source, action, accountAddress, className }) => { + if ( + source.type !== "self" || + !action || + action.type !== TxActionType.TokenTransfer || + action.tokens?.length === 0 + ) { return null; } - }, [parsedTx]); - - if ( - source.type !== "self" || - !action || - action.type !== TxActionType.TokenTransfer || - action.tokens?.length === 0 - ) { - return null; - } - return ( -
- {action.tokens.map((token, i) => ( - - ))} -
- ); -}; + return ( +
+ {action.tokens.map((token, i) => ( + + ))} +
+ ); + } +); function capitalize(word: string) { const lower = word.toLowerCase(); diff --git a/src/app/components/blocks/TokenAmount.tsx b/src/app/components/blocks/TokenAmount.tsx index ccdda7288..16c76a93d 100644 --- a/src/app/components/blocks/TokenAmount.tsx +++ b/src/app/components/blocks/TokenAmount.tsx @@ -2,13 +2,16 @@ import { memo } from "react"; import classNames from "clsx"; import BigNumber from "bignumber.js"; -import { AccountAsset, TokenType } from "core/types"; +import { TokenType } from "core/types"; import { useToken } from "app/hooks"; import { LARGE_AMOUNT } from "app/utils/largeAmount"; +import { ReactComponent as XSymbolIcon } from "app/icons/xsymbol.svg"; + import PrettyAmount from "../elements/PrettyAmount"; import FiatAmount from "../elements/FiatAmount"; import AssetLogo from "../elements/AssetLogo"; +import NftAvatar from "../elements/NftAvatar"; import Dot from "../elements/Dot"; type TokenAmountProps = { @@ -27,7 +30,7 @@ const TokenAmount = memo( if (!tokenInfo) return null; if (tokenInfo.tokenType === TokenType.Asset) { - const { name, symbol, decimals, priceUSD } = tokenInfo as AccountAsset; + const { name, symbol, decimals, priceUSD } = tokenInfo; const usdAmount = amount ? new BigNumber(amount) @@ -78,7 +81,34 @@ const TokenAmount = memo( ); } else { - return null; + const { name, thumbnailUrl } = tokenInfo; + + return ( +
+
+ {amount && +amount > 1 && ( +
+ + + +
+ )} + +
{name}
+
+ + +
+ ); } } ); diff --git a/src/app/components/screens/approvals/Transaction.tsx b/src/app/components/screens/approvals/Transaction.tsx index 4845a640c..38a7e5f29 100644 --- a/src/app/components/screens/approvals/Transaction.tsx +++ b/src/app/components/screens/approvals/Transaction.tsx @@ -21,6 +21,7 @@ import { FeeMode, FeeSuggestions, AccountSource, + TxAction, } from "core/types"; import { approveItem, @@ -92,15 +93,6 @@ const ApproveTransaction: FC = ({ approval }) => { [approval, allAccounts] ); - const action = useMemo(() => { - try { - return matchTxAction(txParams); - } catch (err) { - console.warn(err); - return null; - } - }, [txParams]); - const provider = useProvider(); const withLedger = useLedger(); @@ -118,6 +110,7 @@ const ApproveTransaction: FC = ({ approval }) => { approvingRef.current = approving; } + const [action, setAction] = useState(null); const [prepared, setPrepared] = useState<{ tx: Tx; estimatedGasLimit: ethers.BigNumber; @@ -282,6 +275,12 @@ const ApproveTransaction: FC = ({ approval }) => { ] ); + useEffect(() => { + matchTxAction(provider, txParams) + .then((a) => a && setAction(a)) + .catch(console.warn); + }, [provider, txParams]); + useEffect(() => { estimateTx(); }, [estimateTx]); diff --git a/src/app/icons/xsymbol.svg b/src/app/icons/xsymbol.svg new file mode 100644 index 000000000..683e5df08 --- /dev/null +++ b/src/app/icons/xsymbol.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/core/back/approve/index.ts b/src/core/back/approve/index.ts index 86691b5b9..769456957 100644 --- a/src/core/back/approve/index.ts +++ b/src/core/back/approve/index.ts @@ -15,6 +15,7 @@ import { TxParams, TxActionType, TokenActivity, + TxAction, } from "core/types"; import * as repo from "core/repo"; import { saveNonce } from "core/common/nonce"; @@ -24,7 +25,7 @@ import { createTokenActivityKey } from "core/common/tokens"; import { Vault } from "../vault"; import { $accounts, $approvals, approvalResolved } from "../state"; -import { sendRpc } from "../rpc"; +import { sendRpc, getRpcProvider } from "../rpc"; const { serializeTransaction, parseTransaction, keccak256, hexValue } = ethers.utils; @@ -128,6 +129,11 @@ export async function processApprove( const txHash = rpcRes.result; const timeAt = Date.now(); + const txAction = await matchTxAction( + getRpcProvider(chainId), + txParams + ).catch(() => null); + await Promise.all([ saveNonce(chainId, accountAddress, tx.nonce), saveActivity({ @@ -138,12 +144,13 @@ export async function processApprove( accountAddress, txParams, rawTx: rawTx!, + txAction: txAction ?? undefined, txHash, timeAt, pending: 1, }), saveTokenActivity( - txParams, + txAction, chainId, accountAddress, txHash, @@ -226,15 +233,13 @@ async function saveActivity(activity: Activity) { } async function saveTokenActivity( - txParams: TxParams, + action: TxAction | null, chainId: number, accountAddress: string, txHash: string, timeAt: number ) { try { - const action = matchTxAction(txParams); - if (action) { const tokenActivities = new Map(); const addToActivities = (activity: TokenActivity) => { diff --git a/src/core/back/services/txObserver.ts b/src/core/back/services/txObserver.ts index 01b258929..638697489 100644 --- a/src/core/back/services/txObserver.ts +++ b/src/core/back/services/txObserver.ts @@ -14,8 +14,8 @@ import * as repo from "core/repo"; import { createAccountTokenKey, NATIVE_TOKEN_SLUG } from "core/common/tokens"; import { matchTokenTransferEvents } from "core/common/transaction"; -import { sendRpc } from "../rpc"; -import { isUnlocked } from "../state"; +import { sendRpc, getRpcProvider } from "../rpc"; +import { isUnlocked, $accountAddresses } from "../state"; import { addFindTokenRequest } from "../sync"; export async function startTxObserver() { @@ -29,6 +29,7 @@ export async function startTxObserver() { if (pendingTxs.length === 0) return; + const accountAddresses = $accountAddresses.getState(); const txsToUpdate = new Map(); const completeHashes = new Set(); @@ -65,17 +66,20 @@ export async function startTxObserver() { if (!isFailedStatus(result)) { try { - const transfers = matchTokenTransferEvents(result.logs); + const transfers = await matchTokenTransferEvents( + getRpcProvider(tx.chainId), + result.logs + ); + for (const transfer of transfers) { - if ( - transfer.to === tx.accountAddress || - transfer.from === tx.accountAddress - ) { - addFindTokenRequest( - tx.chainId, - tx.accountAddress, - transfer.tokenSlug - ); + for (const transferAddress of [transfer.to, transfer.from]) { + if (accountAddresses.includes(transferAddress)) { + addFindTokenRequest( + tx.chainId, + transferAddress, + transfer.tokenSlug + ); + } } } } catch (err) { diff --git a/src/core/common/tokens.ts b/src/core/common/tokens.ts index c55929bd6..4ae4447a0 100644 --- a/src/core/common/tokens.ts +++ b/src/core/common/tokens.ts @@ -1,6 +1,6 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; +import { Provider } from "@ethersproject/abstract-provider"; -import { ERC1155__factory } from "abi-types"; +import { ERC20__factory, ERC1155__factory, ERC721__factory } from "abi-types"; import { TokenActivity, TokenStandard } from "core/types"; export const NATIVE_TOKEN_SLUG = createTokenSlug({ @@ -59,7 +59,7 @@ export function getNativeTokenLogoUrl(chainTag: string) { } export async function detectNFTStandard( - provider: JsonRpcProvider, + provider: Provider, tokenAddress: string, tokenId: string ) { @@ -72,3 +72,54 @@ export async function detectNFTStandard( return TokenStandard.ERC721; } + +const STUB_ADDRESS = "0x0000000000000000000000000000000000000001"; +const ERC721_IFACE_ID = "0x80ac58cd"; +const ERC1155_IFACE_ID = "0xd9b67a26"; + +export async function isTokenStandardValid( + provider: Provider, + address: string, + standard: TokenStandard +) { + switch (standard) { + case TokenStandard.ERC20: + { + try { + const contract = ERC20__factory.connect(address, provider); + const supply = await contract.totalSupply(); + + return !supply.isZero(); + } catch {} + } + break; + + case TokenStandard.ERC721: + { + const contract = ERC721__factory.connect(address, provider); + + try { + const is721 = await contract.supportsInterface(ERC721_IFACE_ID); + if (is721) return is721; + } catch {} + + try { + await contract.balanceOf(STUB_ADDRESS); + return true; + } catch {} + } + break; + + case TokenStandard.ERC1155: + { + const contract = ERC1155__factory.connect(address, provider); + + try { + return await contract.supportsInterface(ERC1155_IFACE_ID); + } catch {} + } + break; + } + + return false; +} diff --git a/src/core/common/transaction.ts b/src/core/common/transaction.ts index 747b57807..269a71247 100644 --- a/src/core/common/transaction.ts +++ b/src/core/common/transaction.ts @@ -12,11 +12,16 @@ import { TxParams, } from "core/types"; -import { createTokenSlug, NATIVE_TOKEN_SLUG } from "./tokens"; +import { + createTokenSlug, + isTokenStandardValid, + NATIVE_TOKEN_SLUG, +} from "./tokens"; -export function matchTxAction( +export async function matchTxAction( + provider: Provider, txParams: Pick & { value?: ethers.BigNumberish } -): TxAction | null { +): Promise { if (!txParams.to) { if (!isZeroHex(txParams.data)) { return { @@ -51,12 +56,14 @@ export function matchTxAction( : undefined, }); - const parsed = parseStandardTokenTransactionData(txParams.data!); + const parsedAll = parseStandardTokenTransactionData(txParams.data!); - if (!parsed) { + if (parsedAll.length === 0) { return getContractInteractionAction(); } + const parsed = await pickParsed(provider, destination, parsedAll); + return ( match(parsed) // ERC20 @@ -222,7 +229,8 @@ export function matchTxAction( ); } -export function matchTokenTransferEvents( +export async function matchTokenTransferEvents( + provider: Provider, logs: { address: string; data: string; @@ -236,13 +244,18 @@ export function matchTokenTransferEvents( amount: string; }[] = []; - for (const log of logs) { - const event = parseStandardTokenEvent(log); + await Promise.all( + logs.map(async (log) => { + const parsedAll = parseStandardTokenEvent(log); + + if (parsedAll.length === 0) { + return; + } - if (event) { const address = ethStringify(log.address); + const parsed = await pickParsed(provider, address, parsedAll); - match(event) + match(parsed) .with( [TokenStandard.ERC20, { name: "Transfer" }], ([standard, { args }]) => { @@ -304,8 +317,8 @@ export function matchTokenTransferEvents( } ) .otherwise(() => null); - } - } + }) + ); return results; } @@ -321,51 +334,66 @@ export type ParsedTokenTxData = [ export function parseStandardTokenTransactionData( data: string -): ParsedTokenTxData | null { +): ParsedTokenTxData[] { + const parsed: ParsedTokenTxData[] = []; + try { - return [TokenStandard.ERC20, erc20Interface.parseTransaction({ data })]; + parsed.push([ + TokenStandard.ERC20, + erc20Interface.parseTransaction({ data }), + ]); } catch { // ignore and next try to parse with erc721 ABI } try { - return [TokenStandard.ERC721, erc721Interface.parseTransaction({ data })]; + parsed.push([ + TokenStandard.ERC721, + erc721Interface.parseTransaction({ data }), + ]); } catch { // ignore and next try to parse with erc1155 ABI } try { - return [TokenStandard.ERC1155, erc1155Interface.parseTransaction({ data })]; + parsed.push([ + TokenStandard.ERC1155, + erc1155Interface.parseTransaction({ data }), + ]); } catch { // ignore and return null } - return null; + return parsed; } +export type ParsedTokenEvent = [TokenStandard, ethers.utils.LogDescription]; + export function parseStandardTokenEvent(log: { topics: string[]; data: string; -}) { +}): ParsedTokenEvent[] { + const parsed: ParsedTokenEvent[] = []; + try { - return [TokenStandard.ERC20, erc20Interface.parseLog(log)] as const; + parsed.push([TokenStandard.ERC20, erc20Interface.parseLog(log)]); } catch { // ignore and next try to parse with erc721 ABI } try { - return [TokenStandard.ERC721, erc721Interface.parseLog(log)] as const; + parsed.push([TokenStandard.ERC721, erc721Interface.parseLog(log)]); } catch { // ignore and next try to parse with erc1155 ABI } try { - return [TokenStandard.ERC1155, erc1155Interface.parseLog(log)] as const; + parsed.push([TokenStandard.ERC1155, erc1155Interface.parseLog(log)]); } catch { // ignore and return null } - return null; + return parsed; } export async function isSmartContractAddress( @@ -384,6 +412,29 @@ export async function isSmartContractAddress( ); } +async function pickParsed( + provider: Provider, + address: string, + parsedAll: T[] +): Promise { + if (parsedAll.length > 1) { + const valids = await Promise.all( + parsedAll.map(([standard]) => + isTokenStandardValid(provider, address, standard) + ) + ); + + for (let i = 0; i < parsedAll.length; i++) { + const valid = valids[i]; + if (valid) { + return parsedAll[i]; + } + } + } + + return parsedAll[0]; +} + function ethStringify(v: ethers.BigNumberish) { return typeof v === "string" && ethers.utils.isAddress(v) ? ethers.utils.getAddress(v) diff --git a/src/core/types/activity.ts b/src/core/types/activity.ts index 024426666..a46752838 100644 --- a/src/core/types/activity.ts +++ b/src/core/types/activity.ts @@ -76,6 +76,7 @@ export interface TransactionActivity extends TransactionApproval { pending: number; rawTx: string; txHash: string; + txAction?: TxAction; result?: TxReceipt; gasTokenPriceUSD?: string; } From 3ddca91165e2d1e84c03afcbb570298f49c86b83 Mon Sep 17 00:00:00 2001 From: olehkhalin Date: Thu, 11 Aug 2022 14:11:34 -0400 Subject: [PATCH 3/3] Edited tooltip size --- src/app/components/elements/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/Tooltip.tsx b/src/app/components/elements/Tooltip.tsx index 19e539afa..b6fff8acc 100644 --- a/src/app/components/elements/Tooltip.tsx +++ b/src/app/components/elements/Tooltip.tsx @@ -53,7 +53,7 @@ const Tooltip: FC = ({ } interactive={interactive ?? size === "large"} appendTo={overflowElement ?? document.body} - maxWidth={maxWidth ?? (size === "large" ? "18rem" : "20rem")} + maxWidth={maxWidth ?? "18rem"} placement={placement ?? (size === "large" ? "right-start" : "bottom")} hideOnClick={hideOnClick} className="pointer-events-auto"