From 6cee00875c8cb0cb333486e8ce893f20f6a5a820 Mon Sep 17 00:00:00 2001 From: Jibz1 Date: Mon, 13 Feb 2023 06:17:19 -0800 Subject: [PATCH] show call transaction amount on table view and transaction details (#8194) - Show the amount for Call transaction - On the transaction table view only show the SUI coin - Since Epoch change transaction from `0x5` has a lot of `coinBalance` change events, paginate the data - Note using absolute amount since the amount from events could be negative. CC @mystie711 - For txn with multiple `coinType` or `address` do not show the total sent. Screenshot 2023-02-09 at 8 22 36 AM Screenshot 2023-02-09 at 9 35 11 AM Screenshot 2023-02-10 at 11 22 19 AM Screenshot 2023-02-10 at 11 23 14 AM --- .../pagination/Pagination.module.css | 2 +- .../transaction-card/TxCardUtils.tsx | 18 ++-- .../transaction-result/TransactionView.tsx | 74 ++++++++++++----- apps/explorer/src/ui/SenderRecipient.tsx | 48 ++++++----- .../ui/stories/SenderRecipient.stories.tsx | 26 +++--- apps/explorer/src/utils/getAmount.ts | 82 ++++++++++++++++--- 6 files changed, 169 insertions(+), 81 deletions(-) diff --git a/apps/explorer/src/components/pagination/Pagination.module.css b/apps/explorer/src/components/pagination/Pagination.module.css index ca6c0def3aca7..8a11b1377eec5 100644 --- a/apps/explorer/src/components/pagination/Pagination.module.css +++ b/apps/explorer/src/components/pagination/Pagination.module.css @@ -76,7 +76,7 @@ button.gone { /* Desktop */ .desktopfooter { - @apply hidden text-center sm:mt-2 sm:block sm:grid sm:grid-cols-12 sm:text-left; + @apply hidden text-center sm:mt-2 sm:block sm:grid-cols-12 sm:text-left; } .desktopfooter > div:first-child { diff --git a/apps/explorer/src/components/transaction-card/TxCardUtils.tsx b/apps/explorer/src/components/transaction-card/TxCardUtils.tsx index fa5393d04f56b..f6be389742947 100644 --- a/apps/explorer/src/components/transaction-card/TxCardUtils.tsx +++ b/apps/explorer/src/components/transaction-card/TxCardUtils.tsx @@ -54,10 +54,10 @@ export function SuiAmount({ }: { amount: bigint | number | string | undefined | null; }) { - const [formattedAmount] = useFormatCoin(amount, SUI_TYPE_ARG); + const [formattedAmount, coinType] = useFormatCoin(amount, SUI_TYPE_ARG); if (amount) { - const SuiSuffix = SUI; + const SuiSuffix = {coinType}; return (
@@ -201,14 +201,22 @@ export const getDataOnTxDigests = ( getTransferObjectTransaction(txn)?.recipient || getTransferSuiTransaction(txn)?.recipient; - const txnTransfer = getAmount(txn, txEff.effects)?.[0]; + const transfer = getAmount({ + txnData: txEff, + suiCoinOnly: true, + })[0]; + + // use only absolute value of sui amount + const suiAmount = transfer?.amount + ? Math.abs(transfer.amount) + : null; return { txId: digest, status: getExecutionStatusType(txEff)!, txGas: getTotalGasUsed(txEff), - suiAmount: txnTransfer?.amount || null, - coinType: txnTransfer?.coinType || null, + suiAmount, + coinType: transfer?.coinType || null, kind: txKind, From: res.data.sender, timestamp_ms: txEff.timestamp_ms, diff --git a/apps/explorer/src/pages/transaction-result/TransactionView.tsx b/apps/explorer/src/pages/transaction-result/TransactionView.tsx index e3390b1bebcf3..db959353c89ce 100644 --- a/apps/explorer/src/pages/transaction-result/TransactionView.tsx +++ b/apps/explorer/src/pages/transaction-result/TransactionView.tsx @@ -17,7 +17,7 @@ import { type SuiTransactionResponse, } from '@mysten/sui.js'; import clsx from 'clsx'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { ErrorBoundary } from '../../components/error-boundary/ErrorBoundary'; import { @@ -26,6 +26,8 @@ import { } from '../../components/events/eventDisplay'; import Longtext from '../../components/longtext/Longtext'; import ModulesWrapper from '../../components/module/ModulesWrapper'; +// TODO: (Jibz) Create a new pagination component +import Pagination from '../../components/pagination/Pagination'; import { type LinkObj, TxAddresses, @@ -54,6 +56,8 @@ import { Tooltip } from '~/ui/Tooltip'; import { ReactComponent as ChevronDownIcon } from '~/ui/icons/chevron_down.svg'; import { LinkWithQuery } from '~/ui/utils/LinkWithQuery'; +const MAX_RECIPIENTS_PER_PAGE = 10; + function generateMutatedCreated(tx: SuiTransactionResponse) { return [ ...(tx.effects.mutated?.length @@ -290,33 +294,42 @@ function TransactionView({ }: { transaction: SuiTransactionResponse; }) { - const txdetails = getTransactions(transaction.certificate)[0]; - const txKindName = getTransactionKindName(txdetails); + const txnDetails = getTransactions(transaction.certificate)[0]; + const txKindName = getTransactionKindName(txnDetails); const sender = getTransactionSender(transaction.certificate); const gasUsed = transaction?.effects.gasUsed; const [gasFeesExpanded, setGasFeesExpanded] = useState(false); - const txnTransfer = getAmount(txdetails, transaction?.effects); - const sendReceiveRecipients = txnTransfer?.map((item) => ({ - address: item.recipientAddress, - ...(item?.amount - ? { - coin: { - amount: item.amount, - coinType: item?.coinType || null, - }, - } - : {}), - })); + const [recipientsPageNumber, setRecipientsPageNumber] = useState(1); - const [formattedAmount, symbol] = useFormatCoin( - txnTransfer?.[0].amount, - txnTransfer?.[0].coinType + const coinTransfer = useMemo( + () => + getAmount({ + txnData: transaction, + }), + [transaction] ); - const txKindData = formatByTransactionKind(txKindName, txdetails, sender); + const recipients = useMemo(() => { + const startAt = (recipientsPageNumber - 1) * MAX_RECIPIENTS_PER_PAGE; + const endAt = recipientsPageNumber * MAX_RECIPIENTS_PER_PAGE; + return coinTransfer.slice(startAt, endAt); + }, [coinTransfer, recipientsPageNumber]); + + // select the first element in the array, if there are more than one element we don't show the total amount sent but display the individual amounts + // use absolute value + const totalRecipientsCount = coinTransfer.length; + const transferAmount = coinTransfer?.[0]?.amount + ? Math.abs(coinTransfer[0].amount) + : null; + + const [formattedAmount, symbol] = useFormatCoin( + transferAmount, + coinTransfer?.[0]?.coinType + ); + const txKindData = formatByTransactionKind(txKindName, txnDetails, sender); const txEventData = transaction.effects.events?.map(eventToDisplay); let eventTitles: [string, string][] = []; @@ -477,7 +490,8 @@ function TransactionView({ ])} data-testid="transaction-timestamp" > - {txnTransfer?.[0].amount ? ( + {coinTransfer.length === 1 && + formattedAmount ? (
) )} + +
+ {totalRecipientsCount > + MAX_RECIPIENTS_PER_PAGE && ( + + )} +
- {multipleRecipientsList.map((recipient) => ( -
- - {recipient?.coin && ( -
- -
- )} -
- ))} + {multipleRecipientsList.map( + ({ address, amount, coinType }) => ( +
+ + {amount ? ( +
+ +
+ ) : null} +
+ ) + )}
) : null} diff --git a/apps/explorer/src/ui/stories/SenderRecipient.stories.tsx b/apps/explorer/src/ui/stories/SenderRecipient.stories.tsx index d6a0e76e41f4b..6a423302238dd 100644 --- a/apps/explorer/src/ui/stories/SenderRecipient.stories.tsx +++ b/apps/explorer/src/ui/stories/SenderRecipient.stories.tsx @@ -13,30 +13,26 @@ const data = { recipients: [ { address: '0x955d8ddc4a17670bda6b949cbdbc8f5aac820cc7', - coin: { - amount: 1000, - symbol: '0x2::sui::SUI', - }, + + amount: 1000, + coinType: '0x2::sui::SUI', }, { address: '0x9798852b55fcbf352052c9414920ebf7811ce05e', - coin: { - amount: 120_030, - symbol: 'COIN', - }, + + amount: 120_030, + coinType: 'COIN', }, { address: '0xc4173a804406a365e69dfb297d4eaaf002546ebd', - coin: { - amount: 10_050_504, - symbol: 'MIST', - }, + + amount: 10_050_504, + coinType: 'MIST', }, { address: '0xca1e11744de126dd1b116c6a16df4715caea56a3', - coin: { - amount: '1000002', - }, + + amount: 1000002, }, { address: '0x49e095bc33fda565c07937478f201f4344941f03', diff --git a/apps/explorer/src/utils/getAmount.ts b/apps/explorer/src/utils/getAmount.ts index 764a7459abc35..d77d2f89a5e5d 100644 --- a/apps/explorer/src/utils/getAmount.ts +++ b/apps/explorer/src/utils/getAmount.ts @@ -7,11 +7,15 @@ import { getTransferSuiTransaction, getTransferObjectTransaction, getTransactionKindName, + getTransactionSender, + getTransactions, + SUI_TYPE_ARG, } from '@mysten/sui.js'; import type { SuiTransactionKind, TransactionEffects, + SuiTransactionResponse, SuiEvent, } from '@mysten/sui.js'; @@ -32,23 +36,23 @@ const getCoinType = ( }; type FormattedBalance = { - amount?: number | bigint | null; + amount?: number | null; coinType?: string | null; - isCoin?: boolean; - recipientAddress: string; -}[]; + address: string; +}; -export function getAmount( +// For TransferObject, TransferSui, Pay, PaySui, transactions get the amount from the transfer data +export function getTransfersAmount( txnData: SuiTransactionKind, txnEffect?: TransactionEffects -): FormattedBalance | null { +): FormattedBalance[] | null { const txKindName = getTransactionKindName(txnData); if (txKindName === 'TransferObject') { const txn = getTransferObjectTransaction(txnData); return txn?.recipient ? [ { - recipientAddress: txn?.recipient, + address: txn?.recipient, }, ] : null; @@ -59,11 +63,10 @@ export function getAmount( return txn?.recipient ? [ { - recipientAddress: txn.recipient, + address: txn.recipient, amount: txn?.amount, coinType: txnEffect && getCoinType(txnEffect, txn.recipient), - isCoin: true, }, ] : null; @@ -87,10 +90,9 @@ export function getAmount( paySuiData.recipients[0] ) : null, - recipientAddress: + address: paySuiData.recipients[index] || paySuiData.recipients[0], - isCoin: true, }, }; }, @@ -98,10 +100,64 @@ export function getAmount( [key: string]: { amount: number; coinType: string | null; - recipientAddress: string; + address: string; }; } ); - return amountByRecipient ? Object.values(amountByRecipient) : null; } + +// Get transaction amount from coinBalanceChange event for Call Txn +// Aggregate coinBalanceChange by coinType and address +function getTxnAmountFromCoinBalanceEvent( + txEffects: TransactionEffects, + address: string +): FormattedBalance[] { + const events = txEffects?.events || []; + const coinsMeta = {} as { [coinType: string]: FormattedBalance }; + + events.forEach((event) => { + if ( + 'coinBalanceChange' in event && + event?.coinBalanceChange?.changeType && + ['Receive', 'Pay'].includes(event?.coinBalanceChange?.changeType) && + event?.coinBalanceChange?.transactionModule !== 'gas' + ) { + const { coinBalanceChange } = event; + const { coinType, amount, owner, sender } = coinBalanceChange; + const { AddressOwner } = owner as { AddressOwner: string }; + if (AddressOwner === address || address === sender) { + coinsMeta[`${AddressOwner}${coinType}`] = { + amount: + (coinsMeta[`${AddressOwner}${coinType}`]?.amount || 0) + + amount, + coinType: coinType, + address: AddressOwner, + }; + } + } + }); + return Object.values(coinsMeta); +} + +// Get the amount from events and transfer data +// optional flag to get only SUI coin type for table view +export function getAmount({ + txnData, + suiCoinOnly = false, +}: { + txnData: SuiTransactionResponse; + suiCoinOnly?: boolean; +}) { + const { effects, certificate } = txnData; + const txnDetails = getTransactions(certificate)[0]; + const sender = getTransactionSender(certificate); + const suiTransfer = getTransfersAmount(txnDetails, effects); + const coinBalanceChange = getTxnAmountFromCoinBalanceEvent(effects, sender); + const transfers = suiTransfer || coinBalanceChange; + if (suiCoinOnly) { + return transfers?.filter(({ coinType }) => coinType === SUI_TYPE_ARG); + } + + return transfers; +}