Use new history API in explorer (#11449)
This commit is contained in:
parent
4b52306063
commit
4f2f9bd26f
|
@ -2334,9 +2334,9 @@
|
|||
"integrity": "sha512-zLtOIToct1EBTbwldkMJsXC2eCsmWOOP7z6UG0M/sCgnPExtIjvVMCpPESvPnMbQzDZytXVy0nvMbUuK2gZs2A=="
|
||||
},
|
||||
"@solana/web3.js": {
|
||||
"version": "0.66.0",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.0.tgz",
|
||||
"integrity": "sha512-Uw7ooRWLqrq8I5U21mEryvvF/Eqqh4mq4K2W9Sxuz3boxkz7Ed7aAJVj5C5n1fbQr9I1cxxxgC+D5BHnogfS1A==",
|
||||
"version": "0.66.1",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.1.tgz",
|
||||
"integrity": "sha512-AorappmEktL8k0wgJ8nlxbdM3YG+LeeSBBUZUtk+JA2uiRh5pFexsvvViTuTHuzYQbdp66JJyCLc2YcMz8LwEw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"bn.js": "^5.0.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@react-hook/debounce": "^3.0.0",
|
||||
"@solana/web3.js": "^0.66.0",
|
||||
"@solana/web3.js": "^0.66.1",
|
||||
"@testing-library/jest-dom": "^5.11.2",
|
||||
"@testing-library/react": "^10.4.8",
|
||||
"@testing-library/user-event": "^12.1.0",
|
||||
|
|
|
@ -231,10 +231,7 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
|
||||
if (!info || !history || info.lamports === undefined) {
|
||||
return null;
|
||||
} else if (
|
||||
history.fetched === undefined ||
|
||||
history.fetchedRange === undefined
|
||||
) {
|
||||
} else if (history.fetched === undefined) {
|
||||
if (history.status === FetchStatus.Fetching) {
|
||||
return <LoadingCard message="Loading history" />;
|
||||
}
|
||||
|
@ -251,10 +248,8 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
return (
|
||||
<ErrorCard
|
||||
retry={loadMore}
|
||||
retryText="Look back further"
|
||||
text={
|
||||
"No transaction history found since slot " + history.fetchedRange.min
|
||||
}
|
||||
retryText="Try again"
|
||||
text="No transaction history found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -263,18 +258,18 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
const transactions = history.fetched;
|
||||
|
||||
for (var i = 0; i < transactions.length; i++) {
|
||||
const slot = transactions[i].status.slot;
|
||||
const slot = transactions[i].slot;
|
||||
const slotTransactions = [transactions[i]];
|
||||
while (i + 1 < transactions.length) {
|
||||
const nextSlot = transactions[i + 1].status.slot;
|
||||
const nextSlot = transactions[i + 1].slot;
|
||||
if (nextSlot !== slot) break;
|
||||
slotTransactions.push(transactions[++i]);
|
||||
}
|
||||
|
||||
slotTransactions.forEach(({ signature, status }, index) => {
|
||||
slotTransactions.forEach(({ signature, err }) => {
|
||||
let statusText;
|
||||
let statusClass;
|
||||
if (status.err) {
|
||||
if (err) {
|
||||
statusClass = "warning";
|
||||
statusText = "Failed";
|
||||
} else {
|
||||
|
@ -338,6 +333,9 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
</div>
|
||||
|
||||
<div className="card-footer">
|
||||
{history.foundOldest ? (
|
||||
<div className="text-muted text-center">Fetched full history</div>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-primary w-100"
|
||||
onClick={loadMore}
|
||||
|
@ -352,6 +350,7 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
"Load More"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React from "react";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import {
|
||||
PublicKey,
|
||||
ConfirmedSignatureInfo,
|
||||
TransactionSignature,
|
||||
Connection,
|
||||
} 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;
|
||||
fetched?: ConfirmedSignatureInfo[];
|
||||
foundOldest: boolean;
|
||||
}
|
||||
|
||||
type State = { [address: string]: AccountHistory };
|
||||
|
@ -26,8 +26,9 @@ interface Update {
|
|||
type: ActionType.Update;
|
||||
pubkey: PublicKey;
|
||||
status: FetchStatus;
|
||||
fetched?: HistoricalTransaction[];
|
||||
fetchedRange?: SlotRange;
|
||||
fetched?: ConfirmedSignatureInfo[];
|
||||
before?: TransactionSignature;
|
||||
foundOldest?: boolean;
|
||||
}
|
||||
|
||||
interface Add {
|
||||
|
@ -42,6 +43,24 @@ interface Clear {
|
|||
type Action = Update | Add | Clear;
|
||||
type Dispatch = (action: Action) => void;
|
||||
|
||||
function combineFetched(
|
||||
fetched: ConfirmedSignatureInfo[] | undefined,
|
||||
current: ConfirmedSignatureInfo[] | undefined,
|
||||
before: TransactionSignature | undefined
|
||||
) {
|
||||
if (fetched === undefined) {
|
||||
return current;
|
||||
} else if (current === undefined) {
|
||||
return fetched;
|
||||
}
|
||||
|
||||
if (current.length > 0 && current[current.length - 1].signature === before) {
|
||||
return current.concat(fetched);
|
||||
} else {
|
||||
return fetched;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case ActionType.Add: {
|
||||
|
@ -50,6 +69,7 @@ function reducer(state: State, action: Action): State {
|
|||
if (!details[address]) {
|
||||
details[address] = {
|
||||
status: FetchStatus.Fetching,
|
||||
foundOldest: false,
|
||||
};
|
||||
}
|
||||
return details;
|
||||
|
@ -58,18 +78,16 @@ function reducer(state: State, action: Action): State {
|
|||
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,
|
||||
fetched: combineFetched(
|
||||
action.fetched,
|
||||
state[address].fetched,
|
||||
action.before
|
||||
),
|
||||
foundOldest: action.foundOldest || state[address].foundOldest,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -83,9 +101,6 @@ function reducer(state: State, action: Action): State {
|
|||
return state;
|
||||
}
|
||||
|
||||
const ManagerContext = React.createContext<HistoryManager | undefined>(
|
||||
undefined
|
||||
);
|
||||
const StateContext = React.createContext<State | undefined>(undefined);
|
||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
|
@ -95,9 +110,7 @@ export function HistoryProvider({ children }: HistoryProviderProps) {
|
|||
const { accounts, lastFetchedAddress } = useAccounts();
|
||||
const { url } = useCluster();
|
||||
|
||||
const manager = React.useRef(new HistoryManager(url));
|
||||
React.useEffect(() => {
|
||||
manager.current = new HistoryManager(url);
|
||||
dispatch({ type: ActionType.Clear });
|
||||
}, [url]);
|
||||
|
||||
|
@ -110,32 +123,27 @@ export function HistoryProvider({ children }: HistoryProviderProps) {
|
|||
const noHistory = !state[lastFetchedAddress];
|
||||
if (infoFetched && noHistory) {
|
||||
dispatch({ type: ActionType.Add, address: lastFetchedAddress });
|
||||
fetchAccountHistory(
|
||||
dispatch,
|
||||
new PublicKey(lastFetchedAddress),
|
||||
manager.current,
|
||||
true
|
||||
);
|
||||
fetchAccountHistory(dispatch, new PublicKey(lastFetchedAddress), url, {
|
||||
limit: 10,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [accounts, lastFetchedAddress]); // 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
|
||||
url: string,
|
||||
options: { before?: TransactionSignature; limit: number }
|
||||
) {
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
|
@ -145,17 +153,27 @@ async function fetchAccountHistory(
|
|||
|
||||
let status;
|
||||
let fetched;
|
||||
let fetchedRange;
|
||||
let foundOldest;
|
||||
try {
|
||||
await manager.fetchAccountHistory(pubkey, refresh || false);
|
||||
fetched = manager.accountHistory.get(pubkey.toBase58()) || undefined;
|
||||
fetchedRange = manager.accountRanges.get(pubkey.toBase58()) || undefined;
|
||||
const connection = new Connection(url);
|
||||
fetched = await connection.getConfirmedSignaturesForAddress2(
|
||||
pubkey,
|
||||
options
|
||||
);
|
||||
foundOldest = fetched.length < options.limit;
|
||||
status = FetchStatus.Fetched;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch account history", error);
|
||||
status = FetchStatus.FetchFailed;
|
||||
}
|
||||
dispatch({ type: ActionType.Update, status, fetched, fetchedRange, pubkey });
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status,
|
||||
fetched,
|
||||
before: options?.before,
|
||||
pubkey,
|
||||
foundOldest,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAccountHistory(address: string) {
|
||||
|
@ -169,15 +187,22 @@ export function useAccountHistory(address: string) {
|
|||
}
|
||||
|
||||
export function useFetchAccountHistory() {
|
||||
const manager = React.useContext(ManagerContext);
|
||||
const { url } = useCluster();
|
||||
const state = React.useContext(StateContext);
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!manager || !dispatch) {
|
||||
if (!state || !dispatch) {
|
||||
throw new Error(
|
||||
`useFetchAccountHistory must be used within a AccountsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
return (pubkey: PublicKey, refresh?: boolean) => {
|
||||
fetchAccountHistory(dispatch, pubkey, manager, refresh);
|
||||
const before = state[pubkey.toBase58()];
|
||||
if (!refresh && before && before.fetched && before.fetched.length > 0) {
|
||||
const oldest = before.fetched[before.fetched.length - 1].signature;
|
||||
fetchAccountHistory(dispatch, pubkey, url, { before: oldest, limit: 25 });
|
||||
} else {
|
||||
fetchAccountHistory(dispatch, pubkey, url, { limit: 25 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
||||
// Fetch a batch of confirmed signatures but decrease fetch count until
|
||||
// the batch is small enough to be queried for statuses.
|
||||
async fetchConfirmedSignatureBatch(
|
||||
pubkey: PublicKey,
|
||||
start: number,
|
||||
fetchCount: number,
|
||||
forward: boolean
|
||||
): Promise<{
|
||||
batch: Array<TransactionSignature>;
|
||||
batchRange: SlotRange;
|
||||
}> {
|
||||
const fullRange = await this.fullRange(false);
|
||||
const nextRange = (): SlotRange => {
|
||||
if (forward) {
|
||||
return {
|
||||
min: start,
|
||||
max: Math.min(fullRange.max, start + fetchCount - 1),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
min: Math.max(fullRange.min, start - fetchCount + 1),
|
||||
max: start,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let batch: TransactionSignature[] = [];
|
||||
let batchRange = nextRange();
|
||||
while (batchRange.max > batchRange.min) {
|
||||
batch = await this.connection.getConfirmedSignaturesForAddress(
|
||||
pubkey,
|
||||
batchRange.min,
|
||||
batchRange.max
|
||||
);
|
||||
|
||||
// Fetched too many results, refetch with a smaller range (1/8)
|
||||
if (batch.length > 4 * MAX_STATUS_BATCH_SIZE) {
|
||||
fetchCount = Math.ceil(fetchCount / 8);
|
||||
batchRange = nextRange();
|
||||
} else {
|
||||
batch = batch.reverse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { batchRange, batch };
|
||||
}
|
||||
|
||||
async fetchAccountHistory(pubkey: PublicKey, searchForward: boolean) {
|
||||
const address = pubkey.toBase58();
|
||||
|
||||
if (this.accountLock.get(address) === true) return;
|
||||
this.accountLock.set(address, true);
|
||||
|
||||
try {
|
||||
// Start with only 250 slots in case queried account is a vote account
|
||||
let slotFetchCount = 250;
|
||||
const fullRange = await this.fullRange(searchForward);
|
||||
const currentRange = this.accountRanges.get(address);
|
||||
|
||||
// Determine query range based on already queried range
|
||||
let startSlot: number;
|
||||
if (currentRange) {
|
||||
if (searchForward) {
|
||||
startSlot = currentRange.max + 1;
|
||||
} else {
|
||||
startSlot = currentRange.min - 1;
|
||||
}
|
||||
} else {
|
||||
searchForward = false;
|
||||
startSlot = fullRange.max;
|
||||
}
|
||||
|
||||
// Gradually fetch more history if not too many signatures were found
|
||||
let signatures: string[] = [];
|
||||
let range: SlotRange = { min: startSlot, max: startSlot };
|
||||
for (var i = 0; i < 5; i++) {
|
||||
const { batch, batchRange } = await this.fetchConfirmedSignatureBatch(
|
||||
pubkey,
|
||||
startSlot,
|
||||
slotFetchCount,
|
||||
searchForward
|
||||
);
|
||||
|
||||
range.min = Math.min(range.min, batchRange.min);
|
||||
range.max = Math.max(range.max, batchRange.max);
|
||||
|
||||
if (searchForward) {
|
||||
signatures = batch.concat(signatures);
|
||||
startSlot = batchRange.max + 1;
|
||||
} else {
|
||||
signatures = signatures.concat(batch);
|
||||
startSlot = batchRange.min - 1;
|
||||
}
|
||||
|
||||
if (signatures.length > MAX_STATUS_BATCH_SIZE / 2) break;
|
||||
if (range.min <= fullRange.min) break;
|
||||
|
||||
// Bump look-back not that we know the account is probably not a vote account
|
||||
slotFetchCount = 10000;
|
||||
}
|
||||
|
||||
// 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 (searchForward) {
|
||||
newTransactions = transactions.concat(currentTransactions);
|
||||
} else {
|
||||
newTransactions = currentTransactions.concat(transactions);
|
||||
}
|
||||
|
||||
this.accountHistory.set(address, newTransactions);
|
||||
} finally {
|
||||
this.accountLock.set(address, false);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue