2021-07-08 19:02:42 +00:00
|
|
|
import React, { useState, useEffect, useMemo, useContext } from "react";
|
2021-07-23 18:10:08 +00:00
|
|
|
import {
|
|
|
|
useParams,
|
|
|
|
useLocation,
|
|
|
|
useHistory,
|
|
|
|
Switch,
|
|
|
|
Route,
|
|
|
|
} from "react-router-dom";
|
2021-08-31 19:49:52 +00:00
|
|
|
import { BlockTag } from "@ethersproject/abstract-provider";
|
2021-08-08 22:49:45 +00:00
|
|
|
import { getAddress, isAddress } from "@ethersproject/address";
|
2021-07-01 18:21:40 +00:00
|
|
|
import queryString from "query-string";
|
|
|
|
import Blockies from "react-blockies";
|
|
|
|
import StandardFrame from "./StandardFrame";
|
|
|
|
import StandardSubtitle from "./StandardSubtitle";
|
|
|
|
import Copy from "./components/Copy";
|
|
|
|
import ContentFrame from "./ContentFrame";
|
2021-07-23 18:10:08 +00:00
|
|
|
import TabGroup from "./components/TabGroup";
|
|
|
|
import Tab from "./components/Tab";
|
|
|
|
import Contract from "./address/Contract";
|
2021-07-01 18:21:40 +00:00
|
|
|
import UndefinedPageControl from "./search/UndefinedPageControl";
|
|
|
|
import ResultHeader from "./search/ResultHeader";
|
|
|
|
import PendingResults from "./search/PendingResults";
|
|
|
|
import TransactionItem from "./search/TransactionItem";
|
|
|
|
import { SearchController } from "./search/search";
|
2021-07-09 05:07:20 +00:00
|
|
|
import { RuntimeContext } from "./useRuntime";
|
2021-07-03 04:46:15 +00:00
|
|
|
import { useENSCache } from "./useReverseCache";
|
2021-07-01 18:21:40 +00:00
|
|
|
import { useFeeToggler } from "./search/useFeeToggler";
|
2021-07-14 06:52:31 +00:00
|
|
|
import { SelectionContext, useSelection } from "./useSelection";
|
2021-08-31 19:49:52 +00:00
|
|
|
import { useMultipleETHUSDOracle } from "./usePriceOracle";
|
2021-07-01 18:21:40 +00:00
|
|
|
|
|
|
|
type BlockParams = {
|
2021-07-02 19:05:18 +00:00
|
|
|
addressOrName: string;
|
2021-07-01 18:21:40 +00:00
|
|
|
direction?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type PageParams = {
|
|
|
|
p?: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
const AddressTransactions: React.FC = () => {
|
2021-07-09 05:07:20 +00:00
|
|
|
const { provider } = useContext(RuntimeContext);
|
2021-07-01 18:21:40 +00:00
|
|
|
const params = useParams<BlockParams>();
|
|
|
|
const location = useLocation<PageParams>();
|
|
|
|
const history = useHistory();
|
|
|
|
const qs = queryString.parse(location.search);
|
|
|
|
let hash: string | undefined;
|
|
|
|
if (qs.h) {
|
|
|
|
hash = qs.h as string;
|
|
|
|
}
|
|
|
|
|
2021-07-02 19:05:18 +00:00
|
|
|
const [checksummedAddress, setChecksummedAddress] = useState<string>();
|
|
|
|
const [isENS, setENS] = useState<boolean>();
|
2021-07-02 19:20:50 +00:00
|
|
|
const [error, setError] = useState<boolean>();
|
2021-07-02 19:05:18 +00:00
|
|
|
|
|
|
|
// If it looks like it is an ENS name, try to resolve it
|
|
|
|
useEffect(() => {
|
2021-08-08 22:49:45 +00:00
|
|
|
if (isAddress(params.addressOrName)) {
|
2021-07-03 04:46:15 +00:00
|
|
|
setENS(false);
|
|
|
|
setError(false);
|
|
|
|
|
2021-07-02 19:05:18 +00:00
|
|
|
// Normalize to checksummed address
|
2021-08-08 22:49:45 +00:00
|
|
|
const _checksummedAddress = getAddress(params.addressOrName);
|
2021-07-03 04:46:15 +00:00
|
|
|
if (_checksummedAddress !== params.addressOrName) {
|
|
|
|
// Request came with a non-checksummed address; fix the URL
|
|
|
|
history.replace(
|
|
|
|
`/address/${_checksummedAddress}${
|
|
|
|
params.direction ? "/" + params.direction : ""
|
|
|
|
}${location.search}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
setChecksummedAddress(_checksummedAddress);
|
2021-07-02 19:05:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-07-08 19:02:42 +00:00
|
|
|
if (!provider) {
|
|
|
|
return;
|
|
|
|
}
|
2021-07-02 19:05:18 +00:00
|
|
|
const resolveName = async () => {
|
|
|
|
const resolvedAddress = await provider.resolveName(params.addressOrName);
|
|
|
|
if (resolvedAddress !== null) {
|
|
|
|
setENS(true);
|
2021-07-02 19:20:50 +00:00
|
|
|
setError(false);
|
2021-07-03 04:46:15 +00:00
|
|
|
setChecksummedAddress(resolvedAddress);
|
2021-07-02 19:20:50 +00:00
|
|
|
} else {
|
2021-07-03 04:46:15 +00:00
|
|
|
setENS(false);
|
2021-07-02 19:20:50 +00:00
|
|
|
setError(true);
|
2021-07-03 04:46:15 +00:00
|
|
|
setChecksummedAddress(undefined);
|
2021-07-02 19:05:18 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
resolveName();
|
2021-07-08 19:02:42 +00:00
|
|
|
}, [
|
|
|
|
provider,
|
|
|
|
params.addressOrName,
|
|
|
|
history,
|
|
|
|
params.direction,
|
|
|
|
location.search,
|
|
|
|
]);
|
2021-07-01 18:21:40 +00:00
|
|
|
|
|
|
|
const [controller, setController] = useState<SearchController>();
|
|
|
|
useEffect(() => {
|
2021-07-08 19:02:42 +00:00
|
|
|
if (!provider || !checksummedAddress) {
|
2021-07-02 19:05:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-07-01 18:21:40 +00:00
|
|
|
const readFirstPage = async () => {
|
2021-07-08 19:02:42 +00:00
|
|
|
const _controller = await SearchController.firstPage(
|
|
|
|
provider,
|
|
|
|
checksummedAddress
|
|
|
|
);
|
2021-07-01 18:21:40 +00:00
|
|
|
setController(_controller);
|
|
|
|
};
|
|
|
|
const readMiddlePage = async (next: boolean) => {
|
|
|
|
const _controller = await SearchController.middlePage(
|
2021-07-08 19:02:42 +00:00
|
|
|
provider,
|
2021-07-01 18:21:40 +00:00
|
|
|
checksummedAddress,
|
|
|
|
hash!,
|
|
|
|
next
|
|
|
|
);
|
|
|
|
setController(_controller);
|
|
|
|
};
|
|
|
|
const readLastPage = async () => {
|
2021-07-08 19:02:42 +00:00
|
|
|
const _controller = await SearchController.lastPage(
|
|
|
|
provider,
|
|
|
|
checksummedAddress
|
|
|
|
);
|
2021-07-01 18:21:40 +00:00
|
|
|
setController(_controller);
|
|
|
|
};
|
|
|
|
const prevPage = async () => {
|
2021-07-08 19:02:42 +00:00
|
|
|
const _controller = await controller!.prevPage(provider, hash!);
|
2021-07-01 18:21:40 +00:00
|
|
|
setController(_controller);
|
|
|
|
};
|
|
|
|
const nextPage = async () => {
|
2021-07-08 19:02:42 +00:00
|
|
|
const _controller = await controller!.nextPage(provider, hash!);
|
2021-07-01 18:21:40 +00:00
|
|
|
setController(_controller);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Page load from scratch
|
|
|
|
if (params.direction === "first" || params.direction === undefined) {
|
|
|
|
if (!controller?.isFirst || controller.address !== checksummedAddress) {
|
|
|
|
readFirstPage();
|
|
|
|
}
|
|
|
|
} else if (params.direction === "prev") {
|
|
|
|
if (controller && controller.address === checksummedAddress) {
|
|
|
|
prevPage();
|
|
|
|
} else {
|
|
|
|
readMiddlePage(false);
|
|
|
|
}
|
|
|
|
} else if (params.direction === "next") {
|
|
|
|
if (controller && controller.address === checksummedAddress) {
|
|
|
|
nextPage();
|
|
|
|
} else {
|
|
|
|
readMiddlePage(true);
|
|
|
|
}
|
|
|
|
} else if (params.direction === "last") {
|
|
|
|
if (!controller?.isLast || controller.address !== checksummedAddress) {
|
|
|
|
readLastPage();
|
|
|
|
}
|
|
|
|
}
|
2021-07-08 19:02:42 +00:00
|
|
|
}, [provider, checksummedAddress, params.direction, hash, controller]);
|
2021-07-01 18:21:40 +00:00
|
|
|
|
|
|
|
const page = useMemo(() => controller?.getPage(), [controller]);
|
2021-07-08 19:02:42 +00:00
|
|
|
const reverseCache = useENSCache(provider, page);
|
2021-07-01 18:21:40 +00:00
|
|
|
|
2021-08-31 19:49:52 +00:00
|
|
|
const blockTags: BlockTag[] = useMemo(() => {
|
|
|
|
if (!page) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return page.map((p) => p.blockNumber);
|
|
|
|
}, [page]);
|
|
|
|
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
|
|
|
|
|
2021-07-02 19:05:18 +00:00
|
|
|
document.title = `Address ${params.addressOrName} | Otterscan`;
|
2021-07-01 18:21:40 +00:00
|
|
|
|
|
|
|
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
|
|
|
|
|
2021-07-14 07:24:28 +00:00
|
|
|
const selectionCtx = useSelection();
|
2021-07-14 06:52:31 +00:00
|
|
|
|
2021-07-01 18:21:40 +00:00
|
|
|
return (
|
|
|
|
<StandardFrame>
|
2021-07-02 19:20:50 +00:00
|
|
|
{error ? (
|
|
|
|
<span className="text-base">
|
|
|
|
"{params.addressOrName}" is not an ETH address or ENS name.
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
checksummedAddress && (
|
2021-07-01 18:21:40 +00:00
|
|
|
<>
|
2021-07-02 19:20:50 +00:00
|
|
|
<StandardSubtitle>
|
|
|
|
<div className="flex space-x-2 items-baseline">
|
|
|
|
<Blockies
|
|
|
|
className="self-center rounded"
|
|
|
|
seed={checksummedAddress.toLowerCase()}
|
|
|
|
scale={3}
|
|
|
|
/>
|
|
|
|
<span>Address</span>
|
|
|
|
<span className="font-address text-base text-gray-500">
|
|
|
|
{checksummedAddress}
|
|
|
|
</span>
|
|
|
|
<Copy value={checksummedAddress} rounded />
|
|
|
|
{isENS && (
|
|
|
|
<span className="rounded-lg px-2 py-1 bg-gray-200 text-gray-500 text-xs">
|
|
|
|
ENS: {params.addressOrName}
|
|
|
|
</span>
|
2021-07-01 18:21:40 +00:00
|
|
|
)}
|
|
|
|
</div>
|
2021-07-02 19:20:50 +00:00
|
|
|
</StandardSubtitle>
|
2021-07-23 18:10:08 +00:00
|
|
|
<TabGroup>
|
|
|
|
<Tab href={`/address/${checksummedAddress}`}>Overview</Tab>
|
|
|
|
<Tab href={`/address/${checksummedAddress}/contract`}>
|
|
|
|
Contract
|
|
|
|
</Tab>
|
|
|
|
</TabGroup>
|
|
|
|
<Switch>
|
|
|
|
<Route path="/address/:addressOrName" exact>
|
|
|
|
<ContentFrame tabs>
|
2021-07-02 19:20:50 +00:00
|
|
|
<div className="flex justify-between items-baseline py-3">
|
|
|
|
<div className="text-sm text-gray-500">
|
2021-07-23 18:10:08 +00:00
|
|
|
{page === undefined ? (
|
|
|
|
<>Waiting for search results...</>
|
|
|
|
) : (
|
2021-07-02 19:20:50 +00:00
|
|
|
<>{page.length} transactions on this page</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<UndefinedPageControl
|
|
|
|
address={params.addressOrName}
|
2021-07-23 18:10:08 +00:00
|
|
|
isFirst={controller?.isFirst}
|
|
|
|
isLast={controller?.isLast}
|
2021-07-02 19:20:50 +00:00
|
|
|
prevHash={page ? page[0].hash : ""}
|
|
|
|
nextHash={page ? page[page.length - 1].hash : ""}
|
2021-07-23 18:10:08 +00:00
|
|
|
disabled={controller === undefined}
|
2021-07-02 19:20:50 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2021-07-23 18:10:08 +00:00
|
|
|
<ResultHeader
|
|
|
|
feeDisplay={feeDisplay}
|
|
|
|
feeDisplayToggler={feeDisplayToggler}
|
|
|
|
/>
|
|
|
|
{controller ? (
|
|
|
|
<SelectionContext.Provider value={selectionCtx}>
|
|
|
|
{controller.getPage().map((tx) => (
|
|
|
|
<TransactionItem
|
|
|
|
key={tx.hash}
|
|
|
|
tx={tx}
|
|
|
|
ensCache={reverseCache}
|
|
|
|
selectedAddress={checksummedAddress}
|
|
|
|
feeDisplay={feeDisplay}
|
|
|
|
priceMap={priceMap}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
<div className="flex justify-between items-baseline py-3">
|
|
|
|
<div className="text-sm text-gray-500">
|
|
|
|
{page === undefined ? (
|
|
|
|
<>Waiting for search results...</>
|
|
|
|
) : (
|
|
|
|
<>{page.length} transactions on this page</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<UndefinedPageControl
|
|
|
|
address={params.addressOrName}
|
|
|
|
isFirst={controller?.isFirst}
|
|
|
|
isLast={controller?.isLast}
|
|
|
|
prevHash={page ? page[0].hash : ""}
|
|
|
|
nextHash={page ? page[page.length - 1].hash : ""}
|
|
|
|
disabled={controller === undefined}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<ResultHeader
|
|
|
|
feeDisplay={feeDisplay}
|
|
|
|
feeDisplayToggler={feeDisplayToggler}
|
|
|
|
/>
|
|
|
|
</SelectionContext.Provider>
|
|
|
|
) : (
|
|
|
|
<PendingResults />
|
|
|
|
)}
|
|
|
|
</ContentFrame>
|
|
|
|
</Route>
|
|
|
|
<Route path="/address/:addressOrName/contract" exact>
|
|
|
|
<Contract checksummedAddress={checksummedAddress} />
|
|
|
|
</Route>
|
|
|
|
</Switch>
|
2021-07-01 18:21:40 +00:00
|
|
|
</>
|
2021-07-02 19:20:50 +00:00
|
|
|
)
|
|
|
|
)}
|
2021-07-01 18:21:40 +00:00
|
|
|
</StandardFrame>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default React.memo(AddressTransactions);
|