explorer: introduce Token Balances on transaction details page (#14877)
* explorer: introduce Token Balances on transaction details page * fix: run prettier * introduce BigNumber.js * account for case where mint changes * introduce BalanceDelta component * remove unneeded import * break token balances card into own file
This commit is contained in:
parent
c0a6272afd
commit
f2f4003e70
5
explorer/package-lock.json
generated
5
explorer/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
33
explorer/src/components/common/BalanceDelta.tsx
Normal file
33
explorer/src/components/common/BalanceDelta.tsx
Normal file
@ -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 (
|
||||
<span className="badge badge-soft-success">
|
||||
+{isSol ? sols : delta.toString()}
|
||||
</span>
|
||||
);
|
||||
} else if (delta.lt(0)) {
|
||||
return (
|
||||
<span className="badge badge-soft-warning">
|
||||
{isSol ? <>-{sols}</> : delta.toString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="badge badge-soft-secondary">0</span>;
|
||||
}
|
157
explorer/src/components/transaction/TokenBalancesCard.tsx
Normal file
157
explorer/src/components/transaction/TokenBalancesCard.tsx
Normal file
@ -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 (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<Address pubkey={account} link />
|
||||
</td>
|
||||
<td>
|
||||
<Address pubkey={new PublicKey(mint)} link />
|
||||
</td>
|
||||
<td>
|
||||
<BalanceDelta delta={delta} />
|
||||
</td>
|
||||
<td>
|
||||
{balance.uiAmount} {units}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Token Balances</h3>
|
||||
</div>
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-muted">Address</th>
|
||||
<th className="text-muted">Token</th>
|
||||
<th className="text-muted">Change</th>
|
||||
<th className="text-muted">Post Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">{accountRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
@ -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) {
|
||||
<>
|
||||
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
||||
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
|
||||
<TokenBalancesCard signature={signature} />
|
||||
<SignatureContext.Provider value={signature}>
|
||||
<InstructionsSection signature={signature} />
|
||||
</SignatureContext.Provider>
|
||||
@ -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 <span className="badge badge-soft-success">+{sols}</span>;
|
||||
} else {
|
||||
return <span className="badge badge-soft-warning">-{sols}</span>;
|
||||
}
|
||||
};
|
||||
const delta = new BigNumber(post).minus(new BigNumber(pre));
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<Address pubkey={pubkey} link />
|
||||
</td>
|
||||
<td>{renderChange()}</td>
|
||||
<td>
|
||||
<BalanceDelta delta={delta} isSol />
|
||||
</td>
|
||||
<td>{lamportsToSolString(post)}</td>
|
||||
<td>
|
||||
{index === 0 && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user