2021-11-25 09:44:25 +00:00
|
|
|
import {
|
|
|
|
ChangeEventHandler,
|
|
|
|
FormEventHandler,
|
|
|
|
RefObject,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from "react";
|
|
|
|
import { NavigateFunction, useNavigate } from "react-router";
|
2021-08-08 22:49:45 +00:00
|
|
|
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";
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-08 22:49:45 +00:00
|
|
|
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,
|
2021-12-04 19:54:27 +00:00
|
|
|
to: t.to ?? null,
|
2021-07-21 04:42:49 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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(
|
2021-08-08 22:49:45 +00:00
|
|
|
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
|
|
|
|
2021-11-25 09:44:25 +00:00
|
|
|
const doSearch = (q: string, navigate: NavigateFunction) => {
|
2021-11-25 09:28:45 +00:00
|
|
|
if (isAddress(q)) {
|
|
|
|
navigate(`/address/${q}`, { replace: true });
|
|
|
|
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
|
|
|
|
navigate(`/address/${q}`);
|
|
|
|
};
|
2021-11-25 09:44:25 +00:00
|
|
|
|
|
|
|
export const useGenericSearch = (): [
|
|
|
|
RefObject<HTMLInputElement>,
|
|
|
|
ChangeEventHandler<HTMLInputElement>,
|
|
|
|
FormEventHandler<HTMLFormElement>
|
|
|
|
] => {
|
|
|
|
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) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (searchRef.current) {
|
|
|
|
searchRef.current.value = "";
|
|
|
|
}
|
|
|
|
doSearch(searchString, navigate);
|
|
|
|
};
|
|
|
|
|
|
|
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
|
|
useKeyboardShortcut(["/"], () => {
|
|
|
|
searchRef.current?.focus();
|
|
|
|
});
|
|
|
|
|
|
|
|
return [searchRef, handleChange, handleSubmit];
|
|
|
|
};
|