Fix history fetching and inner spl token instructions (#12515)

This commit is contained in:
Justin Starry 2020-09-29 21:24:01 +08:00 committed by GitHub
parent 0d5258b6d3
commit 65cc6a69c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 158 additions and 103 deletions

View File

@ -4,7 +4,7 @@ import {
ConfirmedSignatureInfo,
ParsedInstruction,
} from "@solana/web3.js";
import { FetchStatus } from "providers/cache";
import { CacheEntry, FetchStatus } from "providers/cache";
import {
useAccountHistories,
useFetchAccountHistory,
@ -12,14 +12,18 @@ import {
import {
useAccountOwnedTokens,
TokenInfoWithPubkey,
TOKEN_PROGRAM_ID,
} from "providers/accounts/tokens";
import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard";
import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address";
import { Slot } from "components/common/Slot";
import { useTransactionDetails } from "providers/transactions";
import { useFetchTransactionDetails } from "providers/transactions/details";
import {
Details,
useFetchTransactionDetails,
useTransactionDetailsCache,
} from "providers/transactions/details";
import { coerce } from "superstruct";
import { ParsedInfo } from "validators";
import {
@ -51,6 +55,7 @@ export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const accountHistories = useAccountHistories();
const fetchAccountHistory = useFetchAccountHistory();
const transactionDetailsCache = useTransactionDetailsCache();
const fetchHistories = (refresh?: boolean) => {
tokens.forEach((token) => {
@ -68,11 +73,30 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchedFullHistory = tokens.every((token) => {
const allFoundOldest = tokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.data?.foundOldest === true;
});
const allFetchedSome = tokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.data !== undefined;
});
// Find the oldest slot which we know we have the full history for
let oldestSlot: number | undefined = allFoundOldest ? 0 : undefined;
if (!allFoundOldest && allFetchedSome) {
tokens.forEach((token) => {
const history = accountHistories[token.pubkey.toBase58()];
if (history?.data?.foundOldest === false) {
const earliest =
history.data.fetched[history.data.fetched.length - 1].slot;
if (!oldestSlot) oldestSlot = earliest;
oldestSlot = Math.max(oldestSlot, earliest);
}
});
}
const fetching = tokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.status === FetchStatus.Fetching;
@ -102,6 +126,9 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
if (sigSet.has(tx.signature)) return false;
sigSet.add(tx.signature);
return true;
})
.filter(({ tx }) => {
return oldestSlot !== undefined && tx.slot >= oldestSlot;
});
if (mintAndTxs.length === 0) {
@ -166,14 +193,19 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
</thead>
<tbody className="list">
{mintAndTxs.map(({ mint, tx }) => (
<TokenTransactionRow key={tx.signature} mint={mint} tx={tx} />
<TokenTransactionRow
key={tx.signature}
mint={mint}
tx={tx}
details={transactionDetailsCache[tx.signature]}
/>
))}
</tbody>
</table>
</div>
<div className="card-footer">
{fetchedFullHistory ? (
{allFoundOldest ? (
<div className="text-muted text-center">Fetched full history</div>
) : (
<button
@ -211,104 +243,112 @@ function instructionTypeName(
}
}
function TokenTransactionRow({
mint,
tx,
}: {
mint: PublicKey;
tx: ConfirmedSignatureInfo;
}) {
const details = useTransactionDetails(tx.signature);
const fetchDetails = useFetchTransactionDetails();
const TokenTransactionRow = React.memo(
({
mint,
tx,
details,
}: {
mint: PublicKey;
tx: ConfirmedSignatureInfo;
details: CacheEntry<Details> | undefined;
}) => {
const fetchDetails = useFetchTransactionDetails();
// Fetch details on load
React.useEffect(() => {
if (!details) fetchDetails(tx.signature);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch details on load
React.useEffect(() => {
if (!details) fetchDetails(tx.signature);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const instructions =
details?.data?.transaction?.transaction.message.instructions;
if (instructions) {
const tokenInstructions = instructions.filter(
(ix) => "parsed" in ix && ix.program === "spl-token"
) as ParsedInstruction[];
if (tokenInstructions.length > 0) {
return (
<>
{tokenInstructions.map((ix, index) => {
const typeName = instructionTypeName(ix, tx);
let statusText;
let statusClass;
if (tx.err) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
return (
<tr key={index}>
<td className="w-1">
<Slot slot={tx.slot} />
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>
{statusText}
</span>
</td>
<td>
<Address pubkey={mint} link truncate />
</td>
<td>{typeName}</td>
<td>
<Signature signature={tx.signature} link />
</td>
</tr>
);
})}
</>
);
let statusText: string;
let statusClass: string;
if (tx.err) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
const instructions =
details?.data?.transaction?.transaction.message.instructions;
if (!instructions)
return (
<tr key={tx.signature}>
<td className="w-1">
<Slot slot={tx.slot} />
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>
{statusText}
</span>
</td>
<td>
<Address pubkey={mint} link truncate />
</td>
<td>
<span className="spinner-grow spinner-grow-sm mr-2"></span>
Loading
</td>
<td>
<Signature signature={tx.signature} link />
</td>
</tr>
);
const tokenInstructionNames = instructions
.map((ix): string | undefined => {
if ("parsed" in ix) {
if (ix.program === "spl-token") {
return instructionTypeName(ix, tx);
} else {
return undefined;
}
} else {
if (
ix.accounts.findIndex((account) =>
account.equals(TOKEN_PROGRAM_ID)
) >= 0
) {
return "Unknown (Inner)";
}
return undefined;
}
})
.filter((name) => name !== undefined) as string[];
return (
<>
{tokenInstructionNames.map((typeName, index) => {
return (
<tr key={index}>
<td className="w-1">
<Slot slot={tx.slot} />
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>
{statusText}
</span>
</td>
<td>
<Address pubkey={mint} link truncate />
</td>
<td>{typeName}</td>
<td>
<Signature signature={tx.signature} link />
</td>
</tr>
);
})}
</>
);
}
let statusText;
let statusClass;
if (tx.err) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
return (
<tr key={tx.signature}>
<td className="w-1">
<Slot slot={tx.slot} />
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
</td>
<td>
<Address pubkey={mint} link />
</td>
<td>
<span className="spinner-grow spinner-grow-sm mr-2"></span>
Loading
</td>
<td>
<Signature signature={tx.signature} link />
</td>
</tr>
);
}
);

View File

@ -104,3 +104,18 @@ export function useTransactionDetails(
return context.entries[signature];
}
export type TransactionDetailsCache = {
[key: string]: Cache.CacheEntry<Details>;
};
export function useTransactionDetailsCache(): TransactionDetailsCache {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useTransactionDetailsCache must be used within a TransactionsProvider`
);
}
return context.entries;
}