diff --git a/explorer/package-lock.json b/explorer/package-lock.json index d229007cd3..97939e79d1 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -4747,6 +4747,11 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", diff --git a/explorer/package.json b/explorer/package.json index 1898547066..f188be8dc9 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -22,6 +22,7 @@ "@types/react-router-dom": "^5.1.7", "@types/react-select": "^3.1.2", "@types/socket.io-client": "^1.4.35", + "bignumber.js": "^9.0.1", "bn.js": "^5.1.3", "bootstrap": "^4.6.0", "bs58": "^4.0.1", diff --git a/explorer/src/components/common/BalanceDelta.tsx b/explorer/src/components/common/BalanceDelta.tsx new file mode 100644 index 0000000000..45981dac73 --- /dev/null +++ b/explorer/src/components/common/BalanceDelta.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { BigNumber } from "bignumber.js"; +import { lamportsToSolString } from "utils"; + +export function BalanceDelta({ + delta, + isSol = false, +}: { + delta: BigNumber; + isSol?: boolean; +}) { + let sols; + + if (isSol) { + sols = lamportsToSolString(delta.toNumber()); + } + + if (delta.gt(0)) { + return ( + + +{isSol ? sols : delta.toString()} + + ); + } else if (delta.lt(0)) { + return ( + + {isSol ? <>-{sols} : delta.toString()} + + ); + } + + return 0; +} diff --git a/explorer/src/components/transaction/TokenBalancesCard.tsx b/explorer/src/components/transaction/TokenBalancesCard.tsx new file mode 100644 index 0000000000..10e7e14e18 --- /dev/null +++ b/explorer/src/components/transaction/TokenBalancesCard.tsx @@ -0,0 +1,157 @@ +import React from "react"; +import { + ParsedMessageAccount, + PublicKey, + TokenAmount, + TokenBalance, +} from "@solana/web3.js"; +import { BigNumber } from "bignumber.js"; +import { Address } from "components/common/Address"; +import { BalanceDelta } from "components/common/BalanceDelta"; +import { SignatureProps } from "pages/TransactionDetailsPage"; +import { useCluster } from "providers/cluster"; +import { useTransactionDetails } from "providers/transactions"; +import { TokenRegistry } from "tokenRegistry"; + +type TokenBalanceRow = { + account: PublicKey; + mint: string; + balance: TokenAmount; + delta: BigNumber; + accountIndex: number; +}; + +export function TokenBalancesCard({ signature }: SignatureProps) { + const details = useTransactionDetails(signature); + const { cluster } = useCluster(); + + if (!details) { + return null; + } + + const preTokenBalances = details.data?.transaction?.meta?.preTokenBalances; + const postTokenBalances = details.data?.transaction?.meta?.postTokenBalances; + + const accountKeys = + details.data?.transaction?.transaction.message.accountKeys; + + if (!preTokenBalances || !postTokenBalances || !accountKeys) { + return null; + } + + const rows = generateTokenBalanceRows( + preTokenBalances, + postTokenBalances, + accountKeys + ); + + if (rows.length < 1) { + return null; + } + + const accountRows = rows.map(({ account, delta, balance, mint }) => { + const key = account.toBase58() + mint; + const units = TokenRegistry.get(mint, cluster)?.symbol || "tokens"; + + return ( + + +
+ + +
+ + + + + + {balance.uiAmount} {units} + + + ); + }); + + return ( +
+
+

Token Balances

+
+
+ + + + + + + + + + {accountRows} +
AddressTokenChangePost Balance
+
+
+ ); +} + +function generateTokenBalanceRows( + preTokenBalances: TokenBalance[], + postTokenBalances: TokenBalance[], + accounts: ParsedMessageAccount[] +): TokenBalanceRow[] { + let preBalanceMap: { [index: number]: TokenBalance } = {}; + + preTokenBalances.forEach( + (balance) => (preBalanceMap[balance.accountIndex] = balance) + ); + + let rows: TokenBalanceRow[] = []; + + postTokenBalances.forEach(({ uiTokenAmount, accountIndex, mint }) => { + const preBalance = preBalanceMap[accountIndex]; + const account = accounts[accountIndex].pubkey; + + // case where mint changes + if (preBalance && preBalance.mint !== mint) { + rows.push({ + account: accounts[accountIndex].pubkey, + accountIndex, + balance: { + decimals: preBalance.uiTokenAmount.decimals, + amount: "0", + uiAmount: 0, + }, + delta: new BigNumber(-preBalance.uiTokenAmount.uiAmount), + mint: preBalance.mint, + }); + + rows.push({ + account: accounts[accountIndex].pubkey, + accountIndex, + balance: uiTokenAmount, + delta: new BigNumber(uiTokenAmount.uiAmount), + mint: mint, + }); + return; + } + + let delta; + + if (preBalance) { + delta = new BigNumber(uiTokenAmount.uiAmount).minus( + preBalance.uiTokenAmount.uiAmount + ); + } else { + delta = new BigNumber(uiTokenAmount.uiAmount); + } + + rows.push({ + account, + mint, + balance: uiTokenAmount, + delta, + accountIndex, + }); + }); + + return rows.sort((a, b) => a.accountIndex - b.accountIndex); +} diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx index 4fc0c54d31..e52e4c6a46 100644 --- a/explorer/src/pages/TransactionDetailsPage.tsx +++ b/explorer/src/pages/TransactionDetailsPage.tsx @@ -40,12 +40,15 @@ import { isTokenSwapInstruction } from "components/instruction/token-swap/types" import { TokenSwapDetailsCard } from "components/instruction/TokenSwapDetailsCard"; import { isSerumInstruction } from "components/instruction/serum/types"; import { MemoDetailsCard } from "components/instruction/MemoDetailsCard"; +import { BigNumber } from "bignumber.js"; +import { BalanceDelta } from "components/common/BalanceDelta"; +import { TokenBalancesCard } from "components/transaction/TokenBalancesCard"; const AUTO_REFRESH_INTERVAL = 2000; const ZERO_CONFIRMATION_BAILOUT = 5; export const INNER_INSTRUCTIONS_START_SLOT = 46915769; -type SignatureProps = { +export type SignatureProps = { signature: TransactionSignature; }; @@ -117,6 +120,7 @@ export function TransactionDetailsPage({ signature: raw }: SignatureProps) { <> + @@ -352,23 +356,16 @@ function AccountsCard({ const post = meta.postBalances[index]; const pubkey = account.pubkey; const key = account.pubkey.toBase58(); - const renderChange = () => { - const change = post - pre; - if (change === 0) return ""; - const sols = lamportsToSolString(change); - if (change > 0) { - return +{sols}; - } else { - return -{sols}; - } - }; + const delta = new BigNumber(post).minus(new BigNumber(pre)); return (
- {renderChange()} + + + {lamportsToSolString(post)} {index === 0 && (