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:
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",
|
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
|
"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": {
|
"binary-extensions": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
"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-router-dom": "^5.1.7",
|
||||||
"@types/react-select": "^3.1.2",
|
"@types/react-select": "^3.1.2",
|
||||||
"@types/socket.io-client": "^1.4.35",
|
"@types/socket.io-client": "^1.4.35",
|
||||||
|
"bignumber.js": "^9.0.1",
|
||||||
"bn.js": "^5.1.3",
|
"bn.js": "^5.1.3",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bs58": "^4.0.1",
|
"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 { TokenSwapDetailsCard } from "components/instruction/TokenSwapDetailsCard";
|
||||||
import { isSerumInstruction } from "components/instruction/serum/types";
|
import { isSerumInstruction } from "components/instruction/serum/types";
|
||||||
import { MemoDetailsCard } from "components/instruction/MemoDetailsCard";
|
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 AUTO_REFRESH_INTERVAL = 2000;
|
||||||
const ZERO_CONFIRMATION_BAILOUT = 5;
|
const ZERO_CONFIRMATION_BAILOUT = 5;
|
||||||
export const INNER_INSTRUCTIONS_START_SLOT = 46915769;
|
export const INNER_INSTRUCTIONS_START_SLOT = 46915769;
|
||||||
|
|
||||||
type SignatureProps = {
|
export type SignatureProps = {
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -117,6 +120,7 @@ export function TransactionDetailsPage({ signature: raw }: SignatureProps) {
|
|||||||
<>
|
<>
|
||||||
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
||||||
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
|
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
|
||||||
|
<TokenBalancesCard signature={signature} />
|
||||||
<SignatureContext.Provider value={signature}>
|
<SignatureContext.Provider value={signature}>
|
||||||
<InstructionsSection signature={signature} />
|
<InstructionsSection signature={signature} />
|
||||||
</SignatureContext.Provider>
|
</SignatureContext.Provider>
|
||||||
@ -352,23 +356,16 @@ function AccountsCard({
|
|||||||
const post = meta.postBalances[index];
|
const post = meta.postBalances[index];
|
||||||
const pubkey = account.pubkey;
|
const pubkey = account.pubkey;
|
||||||
const key = account.pubkey.toBase58();
|
const key = account.pubkey.toBase58();
|
||||||
const renderChange = () => {
|
const delta = new BigNumber(post).minus(new BigNumber(pre));
|
||||||
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>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
<td>
|
<td>
|
||||||
<Address pubkey={pubkey} link />
|
<Address pubkey={pubkey} link />
|
||||||
</td>
|
</td>
|
||||||
<td>{renderChange()}</td>
|
<td>
|
||||||
|
<BalanceDelta delta={delta} isSol />
|
||||||
|
</td>
|
||||||
<td>{lamportsToSolString(post)}</td>
|
<td>{lamportsToSolString(post)}</td>
|
||||||
<td>
|
<td>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
|
Reference in New Issue
Block a user