Account history feed
This commit is contained in:
parent
27650572dd
commit
2312658492
|
@ -1269,9 +1269,9 @@
|
|||
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
||||
},
|
||||
"@solana/web3.js": {
|
||||
"version": "0.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.51.0.tgz",
|
||||
"integrity": "sha512-mt2JF9QcpL/K2/LSHgj/yqSQXxvCNkLknfvmDClLeI2VbtYMypGcw4tU4C2GrdTzKRUdzM8ncaGONxLJKgFsTQ==",
|
||||
"version": "0.52.1",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.52.1.tgz",
|
||||
"integrity": "sha512-xv8PknS9sjnMxPXaCZEb8Yk+S0O3lScw91NJyYjnIPYxYnxJ96H7BUCDh1zOj5op4nT8u/n4wRYG+YWT/XRwiA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"bn.js": "^5.0.0",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^0.51.0",
|
||||
"@solana/web3.js": "^0.52.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
|
|
|
@ -4,10 +4,10 @@ import { PublicKey, StakeProgram } from "@solana/web3.js";
|
|||
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Status,
|
||||
FetchStatus,
|
||||
useFetchAccountInfo,
|
||||
useFetchAccountHistory,
|
||||
useAccountInfo,
|
||||
useAccountHistory,
|
||||
Account
|
||||
} from "providers/accounts";
|
||||
import { lamportsToSolString } from "utils";
|
||||
|
@ -17,6 +17,7 @@ import { StakeAccountCards } from "components/account/StakeAccountCards";
|
|||
import ErrorCard from "components/common/ErrorCard";
|
||||
import LoadingCard from "components/common/LoadingCard";
|
||||
import TableCardBody from "components/common/TableCardBody";
|
||||
import { useFetchAccountHistory } from "providers/accounts/history";
|
||||
|
||||
type Props = { address: string };
|
||||
export default function AccountDetails({ address }: Props) {
|
||||
|
@ -40,8 +41,9 @@ export default function AccountDetails({ address }: Props) {
|
|||
|
||||
// Fetch account on load
|
||||
React.useEffect(() => {
|
||||
setSearch(address);
|
||||
if (pubkey) fetchAccount(pubkey);
|
||||
}, [pubkey?.toBase58()]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const searchInput = (
|
||||
<input
|
||||
|
@ -99,10 +101,10 @@ function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
|||
const info = useAccountInfo(address);
|
||||
const refresh = useFetchAccountInfo();
|
||||
|
||||
if (!info || info.status === Status.Checking) {
|
||||
if (!info || info.status === FetchStatus.Fetching) {
|
||||
return <LoadingCard />;
|
||||
} else if (
|
||||
info.status === Status.CheckFailed ||
|
||||
info.status === FetchStatus.FetchFailed ||
|
||||
info.lamports === undefined
|
||||
) {
|
||||
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
|
||||
|
@ -118,22 +120,13 @@ function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
|||
}
|
||||
|
||||
function UnknownAccountCard({ account }: { account: Account }) {
|
||||
const refresh = useFetchAccountInfo();
|
||||
|
||||
const { details, lamports, pubkey } = account;
|
||||
const { details, lamports } = account;
|
||||
if (lamports === undefined) return null;
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Account Overview</h3>
|
||||
<button
|
||||
className="btn btn-white btn-sm"
|
||||
onClick={() => refresh(pubkey)}
|
||||
>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TableCardBody>
|
||||
|
@ -176,40 +169,59 @@ function UnknownAccountCard({ account }: { account: Account }) {
|
|||
function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||
const address = pubkey.toBase58();
|
||||
const info = useAccountInfo(address);
|
||||
const refresh = useFetchAccountHistory();
|
||||
const history = useAccountHistory(address);
|
||||
const fetchAccountHistory = useFetchAccountHistory();
|
||||
const refresh = () => fetchAccountHistory(pubkey, true);
|
||||
const loadMore = () => fetchAccountHistory(pubkey);
|
||||
|
||||
if (!info || info.lamports === undefined) {
|
||||
if (!info || !history || info.lamports === undefined) {
|
||||
return null;
|
||||
} else if (info.status === Status.FetchingHistory) {
|
||||
return <LoadingCard />;
|
||||
} else if (info.history === undefined) {
|
||||
} else if (
|
||||
history.fetched === undefined ||
|
||||
history.fetchedRange === undefined
|
||||
) {
|
||||
if (history.status === FetchStatus.Fetching) {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCard
|
||||
retry={() => refresh(pubkey)}
|
||||
text="Failed to fetch transaction history"
|
||||
/>
|
||||
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
|
||||
);
|
||||
}
|
||||
|
||||
if (info.history.size === 0) {
|
||||
if (history.fetched.length === 0) {
|
||||
return (
|
||||
<ErrorCard
|
||||
retry={() => refresh(pubkey)}
|
||||
text="No recent transaction history found"
|
||||
retry={loadMore}
|
||||
retryText="Look back further"
|
||||
text={
|
||||
"No transaction history found since slot " + history.fetchedRange.min
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const detailsList: React.ReactNode[] = [];
|
||||
info.history.forEach((slotTransactions, slot) => {
|
||||
const signatures = Array.from(slotTransactions.entries()).map(
|
||||
([signature, err]) => {
|
||||
return <code className="mb-2 mb-last-0">{signature}</code>;
|
||||
}
|
||||
);
|
||||
const transactions = history.fetched;
|
||||
|
||||
for (var i = 0; i < transactions.length; i++) {
|
||||
const slot = transactions[i].status.slot;
|
||||
const slotTransactions = [transactions[i]];
|
||||
while (i + 1 < transactions.length) {
|
||||
const nextSlot = transactions[i + 1].status.slot;
|
||||
if (nextSlot !== slot) break;
|
||||
slotTransactions.push(transactions[++i]);
|
||||
}
|
||||
const signatures = slotTransactions.map(({ signature, status }) => {
|
||||
return (
|
||||
<code key={signature} className="mb-2 mb-last-0">
|
||||
{signature}
|
||||
</code>
|
||||
);
|
||||
});
|
||||
|
||||
detailsList.push(
|
||||
<tr>
|
||||
<tr key={slot}>
|
||||
<td className="vertical-top">Slot {slot}</td>
|
||||
<td className="text-right">
|
||||
<div className="d-inline-flex flex-column align-items-end">
|
||||
|
@ -218,22 +230,49 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const fetching = history.status === FetchStatus.Fetching;
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Transaction History</h3>
|
||||
<button
|
||||
className="btn btn-white btn-sm"
|
||||
onClick={() => refresh(pubkey)}
|
||||
disabled={fetching}
|
||||
onClick={refresh}
|
||||
>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
Refresh
|
||||
{fetching ? (
|
||||
<>
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
Refresh
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TableCardBody>{detailsList}</TableCardBody>
|
||||
<div className="card-footer">
|
||||
<button
|
||||
className="btn btn-primary w-100"
|
||||
onClick={loadMore}
|
||||
disabled={fetching}
|
||||
>
|
||||
{fetching ? (
|
||||
<>
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
"Load More"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
|
|||
import {
|
||||
useAccounts,
|
||||
Account,
|
||||
Status,
|
||||
FetchStatus,
|
||||
useFetchAccountInfo
|
||||
} from "../providers/accounts";
|
||||
import { assertUnreachable } from "../utils";
|
||||
|
@ -109,17 +109,15 @@ const renderAccountRow = (account: Account) => {
|
|||
let statusText;
|
||||
let statusClass;
|
||||
switch (account.status) {
|
||||
case Status.CheckFailed:
|
||||
case Status.HistoryFailed:
|
||||
case FetchStatus.FetchFailed:
|
||||
statusClass = "dark";
|
||||
statusText = "Cluster Error";
|
||||
break;
|
||||
case Status.Checking:
|
||||
case Status.FetchingHistory:
|
||||
case FetchStatus.Fetching:
|
||||
statusClass = "info";
|
||||
statusText = "Fetching";
|
||||
break;
|
||||
case Status.Success:
|
||||
case FetchStatus.Fetched:
|
||||
if (account.details?.executable) {
|
||||
statusClass = "dark";
|
||||
statusText = "Executable";
|
||||
|
|
|
@ -2,11 +2,14 @@ import React from "react";
|
|||
|
||||
export default function ErrorCard({
|
||||
retry,
|
||||
retryText,
|
||||
text
|
||||
}: {
|
||||
retry?: () => void;
|
||||
retryText?: string;
|
||||
text: string;
|
||||
}) {
|
||||
const buttonText = retryText || "Try Again";
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body text-center">
|
||||
|
@ -17,12 +20,12 @@ export default function ErrorCard({
|
|||
className="btn btn-white ml-3 d-none d-md-inline"
|
||||
onClick={retry}
|
||||
>
|
||||
Try Again
|
||||
{buttonText}
|
||||
</span>
|
||||
<div className="d-block d-md-none mt-4">
|
||||
<hr></hr>
|
||||
<span className="btn btn-white" onClick={retry}>
|
||||
Try Again
|
||||
{buttonText}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
import React from "react";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { useAccounts, FetchStatus } from "./index";
|
||||
import { useCluster } from "../cluster";
|
||||
import {
|
||||
HistoryManager,
|
||||
HistoricalTransaction,
|
||||
SlotRange
|
||||
} from "./historyManager";
|
||||
|
||||
interface AccountHistory {
|
||||
status: FetchStatus;
|
||||
fetched?: HistoricalTransaction[];
|
||||
fetchedRange?: SlotRange;
|
||||
}
|
||||
|
||||
type State = { [address: string]: AccountHistory };
|
||||
|
||||
export enum ActionType {
|
||||
Update,
|
||||
Add,
|
||||
Remove
|
||||
}
|
||||
|
||||
interface Update {
|
||||
type: ActionType.Update;
|
||||
pubkey: PublicKey;
|
||||
status: FetchStatus;
|
||||
fetched?: HistoricalTransaction[];
|
||||
fetchedRange?: SlotRange;
|
||||
}
|
||||
|
||||
interface Add {
|
||||
type: ActionType.Add;
|
||||
addresses: string[];
|
||||
}
|
||||
|
||||
interface Remove {
|
||||
type: ActionType.Remove;
|
||||
addresses: string[];
|
||||
}
|
||||
|
||||
type Action = Update | Add | Remove;
|
||||
type Dispatch = (action: Action) => void;
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case ActionType.Add: {
|
||||
if (action.addresses.length === 0) return state;
|
||||
const details = { ...state };
|
||||
action.addresses.forEach(address => {
|
||||
if (!details[address]) {
|
||||
details[address] = {
|
||||
status: FetchStatus.Fetching
|
||||
};
|
||||
}
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
case ActionType.Remove: {
|
||||
if (action.addresses.length === 0) return state;
|
||||
const details = { ...state };
|
||||
action.addresses.forEach(address => {
|
||||
delete details[address];
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
case ActionType.Update: {
|
||||
const address = action.pubkey.toBase58();
|
||||
if (state[address]) {
|
||||
const fetched = action.fetched
|
||||
? action.fetched
|
||||
: state[address].fetched;
|
||||
const fetchedRange = action.fetchedRange
|
||||
? action.fetchedRange
|
||||
: state[address].fetchedRange;
|
||||
return {
|
||||
...state,
|
||||
[address]: {
|
||||
status: action.status,
|
||||
fetched,
|
||||
fetchedRange
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const ManagerContext = React.createContext<HistoryManager | undefined>(
|
||||
undefined
|
||||
);
|
||||
const StateContext = React.createContext<State | undefined>(undefined);
|
||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
type HistoryProviderProps = { children: React.ReactNode };
|
||||
export function HistoryProvider({ children }: HistoryProviderProps) {
|
||||
const [state, dispatch] = React.useReducer(reducer, {});
|
||||
const { accounts } = useAccounts();
|
||||
const { url } = useCluster();
|
||||
|
||||
const manager = React.useRef(new HistoryManager(url));
|
||||
React.useEffect(() => {
|
||||
manager.current = new HistoryManager(url);
|
||||
}, [url]);
|
||||
|
||||
// Fetch history for new accounts
|
||||
React.useEffect(() => {
|
||||
const removeAddresses = new Set<string>();
|
||||
const fetchAddresses = new Set<string>();
|
||||
accounts.forEach(({ pubkey, lamports }) => {
|
||||
const address = pubkey.toBase58();
|
||||
if (lamports !== undefined && !state[address])
|
||||
fetchAddresses.add(address);
|
||||
else if (lamports === undefined && state[address])
|
||||
removeAddresses.add(address);
|
||||
});
|
||||
|
||||
const removeList: string[] = [];
|
||||
removeAddresses.forEach(address => {
|
||||
manager.current.removeAccountHistory(address);
|
||||
removeList.push(address);
|
||||
});
|
||||
dispatch({ type: ActionType.Remove, addresses: removeList });
|
||||
|
||||
const fetchList: string[] = [];
|
||||
fetchAddresses.forEach(s => fetchList.push(s));
|
||||
dispatch({ type: ActionType.Add, addresses: fetchList });
|
||||
fetchAddresses.forEach(address => {
|
||||
fetchAccountHistory(
|
||||
dispatch,
|
||||
new PublicKey(address),
|
||||
manager.current,
|
||||
true
|
||||
);
|
||||
});
|
||||
}, [accounts]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<ManagerContext.Provider value={manager.current}>
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
</ManagerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchAccountHistory(
|
||||
dispatch: Dispatch,
|
||||
pubkey: PublicKey,
|
||||
manager: HistoryManager,
|
||||
refresh?: boolean
|
||||
) {
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status: FetchStatus.Fetching,
|
||||
pubkey
|
||||
});
|
||||
|
||||
let status;
|
||||
let fetched;
|
||||
let fetchedRange;
|
||||
try {
|
||||
await manager.fetchAccountHistory(pubkey, refresh || false);
|
||||
fetched = manager.accountHistory.get(pubkey.toBase58()) || undefined;
|
||||
fetchedRange = manager.accountRanges.get(pubkey.toBase58()) || undefined;
|
||||
status = FetchStatus.Fetched;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch account history", error);
|
||||
status = FetchStatus.FetchFailed;
|
||||
}
|
||||
dispatch({ type: ActionType.Update, status, fetched, fetchedRange, pubkey });
|
||||
}
|
||||
|
||||
export function useAccountHistory(address: string) {
|
||||
const context = React.useContext(StateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useAccountHistory must be used within a AccountsProvider`);
|
||||
}
|
||||
|
||||
return context[address];
|
||||
}
|
||||
|
||||
export function useFetchAccountHistory() {
|
||||
const manager = React.useContext(ManagerContext);
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!manager || !dispatch) {
|
||||
throw new Error(
|
||||
`useFetchAccountHistory must be used within a AccountsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
return (pubkey: PublicKey, refresh?: boolean) => {
|
||||
fetchAccountHistory(dispatch, pubkey, manager, refresh);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
TransactionSignature,
|
||||
Connection,
|
||||
PublicKey,
|
||||
SignatureStatus
|
||||
} from "@solana/web3.js";
|
||||
|
||||
const MAX_STATUS_BATCH_SIZE = 256;
|
||||
|
||||
export interface SlotRange {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export type HistoricalTransaction = {
|
||||
signature: TransactionSignature;
|
||||
status: SignatureStatus;
|
||||
};
|
||||
|
||||
// Manage the transaction history for accounts for a cluster
|
||||
export class HistoryManager {
|
||||
accountRanges: Map<string, SlotRange> = new Map();
|
||||
accountHistory: Map<string, HistoricalTransaction[]> = new Map();
|
||||
accountLock: Map<string, boolean> = new Map();
|
||||
_fullRange: SlotRange | undefined;
|
||||
connection: Connection;
|
||||
|
||||
constructor(url: string) {
|
||||
this.connection = new Connection(url);
|
||||
}
|
||||
|
||||
async fullRange(refresh: boolean): Promise<SlotRange> {
|
||||
if (refresh || !this._fullRange) {
|
||||
const max = (await this.connection.getEpochInfo("max")).absoluteSlot;
|
||||
this._fullRange = { min: 0, max };
|
||||
}
|
||||
return this._fullRange;
|
||||
}
|
||||
|
||||
removeAccountHistory(address: string) {
|
||||
this.accountLock.delete(address);
|
||||
this.accountRanges.delete(address);
|
||||
this.accountHistory.delete(address);
|
||||
}
|
||||
|
||||
async fetchAccountHistory(pubkey: PublicKey, refresh: boolean) {
|
||||
const address = pubkey.toBase58();
|
||||
|
||||
if (this.accountLock.get(address) === true) return;
|
||||
this.accountLock.set(address, true);
|
||||
|
||||
try {
|
||||
let slotLookBack = 100;
|
||||
const fullRange = await this.fullRange(refresh);
|
||||
const currentRange = this.accountRanges.get(address);
|
||||
|
||||
// Determine query range based on already queried range
|
||||
let range;
|
||||
if (currentRange) {
|
||||
if (refresh) {
|
||||
const min = currentRange.max + 1;
|
||||
const max = Math.min(min + slotLookBack - 1, fullRange.max);
|
||||
if (max < min) return;
|
||||
range = { min, max };
|
||||
} else {
|
||||
const max = currentRange.min - 1;
|
||||
const min = Math.max(max - slotLookBack + 1, fullRange.min);
|
||||
if (max < min) return;
|
||||
range = { min, max };
|
||||
}
|
||||
} else {
|
||||
const max = fullRange.max;
|
||||
const min = Math.max(fullRange.min, max - slotLookBack + 1);
|
||||
range = { min, max };
|
||||
}
|
||||
|
||||
// Gradually fetch more history if nothing found
|
||||
let signatures: string[] = [];
|
||||
let nextRange = { ...range };
|
||||
for (var i = 0; i < 5; i++) {
|
||||
signatures = (
|
||||
await this.connection.getConfirmedSignaturesForAddress(
|
||||
pubkey,
|
||||
nextRange.min,
|
||||
nextRange.max
|
||||
)
|
||||
).reverse();
|
||||
if (refresh) break;
|
||||
if (signatures.length > 0) break;
|
||||
if (range.min <= fullRange.min) break;
|
||||
|
||||
switch (slotLookBack) {
|
||||
case 100:
|
||||
slotLookBack = 1000;
|
||||
break;
|
||||
case 1000:
|
||||
slotLookBack = 10000;
|
||||
break;
|
||||
}
|
||||
|
||||
range.min = Math.max(nextRange.min - slotLookBack, fullRange.min);
|
||||
nextRange = {
|
||||
min: range.min,
|
||||
max: nextRange.min - 1
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the statuses for all confirmed signatures
|
||||
const transactions: HistoricalTransaction[] = [];
|
||||
while (signatures.length > 0) {
|
||||
const batch = signatures.splice(0, MAX_STATUS_BATCH_SIZE);
|
||||
const statuses = (
|
||||
await this.connection.getSignatureStatuses(batch, {
|
||||
searchTransactionHistory: true
|
||||
})
|
||||
).value;
|
||||
statuses.forEach((status, index) => {
|
||||
if (status !== null) {
|
||||
transactions.push({
|
||||
signature: batch[index],
|
||||
status
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if account lock is still active
|
||||
if (this.accountLock.get(address) !== true) return;
|
||||
|
||||
// Update fetched slot range
|
||||
if (currentRange) {
|
||||
currentRange.max = Math.max(range.max, currentRange.max);
|
||||
currentRange.min = Math.min(range.min, currentRange.min);
|
||||
} else {
|
||||
this.accountRanges.set(address, range);
|
||||
}
|
||||
|
||||
// Exit early if no new confirmed transactions were found
|
||||
const currentTransactions = this.accountHistory.get(address) || [];
|
||||
if (currentTransactions.length > 0 && transactions.length === 0) return;
|
||||
|
||||
// Append / prepend newly fetched statuses
|
||||
let newTransactions;
|
||||
if (refresh) {
|
||||
newTransactions = transactions.concat(currentTransactions);
|
||||
} else {
|
||||
newTransactions = currentTransactions.concat(transactions);
|
||||
}
|
||||
|
||||
this.accountHistory.set(address, newTransactions);
|
||||
} finally {
|
||||
this.accountLock.set(address, false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,17 @@
|
|||
import React from "react";
|
||||
import {
|
||||
PublicKey,
|
||||
Connection,
|
||||
TransactionSignature,
|
||||
TransactionError,
|
||||
SignatureStatus,
|
||||
StakeProgram
|
||||
} from "@solana/web3.js";
|
||||
import { useQuery } from "../utils/url";
|
||||
import { useCluster, ClusterStatus } from "./cluster";
|
||||
import { StakeAccount } from "solana-sdk-wasm";
|
||||
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
|
||||
import { useQuery } from "../../utils/url";
|
||||
import { useCluster, ClusterStatus } from "../cluster";
|
||||
import { HistoryProvider } from "./history";
|
||||
export { useAccountHistory } from "./history";
|
||||
|
||||
export enum Status {
|
||||
Checking,
|
||||
CheckFailed,
|
||||
FetchingHistory,
|
||||
HistoryFailed,
|
||||
Success
|
||||
export enum FetchStatus {
|
||||
Fetching,
|
||||
FetchFailed,
|
||||
Fetched
|
||||
}
|
||||
|
||||
export type History = Map<
|
||||
number,
|
||||
Map<TransactionSignature, TransactionError | null>
|
||||
>;
|
||||
|
||||
export interface Details {
|
||||
executable: boolean;
|
||||
owner: PublicKey;
|
||||
|
@ -34,10 +22,9 @@ export interface Details {
|
|||
export interface Account {
|
||||
id: number;
|
||||
pubkey: PublicKey;
|
||||
status: Status;
|
||||
status: FetchStatus;
|
||||
lamports?: number;
|
||||
details?: Details;
|
||||
history?: History;
|
||||
}
|
||||
|
||||
type Accounts = { [address: string]: Account };
|
||||
|
@ -55,10 +42,9 @@ interface Update {
|
|||
type: ActionType.Update;
|
||||
pubkey: PublicKey;
|
||||
data: {
|
||||
status: Status;
|
||||
status: FetchStatus;
|
||||
lamports?: number;
|
||||
details?: Details;
|
||||
history?: History;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -68,7 +54,7 @@ interface Fetch {
|
|||
}
|
||||
|
||||
type Action = Update | Fetch;
|
||||
export type Dispatch = (action: Action) => void;
|
||||
type Dispatch = (action: Action) => void;
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
|
@ -81,7 +67,7 @@ function reducer(state: State, action: Action): State {
|
|||
[address]: {
|
||||
id: account.id,
|
||||
pubkey: account.pubkey,
|
||||
status: Status.Checking
|
||||
status: FetchStatus.Fetching
|
||||
}
|
||||
};
|
||||
return { ...state, accounts };
|
||||
|
@ -91,7 +77,7 @@ function reducer(state: State, action: Action): State {
|
|||
...state.accounts,
|
||||
[address]: {
|
||||
id: idCounter,
|
||||
status: Status.Checking,
|
||||
status: FetchStatus.Fetching,
|
||||
pubkey: action.pubkey
|
||||
}
|
||||
};
|
||||
|
@ -164,7 +150,7 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
<HistoryProvider>{children}</HistoryProvider>
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
|
@ -213,67 +199,15 @@ async function fetchAccountInfo(
|
|||
data
|
||||
};
|
||||
}
|
||||
fetchStatus = Status.FetchingHistory;
|
||||
fetchAccountHistory(dispatch, pubkey, url);
|
||||
fetchStatus = FetchStatus.Fetched;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch account info", error);
|
||||
fetchStatus = Status.CheckFailed;
|
||||
fetchStatus = FetchStatus.FetchFailed;
|
||||
}
|
||||
const data = { status: fetchStatus, lamports, details };
|
||||
dispatch({ type: ActionType.Update, data, pubkey });
|
||||
}
|
||||
|
||||
async function fetchAccountHistory(
|
||||
dispatch: Dispatch,
|
||||
pubkey: PublicKey,
|
||||
url: string
|
||||
) {
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
data: { status: Status.FetchingHistory },
|
||||
pubkey
|
||||
});
|
||||
|
||||
let history;
|
||||
let status;
|
||||
try {
|
||||
const connection = new Connection(url);
|
||||
const currentSlot = await connection.getSlot();
|
||||
const signatures = await connection.getConfirmedSignaturesForAddress(
|
||||
pubkey,
|
||||
Math.max(0, currentSlot - 10000 + 1),
|
||||
currentSlot
|
||||
);
|
||||
|
||||
let statuses: (SignatureStatus | null)[] = [];
|
||||
if (signatures.length > 0) {
|
||||
statuses = (
|
||||
await connection.getSignatureStatuses(signatures, {
|
||||
searchTransactionHistory: true
|
||||
})
|
||||
).value;
|
||||
}
|
||||
|
||||
history = new Map();
|
||||
for (let i = 0; i < statuses.length; i++) {
|
||||
const status = statuses[i];
|
||||
if (!status) continue;
|
||||
let slotSignatures = history.get(status.slot);
|
||||
if (!slotSignatures) {
|
||||
slotSignatures = new Map();
|
||||
history.set(status.slot, slotSignatures);
|
||||
}
|
||||
slotSignatures.set(signatures[i], status.err);
|
||||
}
|
||||
status = Status.Success;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch account history", error);
|
||||
status = Status.HistoryFailed;
|
||||
}
|
||||
const data = { status, history };
|
||||
dispatch({ type: ActionType.Update, data, pubkey });
|
||||
}
|
||||
|
||||
export function useAccounts() {
|
||||
const context = React.useContext(StateContext);
|
||||
if (!context) {
|
||||
|
@ -297,16 +231,6 @@ export function useAccountInfo(address: string) {
|
|||
return context.accounts[address];
|
||||
}
|
||||
|
||||
export function useAccountsDispatch() {
|
||||
const context = React.useContext(DispatchContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
`useAccountsDispatch must be used within a AccountsProvider`
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useFetchAccountInfo() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
|
@ -320,17 +244,3 @@ export function useFetchAccountInfo() {
|
|||
fetchAccountInfo(dispatch, pubkey, url, status);
|
||||
};
|
||||
}
|
||||
|
||||
export function useFetchAccountHistory() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
throw new Error(
|
||||
`useFetchAccountHistory must be used within a AccountsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
const { url } = useCluster();
|
||||
return (pubkey: PublicKey) => {
|
||||
fetchAccountHistory(dispatch, pubkey, url);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue