Use new history API in explorer (#11449)

This commit is contained in:
Justin Starry 2020-08-07 22:39:22 +08:00 committed by GitHub
parent 4b52306063
commit 4f2f9bd26f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 100 additions and 271 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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,20 +333,24 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
</div>
<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>
{history.foundOldest ? (
<div className="text-muted text-center">Fetched full history</div>
) : (
<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>
);

View File

@ -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>
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.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 });
}
};
}

View File

@ -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);
}
}
}