otterscan/src/search/search.ts

336 lines
8.4 KiB
TypeScript
Raw Normal View History

2021-11-25 09:44:25 +00:00
import {
ChangeEventHandler,
FormEventHandler,
RefObject,
useContext,
2021-11-25 09:44:25 +00:00
useRef,
useState,
} from "react";
import { NavigateFunction, useNavigate } from "react-router";
import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers";
2021-11-25 09:28:45 +00:00
import { isAddress } from "@ethersproject/address";
import { isHexString } from "@ethersproject/bytes";
2021-11-25 09:44:25 +00:00
import useKeyboardShortcut from "use-keyboard-shortcut";
2021-07-01 18:21:40 +00:00
import { PAGE_SIZE } from "../params";
import { ProcessedTransaction, TransactionChunk } from "../types";
import { RuntimeContext } from "../useRuntime";
import { getTransactionBySenderAndNonceFetcher } from "../useErigonHooks";
2021-07-01 18:21:40 +00:00
export class SearchController {
private txs: ProcessedTransaction[];
private pageStart: number;
private pageEnd: number;
private constructor(
readonly address: string,
txs: ProcessedTransaction[],
readonly isFirst: boolean,
readonly isLast: boolean,
boundToStart: boolean
) {
this.txs = txs;
if (boundToStart) {
this.pageStart = 0;
this.pageEnd = Math.min(txs.length, PAGE_SIZE);
} else {
this.pageEnd = txs.length;
this.pageStart = Math.max(0, txs.length - PAGE_SIZE);
}
}
private static rawToProcessed = (provider: JsonRpcProvider, _rawRes: any) => {
const _res: TransactionResponse[] = _rawRes.txs.map((t: any) =>
provider.formatter.transactionResponse(t)
2021-07-01 18:21:40 +00:00
);
return {
txs: _res.map((t, i): ProcessedTransaction => {
const _rawReceipt = _rawRes.receipts[i];
const _receipt = provider.formatter.receipt(_rawReceipt);
return {
blockNumber: t.blockNumber!,
timestamp: provider.formatter.number(_rawReceipt.timestamp),
idx: _receipt.transactionIndex,
hash: t.hash,
from: t.from,
to: t.to ?? null,
createdContractAddress: _receipt.contractAddress,
2021-07-01 18:21:40 +00:00
value: t.value,
fee: _receipt.gasUsed.mul(t.gasPrice!),
gasPrice: t.gasPrice!,
data: t.data,
status: _receipt.status!,
};
}),
firstPage: _rawRes.firstPage,
lastPage: _rawRes.lastPage,
};
};
private static async readBackPage(
provider: JsonRpcProvider,
2021-07-01 18:21:40 +00:00
address: string,
baseBlock: number
): Promise<TransactionChunk> {
const _rawRes = await provider.send("ots_searchTransactionsBefore", [
address,
baseBlock,
PAGE_SIZE,
]);
2021-07-08 19:02:42 +00:00
return this.rawToProcessed(provider, _rawRes);
2021-07-01 18:21:40 +00:00
}
private static async readForwardPage(
provider: JsonRpcProvider,
2021-07-01 18:21:40 +00:00
address: string,
baseBlock: number
): Promise<TransactionChunk> {
const _rawRes = await provider.send("ots_searchTransactionsAfter", [
address,
baseBlock,
PAGE_SIZE,
]);
2021-07-08 19:02:42 +00:00
return this.rawToProcessed(provider, _rawRes);
2021-07-01 18:21:40 +00:00
}
2021-07-08 19:02:42 +00:00
static async firstPage(
provider: JsonRpcProvider,
2021-07-08 19:02:42 +00:00
address: string
): Promise<SearchController> {
const newTxs = await SearchController.readBackPage(provider, address, 0);
2021-07-01 18:21:40 +00:00
return new SearchController(
address,
newTxs.txs,
newTxs.firstPage,
newTxs.lastPage,
true
);
}
static async middlePage(
provider: JsonRpcProvider,
2021-07-01 18:21:40 +00:00
address: string,
hash: string,
next: boolean
): Promise<SearchController> {
const tx = await provider.getTransaction(hash);
const newTxs = next
2021-07-08 19:02:42 +00:00
? await SearchController.readBackPage(provider, address, tx.blockNumber!)
: await SearchController.readForwardPage(
provider,
address,
tx.blockNumber!
);
2021-07-01 18:21:40 +00:00
return new SearchController(
address,
newTxs.txs,
newTxs.firstPage,
newTxs.lastPage,
next
);
}
2021-07-08 19:02:42 +00:00
static async lastPage(
provider: JsonRpcProvider,
2021-07-08 19:02:42 +00:00
address: string
): Promise<SearchController> {
const newTxs = await SearchController.readForwardPage(provider, address, 0);
2021-07-01 18:21:40 +00:00
return new SearchController(
address,
newTxs.txs,
newTxs.firstPage,
newTxs.lastPage,
false
);
}
getPage(): ProcessedTransaction[] {
return this.txs.slice(this.pageStart, this.pageEnd);
}
2021-07-08 19:02:42 +00:00
async prevPage(
provider: JsonRpcProvider,
2021-07-08 19:02:42 +00:00
hash: string
): Promise<SearchController> {
2021-07-01 18:21:40 +00:00
// Already on this page
if (this.txs[this.pageEnd - 1].hash === hash) {
return this;
}
if (this.txs[this.pageStart].hash === hash) {
const overflowPage = this.txs.slice(0, this.pageStart);
const baseBlock = this.txs[0].blockNumber;
const prevPage = await SearchController.readForwardPage(
2021-07-08 19:02:42 +00:00
provider,
2021-07-01 18:21:40 +00:00
this.address,
baseBlock
);
return new SearchController(
this.address,
prevPage.txs.concat(overflowPage),
prevPage.firstPage,
prevPage.lastPage,
false
);
}
return this;
}
2021-07-08 19:02:42 +00:00
async nextPage(
provider: JsonRpcProvider,
2021-07-08 19:02:42 +00:00
hash: string
): Promise<SearchController> {
2021-07-01 18:21:40 +00:00
// Already on this page
if (this.txs[this.pageStart].hash === hash) {
return this;
}
if (this.txs[this.pageEnd - 1].hash === hash) {
const overflowPage = this.txs.slice(this.pageEnd);
const baseBlock = this.txs[this.txs.length - 1].blockNumber;
const nextPage = await SearchController.readBackPage(
2021-07-08 19:02:42 +00:00
provider,
2021-07-01 18:21:40 +00:00
this.address,
baseBlock
);
return new SearchController(
this.address,
overflowPage.concat(nextPage.txs),
nextPage.firstPage,
nextPage.lastPage,
true
);
}
return this;
}
}
2021-11-25 09:28:45 +00:00
const doSearch = async (
provider: JsonRpcProvider,
q: string,
navigate: NavigateFunction
) => {
// Cleanup
q = q.trim();
let maybeAddress = q;
let maybeIndex: string | undefined;
const sepIndex = q.lastIndexOf(":");
if (sepIndex !== -1) {
maybeAddress = q.substring(0, sepIndex);
maybeIndex = q.substring(sepIndex + 1);
}
if (isAddress(maybeAddress)) {
// Plain address + nonce?
if (await navigateToTx(provider, maybeAddress, maybeIndex, navigate)) {
return;
}
// Plain address
navigate(`/address/${maybeAddress}`, { replace: true });
2021-11-25 09:28:45 +00:00
return;
}
if (isHexString(q, 32)) {
navigate(`/tx/${q}`, { replace: true });
return;
}
const blockNumber = parseInt(q);
if (!isNaN(blockNumber)) {
navigate(`/block/${blockNumber}`, { replace: true });
return;
}
// Assume it is an ENS name
const resolvedName = await provider.resolveName(maybeAddress);
if (resolvedName !== null) {
2022-01-24 19:23:16 +00:00
// ENS name + nonce?
if (await navigateToTx(provider, resolvedName, maybeIndex, navigate)) {
return;
}
// ENS name
navigate(`/address/${maybeAddress}`, { replace: true });
return;
}
// TODO: handle default
};
const navigateToTx = async (
provider: JsonRpcProvider,
maybeAddress: string,
maybeIndex: string | undefined,
navigate: NavigateFunction
): Promise<boolean> => {
if (maybeIndex !== undefined) {
try {
let nonce = 0;
if (maybeIndex === "latest") {
const count = await provider.getTransactionCount(maybeAddress);
if (count > 0) {
nonce = count - 1;
}
} else {
nonce = parseInt(maybeIndex);
}
const txHash = await getTransactionBySenderAndNonceFetcher({
provider,
sender: maybeAddress,
nonce,
});
if (txHash) {
navigate(`/tx/${txHash}`, { replace: true });
}
return true;
} catch (err) {
// ignore
}
}
return false;
2021-11-25 09:28:45 +00:00
};
2021-11-25 09:44:25 +00:00
export const useGenericSearch = (): [
RefObject<HTMLInputElement>,
ChangeEventHandler<HTMLInputElement>,
FormEventHandler<HTMLFormElement>
] => {
const { provider } = useContext(RuntimeContext);
2021-11-25 09:44:25 +00:00
const [searchString, setSearchString] = useState<string>("");
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const navigate = useNavigate();
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const searchTerm = e.target.value.trim();
setCanSubmit(searchTerm.length > 0);
setSearchString(searchTerm);
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
if (!canSubmit || !provider) {
2021-11-25 09:44:25 +00:00
return;
}
if (searchRef.current) {
searchRef.current.value = "";
}
doSearch(provider, searchString, navigate);
2021-11-25 09:44:25 +00:00
};
const searchRef = useRef<HTMLInputElement>(null);
useKeyboardShortcut(["/"], () => {
searchRef.current?.focus();
});
return [searchRef, handleChange, handleSubmit];
};