explorer: Token history dropdown filter (#14032)
* feat: add filter on token history card * rename to filter * use unicode ellipsis * better naming * filter options read better and use just pubkey strings * memoize filtered list * only fetch filtered * pre filter tokens * fix prettier
This commit is contained in:
parent
db4ac17259
commit
3a154e8056
|
@ -32,7 +32,7 @@ import {
|
||||||
IX_TITLES,
|
IX_TITLES,
|
||||||
} from "components/instruction/token/types";
|
} from "components/instruction/token/types";
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
import { intoTransactionInstruction } from "utils/tx";
|
import { intoTransactionInstruction, displayAddress } from "utils/tx";
|
||||||
import {
|
import {
|
||||||
isTokenSwapInstruction,
|
isTokenSwapInstruction,
|
||||||
parseTokenSwapInstructionTitle,
|
parseTokenSwapInstructionTitle,
|
||||||
|
@ -43,6 +43,12 @@ import {
|
||||||
} from "components/instruction/serum/types";
|
} from "components/instruction/serum/types";
|
||||||
import { INNER_INSTRUCTIONS_START_SLOT } from "pages/TransactionDetailsPage";
|
import { INNER_INSTRUCTIONS_START_SLOT } from "pages/TransactionDetailsPage";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Location } from "history";
|
||||||
|
import { useQuery } from "utils/url";
|
||||||
|
|
||||||
|
const TRUNCATE_TOKEN_LENGTH = 10;
|
||||||
|
const ALL_TOKENS = "";
|
||||||
|
|
||||||
type InstructionType = {
|
type InstructionType = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -69,20 +75,49 @@ export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
return <TokenHistoryTable tokens={tokens} />;
|
return <TokenHistoryTable tokens={tokens} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useQueryFilter = (): string => {
|
||||||
|
const query = useQuery();
|
||||||
|
const filter = query.get("filter");
|
||||||
|
return filter || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterProps = {
|
||||||
|
filter: string;
|
||||||
|
toggle: () => void;
|
||||||
|
show: boolean;
|
||||||
|
tokens: TokenInfoWithPubkey[];
|
||||||
|
};
|
||||||
|
|
||||||
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 transactionDetailsCache = useTransactionDetailsCache();
|
||||||
|
const [showDropdown, setDropdown] = React.useState(false);
|
||||||
|
const filter = useQueryFilter();
|
||||||
|
|
||||||
const fetchHistories = (refresh?: boolean) => {
|
const filteredTokens = React.useMemo(
|
||||||
tokens.forEach((token) => {
|
() =>
|
||||||
fetchAccountHistory(token.pubkey, refresh);
|
tokens.filter((token) => {
|
||||||
});
|
if (filter === ALL_TOKENS) {
|
||||||
};
|
return true;
|
||||||
|
}
|
||||||
|
return token.info.mint.toBase58() === filter;
|
||||||
|
}),
|
||||||
|
[tokens, filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchHistories = React.useCallback(
|
||||||
|
(refresh?: boolean) => {
|
||||||
|
filteredTokens.forEach((token) => {
|
||||||
|
fetchAccountHistory(token.pubkey, refresh);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[filteredTokens, fetchAccountHistory]
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch histories on load
|
// Fetch histories on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
tokens.forEach((token) => {
|
filteredTokens.forEach((token) => {
|
||||||
const address = token.pubkey.toBase58();
|
const address = token.pubkey.toBase58();
|
||||||
if (!accountHistories[address]) {
|
if (!accountHistories[address]) {
|
||||||
fetchAccountHistory(token.pubkey, true);
|
fetchAccountHistory(token.pubkey, true);
|
||||||
|
@ -90,20 +125,21 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
});
|
});
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const allFoundOldest = tokens.every((token) => {
|
const allFoundOldest = filteredTokens.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 allFetchedSome = filteredTokens.every((token) => {
|
||||||
const history = accountHistories[token.pubkey.toBase58()];
|
const history = accountHistories[token.pubkey.toBase58()];
|
||||||
return history?.data !== undefined;
|
return history?.data !== undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the oldest slot which we know we have the full history for
|
// Find the oldest slot which we know we have the full history for
|
||||||
let oldestSlot: number | undefined = allFoundOldest ? 0 : undefined;
|
let oldestSlot: number | undefined = allFoundOldest ? 0 : undefined;
|
||||||
|
|
||||||
if (!allFoundOldest && allFetchedSome) {
|
if (!allFoundOldest && allFetchedSome) {
|
||||||
tokens.forEach((token) => {
|
filteredTokens.forEach((token) => {
|
||||||
const history = accountHistories[token.pubkey.toBase58()];
|
const history = accountHistories[token.pubkey.toBase58()];
|
||||||
if (history?.data?.foundOldest === false) {
|
if (history?.data?.foundOldest === false) {
|
||||||
const earliest =
|
const earliest =
|
||||||
|
@ -114,18 +150,18 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetching = tokens.some((token) => {
|
const fetching = filteredTokens.some((token) => {
|
||||||
const history = accountHistories[token.pubkey.toBase58()];
|
const history = accountHistories[token.pubkey.toBase58()];
|
||||||
return history?.status === FetchStatus.Fetching;
|
return history?.status === FetchStatus.Fetching;
|
||||||
});
|
});
|
||||||
|
|
||||||
const failed = tokens.some((token) => {
|
const failed = filteredTokens.some((token) => {
|
||||||
const history = accountHistories[token.pubkey.toBase58()];
|
const history = accountHistories[token.pubkey.toBase58()];
|
||||||
return history?.status === FetchStatus.FetchFailed;
|
return history?.status === FetchStatus.FetchFailed;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sigSet = new Set();
|
const sigSet = new Set();
|
||||||
const mintAndTxs = tokens
|
const mintAndTxs = filteredTokens
|
||||||
.map((token) => ({
|
.map((token) => ({
|
||||||
mint: token.info.mint,
|
mint: token.info.mint,
|
||||||
history: accountHistories[token.pubkey.toBase58()],
|
history: accountHistories[token.pubkey.toBase58()],
|
||||||
|
@ -148,6 +184,12 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
return oldestSlot !== undefined && tx.slot >= oldestSlot;
|
return oldestSlot !== undefined && tx.slot >= oldestSlot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!fetching && mintAndTxs.length < 1 && !allFoundOldest) {
|
||||||
|
fetchHistories();
|
||||||
|
}
|
||||||
|
}, [fetching, mintAndTxs, allFoundOldest, fetchHistories]);
|
||||||
|
|
||||||
if (mintAndTxs.length === 0) {
|
if (mintAndTxs.length === 0) {
|
||||||
if (fetching) {
|
if (fetching) {
|
||||||
return <LoadingCard message="Loading history" />;
|
return <LoadingCard message="Loading history" />;
|
||||||
|
@ -178,6 +220,12 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header align-items-center">
|
<div className="card-header align-items-center">
|
||||||
<h3 className="card-header-title">Token History</h3>
|
<h3 className="card-header-title">Token History</h3>
|
||||||
|
<FilterDropdown
|
||||||
|
filter={filter}
|
||||||
|
toggle={() => setDropdown((show) => !show)}
|
||||||
|
show={showDropdown}
|
||||||
|
tokens={tokens}
|
||||||
|
></FilterDropdown>
|
||||||
<button
|
<button
|
||||||
className="btn btn-white btn-sm"
|
className="btn btn-white btn-sm"
|
||||||
disabled={fetching}
|
disabled={fetching}
|
||||||
|
@ -245,6 +293,67 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FilterDropdown = ({ filter, toggle, show, tokens }: FilterProps) => {
|
||||||
|
const { cluster } = useCluster();
|
||||||
|
|
||||||
|
const buildLocation = (location: Location, filter: string) => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
if (filter === ALL_TOKENS) {
|
||||||
|
params.delete("filter");
|
||||||
|
} else {
|
||||||
|
params.set("filter", filter);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
search: params.toString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOptions: string[] = [ALL_TOKENS];
|
||||||
|
const nameLookup: { [mint: string]: string } = {};
|
||||||
|
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
const pubkey = token.info.mint.toBase58();
|
||||||
|
filterOptions.push(pubkey);
|
||||||
|
nameLookup[pubkey] = formatTokenName(pubkey, cluster);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dropdown mr-2">
|
||||||
|
<small className="mr-2">Filter:</small>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{filter === ALL_TOKENS ? "All Tokens" : nameLookup[filter]}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={`token-filter dropdown-menu-right dropdown-menu${
|
||||||
|
show ? " show" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filterOptions.map((filterOption) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={filterOption}
|
||||||
|
to={(location: Location) => buildLocation(location, filterOption)}
|
||||||
|
className={`dropdown-item${
|
||||||
|
filterOption === filter ? " active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{filterOption === ALL_TOKENS
|
||||||
|
? "All Tokens"
|
||||||
|
: formatTokenName(filterOption, cluster)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function instructionTypeName(
|
function instructionTypeName(
|
||||||
ix: ParsedInstruction,
|
ix: ParsedInstruction,
|
||||||
tx: ConfirmedSignatureInfo
|
tx: ConfirmedSignatureInfo
|
||||||
|
@ -478,3 +587,13 @@ function InstructionDetails({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTokenName(pubkey: string, cluster: Cluster): string {
|
||||||
|
let display = displayAddress(pubkey, cluster);
|
||||||
|
|
||||||
|
if (display === pubkey) {
|
||||||
|
display = display.slice(0, TRUNCATE_TOKEN_LENGTH) + "\u2026";
|
||||||
|
}
|
||||||
|
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
|
@ -321,3 +321,8 @@ div.inner-cards {
|
||||||
content:'';
|
content:'';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.token-filter {
|
||||||
|
max-height: 20rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue