From 4f2f9bd26faaf7022360b73b4bc291d00a61b3b7 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 7 Aug 2020 22:39:22 +0800 Subject: [PATCH] Use new history API in explorer (#11449) --- explorer/package-lock.json | 6 +- explorer/package.json | 2 +- explorer/src/components/AccountDetails.tsx | 51 +++-- explorer/src/providers/accounts/history.tsx | 117 ++++++----- .../src/providers/accounts/historyManager.ts | 195 ------------------ 5 files changed, 100 insertions(+), 271 deletions(-) delete mode 100644 explorer/src/providers/accounts/historyManager.ts diff --git a/explorer/package-lock.json b/explorer/package-lock.json index 3d6d20d830..37dc2586de 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -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", diff --git a/explorer/package.json b/explorer/package.json index 6548e48c8b..d7f94c77e3 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -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", diff --git a/explorer/src/components/AccountDetails.tsx b/explorer/src/components/AccountDetails.tsx index cb88583670..2962fb9b2f 100644 --- a/explorer/src/components/AccountDetails.tsx +++ b/explorer/src/components/AccountDetails.tsx @@ -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 ; } @@ -251,10 +248,8 @@ function HistoryCard({ pubkey }: { pubkey: PublicKey }) { return ( ); } @@ -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 }) {
- + {history.foundOldest ? ( +
Fetched full history
+ ) : ( + + )}
); diff --git a/explorer/src/providers/accounts/history.tsx b/explorer/src/providers/accounts/history.tsx index 1bc699dfff..3aa236788a 100644 --- a/explorer/src/providers/accounts/history.tsx +++ b/explorer/src/providers/accounts/history.tsx @@ -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( - undefined -); const StateContext = React.createContext(undefined); const DispatchContext = React.createContext(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 ( - - - - {children} - - - + + + {children} + + ); } 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 }); + } }; } diff --git a/explorer/src/providers/accounts/historyManager.ts b/explorer/src/providers/accounts/historyManager.ts deleted file mode 100644 index 6428252f49..0000000000 --- a/explorer/src/providers/accounts/historyManager.ts +++ /dev/null @@ -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 = new Map(); - accountHistory: Map = new Map(); - accountLock: Map = new Map(); - _fullRange: SlotRange | undefined; - connection: Connection; - - constructor(url: string) { - this.connection = new Connection(url); - } - - async fullRange(refresh: boolean): Promise { - 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; - 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); - } - } -}