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, ConfirmedSignatureInfo,
ParsedInstruction, ParsedInstruction,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { FetchStatus } from "providers/cache"; import { CacheEntry, FetchStatus } from "providers/cache";
import { import {
useAccountHistories, useAccountHistories,
useFetchAccountHistory, useFetchAccountHistory,
@ -12,14 +12,18 @@ import {
import { import {
useAccountOwnedTokens, useAccountOwnedTokens,
TokenInfoWithPubkey, TokenInfoWithPubkey,
TOKEN_PROGRAM_ID,
} from "providers/accounts/tokens"; } from "providers/accounts/tokens";
import { ErrorCard } from "components/common/ErrorCard"; import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard"; import { LoadingCard } from "components/common/LoadingCard";
import { Signature } from "components/common/Signature"; import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address"; import { Address } from "components/common/Address";
import { Slot } from "components/common/Slot"; import { Slot } from "components/common/Slot";
import { useTransactionDetails } from "providers/transactions"; import {
import { useFetchTransactionDetails } from "providers/transactions/details"; Details,
useFetchTransactionDetails,
useTransactionDetailsCache,
} from "providers/transactions/details";
import { coerce } from "superstruct"; import { coerce } from "superstruct";
import { ParsedInfo } from "validators"; import { ParsedInfo } from "validators";
import { import {
@ -51,6 +55,7 @@ export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const accountHistories = useAccountHistories(); const accountHistories = useAccountHistories();
const fetchAccountHistory = useFetchAccountHistory(); const fetchAccountHistory = useFetchAccountHistory();
const transactionDetailsCache = useTransactionDetailsCache();
const fetchHistories = (refresh?: boolean) => { const fetchHistories = (refresh?: boolean) => {
tokens.forEach((token) => { tokens.forEach((token) => {
@ -68,11 +73,30 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
}); });
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchedFullHistory = tokens.every((token) => { const allFoundOldest = tokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()]; const history = accountHistories[token.pubkey.toBase58()];
return history?.data?.foundOldest === true; 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 fetching = tokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()]; const history = accountHistories[token.pubkey.toBase58()];
return history?.status === FetchStatus.Fetching; return history?.status === FetchStatus.Fetching;
@ -102,6 +126,9 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
if (sigSet.has(tx.signature)) return false; if (sigSet.has(tx.signature)) return false;
sigSet.add(tx.signature); sigSet.add(tx.signature);
return true; return true;
})
.filter(({ tx }) => {
return oldestSlot !== undefined && tx.slot >= oldestSlot;
}); });
if (mintAndTxs.length === 0) { if (mintAndTxs.length === 0) {
@ -166,14 +193,19 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
</thead> </thead>
<tbody className="list"> <tbody className="list">
{mintAndTxs.map(({ mint, tx }) => ( {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> </tbody>
</table> </table>
</div> </div>
<div className="card-footer"> <div className="card-footer">
{fetchedFullHistory ? ( {allFoundOldest ? (
<div className="text-muted text-center">Fetched full history</div> <div className="text-muted text-center">Fetched full history</div>
) : ( ) : (
<button <button
@ -211,14 +243,16 @@ function instructionTypeName(
} }
} }
function TokenTransactionRow({ const TokenTransactionRow = React.memo(
({
mint, mint,
tx, tx,
}: { details,
}: {
mint: PublicKey; mint: PublicKey;
tx: ConfirmedSignatureInfo; tx: ConfirmedSignatureInfo;
}) { details: CacheEntry<Details> | undefined;
const details = useTransactionDetails(tx.signature); }) => {
const fetchDetails = useFetchTransactionDetails(); const fetchDetails = useFetchTransactionDetails();
// Fetch details on load // Fetch details on load
@ -226,20 +260,8 @@ function TokenTransactionRow({
if (!details) fetchDetails(tx.signature); if (!details) fetchDetails(tx.signature);
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
const instructions = let statusText: string;
details?.data?.transaction?.transaction.message.instructions; let statusClass: string;
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) { if (tx.err) {
statusClass = "warning"; statusClass = "warning";
statusText = "Failed"; statusText = "Failed";
@ -248,6 +270,60 @@ function TokenTransactionRow({
statusText = "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 ( return (
<tr key={index}> <tr key={index}>
<td className="w-1"> <td className="w-1">
@ -275,40 +351,4 @@ function TokenTransactionRow({
</> </>
); );
} }
} );
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]; 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;
}