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:
Josh 2021-02-08 19:54:03 -08:00 committed by GitHub
parent c0a6272afd
commit f2f4003e70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 205 additions and 12 deletions

View File

@ -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",

View File

@ -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",

View 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>;
}

View 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);
}

View File

@ -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 && (