Fix history fetching and inner spl token instructions (#12515)
This commit is contained in:
parent
0d5258b6d3
commit
65cc6a69c8
|
@ -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,104 +243,112 @@ function instructionTypeName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TokenTransactionRow({
|
const TokenTransactionRow = React.memo(
|
||||||
mint,
|
({
|
||||||
tx,
|
mint,
|
||||||
}: {
|
tx,
|
||||||
mint: PublicKey;
|
details,
|
||||||
tx: ConfirmedSignatureInfo;
|
}: {
|
||||||
}) {
|
mint: PublicKey;
|
||||||
const details = useTransactionDetails(tx.signature);
|
tx: ConfirmedSignatureInfo;
|
||||||
const fetchDetails = useFetchTransactionDetails();
|
details: CacheEntry<Details> | undefined;
|
||||||
|
}) => {
|
||||||
|
const fetchDetails = useFetchTransactionDetails();
|
||||||
|
|
||||||
// Fetch details on load
|
// Fetch details on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
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) {
|
if (tx.err) {
|
||||||
const tokenInstructions = instructions.filter(
|
statusClass = "warning";
|
||||||
(ix) => "parsed" in ix && ix.program === "spl-token"
|
statusText = "Failed";
|
||||||
) as ParsedInstruction[];
|
} else {
|
||||||
if (tokenInstructions.length > 0) {
|
statusClass = "success";
|
||||||
return (
|
statusText = "Success";
|
||||||
<>
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue