Split explorer account details page into tabs (#11450)
This commit is contained in:
parent
bad486823c
commit
67fdf593a2
|
@ -5,7 +5,6 @@ import AccountDetails from "./components/AccountDetails";
|
||||||
import TransactionDetails from "./components/TransactionDetails";
|
import TransactionDetails from "./components/TransactionDetails";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import { TX_ALIASES } from "./providers/transactions";
|
import { TX_ALIASES } from "./providers/transactions";
|
||||||
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
|
||||||
import TopAccountsCard from "components/TopAccountsCard";
|
import TopAccountsCard from "components/TopAccountsCard";
|
||||||
import SupplyCard from "components/SupplyCard";
|
import SupplyCard from "components/SupplyCard";
|
||||||
import StatsCard from "components/StatsCard";
|
import StatsCard from "components/StatsCard";
|
||||||
|
@ -14,6 +13,8 @@ import Navbar from "components/Navbar";
|
||||||
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
||||||
import { SearchBar } from "components/SearchBar";
|
import { SearchBar } from "components/SearchBar";
|
||||||
|
|
||||||
|
const ACCOUNT_ALIASES = ["account", "accounts", "addresses"];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -41,11 +42,26 @@ function App() {
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(
|
path={ACCOUNT_ALIASES.flatMap((account) => [
|
||||||
(account) => `/${account}/:address`
|
`/${account}/:address`,
|
||||||
)}
|
`/${account}/:address/:tab`,
|
||||||
|
])}
|
||||||
|
render={({ match, location }) => {
|
||||||
|
let pathname = `/address/${match.params.address}`;
|
||||||
|
if (match.params.tab) {
|
||||||
|
pathname += `/${match.params.tab}`;
|
||||||
|
}
|
||||||
|
return <Redirect to={{ ...location, pathname }} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={["/address/:address", "/address/:address/:tab"]}
|
||||||
render={({ match }) => (
|
render={({ match }) => (
|
||||||
<AccountDetails address={match.params.address} />
|
<AccountDetails
|
||||||
|
address={match.params.address}
|
||||||
|
tab={match.params.tab}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
|
|
|
@ -21,9 +21,11 @@ import {
|
||||||
import { useCluster, ClusterStatus } from "providers/cluster";
|
import { useCluster, ClusterStatus } from "providers/cluster";
|
||||||
import Address from "./common/Address";
|
import Address from "./common/Address";
|
||||||
import Signature from "./common/Signature";
|
import Signature from "./common/Signature";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { clusterPath } from "utils/url";
|
||||||
|
|
||||||
type Props = { address: string };
|
type Props = { address: string; tab?: string };
|
||||||
export default function AccountDetails({ address }: Props) {
|
export default function AccountDetails({ address, tab }: Props) {
|
||||||
let pubkey: PublicKey | undefined;
|
let pubkey: PublicKey | undefined;
|
||||||
try {
|
try {
|
||||||
pubkey = new PublicKey(address);
|
pubkey = new PublicKey(address);
|
||||||
|
@ -32,6 +34,11 @@ export default function AccountDetails({ address }: Props) {
|
||||||
// TODO handle bad addresses
|
// TODO handle bad addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let moreTab: MoreTabs = "history";
|
||||||
|
if (tab === "history" || tab === "tokens") {
|
||||||
|
moreTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-n3">
|
<div className="container mt-n3">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
@ -41,12 +48,51 @@ export default function AccountDetails({ address }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{pubkey && <AccountCards pubkey={pubkey} />}
|
{pubkey && <AccountCards pubkey={pubkey} />}
|
||||||
{pubkey && <TokensCard pubkey={pubkey} />}
|
{pubkey && <MoreSection pubkey={pubkey} tab={moreTab} />}
|
||||||
{pubkey && <HistoryCard pubkey={pubkey} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoreTabs = "history" | "tokens";
|
||||||
|
function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
if (!info || info.lamports === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body pt-0">
|
||||||
|
<ul className="nav nav-tabs nav-overflow header-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
History
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}/tokens`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
Tokens
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tab === "tokens" && <TokensCard pubkey={pubkey} />}
|
||||||
|
{tab === "history" && <HistoryCard pubkey={pubkey} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
||||||
const fetchAccount = useFetchAccountInfo();
|
const fetchAccount = useFetchAccountInfo();
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
|
@ -56,8 +102,7 @@ function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
|
||||||
// Fetch account on load
|
// Fetch account on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (pubkey && !info && status === ClusterStatus.Connected)
|
if (!info && status === ClusterStatus.Connected) fetchAccount(pubkey);
|
||||||
fetchAccount(pubkey);
|
|
||||||
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (!info || info.status === FetchStatus.Fetching) {
|
if (!info || info.status === FetchStatus.Fetching) {
|
||||||
|
@ -137,6 +182,11 @@ function TokensCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
const fetchAccountTokens = useFetchAccountOwnedTokens();
|
const fetchAccountTokens = useFetchAccountOwnedTokens();
|
||||||
const refresh = () => fetchAccountTokens(pubkey);
|
const refresh = () => fetchAccountTokens(pubkey);
|
||||||
|
|
||||||
|
// Fetch owned tokens
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!ownedTokens) refresh();
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (ownedTokens === undefined) {
|
if (ownedTokens === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -186,7 +236,7 @@ function TokensCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
return (
|
return (
|
||||||
<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">Tokens</h3>
|
<h3 className="card-header-title">Owned Tokens</h3>
|
||||||
<button
|
<button
|
||||||
className="btn btn-white btn-sm"
|
className="btn btn-white btn-sm"
|
||||||
disabled={fetching}
|
disabled={fetching}
|
||||||
|
@ -229,6 +279,10 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
const refresh = () => fetchAccountHistory(pubkey, true);
|
const refresh = () => fetchAccountHistory(pubkey, true);
|
||||||
const loadMore = () => fetchAccountHistory(pubkey);
|
const loadMore = () => fetchAccountHistory(pubkey);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!history) refresh();
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (!info || !history || info.lamports === undefined) {
|
if (!info || !history || info.lamports === undefined) {
|
||||||
return null;
|
return null;
|
||||||
} else if (history.fetched === undefined) {
|
} else if (history.fetched === undefined) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function Address({ pubkey, alignRight, link }: Props) {
|
||||||
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
|
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
|
||||||
<span className="text-monospace">
|
<span className="text-monospace">
|
||||||
{link ? (
|
{link ? (
|
||||||
<Link className="" to={clusterPath(`/accounts/${address}`)}>
|
<Link className="" to={clusterPath(`/address/${address}`)}>
|
||||||
{displayAddress(address)}
|
{displayAddress(address)}
|
||||||
<span className="fe fe-external-link ml-2"></span>
|
<span className="fe fe-external-link ml-2"></span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
Connection,
|
Connection,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useAccounts, FetchStatus } from "./index";
|
import { FetchStatus } from "./index";
|
||||||
import { useCluster } from "../cluster";
|
import { useCluster } from "../cluster";
|
||||||
|
|
||||||
interface AccountHistory {
|
interface AccountHistory {
|
||||||
|
@ -14,16 +14,19 @@ interface AccountHistory {
|
||||||
foundOldest: boolean;
|
foundOldest: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = { [address: string]: AccountHistory };
|
type State = {
|
||||||
|
url: string;
|
||||||
|
map: { [address: string]: AccountHistory };
|
||||||
|
};
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Update,
|
Update,
|
||||||
Add,
|
|
||||||
Clear,
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
|
url: string;
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
status: FetchStatus;
|
status: FetchStatus;
|
||||||
fetched?: ConfirmedSignatureInfo[];
|
fetched?: ConfirmedSignatureInfo[];
|
||||||
|
@ -31,16 +34,12 @@ interface Update {
|
||||||
foundOldest?: boolean;
|
foundOldest?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Add {
|
|
||||||
type: ActionType.Add;
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Clear {
|
interface Clear {
|
||||||
type: ActionType.Clear;
|
type: ActionType.Clear;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | Add | Clear;
|
type Action = Update | Clear;
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function combineFetched(
|
function combineFetched(
|
||||||
|
@ -63,42 +62,44 @@ function combineFetched(
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.Add: {
|
|
||||||
const details = { ...state };
|
|
||||||
const address = action.address;
|
|
||||||
if (!details[address]) {
|
|
||||||
details[address] = {
|
|
||||||
status: FetchStatus.Fetching,
|
|
||||||
foundOldest: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ActionType.Update: {
|
case ActionType.Update: {
|
||||||
|
if (action.url !== state.url) return state;
|
||||||
const address = action.pubkey.toBase58();
|
const address = action.pubkey.toBase58();
|
||||||
if (state[address]) {
|
if (state.map[address]) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
map: {
|
||||||
|
...state.map,
|
||||||
[address]: {
|
[address]: {
|
||||||
status: action.status,
|
status: action.status,
|
||||||
fetched: combineFetched(
|
fetched: combineFetched(
|
||||||
action.fetched,
|
action.fetched,
|
||||||
state[address].fetched,
|
state.map[address].fetched,
|
||||||
action.before
|
action.before
|
||||||
),
|
),
|
||||||
foundOldest: action.foundOldest || state[address].foundOldest,
|
foundOldest: action.foundOldest || state.map[address].foundOldest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
map: {
|
||||||
|
...state.map,
|
||||||
|
[address]: {
|
||||||
|
status: action.status,
|
||||||
|
fetched: action.fetched,
|
||||||
|
foundOldest: action.foundOldest || false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case ActionType.Clear: {
|
case ActionType.Clear: {
|
||||||
return {};
|
return { url: action.url, map: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StateContext = React.createContext<State | undefined>(undefined);
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
|
@ -106,30 +107,13 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
type HistoryProviderProps = { children: React.ReactNode };
|
type HistoryProviderProps = { children: React.ReactNode };
|
||||||
export function HistoryProvider({ children }: HistoryProviderProps) {
|
export function HistoryProvider({ children }: HistoryProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, {});
|
|
||||||
const { accounts, lastFetchedAddress } = useAccounts();
|
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, { url, map: {} });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: ActionType.Clear });
|
dispatch({ type: ActionType.Clear, url });
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
// Fetch history for new accounts
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (lastFetchedAddress) {
|
|
||||||
const infoFetched =
|
|
||||||
accounts[lastFetchedAddress] &&
|
|
||||||
accounts[lastFetchedAddress].lamports !== undefined;
|
|
||||||
const noHistory = !state[lastFetchedAddress];
|
|
||||||
if (infoFetched && noHistory) {
|
|
||||||
dispatch({ type: ActionType.Add, address: lastFetchedAddress });
|
|
||||||
fetchAccountHistory(dispatch, new PublicKey(lastFetchedAddress), url, {
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [accounts, lastFetchedAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
|
@ -149,6 +133,7 @@ async function fetchAccountHistory(
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
status: FetchStatus.Fetching,
|
status: FetchStatus.Fetching,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
|
@ -168,6 +153,7 @@ async function fetchAccountHistory(
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
|
url,
|
||||||
status,
|
status,
|
||||||
fetched,
|
fetched,
|
||||||
before: options?.before,
|
before: options?.before,
|
||||||
|
@ -183,7 +169,7 @@ export function useAccountHistory(address: string) {
|
||||||
throw new Error(`useAccountHistory must be used within a AccountsProvider`);
|
throw new Error(`useAccountHistory must be used within a AccountsProvider`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context[address];
|
return context.map[address];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFetchAccountHistory() {
|
export function useFetchAccountHistory() {
|
||||||
|
@ -197,7 +183,7 @@ export function useFetchAccountHistory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (pubkey: PublicKey, refresh?: boolean) => {
|
return (pubkey: PublicKey, refresh?: boolean) => {
|
||||||
const before = state[pubkey.toBase58()];
|
const before = state.map[pubkey.toBase58()];
|
||||||
if (!refresh && before && before.fetched && before.fetched.length > 0) {
|
if (!refresh && before && before.fetched && before.fetched.length > 0) {
|
||||||
const oldest = before.fetched[before.fetched.length - 1].signature;
|
const oldest = before.fetched[before.fetched.length - 1].signature;
|
||||||
fetchAccountHistory(dispatch, pubkey, url, { before: oldest, limit: 25 });
|
fetchAccountHistory(dispatch, pubkey, url, { before: oldest, limit: 25 });
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StakeAccount } from "solana-sdk-wasm";
|
import { StakeAccount } from "solana-sdk-wasm";
|
||||||
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
|
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
|
||||||
import { useCluster, ClusterStatus } from "../cluster";
|
import { useCluster } from "../cluster";
|
||||||
import { HistoryProvider } from "./history";
|
import { HistoryProvider } from "./history";
|
||||||
import { TokensProvider } from "./tokens";
|
import { TokensProvider } from "./tokens";
|
||||||
export { useAccountHistory } from "./history";
|
export { useAccountHistory } from "./history";
|
||||||
|
@ -29,7 +29,6 @@ export interface Account {
|
||||||
type Accounts = { [address: string]: Account };
|
type Accounts = { [address: string]: Account };
|
||||||
interface State {
|
interface State {
|
||||||
accounts: Accounts;
|
accounts: Accounts;
|
||||||
lastFetchedAddress: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
|
@ -73,7 +72,7 @@ function reducer(state: State, action: Action): State {
|
||||||
status: FetchStatus.Fetching,
|
status: FetchStatus.Fetching,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...state, accounts, lastFetchedAddress: address };
|
return { ...state, accounts };
|
||||||
} else {
|
} else {
|
||||||
const accounts = {
|
const accounts = {
|
||||||
...state.accounts,
|
...state.accounts,
|
||||||
|
@ -82,7 +81,7 @@ function reducer(state: State, action: Action): State {
|
||||||
pubkey: action.pubkey,
|
pubkey: action.pubkey,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...state, accounts, lastFetchedAddress: address };
|
return { ...state, accounts };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,9 +111,6 @@ function reducer(state: State, action: Action): State {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACCOUNT_ALIASES = ["account", "address"];
|
|
||||||
export const ACCOUNT_ALIASES_PLURAL = ["accounts", "addresses"];
|
|
||||||
|
|
||||||
const StateContext = React.createContext<State | undefined>(undefined);
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -122,18 +118,13 @@ type AccountsProviderProps = { children: React.ReactNode };
|
||||||
export function AccountsProvider({ children }: AccountsProviderProps) {
|
export function AccountsProvider({ children }: AccountsProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, {
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
accounts: {},
|
accounts: {},
|
||||||
lastFetchedAddress: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check account statuses on startup and whenever cluster updates
|
// Clear account statuses whenever cluster is changed
|
||||||
const { status, url } = useCluster();
|
const { url } = useCluster();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status === ClusterStatus.Connecting) {
|
|
||||||
dispatch({ type: ActionType.Clear });
|
dispatch({ type: ActionType.Clear });
|
||||||
} else if (status === ClusterStatus.Connected && state.lastFetchedAddress) {
|
}, [url]);
|
||||||
fetchAccountInfo(dispatch, new PublicKey(state.lastFetchedAddress), url);
|
|
||||||
}
|
|
||||||
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Connection, PublicKey } from "@solana/web3.js";
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
import { FetchStatus, useAccounts } from "./index";
|
import { FetchStatus } from "./index";
|
||||||
import { useCluster, Cluster } from "../cluster";
|
import { useCluster } from "../cluster";
|
||||||
import { number, string, boolean, coerce, object, nullable } from "superstruct";
|
import { number, string, boolean, coerce, object, nullable } from "superstruct";
|
||||||
|
|
||||||
export type TokenAccountData = {
|
export type TokenAccountData = {
|
||||||
|
@ -28,22 +28,38 @@ interface AccountTokens {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
|
type: "update";
|
||||||
|
url: string;
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
status: FetchStatus;
|
status: FetchStatus;
|
||||||
tokens?: TokenAccountData[];
|
tokens?: TokenAccountData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | "clear";
|
interface Clear {
|
||||||
type State = { [address: string]: AccountTokens };
|
type: "clear";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = Update | Clear;
|
||||||
|
type State = {
|
||||||
|
url: string;
|
||||||
|
map: { [address: string]: AccountTokens };
|
||||||
|
};
|
||||||
|
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
if (action === "clear") {
|
if (action.type === "clear") {
|
||||||
return {};
|
return {
|
||||||
|
url: action.url,
|
||||||
|
map: {},
|
||||||
|
};
|
||||||
|
} else if (action.url !== state.url) {
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = action.pubkey.toBase58();
|
const address = action.pubkey.toBase58();
|
||||||
let addressEntry = state[address];
|
let addressEntry = state.map[address];
|
||||||
if (addressEntry && action.status === FetchStatus.Fetching) {
|
if (addressEntry && action.status === FetchStatus.Fetching) {
|
||||||
addressEntry = {
|
addressEntry = {
|
||||||
...addressEntry,
|
...addressEntry,
|
||||||
|
@ -58,7 +74,10 @@ function reducer(state: State, action: Action): State {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
map: {
|
||||||
|
...state.map,
|
||||||
[address]: addressEntry,
|
[address]: addressEntry,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,27 +86,13 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
type ProviderProps = { children: React.ReactNode };
|
type ProviderProps = { children: React.ReactNode };
|
||||||
export function TokensProvider({ children }: ProviderProps) {
|
export function TokensProvider({ children }: ProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, {});
|
const { url } = useCluster();
|
||||||
const { cluster, url } = useCluster();
|
const [state, dispatch] = React.useReducer(reducer, { url, map: {} });
|
||||||
const { accounts, lastFetchedAddress } = useAccounts();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch("clear");
|
dispatch({ url, type: "clear" });
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
// Fetch history for new accounts
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (lastFetchedAddress && cluster !== Cluster.MainnetBeta) {
|
|
||||||
const infoFetched =
|
|
||||||
accounts[lastFetchedAddress] &&
|
|
||||||
accounts[lastFetchedAddress].lamports !== undefined;
|
|
||||||
const noRecord = !state[lastFetchedAddress];
|
|
||||||
if (infoFetched && noRecord) {
|
|
||||||
fetchAccountTokens(dispatch, new PublicKey(lastFetchedAddress), url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [accounts, lastFetchedAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
|
@ -107,8 +112,10 @@ async function fetchAccountTokens(
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
type: "update",
|
||||||
status: FetchStatus.Fetching,
|
status: FetchStatus.Fetching,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
|
@ -133,7 +140,7 @@ async function fetchAccountTokens(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status = FetchStatus.FetchFailed;
|
status = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
dispatch({ status, tokens, pubkey });
|
dispatch({ type: "update", url, status, tokens, pubkey });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccountOwnedTokens(address: string) {
|
export function useAccountOwnedTokens(address: string) {
|
||||||
|
@ -145,7 +152,7 @@ export function useAccountOwnedTokens(address: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context[address];
|
return context.map[address];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFetchAccountOwnedTokens() {
|
export function useFetchAccountOwnedTokens() {
|
||||||
|
|
Loading…
Reference in New Issue